create-better-t-stack 1.7.1 → 1.8.1

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 (34) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +106 -105
  3. package/package.json +2 -2
  4. package/template/base/apps/web-base/src/index.css +1 -1
  5. package/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx +37 -0
  6. package/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx +73 -0
  7. package/template/base/apps/web-tanstack-router/src/routes/__root.tsx +1 -2
  8. package/template/base/apps/web-tanstack-start/app.config.ts +17 -0
  9. package/template/base/apps/web-tanstack-start/package.json +52 -0
  10. package/template/base/apps/web-tanstack-start/public/robots.txt +3 -0
  11. package/template/base/apps/web-tanstack-start/src/api.ts +6 -0
  12. package/template/base/apps/web-tanstack-start/src/client.tsx +8 -0
  13. package/template/base/apps/web-tanstack-start/src/components/header.tsx +27 -0
  14. package/template/base/apps/web-tanstack-start/src/index.css +135 -0
  15. package/template/base/apps/web-tanstack-start/src/router.tsx +70 -0
  16. package/template/base/apps/web-tanstack-start/src/routes/__root.tsx +68 -0
  17. package/template/base/apps/web-tanstack-start/src/routes/index.tsx +88 -0
  18. package/template/base/apps/web-tanstack-start/src/ssr.tsx +12 -0
  19. package/template/base/apps/web-tanstack-start/src/utils/trpc.ts +5 -0
  20. package/template/base/apps/web-tanstack-start/tsconfig.json +28 -0
  21. package/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx +69 -0
  22. package/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx +135 -0
  23. package/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts +39 -0
  24. package/template/with-auth/apps/web-tanstack-start/src/components/header.tsx +32 -0
  25. package/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx +139 -0
  26. package/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx +164 -0
  27. package/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx +62 -0
  28. package/template/with-auth/apps/web-tanstack-start/src/router.tsx +76 -0
  29. package/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx +37 -0
  30. package/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx +18 -0
  31. package/template/with-drizzle-sqlite/apps/server/src/db/schema/auth.ts +0 -6
  32. /package/template/base/apps/{web-base → web-react-router}/src/components/mode-toggle.tsx +0 -0
  33. /package/template/base/apps/{web-base → web-react-router}/src/components/theme-provider.tsx +0 -0
  34. /package/template/with-auth/apps/{web-base → web-react-router}/src/utils/trpc.ts +0 -0
@@ -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
+ }