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,149 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { useState, useTransition } from "react";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { CircleAlert } from "lucide-react";
5
+ import { useForm } from "react-hook-form";
6
+
7
+ import { authClient } from "~/lib/auth-client";
8
+ import {
9
+ getAuthErrorMessage,
10
+ resetPasswordSchema,
11
+ type ResetPasswordValues,
12
+ } from "~/lib/auth-schema";
13
+ import { Alert, AlertDescription } from "~/components/ui/alert";
14
+ import { Button } from "~/components/ui/button";
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardFooter,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from "~/components/ui/card";
23
+ import {
24
+ Field,
25
+ FieldError,
26
+ FieldGroup,
27
+ FieldLabel,
28
+ } from "~/components/ui/field";
29
+ import { Input } from "~/components/ui/input";
30
+ import { Spinner } from "~/components/ui/spinner";
31
+
32
+ // token comes from the link in the reset email; null when the link was
33
+ // invalid or has expired
34
+ export function ResetPasswordForm({ token }: { token: string | null }) {
35
+ const [isPending, startTransition] = useTransition();
36
+ const [authError, setAuthError] = useState<string | null>(null);
37
+ const [isDone, setIsDone] = useState(false);
38
+ const form = useForm<ResetPasswordValues>({
39
+ resolver: zodResolver(resetPasswordSchema),
40
+ defaultValues: { password: "", confirmPassword: "" },
41
+ });
42
+
43
+ const onSubmit = form.handleSubmit((values) => {
44
+ if (!token) return;
45
+ setAuthError(null);
46
+
47
+ startTransition(async () => {
48
+ const result = await authClient.resetPassword({
49
+ newPassword: values.password,
50
+ token,
51
+ });
52
+
53
+ if (result.error) {
54
+ setAuthError(getAuthErrorMessage(result.error));
55
+ return;
56
+ }
57
+
58
+ setIsDone(true);
59
+ });
60
+ });
61
+
62
+ if (!token || isDone) {
63
+ return (
64
+ <Card>
65
+ <CardHeader>
66
+ <CardTitle>{isDone ? "Password changed" : "Link expired"}</CardTitle>
67
+ <CardDescription>
68
+ {isDone
69
+ ? "You were signed out on every device. Log in with your new password."
70
+ : "This reset link is invalid or has expired. Ask for a new one."}
71
+ </CardDescription>
72
+ </CardHeader>
73
+ <CardFooter>
74
+ <Button
75
+ className="w-full"
76
+ nativeButton={false}
77
+ render={<Link to={isDone ? "/sign-in" : "/forgot-password"} />}
78
+ >
79
+ {isDone ? "Go to login" : "Send a new link"}
80
+ </Button>
81
+ </CardFooter>
82
+ </Card>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle>Choose a new password</CardTitle>
90
+ <CardDescription>
91
+ You will be signed out on every device after the change.
92
+ </CardDescription>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <form onSubmit={onSubmit} noValidate>
96
+ <FieldGroup>
97
+ <Field data-invalid={Boolean(form.formState.errors.password)}>
98
+ <FieldLabel htmlFor="password">New password</FieldLabel>
99
+ <Input
100
+ id="password"
101
+ type="password"
102
+ autoComplete="new-password"
103
+ aria-invalid={Boolean(form.formState.errors.password)}
104
+ {...form.register("password")}
105
+ />
106
+ <FieldError errors={[form.formState.errors.password]} />
107
+ </Field>
108
+
109
+ <Field
110
+ data-invalid={Boolean(form.formState.errors.confirmPassword)}
111
+ >
112
+ <FieldLabel htmlFor="confirmPassword">
113
+ Confirm new password
114
+ </FieldLabel>
115
+ <Input
116
+ id="confirmPassword"
117
+ type="password"
118
+ autoComplete="new-password"
119
+ aria-invalid={Boolean(form.formState.errors.confirmPassword)}
120
+ {...form.register("confirmPassword")}
121
+ />
122
+ <FieldError errors={[form.formState.errors.confirmPassword]} />
123
+ </Field>
124
+
125
+ {authError ? (
126
+ <Alert variant="destructive">
127
+ <CircleAlert />
128
+ <AlertDescription>{authError}</AlertDescription>
129
+ </Alert>
130
+ ) : null}
131
+
132
+ <Field>
133
+ <Button type="submit" disabled={isPending}>
134
+ {isPending ? (
135
+ <>
136
+ <Spinner data-icon="inline-start" />
137
+ Saving
138
+ </>
139
+ ) : (
140
+ "Save new password"
141
+ )}
142
+ </Button>
143
+ </Field>
144
+ </FieldGroup>
145
+ </form>
146
+ </CardContent>
147
+ </Card>
148
+ );
149
+ }
@@ -1,7 +1,7 @@
1
1
  import { Link, useNavigate } from "@tanstack/react-router";
2
2
  import { useState, useTransition } from "react";
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
- import { LoaderCircle } from "lucide-react";
4
+ import { CircleAlert } from "lucide-react";
5
5
  import { useForm } from "react-hook-form";
6
6
 
7
7
  import { authClient } from "~/lib/auth-client";
@@ -9,13 +9,14 @@ import {
9
9
  getAuthErrorMessage,
10
10
  signInSchema,
11
11
  type SignInValues,
12
+ type SocialProvider,
12
13
  } from "~/lib/auth-schema";
14
+ import { Alert, AlertDescription } from "~/components/ui/alert";
13
15
  import { Button } from "~/components/ui/button";
14
16
  import {
15
17
  Card,
16
18
  CardContent,
17
19
  CardDescription,
18
- CardFooter,
19
20
  CardHeader,
20
21
  CardTitle,
21
22
  } from "~/components/ui/card";
@@ -27,8 +28,11 @@ import {
27
28
  FieldLabel,
28
29
  } from "~/components/ui/field";
29
30
  import { Input } from "~/components/ui/input";
31
+ import { Spinner } from "~/components/ui/spinner";
30
32
 
31
- export function SignInForm() {
33
+ import { SocialButtons } from "./social-buttons";
34
+
35
+ export function SignInForm({ providers }: { providers: SocialProvider[] }) {
32
36
  const navigate = useNavigate();
33
37
  const [isPending, startTransition] = useTransition();
34
38
  const [authError, setAuthError] = useState<string | null>(null);
@@ -59,16 +63,18 @@ export function SignInForm() {
59
63
  });
60
64
 
61
65
  return (
62
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
63
- <CardHeader className="space-y-2">
66
+ <Card>
67
+ <CardHeader>
64
68
  <CardTitle>Login to your account</CardTitle>
65
69
  <CardDescription>
66
70
  Sign in with your email and password to continue to your dashboard.
67
71
  </CardDescription>
68
72
  </CardHeader>
69
73
  <CardContent>
70
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
74
+ <form onSubmit={onSubmit} noValidate>
71
75
  <FieldGroup>
76
+ <SocialButtons providers={providers} />
77
+
72
78
  <Field data-invalid={Boolean(form.formState.errors.email)}>
73
79
  <FieldLabel htmlFor="email">Email</FieldLabel>
74
80
  <Input
@@ -86,7 +92,15 @@ export function SignInForm() {
86
92
  </Field>
87
93
 
88
94
  <Field data-invalid={Boolean(form.formState.errors.password)}>
89
- <FieldLabel htmlFor="password">Password</FieldLabel>
95
+ <div className="flex items-center">
96
+ <FieldLabel htmlFor="password">Password</FieldLabel>
97
+ <Link
98
+ className="ml-auto text-sm underline-offset-4 hover:underline"
99
+ to="/forgot-password"
100
+ >
101
+ Forgot password?
102
+ </Link>
103
+ </div>
90
104
  <Input
91
105
  id="password"
92
106
  type="password"
@@ -97,37 +111,32 @@ export function SignInForm() {
97
111
  />
98
112
  <FieldError errors={[form.formState.errors.password]} />
99
113
  </Field>
100
- </FieldGroup>
101
114
 
102
- {authError ? (
103
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
104
- {authError}
105
- </div>
106
- ) : null}
115
+ {authError ? (
116
+ <Alert variant="destructive">
117
+ <CircleAlert />
118
+ <AlertDescription>{authError}</AlertDescription>
119
+ </Alert>
120
+ ) : null}
107
121
 
108
- <Button className="w-full" type="submit" disabled={isPending}>
109
- {isPending ? (
110
- <>
111
- <LoaderCircle className="mr-2 size-4 animate-spin" />
112
- Signing in
113
- </>
114
- ) : (
115
- "Login"
116
- )}
117
- </Button>
122
+ <Field>
123
+ <Button type="submit" disabled={isPending}>
124
+ {isPending ? (
125
+ <>
126
+ <Spinner data-icon="inline-start" />
127
+ Signing in
128
+ </>
129
+ ) : (
130
+ "Login"
131
+ )}
132
+ </Button>
133
+ <FieldDescription className="text-center">
134
+ Need an account? <Link to="/sign-up">Signup</Link>
135
+ </FieldDescription>
136
+ </Field>
137
+ </FieldGroup>
118
138
  </form>
119
139
  </CardContent>
120
- <CardFooter className="justify-center">
121
- <p className="text-sm text-slate-600">
122
- Need an account?{" "}
123
- <Link
124
- className="font-medium text-sky-700 hover:text-sky-800"
125
- to="/sign-up"
126
- >
127
- Signup
128
- </Link>
129
- </p>
130
- </CardFooter>
131
140
  </Card>
132
141
  );
133
- }
142
+ }
@@ -1,9 +1,10 @@
1
1
  import { useNavigate } from "@tanstack/react-router";
2
2
  import { useState, useTransition } from "react";
3
- import { LoaderCircle, LogOut } from "lucide-react";
3
+ import { LogOut } from "lucide-react";
4
4
 
5
5
  import { authClient } from "~/lib/auth-client";
6
6
  import { Button } from "~/components/ui/button";
7
+ import { Spinner } from "~/components/ui/spinner";
7
8
 
8
9
  export function SignOutButton() {
9
10
  const navigate = useNavigate();
@@ -11,7 +12,7 @@ export function SignOutButton() {
11
12
  const [error, setError] = useState<string | null>(null);
12
13
 
13
14
  return (
14
- <div className="space-y-2">
15
+ <div className="flex flex-col items-start gap-2">
15
16
  <Button
16
17
  variant="outline"
17
18
  onClick={() => {
@@ -31,17 +32,17 @@ export function SignOutButton() {
31
32
  >
32
33
  {isPending ? (
33
34
  <>
34
- <LoaderCircle className="mr-2 size-4 animate-spin" />
35
+ <Spinner data-icon="inline-start" />
35
36
  Signing out
36
37
  </>
37
38
  ) : (
38
39
  <>
39
- <LogOut className="mr-2 size-4" />
40
+ <LogOut data-icon="inline-start" />
40
41
  Sign out
41
42
  </>
42
43
  )}
43
44
  </Button>
44
- {error ? <p className="text-sm text-rose-600">{error}</p> : null}
45
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
45
46
  </div>
46
47
  );
47
- }
48
+ }
@@ -1,7 +1,7 @@
1
1
  import { Link, useNavigate } from "@tanstack/react-router";
2
2
  import { useState, useTransition } from "react";
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
- import { LoaderCircle } from "lucide-react";
4
+ import { CircleAlert } from "lucide-react";
5
5
  import { useForm } from "react-hook-form";
6
6
 
7
7
  import { authClient } from "~/lib/auth-client";
@@ -9,13 +9,14 @@ import {
9
9
  getAuthErrorMessage,
10
10
  signUpSchema,
11
11
  type SignUpValues,
12
+ type SocialProvider,
12
13
  } from "~/lib/auth-schema";
14
+ import { Alert, AlertDescription } from "~/components/ui/alert";
13
15
  import { Button } from "~/components/ui/button";
14
16
  import {
15
17
  Card,
16
18
  CardContent,
17
19
  CardDescription,
18
- CardFooter,
19
20
  CardHeader,
20
21
  CardTitle,
21
22
  } from "~/components/ui/card";
@@ -27,8 +28,11 @@ import {
27
28
  FieldLabel,
28
29
  } from "~/components/ui/field";
29
30
  import { Input } from "~/components/ui/input";
31
+ import { Spinner } from "~/components/ui/spinner";
30
32
 
31
- export function SignUpForm() {
33
+ import { SocialButtons } from "./social-buttons";
34
+
35
+ export function SignUpForm({ providers }: { providers: SocialProvider[] }) {
32
36
  const navigate = useNavigate();
33
37
  const [isPending, startTransition] = useTransition();
34
38
  const [authError, setAuthError] = useState<string | null>(null);
@@ -49,6 +53,8 @@ export function SignUpForm() {
49
53
  name: values.name,
50
54
  email: values.email,
51
55
  password: values.password,
56
+ // Where the link in the verification email leads after confirming
57
+ callbackURL: "/dashboard",
52
58
  });
53
59
 
54
60
  if (result.error) {
@@ -61,8 +67,8 @@ export function SignUpForm() {
61
67
  });
62
68
 
63
69
  return (
64
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
65
- <CardHeader className="space-y-2">
70
+ <Card>
71
+ <CardHeader>
66
72
  <CardTitle>Create your account</CardTitle>
67
73
  <CardDescription>
68
74
  Start with a simple email and password sign-up flow powered by Better
@@ -70,8 +76,10 @@ export function SignUpForm() {
70
76
  </CardDescription>
71
77
  </CardHeader>
72
78
  <CardContent>
73
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
79
+ <form onSubmit={onSubmit} noValidate>
74
80
  <FieldGroup>
81
+ <SocialButtons providers={providers} />
82
+
75
83
  <Field data-invalid={Boolean(form.formState.errors.name)}>
76
84
  <FieldLabel htmlFor="name">Full name</FieldLabel>
77
85
  <Input
@@ -115,37 +123,32 @@ export function SignUpForm() {
115
123
  </FieldDescription>
116
124
  <FieldError errors={[form.formState.errors.password]} />
117
125
  </Field>
118
- </FieldGroup>
119
126
 
120
- {authError ? (
121
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
122
- {authError}
123
- </div>
124
- ) : null}
127
+ {authError ? (
128
+ <Alert variant="destructive">
129
+ <CircleAlert />
130
+ <AlertDescription>{authError}</AlertDescription>
131
+ </Alert>
132
+ ) : null}
125
133
 
126
- <Button className="w-full" type="submit" disabled={isPending}>
127
- {isPending ? (
128
- <>
129
- <LoaderCircle className="mr-2 size-4 animate-spin" />
130
- Creating account
131
- </>
132
- ) : (
133
- "Signup"
134
- )}
135
- </Button>
134
+ <Field>
135
+ <Button type="submit" disabled={isPending}>
136
+ {isPending ? (
137
+ <>
138
+ <Spinner data-icon="inline-start" />
139
+ Creating account
140
+ </>
141
+ ) : (
142
+ "Signup"
143
+ )}
144
+ </Button>
145
+ <FieldDescription className="text-center">
146
+ Already have an account? <Link to="/sign-in">Login</Link>
147
+ </FieldDescription>
148
+ </Field>
149
+ </FieldGroup>
136
150
  </form>
137
151
  </CardContent>
138
- <CardFooter className="justify-center">
139
- <p className="text-sm text-slate-600">
140
- Already have an account?{" "}
141
- <Link
142
- className="font-medium text-sky-700 hover:text-sky-800"
143
- to="/sign-in"
144
- >
145
- Login
146
- </Link>
147
- </p>
148
- </CardFooter>
149
152
  </Card>
150
153
  );
151
- }
154
+ }
@@ -0,0 +1,71 @@
1
+ import { useState, useTransition } from "react";
2
+
3
+ import { authClient } from "~/lib/auth-client";
4
+ import { getAuthErrorMessage, type SocialProvider } from "~/lib/auth-schema";
5
+ import { Button } from "~/components/ui/button";
6
+ import { Field, FieldError, FieldSeparator } from "~/components/ui/field";
7
+ import { Spinner } from "~/components/ui/spinner";
8
+
9
+ const PROVIDER_LABELS: Record<SocialProvider, string> = {
10
+ github: "GitHub",
11
+ google: "Google",
12
+ };
13
+
14
+ // One button per provider turned on in lib/auth.ts; renders nothing when
15
+ // GitHub and Google sign-in are both off
16
+ export function SocialButtons({ providers }: { providers: SocialProvider[] }) {
17
+ const [isPending, startTransition] = useTransition();
18
+ // The provider whose button was clicked, for its "Redirecting" label
19
+ const [chosen, setChosen] = useState<SocialProvider | null>(null);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ if (providers.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ function signIn(provider: SocialProvider) {
27
+ setError(null);
28
+ setChosen(provider);
29
+ startTransition(async () => {
30
+ // Redirects to the provider; the user comes back to the dashboard
31
+ const result = await authClient.signIn.social({
32
+ provider,
33
+ callbackURL: "/dashboard",
34
+ errorCallbackURL: "/sign-in",
35
+ });
36
+
37
+ if (result.error) {
38
+ setError(getAuthErrorMessage(result.error));
39
+ }
40
+ });
41
+ }
42
+
43
+ return (
44
+ <>
45
+ <Field data-invalid={Boolean(error)}>
46
+ {providers.map((provider) => (
47
+ <Button
48
+ key={provider}
49
+ type="button"
50
+ variant="outline"
51
+ disabled={isPending}
52
+ onClick={() => signIn(provider)}
53
+ >
54
+ {isPending && chosen === provider ? (
55
+ <>
56
+ <Spinner data-icon="inline-start" />
57
+ Redirecting
58
+ </>
59
+ ) : (
60
+ `Continue with ${PROVIDER_LABELS[provider]}`
61
+ )}
62
+ </Button>
63
+ ))}
64
+ {error ? <FieldError>{error}</FieldError> : null}
65
+ </Field>
66
+ <FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
67
+ Or continue with email
68
+ </FieldSeparator>
69
+ </>
70
+ );
71
+ }
@@ -1,3 +1,3 @@
1
- import { createAuthClient } from "better-auth/react";
2
-
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
3
  export const authClient = createAuthClient();
@@ -1,12 +1,18 @@
1
- import { createServerFn } from "@tanstack/react-start";
2
- import { getRequestHeaders } from "@tanstack/react-start/server";
3
-
4
- import { auth } from "./auth";
5
-
6
- export const getSession = createServerFn({ method: "GET" }).handler(async () => {
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { getRequestHeaders } from "@tanstack/react-start/server";
3
+
4
+ import { auth, socialProviders } from "./auth";
5
+
6
+ export const getSession = createServerFn({ method: "GET" }).handler(async () => {
7
7
  const session = await auth.api.getSession({
8
8
  headers: getRequestHeaders(),
9
9
  });
10
-
11
- return session;
10
+
11
+ return session;
12
12
  });
13
+
14
+ // GitHub and Google, when their keys are set; read on the server, where the
15
+ // environment variables are
16
+ export const getSocialProviders = createServerFn({ method: "GET" }).handler(
17
+ () => socialProviders
18
+ );
@@ -1,8 +1,13 @@
1
1
  import { z } from "zod";
2
2
 
3
+ const emailSchema = z.email("Enter a valid email address.");
4
+ const passwordSchema = z
5
+ .string()
6
+ .min(8, "Password must be at least 8 characters.");
7
+
3
8
  export const signInSchema = z.object({
4
- email: z.email("Enter a valid email address."),
5
- password: z.string().min(8, "Password must be at least 8 characters."),
9
+ email: emailSchema,
10
+ password: passwordSchema,
6
11
  });
7
12
 
8
13
  export const signUpSchema = signInSchema.extend({
@@ -13,8 +18,28 @@ export const signUpSchema = signInSchema.extend({
13
18
  .max(50, "Name must be 50 characters or fewer."),
14
19
  });
15
20
 
21
+ export const forgotPasswordSchema = z.object({ email: emailSchema });
22
+
23
+ export const resetPasswordSchema = z
24
+ .object({ password: passwordSchema, confirmPassword: z.string() })
25
+ .refine((values) => values.password === values.confirmPassword, {
26
+ message: "Passwords do not match.",
27
+ path: ["confirmPassword"],
28
+ });
29
+
30
+ export const changePasswordSchema = z.object({
31
+ currentPassword: z.string().min(1, "Enter your current password."),
32
+ newPassword: passwordSchema,
33
+ });
34
+
16
35
  export type SignInValues = z.infer<typeof signInSchema>;
17
36
  export type SignUpValues = z.infer<typeof signUpSchema>;
37
+ export type ForgotPasswordValues = z.infer<typeof forgotPasswordSchema>;
38
+ export type ResetPasswordValues = z.infer<typeof resetPasswordSchema>;
39
+ export type ChangePasswordValues = z.infer<typeof changePasswordSchema>;
40
+
41
+ // Sign-in providers the auth server can turn on (see auth.ts)
42
+ export type SocialProvider = "github" | "google";
18
43
 
19
44
  export function getAuthErrorMessage(error: unknown) {
20
45
  if (!error) {