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.
- package/LICENSE +21 -21
- package/README.md +16 -8
- package/dist/args.js +7 -0
- package/dist/builder/buildPlanner.js +7 -1
- package/dist/builder/configGenerator.js +28 -3
- package/dist/builder/index.js +2 -1
- package/dist/builder/moduleSelector.js +6 -1
- package/dist/builder/readmeGenerator.js +8 -2
- package/dist/builder/templateContext.js +1 -0
- package/dist/output/nextSteps.js +2 -4
- package/dist/prompts/stack.js +6 -1
- package/dist/types/index.js +2 -0
- package/dist/ui/messages.js +2 -0
- package/package.json +71 -71
- package/templates/backend/express/.prettierignore +52 -52
- package/templates/backend/express/.prettierrc +16 -16
- package/templates/backend/express/gitignore +3 -1
- package/templates/backend/express/src/env.ts.hbs +13 -2
- package/templates/backend/express/src/index.ts.hbs +31 -2
- package/templates/backend/express/tsconfig.eslint.json +9 -9
- package/templates/backend/nestjs/.prettierignore +52 -52
- package/templates/backend/nestjs/gitignore +3 -1
- package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
- package/templates/backend/nestjs/src/env.ts.hbs +13 -2
- package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
- package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
- package/templates/backend/nestjs/tsconfig.eslint.json +9 -9
- package/templates/frontend/nextjs/app/globals.css +129 -89
- package/templates/frontend/nextjs/app/layout.tsx +36 -34
- package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
- package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
- package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
- package/templates/frontend/nextjs/components/ui/button.tsx +57 -51
- package/templates/frontend/nextjs/components/ui/card.tsx +102 -60
- package/templates/frontend/nextjs/components/ui/field.tsx +238 -67
- package/templates/frontend/nextjs/components/ui/input.tsx +19 -18
- package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
- package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
- package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
- package/templates/frontend/nextjs/components.json +11 -5
- package/templates/frontend/nextjs/config.json +36 -35
- package/templates/frontend/nextjs/eslint.config.mjs +11 -11
- package/templates/frontend/nextjs/lib/utils.ts +1 -6
- package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
- package/templates/frontend/nextjs/postcss.config.mjs +7 -7
- package/templates/frontend/nextjs/public/file.svg +1 -1
- package/templates/frontend/nextjs/public/globe.svg +1 -1
- package/templates/frontend/nextjs/public/window.svg +1 -1
- package/templates/frontend/tanstack/components.json +25 -18
- package/templates/frontend/tanstack/config.json +5 -2
- package/templates/frontend/tanstack/eslint.config.mjs +23 -23
- package/templates/frontend/tanstack/gitignore +39 -39
- package/templates/frontend/tanstack/public/favicon.svg +7 -7
- package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
- package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
- package/templates/frontend/tanstack/src/components/ui/button.tsx +57 -51
- package/templates/frontend/tanstack/src/components/ui/card.tsx +102 -60
- package/templates/frontend/tanstack/src/components/ui/field.tsx +236 -67
- package/templates/frontend/tanstack/src/components/ui/input.tsx +19 -18
- package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
- package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
- package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
- package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
- package/templates/frontend/tanstack/src/router.tsx +31 -31
- package/templates/frontend/tanstack/src/routes/__root.tsx +69 -69
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
- package/templates/frontend/tanstack/src/styles/app.css +100 -55
- package/templates/frontend/tanstack/tsconfig.json +22 -22
- package/templates/modules/agents/AGENTS.md.hbs +245 -0
- package/templates/modules/agents/CLAUDE.md +1 -0
- package/templates/modules/agents/config.json +10 -0
- package/templates/modules/auth/express/config.json +12 -11
- package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
- package/templates/modules/auth/express/src/lib/email.ts +37 -0
- package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
- package/templates/modules/auth/express/src/routes/me.ts +13 -0
- package/templates/modules/auth/nestjs/config.json +13 -11
- package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
- package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
- package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
- package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
- package/templates/modules/auth/nextjs/app/api/auth/[...all]/route.ts +4 -4
- package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
- package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
- package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
- package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
- package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
- package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
- package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
- package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
- package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
- package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
- package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
- package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
- package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
- package/templates/modules/auth/nextjs/config.json +24 -18
- package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -3
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
- package/templates/modules/auth/nextjs/lib/email.ts +43 -0
- package/templates/modules/auth/nextjs/lib/session.ts +12 -0
- package/templates/modules/auth/tanstack/config.json +25 -19
- package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
- package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
- package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
- package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
- package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
- package/templates/modules/auth/tanstack/src/lib/auth-client.ts +2 -2
- package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +14 -8
- package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +98 -38
- package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
- package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
- package/templates/modules/auth/tanstack/src/routes/api/auth/$.ts +15 -15
- package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +119 -111
- package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
- package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
- package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +44 -37
- package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +44 -37
- package/templates/modules/docker/.dockerignore +2 -0
- package/templates/modules/docker/config.json +15 -15
- package/templates/modules/drizzle/express/config.json +26 -26
- package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nestjs/config.json +26 -26
- package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nextjs/config.json +26 -26
- package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -13
- package/templates/modules/drizzle/tanstack/config.json +26 -26
- package/templates/modules/drizzle/tanstack/drizzle.config.ts +12 -12
- package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -13
- package/templates/modules/mongoose/express/config.json +15 -15
- package/templates/modules/mongoose/express/src/lib/db.ts +43 -43
- package/templates/modules/mongoose/express/src/models/User.ts +48 -48
- package/templates/modules/mongoose/nestjs/config.json +16 -15
- package/templates/modules/mongoose/nextjs/config.json +15 -15
- package/templates/modules/mongoose/nextjs/models/User.ts +48 -48
- package/templates/modules/mongoose/tanstack/config.json +16 -16
- package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -48
- package/templates/modules/prisma/express/config.json +27 -27
- package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
- package/templates/modules/prisma/nestjs/config.json +27 -27
- package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
- package/templates/modules/prisma/nextjs/config.json +28 -28
- package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -18
- package/templates/modules/prisma/nextjs/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/config.json +28 -28
- package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -91
- package/templates/modules/prisma/tanstack/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/src/lib/prisma.ts +17 -17
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
- package/templates/modules/vscode/.vscode/extensions.json.hbs +29 -0
- package/templates/modules/vscode/.vscode/settings.json.hbs +22 -0
- package/templates/modules/vscode/config.json +10 -0
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
- package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
- package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
- package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
- package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
- package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
- package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
- package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
- package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
- package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
- package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
- 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 {
|
|
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
|
-
|
|
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
|
|
67
|
-
<CardHeader
|
|
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
|
|
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>
|
|
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
|
-
<
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
{authError ? (
|
|
120
|
+
<Alert variant="destructive">
|
|
121
|
+
<CircleAlert />
|
|
122
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
123
|
+
</Alert>
|
|
124
|
+
) : null}
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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="
|
|
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
|
-
<
|
|
38
|
+
<Spinner data-icon="inline-start" />
|
|
38
39
|
Signing out
|
|
39
40
|
</>
|
|
40
41
|
) : (
|
|
41
42
|
<>
|
|
42
|
-
<LogOut
|
|
43
|
+
<LogOut data-icon="inline-start" />
|
|
43
44
|
Sign out
|
|
44
45
|
</>
|
|
45
46
|
)}
|
|
46
47
|
</Button>
|
|
47
|
-
{error ? <p className="text-sm text-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
69
|
-
<CardHeader
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
{authError ? (
|
|
132
|
+
<Alert variant="destructive">
|
|
133
|
+
<CircleAlert />
|
|
134
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
135
|
+
</Alert>
|
|
136
|
+
) : null}
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|