create-better-t-stack 3.9.0 → 3.11.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 +2 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -3
- package/dist/index.mjs +1 -1
- package/dist/{src-DLvUK0Qf.mjs → src-XVvJUQ_h.mjs} +270 -93
- package/package.json +44 -44
- package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
- package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
- package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
- package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
- package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
- package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
- package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
- package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
- package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
- package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/backend/convex/packages/backend/convex/README.md +4 -4
- package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
- package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
- package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
- package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
- package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
- package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
- package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
- package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
- package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
- package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
- package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
- package/templates/frontend/react/next/package.json.hbs +8 -7
- package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
- package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
- package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
- package/templates/frontend/react/react-router/package.json.hbs +2 -1
- package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
- package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
- package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
- package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
- package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
- package/templates/frontend/react/web-base/components.json +5 -2
- package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
- package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
- package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
- package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
- package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
- package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
- package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
- package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
- package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
- package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /package/templates/frontend/react/web-base/src/lib/{utils.ts → utils.ts.hbs} +0 -0
|
@@ -1,11 +1,156 @@
|
|
|
1
|
+
{{#if (eq backend "convex")}}
|
|
2
|
+
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
|
3
|
+
import {
|
|
4
|
+
useUIMessages,
|
|
5
|
+
useSmoothText,
|
|
6
|
+
type UIMessage,
|
|
7
|
+
} from "@convex-dev/agent/react";
|
|
8
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
9
|
+
import { useMutation } from "convex/react";
|
|
10
|
+
import { Send, Loader2 } from "lucide-react";
|
|
11
|
+
import { useRef, useEffect, useState } from "react";
|
|
12
|
+
import { Streamdown } from "streamdown";
|
|
13
|
+
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/ai")({
|
|
18
|
+
component: RouteComponent,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function MessageContent({
|
|
22
|
+
text,
|
|
23
|
+
isStreaming,
|
|
24
|
+
}: {
|
|
25
|
+
text: string;
|
|
26
|
+
isStreaming: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
const [visibleText] = useSmoothText(text, {
|
|
29
|
+
startStreaming: isStreaming,
|
|
30
|
+
});
|
|
31
|
+
return <Streamdown>{visibleText}</Streamdown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RouteComponent() {
|
|
35
|
+
const [input, setInput] = useState("");
|
|
36
|
+
const [threadId, setThreadId] = useState<string | null>(null);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
38
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
|
|
40
|
+
const createThread = useMutation(api.chat.createNewThread);
|
|
41
|
+
const sendMessage = useMutation(api.chat.sendMessage);
|
|
42
|
+
|
|
43
|
+
const { results: messages } = useUIMessages(
|
|
44
|
+
api.chat.listMessages,
|
|
45
|
+
threadId ? { threadId } : "skip",
|
|
46
|
+
{ initialNumItems: 50, stream: true },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
51
|
+
}, [messages]);
|
|
52
|
+
|
|
53
|
+
const hasStreamingMessage = messages?.some(
|
|
54
|
+
(m: UIMessage) => m.status === "streaming",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
const text = input.trim();
|
|
60
|
+
if (!text || isLoading) return;
|
|
61
|
+
|
|
62
|
+
setIsLoading(true);
|
|
63
|
+
setInput("");
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
let currentThreadId = threadId;
|
|
67
|
+
if (!currentThreadId) {
|
|
68
|
+
currentThreadId = await createThread();
|
|
69
|
+
setThreadId(currentThreadId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await sendMessage({ threadId: currentThreadId, prompt: text });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Failed to send message:", error);
|
|
75
|
+
} finally {
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
|
82
|
+
<div className="overflow-y-auto space-y-4 pb-4">
|
|
83
|
+
{!messages || messages.length === 0 ? (
|
|
84
|
+
<div className="text-center text-muted-foreground mt-8">
|
|
85
|
+
Ask me anything to get started!
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
messages.map((message: UIMessage) => (
|
|
89
|
+
<div
|
|
90
|
+
key={message.key}
|
|
91
|
+
className={`p-3 rounded-lg ${
|
|
92
|
+
message.role === "user"
|
|
93
|
+
? "bg-primary/10 ml-8"
|
|
94
|
+
: "bg-secondary/20 mr-8"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
<p className="text-sm font-semibold mb-1">
|
|
98
|
+
{message.role === "user" ? "You" : "AI Assistant"}
|
|
99
|
+
</p>
|
|
100
|
+
<MessageContent
|
|
101
|
+
text={message.text ?? ""}
|
|
102
|
+
isStreaming={message.status === "streaming"}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
))
|
|
106
|
+
)}
|
|
107
|
+
{isLoading && !hasStreamingMessage && (
|
|
108
|
+
<div className="p-3 rounded-lg bg-secondary/20 mr-8">
|
|
109
|
+
<p className="text-sm font-semibold mb-1">AI Assistant</p>
|
|
110
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
111
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
112
|
+
<span>Thinking...</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<div ref={messagesEndRef} />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<form
|
|
120
|
+
onSubmit={handleSubmit}
|
|
121
|
+
className="w-full flex items-center space-x-2 pt-2 border-t"
|
|
122
|
+
>
|
|
123
|
+
<Input
|
|
124
|
+
name="prompt"
|
|
125
|
+
value={input}
|
|
126
|
+
onChange={(e) => setInput(e.target.value)}
|
|
127
|
+
placeholder="Type your message..."
|
|
128
|
+
className="flex-1"
|
|
129
|
+
autoComplete="off"
|
|
130
|
+
autoFocus
|
|
131
|
+
disabled={isLoading}
|
|
132
|
+
/>
|
|
133
|
+
<Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
|
|
134
|
+
{isLoading ? (
|
|
135
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
136
|
+
) : (
|
|
137
|
+
<Send size={18} />
|
|
138
|
+
)}
|
|
139
|
+
</Button>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
{{else}}
|
|
1
145
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
146
|
import { useChat } from "@ai-sdk/react";
|
|
3
147
|
import { DefaultChatTransport } from "ai";
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
148
|
import { Send } from "lucide-react";
|
|
7
149
|
import { useRef, useEffect, useState } from "react";
|
|
8
|
-
import {
|
|
150
|
+
import { Streamdown } from "streamdown";
|
|
151
|
+
|
|
152
|
+
import { Button } from "@/components/ui/button";
|
|
153
|
+
import { Input } from "@/components/ui/input";
|
|
9
154
|
|
|
10
155
|
export const Route = createFileRoute("/ai")({
|
|
11
156
|
component: RouteComponent,
|
|
@@ -13,7 +158,7 @@ export const Route = createFileRoute("/ai")({
|
|
|
13
158
|
|
|
14
159
|
function RouteComponent() {
|
|
15
160
|
const [input, setInput] = useState("");
|
|
16
|
-
const { messages, sendMessage } = useChat({
|
|
161
|
+
const { messages, sendMessage, status } = useChat({
|
|
17
162
|
transport: new DefaultChatTransport({
|
|
18
163
|
api: {{#if (eq backend "self")}}"/api/ai"{{else}}`${import.meta.env.VITE_SERVER_URL}/ai`{{/if}},
|
|
19
164
|
}),
|
|
@@ -55,7 +200,14 @@ function RouteComponent() {
|
|
|
55
200
|
</p>
|
|
56
201
|
{message.parts?.map((part, index) => {
|
|
57
202
|
if (part.type === "text") {
|
|
58
|
-
return
|
|
203
|
+
return (
|
|
204
|
+
<Streamdown
|
|
205
|
+
key={index}
|
|
206
|
+
isAnimating={status === "streaming" && message.role === "assistant"}
|
|
207
|
+
>
|
|
208
|
+
{part.text}
|
|
209
|
+
</Streamdown>
|
|
210
|
+
);
|
|
59
211
|
}
|
|
60
212
|
return null;
|
|
61
213
|
})}
|
|
@@ -84,4 +236,5 @@ function RouteComponent() {
|
|
|
84
236
|
</form>
|
|
85
237
|
</div>
|
|
86
238
|
);
|
|
87
|
-
}
|
|
239
|
+
}
|
|
240
|
+
{{/if}}
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "next dev",
|
|
6
|
+
"dev": "next dev --port 3001",
|
|
7
7
|
"build": "next build",
|
|
8
8
|
"start": "next start"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"
|
|
11
|
+
"@base-ui/react": "^1.0.0",
|
|
12
|
+
"shadcn": "^3.6.2",
|
|
12
13
|
"@tanstack/react-form": "^1.27.3",
|
|
13
14
|
"class-variance-authority": "^0.7.1",
|
|
14
15
|
"clsx": "^2.1.1",
|
|
15
16
|
"lucide-react": "^0.546.0",
|
|
16
|
-
"next": "^16.0
|
|
17
|
+
"next": "^16.1.0",
|
|
17
18
|
"next-themes": "^0.4.6",
|
|
18
|
-
"react": "19.2.3",
|
|
19
|
-
"react-dom": "19.2.3",
|
|
19
|
+
"react": "^19.2.3",
|
|
20
|
+
"react-dom": "^19.2.3",
|
|
20
21
|
"sonner": "^2.0.5",
|
|
21
22
|
"tailwind-merge": "^3.3.1",
|
|
22
23
|
"tw-animate-css": "^1.3.4",
|
|
@@ -25,8 +26,8 @@
|
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@tailwindcss/postcss": "^4.1.10",
|
|
27
28
|
"@types/node": "^20",
|
|
28
|
-
"@types/react": "19.2.7",
|
|
29
|
-
"@types/react-dom": "19.2.3",
|
|
29
|
+
"@types/react": "^19.2.7",
|
|
30
|
+
"@types/react-dom": "^19.2.3",
|
|
30
31
|
"tailwindcss": "^4.1.10",
|
|
31
32
|
"typescript": "^5"
|
|
32
33
|
}
|
|
@@ -2,7 +2,10 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
3
|
import "../index.css";
|
|
4
4
|
{{#if (eq auth "clerk")}}{{#if (eq backend "convex")}}import { ClerkProvider } from "@clerk/nextjs";
|
|
5
|
-
{{/if}}{{/if}}
|
|
5
|
+
{{/if}}{{/if}}{{#if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
6
|
+
import { getToken } from "@/lib/auth-server";
|
|
7
|
+
{{/if}}
|
|
8
|
+
import Providers from "@/components/providers";
|
|
6
9
|
import Header from "@/components/header";
|
|
7
10
|
|
|
8
11
|
const geistSans = Geist({
|
|
@@ -20,6 +23,29 @@ export const metadata: Metadata = {
|
|
|
20
23
|
description: "{{projectName}}",
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
{{#if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
27
|
+
export default async function RootLayout({
|
|
28
|
+
children,
|
|
29
|
+
}: Readonly<{
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}>) {
|
|
32
|
+
const token = await getToken();
|
|
33
|
+
return (
|
|
34
|
+
<html lang="en" suppressHydrationWarning>
|
|
35
|
+
<body
|
|
36
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
37
|
+
>
|
|
38
|
+
<Providers initialToken={token}>
|
|
39
|
+
<div className="grid grid-rows-[auto_1fr] h-svh">
|
|
40
|
+
<Header />
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</Providers>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
{{else}}
|
|
23
49
|
export default function RootLayout({
|
|
24
50
|
children,
|
|
25
51
|
}: Readonly<{
|
|
@@ -47,3 +73,4 @@ export default function RootLayout({
|
|
|
47
73
|
</html>
|
|
48
74
|
);
|
|
49
75
|
}
|
|
76
|
+
{{/if}}
|
|
@@ -16,12 +16,10 @@ export function ModeToggle() {
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<DropdownMenu>
|
|
19
|
-
<DropdownMenuTrigger
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<span className="sr-only">Toggle theme</span>
|
|
24
|
-
</Button>
|
|
19
|
+
<DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
|
|
20
|
+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
21
|
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
22
|
+
<span className="sr-only">Toggle theme</span>
|
|
25
23
|
</DropdownMenuTrigger>
|
|
26
24
|
<DropdownMenuContent align="end">
|
|
27
25
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
@@ -6,7 +6,7 @@ import { useAuth } from "@clerk/nextjs";
|
|
|
6
6
|
import { ConvexReactClient } from "convex/react";
|
|
7
7
|
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
|
8
8
|
{{else if (eq auth "better-auth")}}
|
|
9
|
-
import {
|
|
9
|
+
import { ConvexReactClient } from "convex/react";
|
|
10
10
|
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
|
11
11
|
import { authClient } from "@/lib/auth-client";
|
|
12
12
|
{{else}}
|
|
@@ -32,9 +32,15 @@ const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
|
32
32
|
{{/if}}
|
|
33
33
|
|
|
34
34
|
export default function Providers({
|
|
35
|
-
children
|
|
35
|
+
children,
|
|
36
|
+
{{#if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
37
|
+
initialToken,
|
|
38
|
+
{{/if}}
|
|
36
39
|
}: {
|
|
37
|
-
children: React.ReactNode
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
{{#if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
42
|
+
initialToken?: string | null;
|
|
43
|
+
{{/if}}
|
|
38
44
|
}) {
|
|
39
45
|
return (
|
|
40
46
|
<ThemeProvider
|
|
@@ -49,7 +55,11 @@ export default function Providers({
|
|
|
49
55
|
{children}
|
|
50
56
|
</ConvexProviderWithClerk>
|
|
51
57
|
{{else if (eq auth "better-auth")}}
|
|
52
|
-
<ConvexBetterAuthProvider
|
|
58
|
+
<ConvexBetterAuthProvider
|
|
59
|
+
client={convex}
|
|
60
|
+
authClient={authClient}
|
|
61
|
+
initialToken={initialToken}
|
|
62
|
+
>
|
|
53
63
|
{children}
|
|
54
64
|
</ConvexBetterAuthProvider>
|
|
55
65
|
{{else}}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"typecheck": "react-router typegen && tsc"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"
|
|
12
|
+
"@base-ui/react": "^1.0.0",
|
|
13
|
+
"shadcn": "^3.6.2",
|
|
13
14
|
"@react-router/fs-routes": "^7.10.1",
|
|
14
15
|
"@react-router/node": "^7.10.1",
|
|
15
16
|
"@react-router/serve": "^7.10.1",
|
|
@@ -14,12 +14,10 @@ export function ModeToggle() {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<DropdownMenu>
|
|
17
|
-
<DropdownMenuTrigger
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<span className="sr-only">Toggle theme</span>
|
|
22
|
-
</Button>
|
|
17
|
+
<DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
|
|
18
|
+
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
19
|
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
20
|
+
<span className="sr-only">Toggle theme</span>
|
|
23
21
|
</DropdownMenuTrigger>
|
|
24
22
|
<DropdownMenuContent align="end">
|
|
25
23
|
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
|
@@ -14,12 +14,10 @@ export function ModeToggle() {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<DropdownMenu>
|
|
17
|
-
<DropdownMenuTrigger
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<span className="sr-only">Toggle theme</span>
|
|
22
|
-
</Button>
|
|
17
|
+
<DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
|
|
18
|
+
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
19
|
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
20
|
+
<span className="sr-only">Toggle theme</span>
|
|
23
21
|
</DropdownMenuTrigger>
|
|
24
22
|
<DropdownMenuContent align="end">
|
|
25
23
|
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
|
@@ -35,7 +35,13 @@ export function getRouter() {
|
|
|
35
35
|
unsavedChangesWarning: false,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
{{#if (eq auth "better-auth")}}
|
|
39
|
+
const convexQueryClient = new ConvexQueryClient(convex, {
|
|
40
|
+
expectAuth: true,
|
|
41
|
+
});
|
|
42
|
+
{{else}}
|
|
38
43
|
const convexQueryClient = new ConvexQueryClient(convex);
|
|
44
|
+
{{/if}}
|
|
39
45
|
|
|
40
46
|
const queryClient: QueryClient = new QueryClient({
|
|
41
47
|
defaultOptions: {
|
|
@@ -37,20 +37,12 @@ const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => {
|
|
|
37
37
|
});
|
|
38
38
|
{{else if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
39
39
|
import { createServerFn } from "@tanstack/react-start";
|
|
40
|
-
import { getRequest, getCookie } from "@tanstack/react-start/server";
|
|
41
40
|
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
|
42
|
-
import { fetchSession, getCookieName } from "@convex-dev/better-auth/react-start";
|
|
43
41
|
import { authClient } from "@/lib/auth-client";
|
|
44
|
-
import {
|
|
42
|
+
import { getToken } from "@/lib/auth-server";
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const sessionCookieName = getCookieName(createAuth);
|
|
49
|
-
const token = getCookie(sessionCookieName);
|
|
50
|
-
return {
|
|
51
|
-
userId: session?.user.id,
|
|
52
|
-
token,
|
|
53
|
-
};
|
|
44
|
+
const getAuth = createServerFn({ method: "GET" }).handler(async () => {
|
|
45
|
+
return await getToken();
|
|
54
46
|
});
|
|
55
47
|
{{/if}}
|
|
56
48
|
|
|
@@ -113,11 +105,14 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
|
|
113
105
|
},
|
|
114
106
|
{{else if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
115
107
|
beforeLoad: async (ctx) => {
|
|
116
|
-
const
|
|
108
|
+
const token = await getAuth();
|
|
117
109
|
if (token) {
|
|
118
110
|
ctx.context.convexQueryClient.serverHttpClient?.setAuth(token);
|
|
119
111
|
}
|
|
120
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
isAuthenticated: !!token,
|
|
114
|
+
token,
|
|
115
|
+
};
|
|
121
116
|
},
|
|
122
117
|
{{/if}}
|
|
123
118
|
});
|
|
@@ -148,7 +143,11 @@ function RootDocument() {
|
|
|
148
143
|
{{else if (and (eq backend "convex") (eq auth "better-auth"))}}
|
|
149
144
|
const context = useRouteContext({ from: Route.id });
|
|
150
145
|
return (
|
|
151
|
-
<ConvexBetterAuthProvider
|
|
146
|
+
<ConvexBetterAuthProvider
|
|
147
|
+
client={context.convexClient}
|
|
148
|
+
authClient={authClient}
|
|
149
|
+
initialToken={context.token}
|
|
150
|
+
>
|
|
152
151
|
<html lang="en" className="dark">
|
|
153
152
|
<head>
|
|
154
153
|
<HeadContent />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
-
"style": "
|
|
3
|
+
"style": "base-lyra",
|
|
4
4
|
"rsc": false,
|
|
5
5
|
"tsx": true,
|
|
6
6
|
"tailwind": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"cssVariables": true,
|
|
11
11
|
"prefix": ""
|
|
12
12
|
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
13
14
|
"aliases": {
|
|
14
15
|
"components": "@/components",
|
|
15
16
|
"utils": "@/lib/utils",
|
|
@@ -17,5 +18,7 @@
|
|
|
17
18
|
"lib": "@/lib",
|
|
18
19
|
"hooks": "@/hooks"
|
|
19
20
|
},
|
|
20
|
-
"
|
|
21
|
+
"menuColor": "default",
|
|
22
|
+
"menuAccent": "subtle",
|
|
23
|
+
"registries": {}
|
|
21
24
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from '@base-ui/react/button'
|
|
2
|
+
import { cva } from 'class-variance-authority'
|
|
3
|
+
import type {VariantProps} from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
13
|
+
outline:
|
|
14
|
+
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
|
15
|
+
secondary:
|
|
16
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
17
|
+
ghost:
|
|
18
|
+
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
|
19
|
+
destructive:
|
|
20
|
+
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default:
|
|
25
|
+
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
26
|
+
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
|
|
29
|
+
icon: 'size-8',
|
|
30
|
+
'icon-xs': "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
'icon-sm': 'size-7 rounded-none',
|
|
32
|
+
'icon-lg': 'size-9',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: 'default',
|
|
37
|
+
size: 'default',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
function Button({
|
|
43
|
+
className,
|
|
44
|
+
variant = 'default',
|
|
45
|
+
size = 'default',
|
|
46
|
+
...props
|
|
47
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
48
|
+
return (
|
|
49
|
+
<ButtonPrimitive
|
|
50
|
+
data-slot="button"
|
|
51
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = 'default',
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
14
|
+
className={cn(
|
|
15
|
+
'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
'gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
className={cn(
|
|
41
|
+
'text-sm font-medium group-data-[size=sm]/card:text-sm',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="card-description"
|
|
53
|
+
className={cn('text-muted-foreground text-xs/relaxed', className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
data-slot="card-action"
|
|
63
|
+
className={cn(
|
|
64
|
+
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="card-content"
|
|
76
|
+
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
data-slot="card-footer"
|
|
86
|
+
className={cn(
|
|
87
|
+
'rounded-none border-t p-4 group-data-[size=sm]/card:p-3 flex items-center',
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
Card,
|
|
97
|
+
CardHeader,
|
|
98
|
+
CardFooter,
|
|
99
|
+
CardTitle,
|
|
100
|
+
CardAction,
|
|
101
|
+
CardDescription,
|
|
102
|
+
CardContent,
|
|
103
|
+
}
|