create-better-t-stack 0.1.0 → 1.0.2

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 (86) hide show
  1. package/README.md +59 -49
  2. package/dist/index.js +135 -305
  3. package/package.json +19 -11
  4. package/template/base/_gitignore +2 -0
  5. package/template/base/package.json +18 -0
  6. package/template/base/packages/client/_gitignore +23 -0
  7. package/template/base/packages/client/components.json +21 -0
  8. package/template/base/packages/client/index.html +12 -0
  9. package/template/base/packages/client/package.json +49 -0
  10. package/template/base/packages/client/src/components/header.tsx +31 -0
  11. package/template/base/packages/client/src/components/loader.tsx +9 -0
  12. package/template/base/packages/client/src/components/mode-toggle.tsx +37 -0
  13. package/template/base/packages/client/src/components/theme-provider.tsx +73 -0
  14. package/template/base/packages/client/src/components/ui/button.tsx +57 -0
  15. package/template/base/packages/client/src/components/ui/card.tsx +92 -0
  16. package/template/base/packages/client/src/components/ui/checkbox.tsx +30 -0
  17. package/template/base/packages/client/src/components/ui/dropdown-menu.tsx +199 -0
  18. package/template/base/packages/client/src/components/ui/input.tsx +22 -0
  19. package/template/base/packages/client/src/components/ui/label.tsx +24 -0
  20. package/template/base/packages/client/src/components/ui/skeleton.tsx +15 -0
  21. package/template/base/packages/client/src/components/ui/sonner.tsx +29 -0
  22. package/template/base/packages/client/src/index.css +119 -0
  23. package/template/base/packages/client/src/lib/utils.ts +6 -0
  24. package/template/base/packages/client/src/main.tsx +72 -0
  25. package/template/base/packages/client/src/routes/__root.tsx +58 -0
  26. package/template/base/packages/client/src/routes/index.tsx +89 -0
  27. package/template/base/packages/client/src/utils/trpc.ts +4 -0
  28. package/template/base/packages/client/tsconfig.json +18 -0
  29. package/template/base/packages/client/vite.config.ts +14 -0
  30. package/template/base/packages/server/_gitignore +36 -0
  31. package/template/base/packages/server/package.json +27 -0
  32. package/template/base/packages/server/src/index.ts +41 -0
  33. package/template/base/packages/server/src/lib/context.ts +13 -0
  34. package/template/base/packages/server/src/lib/trpc.ts +8 -0
  35. package/template/base/packages/server/src/routers/index.ts +11 -0
  36. package/template/base/packages/server/tsconfig.json +18 -0
  37. package/template/base/turbo.json +27 -0
  38. package/template/examples/todo/packages/client/src/routes/todos.tsx +128 -0
  39. package/template/examples/todo/packages/server/src/routers/with-drizzle-todo.ts +44 -0
  40. package/template/examples/todo/packages/server/src/routers/with-prisma-todo.ts +55 -0
  41. package/template/with-auth/packages/client/src/components/auth-forms.tsx +13 -0
  42. package/template/with-auth/packages/client/src/components/header.tsx +34 -0
  43. package/template/with-auth/packages/client/src/components/sign-in-form.tsx +139 -0
  44. package/template/with-auth/packages/client/src/components/sign-up-form.tsx +164 -0
  45. package/template/with-auth/packages/client/src/components/user-menu.tsx +62 -0
  46. package/template/with-auth/packages/client/src/lib/auth-client.ts +5 -0
  47. package/template/with-auth/packages/client/src/main.tsx +78 -0
  48. package/template/with-auth/packages/client/src/routes/dashboard.tsx +36 -0
  49. package/template/with-auth/packages/client/src/routes/login.tsx +11 -0
  50. package/template/with-auth/packages/server/src/index.ts +46 -0
  51. package/template/with-auth/packages/server/src/lib/trpc.ts +24 -0
  52. package/template/with-auth/packages/server/src/routers/index.ts +19 -0
  53. package/template/with-biome/biome.json +42 -0
  54. package/template/with-drizzle-postgres/packages/server/drizzle.config.ts +10 -0
  55. package/template/with-drizzle-postgres/packages/server/src/db/index.ts +5 -0
  56. package/template/with-drizzle-postgres/packages/server/src/db/schema/auth.ts +47 -0
  57. package/template/with-drizzle-postgres/packages/server/src/db/schema/todo.ts +7 -0
  58. package/template/with-drizzle-postgres/packages/server/src/routers/todo.ts +44 -0
  59. package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts +15 -0
  60. package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/context.ts +18 -0
  61. package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/trpc.ts +24 -0
  62. package/template/with-drizzle-sqlite/packages/server/drizzle.config.ts +11 -0
  63. package/template/with-drizzle-sqlite/packages/server/src/db/index.ts +9 -0
  64. package/template/with-drizzle-sqlite/packages/server/src/db/schema/auth.ts +61 -0
  65. package/template/with-drizzle-sqlite/packages/server/src/db/schema/todo.ts +7 -0
  66. package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts +15 -0
  67. package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/context.ts +18 -0
  68. package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/trpc.ts +24 -0
  69. package/template/with-husky/.husky/pre-commit +1 -0
  70. package/template/with-prisma-postgres/packages/server/prisma/index.ts +5 -0
  71. package/template/with-prisma-postgres/packages/server/prisma/schema/auth.prisma +59 -0
  72. package/template/with-prisma-postgres/packages/server/prisma/schema/schema.prisma +9 -0
  73. package/template/with-prisma-postgres/packages/server/prisma/schema/todo.prisma +7 -0
  74. package/template/with-prisma-postgres/packages/server/src/with-auth-lib/auth.ts +17 -0
  75. package/template/with-prisma-postgres/packages/server/src/with-auth-lib/context.ts +18 -0
  76. package/template/with-prisma-postgres/packages/server/src/with-auth-lib/trpc.ts +24 -0
  77. package/template/with-prisma-sqlite/packages/server/prisma/index.ts +5 -0
  78. package/template/with-prisma-sqlite/packages/server/prisma/schema/auth.prisma +59 -0
  79. package/template/with-prisma-sqlite/packages/server/prisma/schema/schema.prisma +8 -0
  80. package/template/with-prisma-sqlite/packages/server/prisma/schema/todo.prisma +7 -0
  81. package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/auth.ts +17 -0
  82. package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/context.ts +18 -0
  83. package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/trpc.ts +24 -0
  84. package/template/with-pwa/packages/client/public/logo.png +0 -0
  85. package/template/with-pwa/packages/client/pwa-assets.config.ts +12 -0
  86. package/template/with-pwa/packages/client/vite.config.ts +35 -0
@@ -0,0 +1,128 @@
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 { trpc } from "@/utils/trpc";
12
+ import { createFileRoute } from "@tanstack/react-router";
13
+ import { Loader2, Trash2 } from "lucide-react";
14
+ import { useState } from "react";
15
+
16
+ export const Route = createFileRoute("/todos")({
17
+ component: TodosRoute,
18
+ });
19
+
20
+ function TodosRoute() {
21
+ const [newTodoText, setNewTodoText] = useState("");
22
+
23
+ const todos = trpc.todo.getAll.useQuery();
24
+ const createMutation = trpc.todo.create.useMutation({
25
+ onSuccess: () => {
26
+ todos.refetch();
27
+ setNewTodoText("");
28
+ },
29
+ });
30
+ const toggleMutation = trpc.todo.toggle.useMutation({
31
+ onSuccess: () => todos.refetch(),
32
+ });
33
+ const deleteMutation = trpc.todo.delete.useMutation({
34
+ onSuccess: () => todos.refetch(),
35
+ });
36
+
37
+ const handleAddTodo = (e: React.FormEvent) => {
38
+ e.preventDefault();
39
+ if (newTodoText.trim()) {
40
+ createMutation.mutate({ text: newTodoText });
41
+ }
42
+ };
43
+
44
+ const handleToggleTodo = (id: number, completed: boolean) => {
45
+ toggleMutation.mutate({ id, completed: !completed });
46
+ };
47
+
48
+ const handleDeleteTodo = (id: number) => {
49
+ deleteMutation.mutate({ id });
50
+ };
51
+
52
+ return (
53
+ <div className="container mx-auto max-w-md py-10">
54
+ <Card>
55
+ <CardHeader>
56
+ <CardTitle>Todo List</CardTitle>
57
+ <CardDescription>Manage your tasks efficiently</CardDescription>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <form
61
+ onSubmit={handleAddTodo}
62
+ className="mb-6 flex items-center space-x-2"
63
+ >
64
+ <Input
65
+ value={newTodoText}
66
+ onChange={(e) => setNewTodoText(e.target.value)}
67
+ placeholder="Add a new task..."
68
+ disabled={createMutation.isPending}
69
+ />
70
+ <Button
71
+ type="submit"
72
+ disabled={createMutation.isPending || !newTodoText.trim()}
73
+ >
74
+ {createMutation.isPending ? (
75
+ <Loader2 className="h-4 w-4 animate-spin" />
76
+ ) : (
77
+ "Add"
78
+ )}
79
+ </Button>
80
+ </form>
81
+
82
+ {todos.isLoading ? (
83
+ <div className="flex justify-center py-4">
84
+ <Loader2 className="h-6 w-6 animate-spin" />
85
+ </div>
86
+ ) : todos.data?.length === 0 ? (
87
+ <p className="py-4 text-center text-muted-foreground">
88
+ No todos yet. Add one above!
89
+ </p>
90
+ ) : (
91
+ <ul className="space-y-2">
92
+ {todos.data?.map((todo) => (
93
+ <li
94
+ key={todo.id}
95
+ className="flex items-center justify-between rounded-md border p-2"
96
+ >
97
+ <div className="flex items-center space-x-2">
98
+ <Checkbox
99
+ checked={todo.completed}
100
+ onCheckedChange={() =>
101
+ handleToggleTodo(todo.id, todo.completed)
102
+ }
103
+ id={`todo-${todo.id}`}
104
+ />
105
+ <label
106
+ htmlFor={`todo-${todo.id}`}
107
+ className={`${todo.completed ? "text-muted-foreground line-through" : ""}`}
108
+ >
109
+ {todo.text}
110
+ </label>
111
+ </div>
112
+ <Button
113
+ variant="ghost"
114
+ size="icon"
115
+ onClick={() => handleDeleteTodo(todo.id)}
116
+ aria-label="Delete todo"
117
+ >
118
+ <Trash2 className="h-4 w-4" />
119
+ </Button>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ )}
124
+ </CardContent>
125
+ </Card>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { router, publicProcedure } from "../lib/trpc";
3
+ import { todo } from "../db/schema/todo";
4
+ import { eq } from "drizzle-orm";
5
+ import { db } from "../db";
6
+
7
+ export const todoRouter = router({
8
+ getAll: publicProcedure.query(async () => {
9
+ return await db.select().from(todo).all();
10
+ }),
11
+
12
+ create: publicProcedure
13
+ .input(z.object({ text: z.string().min(1) }))
14
+ .mutation(async ({ input }) => {
15
+ return await db
16
+ .insert(todo)
17
+ .values({
18
+ text: input.text,
19
+ })
20
+ .returning()
21
+ .get();
22
+ }),
23
+
24
+ toggle: publicProcedure
25
+ .input(z.object({ id: z.number(), completed: z.boolean() }))
26
+ .mutation(async ({ input }) => {
27
+ return await db
28
+ .update(todo)
29
+ .set({ completed: input.completed })
30
+ .where(eq(todo.id, input.id))
31
+ .returning()
32
+ .get();
33
+ }),
34
+
35
+ delete: publicProcedure
36
+ .input(z.object({ id: z.number() }))
37
+ .mutation(async ({ input }) => {
38
+ return await db
39
+ .delete(todo)
40
+ .where(eq(todo.id, input.id))
41
+ .returning()
42
+ .get();
43
+ }),
44
+ });
@@ -0,0 +1,55 @@
1
+ import { TRPCError } from "@trpc/server";
2
+ import { z } from "zod";
3
+ import prisma from "../../prisma";
4
+ import { publicProcedure, router } from "../lib/trpc";
5
+
6
+ export const todoRouter = router({
7
+ getAll: publicProcedure.query(async () => {
8
+ return await prisma.todo.findMany({
9
+ orderBy: {
10
+ id: "asc"
11
+ }
12
+ });
13
+ }),
14
+
15
+ create: publicProcedure
16
+ .input(z.object({ text: z.string().min(1) }))
17
+ .mutation(async ({ input }) => {
18
+ return await prisma.todo.create({
19
+ data: {
20
+ text: input.text,
21
+ },
22
+ });
23
+ }),
24
+
25
+ toggle: publicProcedure
26
+ .input(z.object({ id: z.number(), completed: z.boolean() }))
27
+ .mutation(async ({ input }) => {
28
+ try {
29
+ return await prisma.todo.update({
30
+ where: { id: input.id },
31
+ data: { completed: input.completed },
32
+ });
33
+ } catch (error) {
34
+ throw new TRPCError({
35
+ code: "NOT_FOUND",
36
+ message: "Todo not found",
37
+ });
38
+ }
39
+ }),
40
+
41
+ delete: publicProcedure
42
+ .input(z.object({ id: z.number() }))
43
+ .mutation(async ({ input }) => {
44
+ try {
45
+ return await prisma.todo.delete({
46
+ where: { id: input.id },
47
+ });
48
+ } catch (error) {
49
+ throw new TRPCError({
50
+ code: "NOT_FOUND",
51
+ message: "Todo not found",
52
+ });
53
+ }
54
+ }),
55
+ });
@@ -0,0 +1,13 @@
1
+ import { useState } from "react";
2
+ import SignInForm from "./sign-in-form";
3
+ import SignUpForm from "./sign-up-form";
4
+
5
+ export default function AuthForms() {
6
+ const [showSignIn, setShowSignIn] = useState(false);
7
+
8
+ return showSignIn ? (
9
+ <SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
10
+ ) : (
11
+ <SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ModeToggle } from "./mode-toggle";
3
+ import UserMenu from "./user-menu";
4
+
5
+ export default function Header() {
6
+ const links = [
7
+ { to: "/", label: "Home" },
8
+ { to: "/dashboard", label: "Dashboard" },
9
+ ];
10
+
11
+ return (
12
+ <div>
13
+ <div className="flex flex-row items-center justify-between px-2 py-1">
14
+ <nav className="flex gap-4 text-lg">
15
+ {links.map(({ to, label }) => (
16
+ <Link
17
+ key={to}
18
+ to={to}
19
+ activeProps={{ className: "font-bold" }}
20
+ activeOptions={{ exact: true }}
21
+ >
22
+ {label}
23
+ </Link>
24
+ ))}
25
+ </nav>
26
+ <div className="flex items-center gap-2">
27
+ <ModeToggle />
28
+ <UserMenu />
29
+ </div>
30
+ </div>
31
+ <hr />
32
+ </div>
33
+ );
34
+ }
@@ -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
+ toast.success("Sign in successful");
35
+ navigate({
36
+ to: "/dashboard",
37
+ });
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 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
+ toast.success("Sign up successful");
37
+ navigate({
38
+ to: "/dashboard",
39
+ });
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 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
+ }
@@ -0,0 +1,5 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ export const authClient = createAuthClient({
4
+ baseURL: import.meta.env.VITE_SERVER_URL,
5
+ });