better-ts-stack 2.1.2 → 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 (117) hide show
  1. package/README.md +16 -8
  2. package/dist/args.js +2 -1
  3. package/dist/builder/buildPlanner.js +7 -1
  4. package/dist/builder/configGenerator.js +28 -3
  5. package/dist/builder/index.js +2 -1
  6. package/dist/builder/readmeGenerator.js +2 -2
  7. package/dist/output/nextSteps.js +2 -4
  8. package/dist/prompts/stack.js +3 -5
  9. package/dist/types/index.js +1 -1
  10. package/package.json +1 -1
  11. package/templates/backend/express/src/env.ts.hbs +13 -2
  12. package/templates/backend/express/src/index.ts.hbs +31 -2
  13. package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
  14. package/templates/backend/nestjs/src/env.ts.hbs +13 -2
  15. package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
  16. package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
  17. package/templates/frontend/nextjs/app/globals.css +119 -79
  18. package/templates/frontend/nextjs/app/layout.tsx +13 -11
  19. package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
  20. package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
  21. package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
  22. package/templates/frontend/nextjs/components/ui/button.tsx +33 -27
  23. package/templates/frontend/nextjs/components/ui/card.tsx +76 -34
  24. package/templates/frontend/nextjs/components/ui/field.tsx +203 -32
  25. package/templates/frontend/nextjs/components/ui/input.tsx +8 -7
  26. package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
  27. package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
  28. package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
  29. package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
  30. package/templates/frontend/nextjs/components.json +11 -5
  31. package/templates/frontend/nextjs/config.json +4 -3
  32. package/templates/frontend/nextjs/lib/utils.ts +1 -6
  33. package/templates/frontend/tanstack/components.json +12 -5
  34. package/templates/frontend/tanstack/config.json +5 -2
  35. package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
  36. package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
  37. package/templates/frontend/tanstack/src/components/ui/button.tsx +33 -27
  38. package/templates/frontend/tanstack/src/components/ui/card.tsx +76 -34
  39. package/templates/frontend/tanstack/src/components/ui/field.tsx +201 -32
  40. package/templates/frontend/tanstack/src/components/ui/input.tsx +8 -7
  41. package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
  42. package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
  43. package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
  44. package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
  45. package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
  46. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
  47. package/templates/frontend/tanstack/src/styles/app.css +100 -55
  48. package/templates/modules/agents/AGENTS.md.hbs +29 -21
  49. package/templates/modules/auth/express/config.json +12 -11
  50. package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
  51. package/templates/modules/auth/express/src/lib/email.ts +37 -0
  52. package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
  53. package/templates/modules/auth/express/src/routes/me.ts +13 -0
  54. package/templates/modules/auth/nestjs/config.json +13 -11
  55. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
  56. package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
  57. package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
  58. package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
  59. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
  60. package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
  61. package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
  62. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
  63. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
  64. package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
  65. package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
  66. package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
  67. package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
  68. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
  69. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
  70. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
  71. package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
  72. package/templates/modules/auth/nextjs/config.json +8 -2
  73. package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
  74. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
  75. package/templates/modules/auth/nextjs/lib/email.ts +43 -0
  76. package/templates/modules/auth/nextjs/lib/session.ts +12 -0
  77. package/templates/modules/auth/tanstack/config.json +9 -3
  78. package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
  79. package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
  80. package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
  81. package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
  82. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
  83. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
  84. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
  85. package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
  86. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +7 -1
  87. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
  88. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +61 -1
  89. package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
  90. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
  91. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +40 -32
  92. package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
  93. package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
  94. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +19 -12
  95. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +21 -14
  96. package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
  97. package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
  98. package/templates/modules/mongoose/nestjs/config.json +2 -1
  99. package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
  100. package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
  101. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
  102. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
  103. package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
  104. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
  105. package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
  106. package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
  107. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
  108. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
  109. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
  110. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
  111. package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
  112. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
  113. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
  114. package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
  115. package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
  116. package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
  117. package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
@@ -0,0 +1,123 @@
1
+ import { useState, useTransition } from "react";
2
+ import { zodResolver } from "@hookform/resolvers/zod";
3
+ import { CircleAlert, CircleCheck } from "lucide-react";
4
+ import { useForm } from "react-hook-form";
5
+
6
+ import { authClient } from "~/lib/auth-client";
7
+ import {
8
+ changePasswordSchema,
9
+ getAuthErrorMessage,
10
+ type ChangePasswordValues,
11
+ } from "~/lib/auth-schema";
12
+ import { Alert, AlertDescription } from "~/components/ui/alert";
13
+ import { Button } from "~/components/ui/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from "~/components/ui/card";
21
+ import {
22
+ Field,
23
+ FieldError,
24
+ FieldGroup,
25
+ FieldLabel,
26
+ } from "~/components/ui/field";
27
+ import { Input } from "~/components/ui/input";
28
+ import { Spinner } from "~/components/ui/spinner";
29
+
30
+ export function ChangePasswordForm() {
31
+ const [isPending, startTransition] = useTransition();
32
+ const [message, setMessage] = useState<{
33
+ text: string;
34
+ isError: boolean;
35
+ } | null>(null);
36
+ const form = useForm<ChangePasswordValues>({
37
+ resolver: zodResolver(changePasswordSchema),
38
+ defaultValues: { currentPassword: "", newPassword: "" },
39
+ });
40
+
41
+ const onSubmit = form.handleSubmit((values) => {
42
+ setMessage(null);
43
+
44
+ startTransition(async () => {
45
+ // Other devices are signed out; this one stays signed in
46
+ const result = await authClient.changePassword({
47
+ currentPassword: values.currentPassword,
48
+ newPassword: values.newPassword,
49
+ revokeOtherSessions: true,
50
+ });
51
+
52
+ if (result.error) {
53
+ setMessage({ text: getAuthErrorMessage(result.error), isError: true });
54
+ return;
55
+ }
56
+
57
+ form.reset();
58
+ setMessage({ text: "Password changed.", isError: false });
59
+ });
60
+ });
61
+
62
+ return (
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>Change password</CardTitle>
66
+ <CardDescription>
67
+ Other devices are signed out after the change.
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent>
71
+ <form onSubmit={onSubmit} noValidate>
72
+ <FieldGroup>
73
+ <Field
74
+ data-invalid={Boolean(form.formState.errors.currentPassword)}
75
+ >
76
+ <FieldLabel htmlFor="currentPassword">Current password</FieldLabel>
77
+ <Input
78
+ id="currentPassword"
79
+ type="password"
80
+ autoComplete="current-password"
81
+ aria-invalid={Boolean(form.formState.errors.currentPassword)}
82
+ {...form.register("currentPassword")}
83
+ />
84
+ <FieldError errors={[form.formState.errors.currentPassword]} />
85
+ </Field>
86
+
87
+ <Field data-invalid={Boolean(form.formState.errors.newPassword)}>
88
+ <FieldLabel htmlFor="newPassword">New password</FieldLabel>
89
+ <Input
90
+ id="newPassword"
91
+ type="password"
92
+ autoComplete="new-password"
93
+ aria-invalid={Boolean(form.formState.errors.newPassword)}
94
+ {...form.register("newPassword")}
95
+ />
96
+ <FieldError errors={[form.formState.errors.newPassword]} />
97
+ </Field>
98
+
99
+ {message ? (
100
+ <Alert variant={message.isError ? "destructive" : "default"}>
101
+ {message.isError ? <CircleAlert /> : <CircleCheck />}
102
+ <AlertDescription>{message.text}</AlertDescription>
103
+ </Alert>
104
+ ) : null}
105
+
106
+ <Field orientation="horizontal">
107
+ <Button type="submit" disabled={isPending}>
108
+ {isPending ? (
109
+ <>
110
+ <Spinner data-icon="inline-start" />
111
+ Saving
112
+ </>
113
+ ) : (
114
+ "Change password"
115
+ )}
116
+ </Button>
117
+ </Field>
118
+ </FieldGroup>
119
+ </form>
120
+ </CardContent>
121
+ </Card>
122
+ );
123
+ }
@@ -0,0 +1,89 @@
1
+ import { useState, useTransition } from "react";
2
+ import { BadgeCheck, MailWarning } from "lucide-react";
3
+
4
+ import { authClient } from "~/lib/auth-client";
5
+ import { getAuthErrorMessage } from "~/lib/auth-schema";
6
+ import { Badge } from "~/components/ui/badge";
7
+ import { Button } from "~/components/ui/button";
8
+ import {
9
+ Card,
10
+ CardAction,
11
+ CardContent,
12
+ CardDescription,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from "~/components/ui/card";
16
+ import { Spinner } from "~/components/ui/spinner";
17
+
18
+ // Shows whether the email address is verified, with a button to send the
19
+ // verification email again
20
+ export function EmailVerificationCard({
21
+ email,
22
+ emailVerified,
23
+ }: {
24
+ email: string;
25
+ emailVerified: boolean;
26
+ }) {
27
+ const [isPending, startTransition] = useTransition();
28
+ const [message, setMessage] = useState<string | null>(null);
29
+
30
+ function resend() {
31
+ setMessage(null);
32
+ startTransition(async () => {
33
+ const result = await authClient.sendVerificationEmail({
34
+ email,
35
+ callbackURL: "/dashboard",
36
+ });
37
+ setMessage(
38
+ result.error
39
+ ? getAuthErrorMessage(result.error)
40
+ : "Verification email sent. Without an email provider set up, the link is printed in the server terminal."
41
+ );
42
+ });
43
+ }
44
+
45
+ return (
46
+ <Card>
47
+ <CardHeader>
48
+ <CardTitle>
49
+ {emailVerified ? "Email verified" : "Verify your email"}
50
+ </CardTitle>
51
+ <CardDescription>
52
+ {emailVerified
53
+ ? `${email} is confirmed.`
54
+ : `We sent a link to ${email}. Open it to confirm the address.`}
55
+ </CardDescription>
56
+ <CardAction>
57
+ {emailVerified ? (
58
+ <Badge variant="secondary">
59
+ <BadgeCheck data-icon="inline-start" />
60
+ Verified
61
+ </Badge>
62
+ ) : (
63
+ <Badge variant="outline">
64
+ <MailWarning data-icon="inline-start" />
65
+ Pending
66
+ </Badge>
67
+ )}
68
+ </CardAction>
69
+ </CardHeader>
70
+ {emailVerified ? null : (
71
+ <CardContent className="flex flex-col items-start gap-3">
72
+ <Button variant="outline" onClick={resend} disabled={isPending}>
73
+ {isPending ? (
74
+ <>
75
+ <Spinner data-icon="inline-start" />
76
+ Sending
77
+ </>
78
+ ) : (
79
+ "Send the email again"
80
+ )}
81
+ </Button>
82
+ {message ? (
83
+ <p className="text-sm text-muted-foreground">{message}</p>
84
+ ) : null}
85
+ </CardContent>
86
+ )}
87
+ </Card>
88
+ );
89
+ }
@@ -0,0 +1,131 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { useState, useTransition } from "react";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { CircleAlert, MailCheck } from "lucide-react";
5
+ import { useForm } from "react-hook-form";
6
+
7
+ import { authClient } from "~/lib/auth-client";
8
+ import {
9
+ forgotPasswordSchema,
10
+ getAuthErrorMessage,
11
+ type ForgotPasswordValues,
12
+ } from "~/lib/auth-schema";
13
+ import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
14
+ import { Button } from "~/components/ui/button";
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from "~/components/ui/card";
22
+ import {
23
+ Field,
24
+ FieldDescription,
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
+ export function ForgotPasswordForm() {
33
+ const [isPending, startTransition] = useTransition();
34
+ const [authError, setAuthError] = useState<string | null>(null);
35
+ const [isSent, setIsSent] = useState(false);
36
+ const form = useForm<ForgotPasswordValues>({
37
+ resolver: zodResolver(forgotPasswordSchema),
38
+ defaultValues: { email: "" },
39
+ });
40
+
41
+ const onSubmit = form.handleSubmit((values) => {
42
+ setAuthError(null);
43
+
44
+ startTransition(async () => {
45
+ // The link in the email opens /reset-password with a one-time token
46
+ const result = await authClient.requestPasswordReset({
47
+ email: values.email,
48
+ redirectTo: "/reset-password",
49
+ });
50
+
51
+ if (result.error) {
52
+ setAuthError(getAuthErrorMessage(result.error));
53
+ return;
54
+ }
55
+
56
+ setIsSent(true);
57
+ });
58
+ });
59
+
60
+ return (
61
+ <Card>
62
+ <CardHeader>
63
+ <CardTitle>Forgot your password?</CardTitle>
64
+ <CardDescription>
65
+ Enter your email and we will send you a link to choose a new one.
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ {isSent ? (
70
+ <FieldGroup>
71
+ <Alert>
72
+ <MailCheck />
73
+ <AlertTitle>Check your email</AlertTitle>
74
+ <AlertDescription>
75
+ If an account exists for that email, a reset link is on its
76
+ way. Without an email provider set up, the link is printed in
77
+ the server terminal.
78
+ </AlertDescription>
79
+ </Alert>
80
+ <FieldDescription className="text-center">
81
+ <Link to="/sign-in">Back to login</Link>
82
+ </FieldDescription>
83
+ </FieldGroup>
84
+ ) : (
85
+ <form onSubmit={onSubmit} noValidate>
86
+ <FieldGroup>
87
+ <Field data-invalid={Boolean(form.formState.errors.email)}>
88
+ <FieldLabel htmlFor="email">Email</FieldLabel>
89
+ <Input
90
+ id="email"
91
+ type="email"
92
+ autoComplete="email"
93
+ aria-invalid={Boolean(form.formState.errors.email)}
94
+ placeholder="you@example.com"
95
+ {...form.register("email")}
96
+ />
97
+ <FieldDescription>
98
+ Use the email you registered with.
99
+ </FieldDescription>
100
+ <FieldError errors={[form.formState.errors.email]} />
101
+ </Field>
102
+
103
+ {authError ? (
104
+ <Alert variant="destructive">
105
+ <CircleAlert />
106
+ <AlertDescription>{authError}</AlertDescription>
107
+ </Alert>
108
+ ) : null}
109
+
110
+ <Field>
111
+ <Button type="submit" disabled={isPending}>
112
+ {isPending ? (
113
+ <>
114
+ <Spinner data-icon="inline-start" />
115
+ Sending link
116
+ </>
117
+ ) : (
118
+ "Send reset link"
119
+ )}
120
+ </Button>
121
+ <FieldDescription className="text-center">
122
+ Remembered it? <Link to="/sign-in">Back to login</Link>
123
+ </FieldDescription>
124
+ </Field>
125
+ </FieldGroup>
126
+ </form>
127
+ )}
128
+ </CardContent>
129
+ </Card>
130
+ );
131
+ }
@@ -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
+ }