better-ts-stack 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +16 -8
  3. package/dist/args.js +7 -0
  4. package/dist/builder/buildPlanner.js +7 -1
  5. package/dist/builder/configGenerator.js +28 -3
  6. package/dist/builder/index.js +2 -1
  7. package/dist/builder/moduleSelector.js +6 -1
  8. package/dist/builder/readmeGenerator.js +8 -2
  9. package/dist/builder/templateContext.js +1 -0
  10. package/dist/output/nextSteps.js +2 -4
  11. package/dist/prompts/stack.js +6 -1
  12. package/dist/types/index.js +2 -0
  13. package/dist/ui/messages.js +2 -0
  14. package/package.json +71 -71
  15. package/templates/backend/express/.prettierignore +52 -52
  16. package/templates/backend/express/.prettierrc +16 -16
  17. package/templates/backend/express/gitignore +3 -1
  18. package/templates/backend/express/src/env.ts.hbs +13 -2
  19. package/templates/backend/express/src/index.ts.hbs +31 -2
  20. package/templates/backend/express/tsconfig.eslint.json +9 -9
  21. package/templates/backend/nestjs/.prettierignore +52 -52
  22. package/templates/backend/nestjs/gitignore +3 -1
  23. package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
  24. package/templates/backend/nestjs/src/env.ts.hbs +13 -2
  25. package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
  26. package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
  27. package/templates/backend/nestjs/tsconfig.eslint.json +9 -9
  28. package/templates/frontend/nextjs/app/globals.css +129 -89
  29. package/templates/frontend/nextjs/app/layout.tsx +36 -34
  30. package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
  31. package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
  32. package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
  33. package/templates/frontend/nextjs/components/ui/button.tsx +57 -51
  34. package/templates/frontend/nextjs/components/ui/card.tsx +102 -60
  35. package/templates/frontend/nextjs/components/ui/field.tsx +238 -67
  36. package/templates/frontend/nextjs/components/ui/input.tsx +19 -18
  37. package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
  38. package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
  39. package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
  40. package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
  41. package/templates/frontend/nextjs/components.json +11 -5
  42. package/templates/frontend/nextjs/config.json +36 -35
  43. package/templates/frontend/nextjs/eslint.config.mjs +11 -11
  44. package/templates/frontend/nextjs/lib/utils.ts +1 -6
  45. package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
  46. package/templates/frontend/nextjs/postcss.config.mjs +7 -7
  47. package/templates/frontend/nextjs/public/file.svg +1 -1
  48. package/templates/frontend/nextjs/public/globe.svg +1 -1
  49. package/templates/frontend/nextjs/public/window.svg +1 -1
  50. package/templates/frontend/tanstack/components.json +25 -18
  51. package/templates/frontend/tanstack/config.json +5 -2
  52. package/templates/frontend/tanstack/eslint.config.mjs +23 -23
  53. package/templates/frontend/tanstack/gitignore +39 -39
  54. package/templates/frontend/tanstack/public/favicon.svg +7 -7
  55. package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
  56. package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
  57. package/templates/frontend/tanstack/src/components/ui/button.tsx +57 -51
  58. package/templates/frontend/tanstack/src/components/ui/card.tsx +102 -60
  59. package/templates/frontend/tanstack/src/components/ui/field.tsx +236 -67
  60. package/templates/frontend/tanstack/src/components/ui/input.tsx +19 -18
  61. package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
  62. package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
  63. package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
  64. package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
  65. package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
  66. package/templates/frontend/tanstack/src/router.tsx +31 -31
  67. package/templates/frontend/tanstack/src/routes/__root.tsx +69 -69
  68. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
  69. package/templates/frontend/tanstack/src/styles/app.css +100 -55
  70. package/templates/frontend/tanstack/tsconfig.json +22 -22
  71. package/templates/modules/agents/AGENTS.md.hbs +245 -0
  72. package/templates/modules/agents/CLAUDE.md +1 -0
  73. package/templates/modules/agents/config.json +10 -0
  74. package/templates/modules/auth/express/config.json +12 -11
  75. package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
  76. package/templates/modules/auth/express/src/lib/email.ts +37 -0
  77. package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
  78. package/templates/modules/auth/express/src/routes/me.ts +13 -0
  79. package/templates/modules/auth/nestjs/config.json +13 -11
  80. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
  81. package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
  82. package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
  83. package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
  84. package/templates/modules/auth/nextjs/app/api/auth/[...all]/route.ts +4 -4
  85. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
  86. package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
  87. package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
  88. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
  89. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
  90. package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
  91. package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
  92. package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
  93. package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
  94. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
  95. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
  96. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
  97. package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
  98. package/templates/modules/auth/nextjs/config.json +24 -18
  99. package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -3
  100. package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
  101. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
  102. package/templates/modules/auth/nextjs/lib/email.ts +43 -0
  103. package/templates/modules/auth/nextjs/lib/session.ts +12 -0
  104. package/templates/modules/auth/tanstack/config.json +25 -19
  105. package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
  106. package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
  107. package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
  108. package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
  109. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
  110. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
  111. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
  112. package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
  113. package/templates/modules/auth/tanstack/src/lib/auth-client.ts +2 -2
  114. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +14 -8
  115. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
  116. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +98 -38
  117. package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
  118. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
  119. package/templates/modules/auth/tanstack/src/routes/api/auth/$.ts +15 -15
  120. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +119 -111
  121. package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
  122. package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
  123. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +44 -37
  124. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +44 -37
  125. package/templates/modules/docker/.dockerignore +2 -0
  126. package/templates/modules/docker/config.json +15 -15
  127. package/templates/modules/drizzle/express/config.json +26 -26
  128. package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
  129. package/templates/modules/drizzle/nestjs/config.json +26 -26
  130. package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
  131. package/templates/modules/drizzle/nextjs/config.json +26 -26
  132. package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -13
  133. package/templates/modules/drizzle/tanstack/config.json +26 -26
  134. package/templates/modules/drizzle/tanstack/drizzle.config.ts +12 -12
  135. package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -13
  136. package/templates/modules/mongoose/express/config.json +15 -15
  137. package/templates/modules/mongoose/express/src/lib/db.ts +43 -43
  138. package/templates/modules/mongoose/express/src/models/User.ts +48 -48
  139. package/templates/modules/mongoose/nestjs/config.json +16 -15
  140. package/templates/modules/mongoose/nextjs/config.json +15 -15
  141. package/templates/modules/mongoose/nextjs/models/User.ts +48 -48
  142. package/templates/modules/mongoose/tanstack/config.json +16 -16
  143. package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -48
  144. package/templates/modules/prisma/express/config.json +27 -27
  145. package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
  146. package/templates/modules/prisma/nestjs/config.json +27 -27
  147. package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
  148. package/templates/modules/prisma/nextjs/config.json +28 -28
  149. package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -18
  150. package/templates/modules/prisma/nextjs/prisma.config.ts +14 -14
  151. package/templates/modules/prisma/tanstack/config.json +28 -28
  152. package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -91
  153. package/templates/modules/prisma/tanstack/prisma.config.ts +14 -14
  154. package/templates/modules/prisma/tanstack/src/lib/prisma.ts +17 -17
  155. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
  156. package/templates/modules/vscode/.vscode/extensions.json.hbs +29 -0
  157. package/templates/modules/vscode/.vscode/settings.json.hbs +22 -0
  158. package/templates/modules/vscode/config.json +10 -0
  159. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
  160. package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
  161. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
  162. package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
  163. package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
  164. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
  165. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
  166. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
  167. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
  168. package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
  169. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
  170. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
  171. package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
  172. package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
  173. package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
  174. package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useState, useTransition } from "react";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { CircleAlert, MailCheck } from "lucide-react";
7
+ import { useForm } from "react-hook-form";
8
+
9
+ import { authClient } from "@/lib/auth-client";
10
+ import {
11
+ forgotPasswordSchema,
12
+ getAuthErrorMessage,
13
+ type ForgotPasswordValues,
14
+ } from "@/lib/auth-schema";
15
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
16
+ import { Button } from "@/components/ui/button";
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from "@/components/ui/card";
24
+ import {
25
+ Field,
26
+ FieldDescription,
27
+ FieldError,
28
+ FieldGroup,
29
+ FieldLabel,
30
+ } from "@/components/ui/field";
31
+ import { Input } from "@/components/ui/input";
32
+ import { Spinner } from "@/components/ui/spinner";
33
+
34
+ export function ForgotPasswordForm() {
35
+ const [isPending, startTransition] = useTransition();
36
+ const [authError, setAuthError] = useState<string | null>(null);
37
+ const [isSent, setIsSent] = useState(false);
38
+ const form = useForm<ForgotPasswordValues>({
39
+ resolver: zodResolver(forgotPasswordSchema),
40
+ defaultValues: { email: "" },
41
+ });
42
+
43
+ const onSubmit = form.handleSubmit((values) => {
44
+ setAuthError(null);
45
+
46
+ startTransition(async () => {
47
+ // The link in the email opens /reset-password with a one-time token
48
+ const result = await authClient.requestPasswordReset({
49
+ email: values.email,
50
+ redirectTo: "/reset-password",
51
+ });
52
+
53
+ if (result.error) {
54
+ setAuthError(getAuthErrorMessage(result.error));
55
+ return;
56
+ }
57
+
58
+ setIsSent(true);
59
+ });
60
+ });
61
+
62
+ return (
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>Forgot your password?</CardTitle>
66
+ <CardDescription>
67
+ Enter your email and we will send you a link to choose a new one.
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent>
71
+ {isSent ? (
72
+ <FieldGroup>
73
+ <Alert>
74
+ <MailCheck />
75
+ <AlertTitle>Check your email</AlertTitle>
76
+ <AlertDescription>
77
+ If an account exists for that email, a reset link is on its
78
+ way. Without an email provider set up, the link is printed in
79
+ the server terminal.
80
+ </AlertDescription>
81
+ </Alert>
82
+ <FieldDescription className="text-center">
83
+ <Link href="/sign-in">Back to login</Link>
84
+ </FieldDescription>
85
+ </FieldGroup>
86
+ ) : (
87
+ <form onSubmit={onSubmit} noValidate>
88
+ <FieldGroup>
89
+ <Field data-invalid={Boolean(form.formState.errors.email)}>
90
+ <FieldLabel htmlFor="email">Email</FieldLabel>
91
+ <Input
92
+ id="email"
93
+ type="email"
94
+ autoComplete="email"
95
+ aria-invalid={Boolean(form.formState.errors.email)}
96
+ placeholder="you@example.com"
97
+ {...form.register("email")}
98
+ />
99
+ <FieldDescription>
100
+ Use the email you registered with.
101
+ </FieldDescription>
102
+ <FieldError errors={[form.formState.errors.email]} />
103
+ </Field>
104
+
105
+ {authError ? (
106
+ <Alert variant="destructive">
107
+ <CircleAlert />
108
+ <AlertDescription>{authError}</AlertDescription>
109
+ </Alert>
110
+ ) : null}
111
+
112
+ <Field>
113
+ <Button type="submit" disabled={isPending}>
114
+ {isPending ? (
115
+ <>
116
+ <Spinner data-icon="inline-start" />
117
+ Sending link
118
+ </>
119
+ ) : (
120
+ "Send reset link"
121
+ )}
122
+ </Button>
123
+ <FieldDescription className="text-center">
124
+ Remembered it? <Link href="/sign-in">Back to login</Link>
125
+ </FieldDescription>
126
+ </Field>
127
+ </FieldGroup>
128
+ </form>
129
+ )}
130
+ </CardContent>
131
+ </Card>
132
+ );
133
+ }
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { useState, useTransition } from "react";
6
+ import { zodResolver } from "@hookform/resolvers/zod";
7
+ import { CircleAlert } from "lucide-react";
8
+ import { useForm } from "react-hook-form";
9
+
10
+ import { authClient } from "@/lib/auth-client";
11
+ import {
12
+ getAuthErrorMessage,
13
+ resetPasswordSchema,
14
+ type ResetPasswordValues,
15
+ } from "@/lib/auth-schema";
16
+ import { Alert, AlertDescription } from "@/components/ui/alert";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Card,
20
+ CardContent,
21
+ CardDescription,
22
+ CardFooter,
23
+ CardHeader,
24
+ CardTitle,
25
+ } from "@/components/ui/card";
26
+ import {
27
+ Field,
28
+ FieldError,
29
+ FieldGroup,
30
+ FieldLabel,
31
+ } from "@/components/ui/field";
32
+ import { Input } from "@/components/ui/input";
33
+ import { Spinner } from "@/components/ui/spinner";
34
+
35
+ // The link in the reset email opens /reset-password?token=..., or
36
+ // ?error=INVALID_TOKEN when the token is invalid or has expired. Reading it here,
37
+ // in the browser, lets the page itself be static
38
+ export function ResetPasswordForm() {
39
+ const searchParams = useSearchParams();
40
+ const token = searchParams.get("error") ? null : searchParams.get("token");
41
+ const [isPending, startTransition] = useTransition();
42
+ const [authError, setAuthError] = useState<string | null>(null);
43
+ const [isDone, setIsDone] = useState(false);
44
+ const form = useForm<ResetPasswordValues>({
45
+ resolver: zodResolver(resetPasswordSchema),
46
+ defaultValues: { password: "", confirmPassword: "" },
47
+ });
48
+
49
+ const onSubmit = form.handleSubmit((values) => {
50
+ if (!token) return;
51
+ setAuthError(null);
52
+
53
+ startTransition(async () => {
54
+ const result = await authClient.resetPassword({
55
+ newPassword: values.password,
56
+ token,
57
+ });
58
+
59
+ if (result.error) {
60
+ setAuthError(getAuthErrorMessage(result.error));
61
+ return;
62
+ }
63
+
64
+ setIsDone(true);
65
+ });
66
+ });
67
+
68
+ if (!token || isDone) {
69
+ return (
70
+ <Card>
71
+ <CardHeader>
72
+ <CardTitle>{isDone ? "Password changed" : "Link expired"}</CardTitle>
73
+ <CardDescription>
74
+ {isDone
75
+ ? "You were signed out on every device. Log in with your new password."
76
+ : "This reset link is invalid or has expired. Ask for a new one."}
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardFooter>
80
+ <Button
81
+ className="w-full"
82
+ nativeButton={false}
83
+ render={<Link href={isDone ? "/sign-in" : "/forgot-password"} />}
84
+ >
85
+ {isDone ? "Go to login" : "Send a new link"}
86
+ </Button>
87
+ </CardFooter>
88
+ </Card>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Card>
94
+ <CardHeader>
95
+ <CardTitle>Choose a new password</CardTitle>
96
+ <CardDescription>
97
+ You will be signed out on every device after the change.
98
+ </CardDescription>
99
+ </CardHeader>
100
+ <CardContent>
101
+ <form onSubmit={onSubmit} noValidate>
102
+ <FieldGroup>
103
+ <Field data-invalid={Boolean(form.formState.errors.password)}>
104
+ <FieldLabel htmlFor="password">New password</FieldLabel>
105
+ <Input
106
+ id="password"
107
+ type="password"
108
+ autoComplete="new-password"
109
+ aria-invalid={Boolean(form.formState.errors.password)}
110
+ {...form.register("password")}
111
+ />
112
+ <FieldError errors={[form.formState.errors.password]} />
113
+ </Field>
114
+
115
+ <Field
116
+ data-invalid={Boolean(form.formState.errors.confirmPassword)}
117
+ >
118
+ <FieldLabel htmlFor="confirmPassword">
119
+ Confirm new password
120
+ </FieldLabel>
121
+ <Input
122
+ id="confirmPassword"
123
+ type="password"
124
+ autoComplete="new-password"
125
+ aria-invalid={Boolean(form.formState.errors.confirmPassword)}
126
+ {...form.register("confirmPassword")}
127
+ />
128
+ <FieldError errors={[form.formState.errors.confirmPassword]} />
129
+ </Field>
130
+
131
+ {authError ? (
132
+ <Alert variant="destructive">
133
+ <CircleAlert />
134
+ <AlertDescription>{authError}</AlertDescription>
135
+ </Alert>
136
+ ) : null}
137
+
138
+ <Field>
139
+ <Button type="submit" disabled={isPending}>
140
+ {isPending ? (
141
+ <>
142
+ <Spinner data-icon="inline-start" />
143
+ Saving
144
+ </>
145
+ ) : (
146
+ "Save new password"
147
+ )}
148
+ </Button>
149
+ </Field>
150
+ </FieldGroup>
151
+ </form>
152
+ </CardContent>
153
+ </Card>
154
+ );
155
+ }
@@ -4,7 +4,7 @@ import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useState, useTransition } from "react";
6
6
  import { zodResolver } from "@hookform/resolvers/zod";
7
- import { LoaderCircle } from "lucide-react";
7
+ import { CircleAlert } from "lucide-react";
8
8
  import { useForm } from "react-hook-form";
9
9
 
10
10
  import { authClient } from "@/lib/auth-client";
@@ -12,13 +12,14 @@ import {
12
12
  getAuthErrorMessage,
13
13
  signInSchema,
14
14
  type SignInValues,
15
+ type SocialProvider,
15
16
  } from "@/lib/auth-schema";
17
+ import { Alert, AlertDescription } from "@/components/ui/alert";
16
18
  import { Button } from "@/components/ui/button";
17
19
  import {
18
20
  Card,
19
21
  CardContent,
20
22
  CardDescription,
21
- CardFooter,
22
23
  CardHeader,
23
24
  CardTitle,
24
25
  } from "@/components/ui/card";
@@ -30,8 +31,11 @@ import {
30
31
  FieldLabel,
31
32
  } from "@/components/ui/field";
32
33
  import { Input } from "@/components/ui/input";
34
+ import { Spinner } from "@/components/ui/spinner";
33
35
 
34
- export function SignInForm() {
36
+ import { SocialButtons } from "./social-buttons";
37
+
38
+ export function SignInForm({ providers }: { providers: SocialProvider[] }) {
35
39
  const router = useRouter();
36
40
  const [isPending, startTransition] = useTransition();
37
41
  const [authError, setAuthError] = useState<string | null>(null);
@@ -63,16 +67,18 @@ export function SignInForm() {
63
67
  });
64
68
 
65
69
  return (
66
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
67
- <CardHeader className="space-y-2">
70
+ <Card>
71
+ <CardHeader>
68
72
  <CardTitle>Login to your account</CardTitle>
69
73
  <CardDescription>
70
74
  Sign in with your email and password to continue to your dashboard.
71
75
  </CardDescription>
72
76
  </CardHeader>
73
77
  <CardContent>
74
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
78
+ <form onSubmit={onSubmit} noValidate>
75
79
  <FieldGroup>
80
+ <SocialButtons providers={providers} />
81
+
76
82
  <Field data-invalid={Boolean(form.formState.errors.email)}>
77
83
  <FieldLabel htmlFor="email">Email</FieldLabel>
78
84
  <Input
@@ -83,12 +89,22 @@ export function SignInForm() {
83
89
  placeholder="you@example.com"
84
90
  {...form.register("email")}
85
91
  />
86
- <FieldDescription>Use the email you registered with.</FieldDescription>
92
+ <FieldDescription>
93
+ Use the email you registered with.
94
+ </FieldDescription>
87
95
  <FieldError errors={[form.formState.errors.email]} />
88
96
  </Field>
89
97
 
90
98
  <Field data-invalid={Boolean(form.formState.errors.password)}>
91
- <FieldLabel htmlFor="password">Password</FieldLabel>
99
+ <div className="flex items-center">
100
+ <FieldLabel htmlFor="password">Password</FieldLabel>
101
+ <Link
102
+ className="ml-auto text-sm underline-offset-4 hover:underline"
103
+ href="/forgot-password"
104
+ >
105
+ Forgot password?
106
+ </Link>
107
+ </div>
92
108
  <Input
93
109
  id="password"
94
110
  type="password"
@@ -99,34 +115,32 @@ export function SignInForm() {
99
115
  />
100
116
  <FieldError errors={[form.formState.errors.password]} />
101
117
  </Field>
102
- </FieldGroup>
103
118
 
104
- {authError ? (
105
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
106
- {authError}
107
- </div>
108
- ) : null}
119
+ {authError ? (
120
+ <Alert variant="destructive">
121
+ <CircleAlert />
122
+ <AlertDescription>{authError}</AlertDescription>
123
+ </Alert>
124
+ ) : null}
109
125
 
110
- <Button className="w-full" type="submit" disabled={isPending}>
111
- {isPending ? (
112
- <>
113
- <LoaderCircle className="mr-2 size-4 animate-spin" />
114
- Signing in
115
- </>
116
- ) : (
117
- "Login"
118
- )}
119
- </Button>
126
+ <Field>
127
+ <Button type="submit" disabled={isPending}>
128
+ {isPending ? (
129
+ <>
130
+ <Spinner data-icon="inline-start" />
131
+ Signing in
132
+ </>
133
+ ) : (
134
+ "Login"
135
+ )}
136
+ </Button>
137
+ <FieldDescription className="text-center">
138
+ Need an account? <Link href="/sign-up">Signup</Link>
139
+ </FieldDescription>
140
+ </Field>
141
+ </FieldGroup>
120
142
  </form>
121
143
  </CardContent>
122
- <CardFooter className="justify-center">
123
- <p className="text-sm text-slate-600">
124
- Need an account?{" "}
125
- <Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-up">
126
- Signup
127
- </Link>
128
- </p>
129
- </CardFooter>
130
144
  </Card>
131
145
  );
132
146
  }
@@ -2,10 +2,11 @@
2
2
 
3
3
  import { useRouter } from "next/navigation";
4
4
  import { useState, useTransition } from "react";
5
- import { LoaderCircle, LogOut } from "lucide-react";
5
+ import { LogOut } from "lucide-react";
6
6
 
7
7
  import { authClient } from "@/lib/auth-client";
8
8
  import { Button } from "@/components/ui/button";
9
+ import { Spinner } from "@/components/ui/spinner";
9
10
 
10
11
  export function SignOutButton() {
11
12
  const router = useRouter();
@@ -13,7 +14,7 @@ export function SignOutButton() {
13
14
  const [error, setError] = useState<string | null>(null);
14
15
 
15
16
  return (
16
- <div className="space-y-2">
17
+ <div className="flex flex-col items-start gap-2">
17
18
  <Button
18
19
  variant="outline"
19
20
  onClick={() => {
@@ -34,17 +35,17 @@ export function SignOutButton() {
34
35
  >
35
36
  {isPending ? (
36
37
  <>
37
- <LoaderCircle className="mr-2 size-4 animate-spin" />
38
+ <Spinner data-icon="inline-start" />
38
39
  Signing out
39
40
  </>
40
41
  ) : (
41
42
  <>
42
- <LogOut className="mr-2 size-4" />
43
+ <LogOut data-icon="inline-start" />
43
44
  Sign out
44
45
  </>
45
46
  )}
46
47
  </Button>
47
- {error ? <p className="text-sm text-rose-600">{error}</p> : null}
48
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
48
49
  </div>
49
50
  );
50
51
  }
@@ -4,7 +4,7 @@ import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useState, useTransition } from "react";
6
6
  import { zodResolver } from "@hookform/resolvers/zod";
7
- import { LoaderCircle } from "lucide-react";
7
+ import { CircleAlert } from "lucide-react";
8
8
  import { useForm } from "react-hook-form";
9
9
 
10
10
  import { authClient } from "@/lib/auth-client";
@@ -12,13 +12,14 @@ import {
12
12
  getAuthErrorMessage,
13
13
  signUpSchema,
14
14
  type SignUpValues,
15
+ type SocialProvider,
15
16
  } from "@/lib/auth-schema";
17
+ import { Alert, AlertDescription } from "@/components/ui/alert";
16
18
  import { Button } from "@/components/ui/button";
17
19
  import {
18
20
  Card,
19
21
  CardContent,
20
22
  CardDescription,
21
- CardFooter,
22
23
  CardHeader,
23
24
  CardTitle,
24
25
  } from "@/components/ui/card";
@@ -30,8 +31,11 @@ import {
30
31
  FieldLabel,
31
32
  } from "@/components/ui/field";
32
33
  import { Input } from "@/components/ui/input";
34
+ import { Spinner } from "@/components/ui/spinner";
33
35
 
34
- export function SignUpForm() {
36
+ import { SocialButtons } from "./social-buttons";
37
+
38
+ export function SignUpForm({ providers }: { providers: SocialProvider[] }) {
35
39
  const router = useRouter();
36
40
  const [isPending, startTransition] = useTransition();
37
41
  const [authError, setAuthError] = useState<string | null>(null);
@@ -52,6 +56,8 @@ export function SignUpForm() {
52
56
  name: values.name,
53
57
  email: values.email,
54
58
  password: values.password,
59
+ // Where the link in the verification email leads after confirming
60
+ callbackURL: "/dashboard",
55
61
  });
56
62
 
57
63
  if (result.error) {
@@ -65,8 +71,8 @@ export function SignUpForm() {
65
71
  });
66
72
 
67
73
  return (
68
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
69
- <CardHeader className="space-y-2">
74
+ <Card>
75
+ <CardHeader>
70
76
  <CardTitle>Create your account</CardTitle>
71
77
  <CardDescription>
72
78
  Start with a simple email and password sign-up flow powered by Better
@@ -74,8 +80,10 @@ export function SignUpForm() {
74
80
  </CardDescription>
75
81
  </CardHeader>
76
82
  <CardContent>
77
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
83
+ <form onSubmit={onSubmit} noValidate>
78
84
  <FieldGroup>
85
+ <SocialButtons providers={providers} />
86
+
79
87
  <Field data-invalid={Boolean(form.formState.errors.name)}>
80
88
  <FieldLabel htmlFor="name">Full name</FieldLabel>
81
89
  <Input
@@ -119,34 +127,32 @@ export function SignUpForm() {
119
127
  </FieldDescription>
120
128
  <FieldError errors={[form.formState.errors.password]} />
121
129
  </Field>
122
- </FieldGroup>
123
130
 
124
- {authError ? (
125
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
126
- {authError}
127
- </div>
128
- ) : null}
131
+ {authError ? (
132
+ <Alert variant="destructive">
133
+ <CircleAlert />
134
+ <AlertDescription>{authError}</AlertDescription>
135
+ </Alert>
136
+ ) : null}
129
137
 
130
- <Button className="w-full" type="submit" disabled={isPending}>
131
- {isPending ? (
132
- <>
133
- <LoaderCircle className="mr-2 size-4 animate-spin" />
134
- Creating account
135
- </>
136
- ) : (
137
- "Signup"
138
- )}
139
- </Button>
138
+ <Field>
139
+ <Button type="submit" disabled={isPending}>
140
+ {isPending ? (
141
+ <>
142
+ <Spinner data-icon="inline-start" />
143
+ Creating account
144
+ </>
145
+ ) : (
146
+ "Signup"
147
+ )}
148
+ </Button>
149
+ <FieldDescription className="text-center">
150
+ Already have an account? <Link href="/sign-in">Login</Link>
151
+ </FieldDescription>
152
+ </Field>
153
+ </FieldGroup>
140
154
  </form>
141
155
  </CardContent>
142
- <CardFooter className="justify-center">
143
- <p className="text-sm text-slate-600">
144
- Already have an account?{" "}
145
- <Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-in">
146
- Login
147
- </Link>
148
- </p>
149
- </CardFooter>
150
156
  </Card>
151
157
  );
152
158
  }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+
5
+ import { authClient } from "@/lib/auth-client";
6
+ import { getAuthErrorMessage, type SocialProvider } from "@/lib/auth-schema";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Field, FieldError, FieldSeparator } from "@/components/ui/field";
9
+ import { Spinner } from "@/components/ui/spinner";
10
+
11
+ const PROVIDER_LABELS: Record<SocialProvider, string> = {
12
+ github: "GitHub",
13
+ google: "Google",
14
+ };
15
+
16
+ // One button per provider turned on in lib/auth.ts; renders nothing when
17
+ // GitHub and Google sign-in are both off
18
+ export function SocialButtons({ providers }: { providers: SocialProvider[] }) {
19
+ const [isPending, startTransition] = useTransition();
20
+ // The provider whose button was clicked, for its "Redirecting" label
21
+ const [chosen, setChosen] = useState<SocialProvider | null>(null);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ if (providers.length === 0) {
25
+ return null;
26
+ }
27
+
28
+ function signIn(provider: SocialProvider) {
29
+ setError(null);
30
+ setChosen(provider);
31
+ startTransition(async () => {
32
+ // Redirects to the provider; the user comes back to the dashboard
33
+ const result = await authClient.signIn.social({
34
+ provider,
35
+ callbackURL: "/dashboard",
36
+ errorCallbackURL: "/sign-in",
37
+ });
38
+
39
+ if (result.error) {
40
+ setError(getAuthErrorMessage(result.error));
41
+ }
42
+ });
43
+ }
44
+
45
+ return (
46
+ <>
47
+ <Field data-invalid={Boolean(error)}>
48
+ {providers.map((provider) => (
49
+ <Button
50
+ key={provider}
51
+ type="button"
52
+ variant="outline"
53
+ disabled={isPending}
54
+ onClick={() => signIn(provider)}
55
+ >
56
+ {isPending && chosen === provider ? (
57
+ <>
58
+ <Spinner data-icon="inline-start" />
59
+ Redirecting
60
+ </>
61
+ ) : (
62
+ `Continue with ${PROVIDER_LABELS[provider]}`
63
+ )}
64
+ </Button>
65
+ ))}
66
+ {error ? <FieldError>{error}</FieldError> : null}
67
+ </Field>
68
+ <FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
69
+ Or continue with email
70
+ </FieldSeparator>
71
+ </>
72
+ );
73
+ }