create-better-t-stack 1.4.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/index.js +150 -98
  2. package/package.json +1 -1
  3. package/template/base/apps/{web → web-base}/_gitignore +3 -0
  4. package/template/base/apps/{web → web-base}/components.json +2 -2
  5. package/template/base/apps/{web → web-base}/src/components/mode-toggle.tsx +1 -1
  6. package/template/base/apps/{web → web-base}/src/components/theme-provider.tsx +1 -1
  7. package/template/base/apps/web-base/src/components/ui/button.tsx +59 -0
  8. package/template/base/apps/{web → web-base}/src/components/ui/card.tsx +10 -10
  9. package/template/base/apps/{web → web-base}/src/components/ui/checkbox.tsx +6 -6
  10. package/template/base/apps/web-base/src/components/ui/dropdown-menu.tsx +255 -0
  11. package/template/base/apps/web-base/src/components/ui/input.tsx +21 -0
  12. package/template/base/apps/web-base/src/components/ui/label.tsx +24 -0
  13. package/template/base/apps/web-base/src/components/ui/skeleton.tsx +13 -0
  14. package/template/base/apps/web-base/src/components/ui/sonner.tsx +23 -0
  15. package/template/base/apps/web-base/src/index.css +134 -0
  16. package/template/base/apps/web-react-router/package.json +49 -0
  17. package/template/base/apps/web-react-router/public/favicon.ico +0 -0
  18. package/template/base/apps/web-react-router/react-router.config.ts +6 -0
  19. package/template/base/apps/web-react-router/src/components/header.tsx +31 -0
  20. package/template/base/apps/web-react-router/src/root.tsx +91 -0
  21. package/template/base/apps/web-react-router/src/routes/_index.tsx +89 -0
  22. package/template/base/apps/web-react-router/src/routes.ts +4 -0
  23. package/template/base/apps/web-react-router/tsconfig.json +27 -0
  24. package/template/base/apps/web-react-router/vite.config.ts +8 -0
  25. package/template/base/apps/{web → web-tanstack-router}/package.json +2 -2
  26. package/template/base/apps/{web → web-tanstack-router}/src/routes/index.tsx +1 -4
  27. package/template/examples/ai/apps/web-react-router/src/routes/ai.tsx +64 -0
  28. package/template/examples/todo/apps/web-react-router/src/routes/todos.tsx +130 -0
  29. package/template/examples/todo/apps/{web → web-tanstack-router}/src/routes/todos.tsx +6 -8
  30. package/template/with-auth/apps/web-react-router/src/components/header.tsx +34 -0
  31. package/template/with-auth/apps/web-react-router/src/components/sign-in-form.tsx +135 -0
  32. package/template/with-auth/apps/web-react-router/src/components/sign-up-form.tsx +160 -0
  33. package/template/with-auth/apps/web-react-router/src/components/user-menu.tsx +60 -0
  34. package/template/with-auth/apps/web-react-router/src/routes/dashboard.tsx +30 -0
  35. package/template/with-auth/apps/web-react-router/src/routes/login.tsx +13 -0
  36. package/template/base/apps/web/src/components/ui/button.tsx +0 -57
  37. package/template/base/apps/web/src/components/ui/dropdown-menu.tsx +0 -199
  38. package/template/base/apps/web/src/components/ui/input.tsx +0 -22
  39. package/template/base/apps/web/src/components/ui/label.tsx +0 -24
  40. package/template/base/apps/web/src/components/ui/skeleton.tsx +0 -15
  41. package/template/base/apps/web/src/components/ui/sonner.tsx +0 -29
  42. package/template/base/apps/web/src/index.css +0 -119
  43. package/template/with-pwa/apps/web/vite.config.ts +0 -35
  44. /package/template/base/apps/{web → web-base}/src/components/loader.tsx +0 -0
  45. /package/template/base/apps/{web → web-base}/src/lib/utils.ts +0 -0
  46. /package/template/base/apps/{web → web-base}/src/utils/trpc.ts +0 -0
  47. /package/template/base/apps/{web → web-tanstack-router}/index.html +0 -0
  48. /package/template/base/apps/{web → web-tanstack-router}/src/components/header.tsx +0 -0
  49. /package/template/base/apps/{web → web-tanstack-router}/src/main.tsx +0 -0
  50. /package/template/base/apps/{web → web-tanstack-router}/src/routes/__root.tsx +0 -0
  51. /package/template/base/apps/{web → web-tanstack-router}/tsconfig.json +0 -0
  52. /package/template/base/apps/{web → web-tanstack-router}/vite.config.ts +0 -0
  53. /package/template/examples/ai/apps/{web → web-tanstack-router}/src/routes/ai.tsx +0 -0
  54. /package/template/with-auth/apps/{web → web-base}/src/lib/auth-client.ts +0 -0
  55. /package/template/with-auth/apps/{web → web-base}/src/utils/trpc.ts +0 -0
  56. /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/header.tsx +0 -0
  57. /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/sign-in-form.tsx +0 -0
  58. /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/sign-up-form.tsx +0 -0
  59. /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/user-menu.tsx +0 -0
  60. /package/template/with-auth/apps/{web → web-tanstack-router}/src/routes/dashboard.tsx +0 -0
  61. /package/template/with-auth/apps/{web → web-tanstack-router}/src/routes/login.tsx +0 -0
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "react-router build",
7
+ "dev": "react-router dev",
8
+ "start": "react-router-serve ./build/server/index.js",
9
+ "typecheck": "react-router typegen && tsc"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-checkbox": "^1.1.4",
13
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
14
+ "@radix-ui/react-label": "^2.1.2",
15
+ "@radix-ui/react-slot": "^1.1.2",
16
+ "@react-router/fs-routes": "^7.4.1",
17
+ "@react-router/node": "^7.4.1",
18
+ "@react-router/serve": "^7.4.1",
19
+ "@tanstack/react-form": "^1.2.3",
20
+ "@tanstack/react-query": "^5.71.3",
21
+ "@trpc/client": "^11.0.1",
22
+ "@trpc/server": "^11.0.1",
23
+ "@trpc/tanstack-react-query": "^11.0.1",
24
+ "class-variance-authority": "^0.7.1",
25
+ "clsx": "^2.1.1",
26
+ "isbot": "^5.1.17",
27
+ "lucide-react": "^0.487.0",
28
+ "next-themes": "^0.4.6",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "react-router": "^7.4.1",
32
+ "sonner": "^2.0.3",
33
+ "tailwind-merge": "^3.1.0",
34
+ "tw-animate-css": "^1.2.5"
35
+ },
36
+ "devDependencies": {
37
+ "@react-router/dev": "^7.4.1",
38
+ "@tailwindcss/vite": "^4.0.0",
39
+ "@tanstack/react-query-devtools": "^5.71.3",
40
+ "@types/node": "^20",
41
+ "@types/react": "^19.0.1",
42
+ "@types/react-dom": "^19.0.1",
43
+ "react-router-devtools": "^1.1.0",
44
+ "tailwindcss": "^4.0.0",
45
+ "typescript": "^5.7.2",
46
+ "vite": "^5.4.11",
47
+ "vite-tsconfig-paths": "^5.1.4"
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ ssr: false,
5
+ appDirectory: "src",
6
+ } satisfies Config;
@@ -0,0 +1,31 @@
1
+ import { NavLink } from "react-router";
2
+ import { ModeToggle } from "./mode-toggle";
3
+
4
+ export default function Header() {
5
+ const links = [
6
+ { to: "/", label: "Home" },
7
+ ];
8
+
9
+ return (
10
+ <div>
11
+ <div className="flex flex-row items-center justify-between px-2 py-1">
12
+ <nav className="flex gap-4 text-lg">
13
+ {links.map(({ to, label }) => (
14
+ <NavLink
15
+ key={to}
16
+ to={to}
17
+ className={({ isActive }) => (isActive ? "font-bold" : "")}
18
+ end
19
+ >
20
+ {label}
21
+ </NavLink>
22
+ ))}
23
+ </nav>
24
+ <div className="flex items-center gap-2">
25
+ <ModeToggle />
26
+ </div>
27
+ </div>
28
+ <hr />
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,91 @@
1
+ import { QueryClientProvider } from "@tanstack/react-query";
2
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
3
+ import {
4
+ isRouteErrorResponse,
5
+ Links,
6
+ Meta,
7
+ Outlet,
8
+ Scripts,
9
+ ScrollRestoration,
10
+ } from "react-router";
11
+ import type { Route } from "./+types/root";
12
+ import "./index.css";
13
+ import Header from "./components/header";
14
+ import { ThemeProvider } from "./components/theme-provider";
15
+ import { queryClient } from "./utils/trpc";
16
+ import { Toaster } from "./components/ui/sonner";
17
+
18
+ export const links: Route.LinksFunction = () => [
19
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
20
+ {
21
+ rel: "preconnect",
22
+ href: "https://fonts.gstatic.com",
23
+ crossOrigin: "anonymous",
24
+ },
25
+ {
26
+ rel: "stylesheet",
27
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
28
+ },
29
+ ];
30
+
31
+ export function Layout({ children }: { children: React.ReactNode }) {
32
+ return (
33
+ <html lang="en">
34
+ <head>
35
+ <meta charSet="utf-8" />
36
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
37
+ <Meta />
38
+ <Links />
39
+ </head>
40
+ <body>
41
+ {children}
42
+ <ScrollRestoration />
43
+ <Scripts />
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+
49
+ export default function App() {
50
+ return (
51
+ <QueryClientProvider client={queryClient}>
52
+ <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
53
+ <div className="grid grid-rows-[auto_1fr] h-svh">
54
+ <Header />
55
+ <Outlet />
56
+ </div>
57
+ <Toaster richColors />
58
+ </ThemeProvider>
59
+ <ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
60
+ </QueryClientProvider>
61
+ );
62
+ }
63
+
64
+ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
65
+ let message = "Oops!";
66
+ let details = "An unexpected error occurred.";
67
+ let stack: string | undefined;
68
+
69
+ if (isRouteErrorResponse(error)) {
70
+ message = error.status === 404 ? "404" : "Error";
71
+ details =
72
+ error.status === 404
73
+ ? "The requested page could not be found."
74
+ : error.statusText || details;
75
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
76
+ details = error.message;
77
+ stack = error.stack;
78
+ }
79
+
80
+ return (
81
+ <main className="pt-16 p-4 container mx-auto">
82
+ <h1>{message}</h1>
83
+ <p>{details}</p>
84
+ {stack && (
85
+ <pre className="w-full p-4 overflow-x-auto">
86
+ <code>{stack}</code>
87
+ </pre>
88
+ )}
89
+ </main>
90
+ );
91
+ }
@@ -0,0 +1,89 @@
1
+ import type { Route } from "./+types/_index";
2
+ import { trpc } from "@/utils/trpc";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ const TITLE_TEXT = `
6
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
7
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
8
+ ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
9
+ ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
10
+ ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
11
+ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
12
+
13
+ ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
14
+ ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
15
+ ██║ ███████╗ ██║ ███████║██║ █████╔╝
16
+ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
17
+ ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
18
+ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
19
+ `;
20
+
21
+ export function meta({}: Route.MetaArgs) {
22
+ return [{ title: "My App" }, { name: "description", content: "My App" }];
23
+ }
24
+
25
+ export default function Home() {
26
+ const healthCheck = useQuery(trpc.healthCheck.queryOptions());
27
+
28
+ return (
29
+ <div className="container mx-auto max-w-3xl px-4 py-2">
30
+ <pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
31
+ <div className="grid gap-6">
32
+ <section className="rounded-lg border p-4">
33
+ <h2 className="mb-2 font-medium">API Status</h2>
34
+ <div className="flex items-center gap-2">
35
+ <div
36
+ className={`h-2 w-2 rounded-full ${
37
+ healthCheck.data ? "bg-green-500" : "bg-red-500"
38
+ }`}
39
+ />
40
+ <span className="text-sm text-muted-foreground">
41
+ {healthCheck.isLoading
42
+ ? "Checking..."
43
+ : healthCheck.data
44
+ ? "Connected"
45
+ : "Disconnected"}
46
+ </span>
47
+ </div>
48
+ </section>
49
+
50
+ <section>
51
+ <h2 className="mb-3 font-medium">Core Features</h2>
52
+ <ul className="grid grid-cols-2 gap-3">
53
+ <FeatureItem
54
+ title="Type-Safe API"
55
+ description="End-to-end type safety with tRPC"
56
+ />
57
+ <FeatureItem
58
+ title="Modern React"
59
+ description="TanStack Router + TanStack Query"
60
+ />
61
+ <FeatureItem
62
+ title="Fast Backend"
63
+ description="Lightweight Hono server"
64
+ />
65
+ <FeatureItem
66
+ title="Beautiful UI"
67
+ description="TailwindCSS + shadcn/ui components"
68
+ />
69
+ </ul>
70
+ </section>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ function FeatureItem({
77
+ title,
78
+ description,
79
+ }: {
80
+ title: string;
81
+ description: string;
82
+ }) {
83
+ return (
84
+ <li className="border-l-2 border-primary py-1 pl-3">
85
+ <h3 className="font-medium">{title}</h3>
86
+ <p className="text-sm text-muted-foreground">{description}</p>
87
+ </li>
88
+ );
89
+ }
@@ -0,0 +1,4 @@
1
+ import { type RouteConfig } from "@react-router/dev/routes";
2
+ import { flatRoutes } from "@react-router/fs-routes";
3
+
4
+ export default flatRoutes() satisfies RouteConfig;
@@ -0,0 +1,27 @@
1
+ {
2
+ "include": [
3
+ "**/*",
4
+ "**/.server/**/*",
5
+ "**/.client/**/*",
6
+ ".react-router/types/**/*"
7
+ ],
8
+ "compilerOptions": {
9
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
10
+ "types": ["node", "vite/client"],
11
+ "target": "ES2022",
12
+ "module": "ES2022",
13
+ "moduleResolution": "bundler",
14
+ "jsx": "react-jsx",
15
+ "rootDirs": [".", "./.react-router/types"],
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@/*": ["./src/*"]
19
+ },
20
+ "esModuleInterop": true,
21
+ "verbatimModuleSyntax": true,
22
+ "noEmit": true,
23
+ "resolveJsonModule": true,
24
+ "skipLibCheck": true,
25
+ "strict": true
26
+ }
27
+ }
@@ -0,0 +1,8 @@
1
+ import { reactRouter } from "@react-router/dev/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { defineConfig } from "vite";
4
+ import tsconfigPaths from "vite-tsconfig-paths";
5
+
6
+ export default defineConfig({
7
+ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
8
+ });
@@ -39,12 +39,12 @@
39
39
  "class-variance-authority": "^0.7.1",
40
40
  "clsx": "^2.1.1",
41
41
  "lucide-react": "^0.473.0",
42
- "next-themes": "^0.4.6",
42
+ "next-themes": "^0.4.6",
43
43
  "react": "^19.0.0",
44
44
  "react-dom": "^19.0.0",
45
45
  "sonner": "^1.7.4",
46
46
  "tailwind-merge": "^2.6.0",
47
- "tailwindcss-animate": "^1.0.7",
47
+ "tw-animate-css": "^1.2.5",
48
48
  "zod": "^3.24.2"
49
49
  }
50
50
  }
@@ -1,8 +1,6 @@
1
- import { Button } from "@/components/ui/button";
2
1
  import { trpc } from "@/utils/trpc";
3
2
  import { useQuery } from "@tanstack/react-query";
4
- import { Link, createFileRoute } from "@tanstack/react-router";
5
- import { ArrowRight } from "lucide-react";
3
+ import { createFileRoute } from "@tanstack/react-router";
6
4
 
7
5
  export const Route = createFileRoute("/")({
8
6
  component: HomeComponent,
@@ -68,7 +66,6 @@ function HomeComponent() {
68
66
  />
69
67
  </ul>
70
68
  </section>
71
- <div id="buttons" />
72
69
  </div>
73
70
  </div>
74
71
  );
@@ -0,0 +1,64 @@
1
+ import { useChat } from "@ai-sdk/react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Send } from "lucide-react";
5
+ import { useRef, useEffect } from "react";
6
+
7
+ export default function AI() {
8
+ const { messages, input, handleInputChange, handleSubmit } = useChat({
9
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
10
+ });
11
+
12
+ const messagesEndRef = useRef<HTMLDivElement>(null);
13
+
14
+ useEffect(() => {
15
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
16
+ }, [messages]);
17
+
18
+ return (
19
+ <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
20
+ <div className="overflow-y-auto space-y-4 pb-4">
21
+ {messages.length === 0 ? (
22
+ <div className="text-center text-muted-foreground mt-8">
23
+ Ask me anything to get started!
24
+ </div>
25
+ ) : (
26
+ messages.map((message) => (
27
+ <div
28
+ key={message.id}
29
+ className={`p-3 rounded-lg ${
30
+ message.role === "user"
31
+ ? "bg-primary/10 ml-8"
32
+ : "bg-secondary/20 mr-8"
33
+ }`}
34
+ >
35
+ <p className="text-sm font-semibold mb-1">
36
+ {message.role === "user" ? "You" : "AI Assistant"}
37
+ </p>
38
+ <div className="whitespace-pre-wrap">{message.content}</div>
39
+ </div>
40
+ ))
41
+ )}
42
+ <div ref={messagesEndRef} />
43
+ </div>
44
+
45
+ <form
46
+ onSubmit={handleSubmit}
47
+ className="w-full flex items-center space-x-2 pt-2 border-t"
48
+ >
49
+ <Input
50
+ name="prompt"
51
+ value={input}
52
+ onChange={handleInputChange}
53
+ placeholder="Type your message..."
54
+ className="flex-1"
55
+ autoComplete="off"
56
+ autoFocus
57
+ />
58
+ <Button type="submit" size="icon">
59
+ <Send size={18} />
60
+ </Button>
61
+ </form>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,130 @@
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 { useMutation, useQuery } from "@tanstack/react-query";
13
+ import { Loader2, Trash2 } from "lucide-react";
14
+ import { useState } from "react";
15
+
16
+ export default function Todos() {
17
+ const [newTodoText, setNewTodoText] = useState("");
18
+
19
+ const todos = useQuery(trpc.todo.getAll.queryOptions());
20
+ const createMutation = useMutation(
21
+ trpc.todo.create.mutationOptions({
22
+ onSuccess: () => {
23
+ todos.refetch();
24
+ setNewTodoText("");
25
+ },
26
+ })
27
+ );
28
+ const toggleMutation = useMutation(
29
+ trpc.todo.toggle.mutationOptions({
30
+ onSuccess: () => todos.refetch(),
31
+ })
32
+ );
33
+ const deleteMutation = useMutation(
34
+ trpc.todo.delete.mutationOptions({
35
+ onSuccess: () => todos.refetch(),
36
+ })
37
+ );
38
+
39
+ const handleAddTodo = (e: React.FormEvent) => {
40
+ e.preventDefault();
41
+ if (newTodoText.trim()) {
42
+ createMutation.mutate({ text: newTodoText });
43
+ }
44
+ };
45
+
46
+ const handleToggleTodo = (id: number, completed: boolean) => {
47
+ toggleMutation.mutate({ id, completed: !completed });
48
+ };
49
+
50
+ const handleDeleteTodo = (id: number) => {
51
+ deleteMutation.mutate({ id });
52
+ };
53
+
54
+ return (
55
+ <div className="w-full mx-auto max-w-md py-10">
56
+ <Card>
57
+ <CardHeader>
58
+ <CardTitle>Todo List</CardTitle>
59
+ <CardDescription>Manage your tasks efficiently</CardDescription>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <form
63
+ onSubmit={handleAddTodo}
64
+ className="mb-6 flex items-center space-x-2"
65
+ >
66
+ <Input
67
+ value={newTodoText}
68
+ onChange={(e) => setNewTodoText(e.target.value)}
69
+ placeholder="Add a new task..."
70
+ disabled={createMutation.isPending}
71
+ />
72
+ <Button
73
+ type="submit"
74
+ disabled={createMutation.isPending || !newTodoText.trim()}
75
+ >
76
+ {createMutation.isPending ? (
77
+ <Loader2 className="h-4 w-4 animate-spin" />
78
+ ) : (
79
+ "Add"
80
+ )}
81
+ </Button>
82
+ </form>
83
+
84
+ {todos.isLoading ? (
85
+ <div className="flex justify-center py-4">
86
+ <Loader2 className="h-6 w-6 animate-spin" />
87
+ </div>
88
+ ) : todos.data?.length === 0 ? (
89
+ <p className="py-4 text-center">
90
+ No todos yet. Add one above!
91
+ </p>
92
+ ) : (
93
+ <ul className="space-y-2">
94
+ {todos.data?.map((todo) => (
95
+ <li
96
+ key={todo.id}
97
+ className="flex items-center justify-between rounded-md border p-2"
98
+ >
99
+ <div className="flex items-center space-x-2">
100
+ <Checkbox
101
+ checked={todo.completed}
102
+ onCheckedChange={() =>
103
+ handleToggleTodo(todo.id, todo.completed)
104
+ }
105
+ id={`todo-${todo.id}`}
106
+ />
107
+ <label
108
+ htmlFor={`todo-${todo.id}`}
109
+ className={`${todo.completed ? "line-through" : ""}`}
110
+ >
111
+ {todo.text}
112
+ </label>
113
+ </div>
114
+ <Button
115
+ variant="ghost"
116
+ size="icon"
117
+ onClick={() => handleDeleteTodo(todo.id)}
118
+ aria-label="Delete todo"
119
+ >
120
+ <Trash2 className="h-4 w-4" />
121
+ </Button>
122
+ </li>
123
+ ))}
124
+ </ul>
125
+ )}
126
+ </CardContent>
127
+ </Card>
128
+ </div>
129
+ );
130
+ }
@@ -28,17 +28,17 @@ function TodosRoute() {
28
28
  todos.refetch();
29
29
  setNewTodoText("");
30
30
  },
31
- })
31
+ }),
32
32
  );
33
33
  const toggleMutation = useMutation(
34
34
  trpc.todo.toggle.mutationOptions({
35
35
  onSuccess: () => todos.refetch(),
36
- })
36
+ }),
37
37
  );
38
38
  const deleteMutation = useMutation(
39
39
  trpc.todo.delete.mutationOptions({
40
40
  onSuccess: () => todos.refetch(),
41
- })
41
+ }),
42
42
  );
43
43
 
44
44
  const handleAddTodo = (e: React.FormEvent) => {
@@ -57,7 +57,7 @@ function TodosRoute() {
57
57
  };
58
58
 
59
59
  return (
60
- <div className="container mx-auto max-w-md py-10">
60
+ <div className="mx-auto w-full max-w-md py-10">
61
61
  <Card>
62
62
  <CardHeader>
63
63
  <CardTitle>Todo List</CardTitle>
@@ -91,9 +91,7 @@ function TodosRoute() {
91
91
  <Loader2 className="h-6 w-6 animate-spin" />
92
92
  </div>
93
93
  ) : todos.data?.length === 0 ? (
94
- <p className="py-4 text-center text-muted-foreground">
95
- No todos yet. Add one above!
96
- </p>
94
+ <p className="py-4 text-center">No todos yet. Add one above!</p>
97
95
  ) : (
98
96
  <ul className="space-y-2">
99
97
  {todos.data?.map((todo) => (
@@ -111,7 +109,7 @@ function TodosRoute() {
111
109
  />
112
110
  <label
113
111
  htmlFor={`todo-${todo.id}`}
114
- className={`${todo.completed ? "text-muted-foreground line-through" : ""}`}
112
+ className={`${todo.completed ? "line-through" : ""}`}
115
113
  >
116
114
  {todo.text}
117
115
  </label>
@@ -0,0 +1,34 @@
1
+ import { NavLink } from "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
+ <NavLink
17
+ key={to}
18
+ to={to}
19
+ className={({ isActive }) => (isActive ? "font-bold" : "")}
20
+ end
21
+ >
22
+ {label}
23
+ </NavLink>
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
+ }