create-better-t-stack 1.7.0 → 1.8.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/README.md +6 -4
- package/dist/index.js +106 -102
- package/package.json +4 -6
- package/template/base/apps/web-base/src/index.css +1 -1
- package/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx +37 -0
- package/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx +73 -0
- package/template/base/apps/web-tanstack-router/src/routes/__root.tsx +1 -2
- package/template/base/apps/web-tanstack-start/app.config.ts +17 -0
- package/template/base/apps/web-tanstack-start/package.json +52 -0
- package/template/base/apps/web-tanstack-start/public/robots.txt +3 -0
- package/template/base/apps/web-tanstack-start/src/api.ts +6 -0
- package/template/base/apps/web-tanstack-start/src/client.tsx +8 -0
- package/template/base/apps/web-tanstack-start/src/components/header.tsx +27 -0
- package/template/base/apps/web-tanstack-start/src/index.css +135 -0
- package/template/base/apps/web-tanstack-start/src/router.tsx +70 -0
- package/template/base/apps/web-tanstack-start/src/routes/__root.tsx +68 -0
- package/template/base/apps/web-tanstack-start/src/routes/index.tsx +88 -0
- package/template/base/apps/web-tanstack-start/src/ssr.tsx +12 -0
- package/template/base/apps/web-tanstack-start/src/utils/trpc.ts +5 -0
- package/template/base/apps/web-tanstack-start/tsconfig.json +28 -0
- package/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx +69 -0
- package/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx +135 -0
- package/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts +39 -0
- package/template/with-auth/apps/web-tanstack-start/src/components/header.tsx +32 -0
- package/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx +139 -0
- package/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx +164 -0
- package/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx +62 -0
- package/template/with-auth/apps/web-tanstack-start/src/router.tsx +76 -0
- package/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx +37 -0
- package/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx +18 -0
- package/template/with-drizzle-sqlite/apps/server/src/db/schema/auth.ts +0 -6
- /package/template/base/apps/{web-base → web-react-router}/src/components/mode-toggle.tsx +0 -0
- /package/template/base/apps/{web-base → web-react-router}/src/components/theme-provider.tsx +0 -0
- /package/template/with-auth/apps/{web-base → web-react-router}/src/utils/trpc.ts +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true,
|
|
23
|
+
"baseUrl": ".",
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": ["./src/*"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { useChat } from "@ai-sdk/react";
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Send } from "lucide-react";
|
|
6
|
+
import { useRef, useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute("/ai")({
|
|
9
|
+
component: RouteComponent,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function RouteComponent() {
|
|
13
|
+
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
|
14
|
+
api: `${import.meta.env.VITE_SERVER_URL}/ai`,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
21
|
+
}, [messages]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
|
25
|
+
<div className="overflow-y-auto space-y-4 pb-4">
|
|
26
|
+
{messages.length === 0 ? (
|
|
27
|
+
<div className="text-center text-muted-foreground mt-8">
|
|
28
|
+
Ask me anything to get started!
|
|
29
|
+
</div>
|
|
30
|
+
) : (
|
|
31
|
+
messages.map((message) => (
|
|
32
|
+
<div
|
|
33
|
+
key={message.id}
|
|
34
|
+
className={`p-3 rounded-lg ${
|
|
35
|
+
message.role === "user"
|
|
36
|
+
? "bg-primary/10 ml-8"
|
|
37
|
+
: "bg-secondary/20 mr-8"
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
<p className="text-sm font-semibold mb-1">
|
|
41
|
+
{message.role === "user" ? "You" : "AI Assistant"}
|
|
42
|
+
</p>
|
|
43
|
+
<div className="whitespace-pre-wrap">{message.content}</div>
|
|
44
|
+
</div>
|
|
45
|
+
))
|
|
46
|
+
)}
|
|
47
|
+
<div ref={messagesEndRef} />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<form
|
|
51
|
+
onSubmit={handleSubmit}
|
|
52
|
+
className="w-full flex items-center space-x-2 pt-2 border-t"
|
|
53
|
+
>
|
|
54
|
+
<Input
|
|
55
|
+
name="prompt"
|
|
56
|
+
value={input}
|
|
57
|
+
onChange={handleInputChange}
|
|
58
|
+
placeholder="Type your message..."
|
|
59
|
+
className="flex-1"
|
|
60
|
+
autoComplete="off"
|
|
61
|
+
autoFocus
|
|
62
|
+
/>
|
|
63
|
+
<Button type="submit" size="icon">
|
|
64
|
+
<Send size={18} />
|
|
65
|
+
</Button>
|
|
66
|
+
</form>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "@/components/ui/card";
|
|
9
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { useTRPC } from "@/utils/trpc";
|
|
12
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
13
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
14
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/todos")({
|
|
18
|
+
component: TodosRoute,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function TodosRoute() {
|
|
22
|
+
const trpc = useTRPC();
|
|
23
|
+
|
|
24
|
+
const [newTodoText, setNewTodoText] = useState("");
|
|
25
|
+
|
|
26
|
+
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
|
27
|
+
const createMutation = useMutation(
|
|
28
|
+
trpc.todo.create.mutationOptions({
|
|
29
|
+
onSuccess: () => {
|
|
30
|
+
todos.refetch();
|
|
31
|
+
setNewTodoText("");
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
const toggleMutation = useMutation(
|
|
36
|
+
trpc.todo.toggle.mutationOptions({
|
|
37
|
+
onSuccess: () => todos.refetch(),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
const deleteMutation = useMutation(
|
|
41
|
+
trpc.todo.delete.mutationOptions({
|
|
42
|
+
onSuccess: () => todos.refetch(),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handleAddTodo = (e: React.FormEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (newTodoText.trim()) {
|
|
49
|
+
createMutation.mutate({ text: newTodoText });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleToggleTodo = (id: number, completed: boolean) => {
|
|
54
|
+
toggleMutation.mutate({ id, completed: !completed });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleDeleteTodo = (id: number) => {
|
|
58
|
+
deleteMutation.mutate({ id });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="mx-auto w-full max-w-md py-10">
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle>Todo List</CardTitle>
|
|
66
|
+
<CardDescription>Manage your tasks efficiently</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent>
|
|
69
|
+
<form
|
|
70
|
+
onSubmit={handleAddTodo}
|
|
71
|
+
className="mb-6 flex items-center space-x-2"
|
|
72
|
+
>
|
|
73
|
+
<Input
|
|
74
|
+
value={newTodoText}
|
|
75
|
+
onChange={(e) => setNewTodoText(e.target.value)}
|
|
76
|
+
placeholder="Add a new task..."
|
|
77
|
+
disabled={createMutation.isPending}
|
|
78
|
+
/>
|
|
79
|
+
<Button
|
|
80
|
+
type="submit"
|
|
81
|
+
disabled={createMutation.isPending || !newTodoText.trim()}
|
|
82
|
+
>
|
|
83
|
+
{createMutation.isPending ? (
|
|
84
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
85
|
+
) : (
|
|
86
|
+
"Add"
|
|
87
|
+
)}
|
|
88
|
+
</Button>
|
|
89
|
+
</form>
|
|
90
|
+
|
|
91
|
+
{todos.isLoading ? (
|
|
92
|
+
<div className="flex justify-center py-4">
|
|
93
|
+
<Loader2 className="h-6 w-6 animate-spin" />
|
|
94
|
+
</div>
|
|
95
|
+
) : todos.data?.length === 0 ? (
|
|
96
|
+
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
|
97
|
+
) : (
|
|
98
|
+
<ul className="space-y-2">
|
|
99
|
+
{todos.data?.map((todo) => (
|
|
100
|
+
<li
|
|
101
|
+
key={todo.id}
|
|
102
|
+
className="flex items-center justify-between rounded-md border p-2"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-center space-x-2">
|
|
105
|
+
<Checkbox
|
|
106
|
+
checked={todo.completed}
|
|
107
|
+
onCheckedChange={() =>
|
|
108
|
+
handleToggleTodo(todo.id, todo.completed)
|
|
109
|
+
}
|
|
110
|
+
id={`todo-${todo.id}`}
|
|
111
|
+
/>
|
|
112
|
+
<label
|
|
113
|
+
htmlFor={`todo-${todo.id}`}
|
|
114
|
+
className={`${todo.completed ? "line-through" : ""}`}
|
|
115
|
+
>
|
|
116
|
+
{todo.text}
|
|
117
|
+
</label>
|
|
118
|
+
</div>
|
|
119
|
+
<Button
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="icon"
|
|
122
|
+
onClick={() => handleDeleteTodo(todo.id)}
|
|
123
|
+
aria-label="Delete todo"
|
|
124
|
+
>
|
|
125
|
+
<Trash2 className="h-4 w-4" />
|
|
126
|
+
</Button>
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)}
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AppRouter } from "../../../server/src/routers";
|
|
2
|
+
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
4
|
+
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
export const queryClient = new QueryClient({
|
|
8
|
+
queryCache: new QueryCache({
|
|
9
|
+
onError: (error) => {
|
|
10
|
+
toast.error(error.message, {
|
|
11
|
+
action: {
|
|
12
|
+
label: "retry",
|
|
13
|
+
onClick: () => {
|
|
14
|
+
queryClient.invalidateQueries();
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const trpcClient = createTRPCClient<AppRouter>({
|
|
23
|
+
links: [
|
|
24
|
+
httpBatchLink({
|
|
25
|
+
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
26
|
+
fetch(url, options) {
|
|
27
|
+
return fetch(url, {
|
|
28
|
+
...options,
|
|
29
|
+
credentials: "include",
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
37
|
+
client: trpcClient,
|
|
38
|
+
queryClient,
|
|
39
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import UserMenu from "./user-menu";
|
|
3
|
+
|
|
4
|
+
export default function Header() {
|
|
5
|
+
const links = [
|
|
6
|
+
{ to: "/", label: "Home" },
|
|
7
|
+
{ to: "/dashboard", label: "Dashboard" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
13
|
+
<nav className="flex gap-4 text-lg">
|
|
14
|
+
{links.map(({ to, label }) => (
|
|
15
|
+
<Link
|
|
16
|
+
key={to}
|
|
17
|
+
to={to}
|
|
18
|
+
activeProps={{ className: "font-bold" }}
|
|
19
|
+
activeOptions={{ exact: true }}
|
|
20
|
+
>
|
|
21
|
+
{label}
|
|
22
|
+
</Link>
|
|
23
|
+
))}
|
|
24
|
+
</nav>
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<UserMenu />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<hr />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { useForm } from "@tanstack/react-form";
|
|
3
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import Loader from "./loader";
|
|
7
|
+
import { Button } from "./ui/button";
|
|
8
|
+
import { Input } from "./ui/input";
|
|
9
|
+
import { Label } from "./ui/label";
|
|
10
|
+
|
|
11
|
+
export default function SignInForm({
|
|
12
|
+
onSwitchToSignUp,
|
|
13
|
+
}: {
|
|
14
|
+
onSwitchToSignUp: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const navigate = useNavigate({
|
|
17
|
+
from: "/",
|
|
18
|
+
});
|
|
19
|
+
const { isPending } = authClient.useSession();
|
|
20
|
+
|
|
21
|
+
const form = useForm({
|
|
22
|
+
defaultValues: {
|
|
23
|
+
email: "",
|
|
24
|
+
password: "",
|
|
25
|
+
},
|
|
26
|
+
onSubmit: async ({ value }) => {
|
|
27
|
+
await authClient.signIn.email(
|
|
28
|
+
{
|
|
29
|
+
email: value.email,
|
|
30
|
+
password: value.password,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
navigate({
|
|
35
|
+
to: "/dashboard",
|
|
36
|
+
});
|
|
37
|
+
toast.success("Sign in successful");
|
|
38
|
+
},
|
|
39
|
+
onError: (error) => {
|
|
40
|
+
toast.error(error.error.message);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
validators: {
|
|
46
|
+
onSubmit: z.object({
|
|
47
|
+
email: z.string().email("Invalid email address"),
|
|
48
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (isPending) {
|
|
54
|
+
return <Loader />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
|
59
|
+
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
|
60
|
+
|
|
61
|
+
<form
|
|
62
|
+
onSubmit={(e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
void form.handleSubmit();
|
|
66
|
+
}}
|
|
67
|
+
className="space-y-4"
|
|
68
|
+
>
|
|
69
|
+
<div>
|
|
70
|
+
<form.Field name="email">
|
|
71
|
+
{(field) => (
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<Label htmlFor={field.name}>Email</Label>
|
|
74
|
+
<Input
|
|
75
|
+
id={field.name}
|
|
76
|
+
name={field.name}
|
|
77
|
+
type="email"
|
|
78
|
+
value={field.state.value}
|
|
79
|
+
onBlur={field.handleBlur}
|
|
80
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
81
|
+
/>
|
|
82
|
+
{field.state.meta.errors.map((error) => (
|
|
83
|
+
<p key={error?.message} className="text-red-500">
|
|
84
|
+
{error?.message}
|
|
85
|
+
</p>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</form.Field>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<form.Field name="password">
|
|
94
|
+
{(field) => (
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<Label htmlFor={field.name}>Password</Label>
|
|
97
|
+
<Input
|
|
98
|
+
id={field.name}
|
|
99
|
+
name={field.name}
|
|
100
|
+
type="password"
|
|
101
|
+
value={field.state.value}
|
|
102
|
+
onBlur={field.handleBlur}
|
|
103
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
104
|
+
/>
|
|
105
|
+
{field.state.meta.errors.map((error) => (
|
|
106
|
+
<p key={error?.message} className="text-red-500">
|
|
107
|
+
{error?.message}
|
|
108
|
+
</p>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</form.Field>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<form.Subscribe>
|
|
116
|
+
{(state) => (
|
|
117
|
+
<Button
|
|
118
|
+
type="submit"
|
|
119
|
+
className="w-full"
|
|
120
|
+
disabled={!state.canSubmit || state.isSubmitting}
|
|
121
|
+
>
|
|
122
|
+
{state.isSubmitting ? "Submitting..." : "Sign In"}
|
|
123
|
+
</Button>
|
|
124
|
+
)}
|
|
125
|
+
</form.Subscribe>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
<div className="mt-4 text-center">
|
|
129
|
+
<Button
|
|
130
|
+
variant="link"
|
|
131
|
+
onClick={onSwitchToSignUp}
|
|
132
|
+
className="text-indigo-600 hover:text-indigo-800"
|
|
133
|
+
>
|
|
134
|
+
Need an account? Sign Up
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { useForm } from "@tanstack/react-form";
|
|
3
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import Loader from "./loader";
|
|
7
|
+
import { Button } from "./ui/button";
|
|
8
|
+
import { Input } from "./ui/input";
|
|
9
|
+
import { Label } from "./ui/label";
|
|
10
|
+
|
|
11
|
+
export default function SignUpForm({
|
|
12
|
+
onSwitchToSignIn,
|
|
13
|
+
}: {
|
|
14
|
+
onSwitchToSignIn: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const navigate = useNavigate({
|
|
17
|
+
from: "/",
|
|
18
|
+
});
|
|
19
|
+
const { isPending } = authClient.useSession();
|
|
20
|
+
|
|
21
|
+
const form = useForm({
|
|
22
|
+
defaultValues: {
|
|
23
|
+
email: "",
|
|
24
|
+
password: "",
|
|
25
|
+
name: "",
|
|
26
|
+
},
|
|
27
|
+
onSubmit: async ({ value }) => {
|
|
28
|
+
await authClient.signUp.email(
|
|
29
|
+
{
|
|
30
|
+
email: value.email,
|
|
31
|
+
password: value.password,
|
|
32
|
+
name: value.name,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
onSuccess: () => {
|
|
36
|
+
navigate({
|
|
37
|
+
to: "/dashboard",
|
|
38
|
+
});
|
|
39
|
+
toast.success("Sign up successful");
|
|
40
|
+
},
|
|
41
|
+
onError: (error) => {
|
|
42
|
+
toast.error(error.error.message);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
validators: {
|
|
48
|
+
onSubmit: z.object({
|
|
49
|
+
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
50
|
+
email: z.string().email("Invalid email address"),
|
|
51
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (isPending) {
|
|
57
|
+
return <Loader />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
|
62
|
+
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
|
63
|
+
|
|
64
|
+
<form
|
|
65
|
+
onSubmit={(e) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
e.stopPropagation();
|
|
68
|
+
void form.handleSubmit();
|
|
69
|
+
}}
|
|
70
|
+
className="space-y-4"
|
|
71
|
+
>
|
|
72
|
+
<div>
|
|
73
|
+
<form.Field name="name">
|
|
74
|
+
{(field) => (
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<Label htmlFor={field.name}>Name</Label>
|
|
77
|
+
<Input
|
|
78
|
+
id={field.name}
|
|
79
|
+
name={field.name}
|
|
80
|
+
value={field.state.value}
|
|
81
|
+
onBlur={field.handleBlur}
|
|
82
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
83
|
+
/>
|
|
84
|
+
{field.state.meta.errors.map((error) => (
|
|
85
|
+
<p key={error?.message} className="text-red-500">
|
|
86
|
+
{error?.message}
|
|
87
|
+
</p>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</form.Field>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div>
|
|
95
|
+
<form.Field name="email">
|
|
96
|
+
{(field) => (
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<Label htmlFor={field.name}>Email</Label>
|
|
99
|
+
<Input
|
|
100
|
+
id={field.name}
|
|
101
|
+
name={field.name}
|
|
102
|
+
type="email"
|
|
103
|
+
value={field.state.value}
|
|
104
|
+
onBlur={field.handleBlur}
|
|
105
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
106
|
+
/>
|
|
107
|
+
{field.state.meta.errors.map((error) => (
|
|
108
|
+
<p key={error?.message} className="text-red-500">
|
|
109
|
+
{error?.message}
|
|
110
|
+
</p>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</form.Field>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div>
|
|
118
|
+
<form.Field name="password">
|
|
119
|
+
{(field) => (
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
<Label htmlFor={field.name}>Password</Label>
|
|
122
|
+
<Input
|
|
123
|
+
id={field.name}
|
|
124
|
+
name={field.name}
|
|
125
|
+
type="password"
|
|
126
|
+
value={field.state.value}
|
|
127
|
+
onBlur={field.handleBlur}
|
|
128
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
129
|
+
/>
|
|
130
|
+
{field.state.meta.errors.map((error) => (
|
|
131
|
+
<p key={error?.message} className="text-red-500">
|
|
132
|
+
{error?.message}
|
|
133
|
+
</p>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</form.Field>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<form.Subscribe>
|
|
141
|
+
{(state) => (
|
|
142
|
+
<Button
|
|
143
|
+
type="submit"
|
|
144
|
+
className="w-full"
|
|
145
|
+
disabled={!state.canSubmit || state.isSubmitting}
|
|
146
|
+
>
|
|
147
|
+
{state.isSubmitting ? "Submitting..." : "Sign Up"}
|
|
148
|
+
</Button>
|
|
149
|
+
)}
|
|
150
|
+
</form.Subscribe>
|
|
151
|
+
</form>
|
|
152
|
+
|
|
153
|
+
<div className="mt-4 text-center">
|
|
154
|
+
<Button
|
|
155
|
+
variant="link"
|
|
156
|
+
onClick={onSwitchToSignIn}
|
|
157
|
+
className="text-indigo-600 hover:text-indigo-800"
|
|
158
|
+
>
|
|
159
|
+
Already have an account? Sign In
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DropdownMenu,
|
|
3
|
+
DropdownMenuContent,
|
|
4
|
+
DropdownMenuItem,
|
|
5
|
+
DropdownMenuLabel,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from "@/components/ui/dropdown-menu";
|
|
9
|
+
import { authClient } from "@/lib/auth-client";
|
|
10
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
11
|
+
import { Button } from "./ui/button";
|
|
12
|
+
import { Skeleton } from "./ui/skeleton";
|
|
13
|
+
import { Link } from "@tanstack/react-router";
|
|
14
|
+
|
|
15
|
+
export default function UserMenu() {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { data: session, isPending } = authClient.useSession();
|
|
18
|
+
|
|
19
|
+
if (isPending) {
|
|
20
|
+
return <Skeleton className="h-9 w-24" />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!session) {
|
|
24
|
+
return (
|
|
25
|
+
<Button variant="outline" asChild>
|
|
26
|
+
<Link to="/login">Sign In</Link>
|
|
27
|
+
</Button>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenuTrigger asChild>
|
|
34
|
+
<Button variant="outline">{session.user.name}</Button>
|
|
35
|
+
</DropdownMenuTrigger>
|
|
36
|
+
<DropdownMenuContent className="bg-card">
|
|
37
|
+
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
38
|
+
<DropdownMenuSeparator />
|
|
39
|
+
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
|
|
40
|
+
<DropdownMenuItem asChild>
|
|
41
|
+
<Button
|
|
42
|
+
variant="destructive"
|
|
43
|
+
className="w-full"
|
|
44
|
+
onClick={() => {
|
|
45
|
+
authClient.signOut({
|
|
46
|
+
fetchOptions: {
|
|
47
|
+
onSuccess: () => {
|
|
48
|
+
navigate({
|
|
49
|
+
to: "/",
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
Sign Out
|
|
57
|
+
</Button>
|
|
58
|
+
</DropdownMenuItem>
|
|
59
|
+
</DropdownMenuContent>
|
|
60
|
+
</DropdownMenu>
|
|
61
|
+
);
|
|
62
|
+
}
|