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.
- package/dist/index.js +150 -98
- package/package.json +1 -1
- package/template/base/apps/{web → web-base}/_gitignore +3 -0
- package/template/base/apps/{web → web-base}/components.json +2 -2
- package/template/base/apps/{web → web-base}/src/components/mode-toggle.tsx +1 -1
- package/template/base/apps/{web → web-base}/src/components/theme-provider.tsx +1 -1
- package/template/base/apps/web-base/src/components/ui/button.tsx +59 -0
- package/template/base/apps/{web → web-base}/src/components/ui/card.tsx +10 -10
- package/template/base/apps/{web → web-base}/src/components/ui/checkbox.tsx +6 -6
- package/template/base/apps/web-base/src/components/ui/dropdown-menu.tsx +255 -0
- package/template/base/apps/web-base/src/components/ui/input.tsx +21 -0
- package/template/base/apps/web-base/src/components/ui/label.tsx +24 -0
- package/template/base/apps/web-base/src/components/ui/skeleton.tsx +13 -0
- package/template/base/apps/web-base/src/components/ui/sonner.tsx +23 -0
- package/template/base/apps/web-base/src/index.css +134 -0
- package/template/base/apps/web-react-router/package.json +49 -0
- package/template/base/apps/web-react-router/public/favicon.ico +0 -0
- package/template/base/apps/web-react-router/react-router.config.ts +6 -0
- package/template/base/apps/web-react-router/src/components/header.tsx +31 -0
- package/template/base/apps/web-react-router/src/root.tsx +91 -0
- package/template/base/apps/web-react-router/src/routes/_index.tsx +89 -0
- package/template/base/apps/web-react-router/src/routes.ts +4 -0
- package/template/base/apps/web-react-router/tsconfig.json +27 -0
- package/template/base/apps/web-react-router/vite.config.ts +8 -0
- package/template/base/apps/{web → web-tanstack-router}/package.json +2 -2
- package/template/base/apps/{web → web-tanstack-router}/src/routes/index.tsx +1 -4
- package/template/examples/ai/apps/web-react-router/src/routes/ai.tsx +64 -0
- package/template/examples/todo/apps/web-react-router/src/routes/todos.tsx +130 -0
- package/template/examples/todo/apps/{web → web-tanstack-router}/src/routes/todos.tsx +6 -8
- package/template/with-auth/apps/web-react-router/src/components/header.tsx +34 -0
- package/template/with-auth/apps/web-react-router/src/components/sign-in-form.tsx +135 -0
- package/template/with-auth/apps/web-react-router/src/components/sign-up-form.tsx +160 -0
- package/template/with-auth/apps/web-react-router/src/components/user-menu.tsx +60 -0
- package/template/with-auth/apps/web-react-router/src/routes/dashboard.tsx +30 -0
- package/template/with-auth/apps/web-react-router/src/routes/login.tsx +13 -0
- package/template/base/apps/web/src/components/ui/button.tsx +0 -57
- package/template/base/apps/web/src/components/ui/dropdown-menu.tsx +0 -199
- package/template/base/apps/web/src/components/ui/input.tsx +0 -22
- package/template/base/apps/web/src/components/ui/label.tsx +0 -24
- package/template/base/apps/web/src/components/ui/skeleton.tsx +0 -15
- package/template/base/apps/web/src/components/ui/sonner.tsx +0 -29
- package/template/base/apps/web/src/index.css +0 -119
- package/template/with-pwa/apps/web/vite.config.ts +0 -35
- /package/template/base/apps/{web → web-base}/src/components/loader.tsx +0 -0
- /package/template/base/apps/{web → web-base}/src/lib/utils.ts +0 -0
- /package/template/base/apps/{web → web-base}/src/utils/trpc.ts +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/index.html +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/src/components/header.tsx +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/src/main.tsx +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/src/routes/__root.tsx +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/tsconfig.json +0 -0
- /package/template/base/apps/{web → web-tanstack-router}/vite.config.ts +0 -0
- /package/template/examples/ai/apps/{web → web-tanstack-router}/src/routes/ai.tsx +0 -0
- /package/template/with-auth/apps/{web → web-base}/src/lib/auth-client.ts +0 -0
- /package/template/with-auth/apps/{web → web-base}/src/utils/trpc.ts +0 -0
- /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/header.tsx +0 -0
- /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/sign-in-form.tsx +0 -0
- /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/sign-up-form.tsx +0 -0
- /package/template/with-auth/apps/{web → web-tanstack-router}/src/components/user-menu.tsx +0 -0
- /package/template/with-auth/apps/{web → web-tanstack-router}/src/routes/dashboard.tsx +0 -0
- /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
|
+
}
|
|
Binary file
|
|
@@ -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,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
|
-
|
|
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
|
-
"
|
|
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 {
|
|
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="
|
|
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
|
|
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 ? "
|
|
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
|
+
}
|