@beignet/cli 0.0.3 → 0.0.5
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/CHANGELOG.md +219 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
import { packageRunner, } from "./shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* Application shell for generated apps: root layout and providers, public
|
|
4
|
+
* landing page, auth pages, the authenticated sidebar shell, settings pages,
|
|
5
|
+
* the typed client setup, and the shadcn-styled todos worked example.
|
|
6
|
+
*
|
|
7
|
+
* These templates assume the shadcn stack from `shadcn.ts` is installed
|
|
8
|
+
* alongside them.
|
|
9
|
+
*/
|
|
10
|
+
const appProviders = `"use client";
|
|
11
|
+
|
|
12
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
13
|
+
import { type ReactNode, useState } from "react";
|
|
14
|
+
import { makeQueryClient } from "@/client";
|
|
15
|
+
|
|
16
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
17
|
+
const [queryClient] = useState(() => makeQueryClient());
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
const rootLayout = `import "./globals.css";
|
|
25
|
+
import type { Metadata } from "next";
|
|
26
|
+
import type { ReactNode } from "react";
|
|
27
|
+
import { ThemeProvider } from "@/components/theme-provider";
|
|
28
|
+
import { Providers } from "./providers";
|
|
29
|
+
|
|
30
|
+
export const metadata: Metadata = {
|
|
31
|
+
title: "Beignet app",
|
|
32
|
+
description: "A contract-first Next.js app.",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
36
|
+
return (
|
|
37
|
+
<html lang="en" suppressHydrationWarning>
|
|
38
|
+
<body>
|
|
39
|
+
<ThemeProvider
|
|
40
|
+
attribute="class"
|
|
41
|
+
defaultTheme="system"
|
|
42
|
+
enableSystem
|
|
43
|
+
disableTransitionOnChange
|
|
44
|
+
>
|
|
45
|
+
<Providers>{children}</Providers>
|
|
46
|
+
</ThemeProvider>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
const landingPage = `import Link from "next/link";
|
|
53
|
+
import { Button } from "@/components/ui/button";
|
|
54
|
+
|
|
55
|
+
export default function HomePage() {
|
|
56
|
+
return (
|
|
57
|
+
<main className="flex min-h-svh flex-col">
|
|
58
|
+
<header className="flex items-center justify-between px-6 py-4">
|
|
59
|
+
<span className="font-heading text-sm font-semibold">Beignet app</span>
|
|
60
|
+
<nav className="flex items-center gap-2" aria-label="Account">
|
|
61
|
+
<Button asChild variant="ghost">
|
|
62
|
+
<Link href="/sign-in">Sign in</Link>
|
|
63
|
+
</Button>
|
|
64
|
+
<Button asChild>
|
|
65
|
+
<Link href="/sign-up">Create account</Link>
|
|
66
|
+
</Button>
|
|
67
|
+
</nav>
|
|
68
|
+
</header>
|
|
69
|
+
<section className="flex flex-1 flex-col items-center justify-center px-6 py-16 text-center">
|
|
70
|
+
<p className="text-[13px] font-bold tracking-[0.12em] text-primary uppercase">
|
|
71
|
+
Beignet starter
|
|
72
|
+
</p>
|
|
73
|
+
<h1 className="mt-3 max-w-xl font-heading text-4xl font-semibold tracking-tight">
|
|
74
|
+
Type-safe full-stack workflows without codegen.
|
|
75
|
+
</h1>
|
|
76
|
+
<p className="mt-4 max-w-lg text-muted-foreground">
|
|
77
|
+
Contracts, use cases, ports, a typed client, React Query, and form
|
|
78
|
+
validation are wired together so you can start shipping product code
|
|
79
|
+
immediately.
|
|
80
|
+
</p>
|
|
81
|
+
<div className="mt-8 flex gap-3">
|
|
82
|
+
<Button asChild size="lg">
|
|
83
|
+
<Link href="/sign-up">Get started</Link>
|
|
84
|
+
</Button>
|
|
85
|
+
<Button asChild size="lg" variant="outline">
|
|
86
|
+
<Link href="/sign-in">Sign in</Link>
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
<nav
|
|
90
|
+
className="mt-10 flex gap-5 text-sm text-muted-foreground"
|
|
91
|
+
aria-label="Starter links"
|
|
92
|
+
>
|
|
93
|
+
<a className="hover:text-foreground" href="/api/health">
|
|
94
|
+
Health
|
|
95
|
+
</a>
|
|
96
|
+
<a className="hover:text-foreground" href="/api/openapi">
|
|
97
|
+
OpenAPI
|
|
98
|
+
</a>
|
|
99
|
+
<a className="hover:text-foreground" href="https://beignetjs.com">
|
|
100
|
+
Docs
|
|
101
|
+
</a>
|
|
102
|
+
</nav>
|
|
103
|
+
</section>
|
|
104
|
+
</main>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
const authLayout = `import type { ReactNode } from "react";
|
|
109
|
+
|
|
110
|
+
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
111
|
+
return (
|
|
112
|
+
<main className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
|
|
113
|
+
<div className="w-full max-w-sm">{children}</div>
|
|
114
|
+
</main>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
const signInPage = `"use client";
|
|
119
|
+
|
|
120
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
121
|
+
import Link from "next/link";
|
|
122
|
+
import { useRouter } from "next/navigation";
|
|
123
|
+
import { useForm } from "react-hook-form";
|
|
124
|
+
import { authClient } from "@/client/auth-client";
|
|
125
|
+
import { Button } from "@/components/ui/button";
|
|
126
|
+
import {
|
|
127
|
+
Card,
|
|
128
|
+
CardContent,
|
|
129
|
+
CardDescription,
|
|
130
|
+
CardHeader,
|
|
131
|
+
CardTitle,
|
|
132
|
+
} from "@/components/ui/card";
|
|
133
|
+
import { Input } from "@/components/ui/input";
|
|
134
|
+
import { Label } from "@/components/ui/label";
|
|
135
|
+
|
|
136
|
+
type SignInValues = {
|
|
137
|
+
email: string;
|
|
138
|
+
password: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default function SignInPage() {
|
|
142
|
+
const router = useRouter();
|
|
143
|
+
const form = useForm<SignInValues>({
|
|
144
|
+
defaultValues: { email: "", password: "" },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
148
|
+
form.clearErrors("root");
|
|
149
|
+
const result = await authClient.signIn.email({
|
|
150
|
+
email: values.email,
|
|
151
|
+
password: values.password,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
form.setError(
|
|
156
|
+
"root",
|
|
157
|
+
rootFormError(result.error, result.error.message || "Could not sign in."),
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
router.push("/dashboard");
|
|
163
|
+
router.refresh();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card>
|
|
168
|
+
<CardHeader>
|
|
169
|
+
<CardTitle>Sign in</CardTitle>
|
|
170
|
+
<CardDescription>Welcome back. Enter your details below.</CardDescription>
|
|
171
|
+
</CardHeader>
|
|
172
|
+
<CardContent>
|
|
173
|
+
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
|
174
|
+
<div className="flex flex-col gap-2">
|
|
175
|
+
<Label htmlFor="email">Email</Label>
|
|
176
|
+
<Input
|
|
177
|
+
id="email"
|
|
178
|
+
type="email"
|
|
179
|
+
autoComplete="email"
|
|
180
|
+
placeholder="you@example.com"
|
|
181
|
+
required
|
|
182
|
+
{...form.register("email")}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="flex flex-col gap-2">
|
|
186
|
+
<Label htmlFor="password">Password</Label>
|
|
187
|
+
<Input
|
|
188
|
+
id="password"
|
|
189
|
+
type="password"
|
|
190
|
+
autoComplete="current-password"
|
|
191
|
+
required
|
|
192
|
+
{...form.register("password")}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
{form.formState.errors.root ? (
|
|
196
|
+
<p className="text-sm text-destructive">
|
|
197
|
+
{form.formState.errors.root.message}
|
|
198
|
+
</p>
|
|
199
|
+
) : null}
|
|
200
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
201
|
+
{form.formState.isSubmitting ? "Signing in..." : "Sign in"}
|
|
202
|
+
</Button>
|
|
203
|
+
</form>
|
|
204
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
205
|
+
No account yet?{" "}
|
|
206
|
+
<Link
|
|
207
|
+
className="text-foreground underline underline-offset-4"
|
|
208
|
+
href="/sign-up"
|
|
209
|
+
>
|
|
210
|
+
Create one
|
|
211
|
+
</Link>
|
|
212
|
+
</p>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
const signUpPage = `"use client";
|
|
219
|
+
|
|
220
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
221
|
+
import Link from "next/link";
|
|
222
|
+
import { useRouter } from "next/navigation";
|
|
223
|
+
import { useForm } from "react-hook-form";
|
|
224
|
+
import { authClient } from "@/client/auth-client";
|
|
225
|
+
import { Button } from "@/components/ui/button";
|
|
226
|
+
import {
|
|
227
|
+
Card,
|
|
228
|
+
CardContent,
|
|
229
|
+
CardDescription,
|
|
230
|
+
CardHeader,
|
|
231
|
+
CardTitle,
|
|
232
|
+
} from "@/components/ui/card";
|
|
233
|
+
import { Input } from "@/components/ui/input";
|
|
234
|
+
import { Label } from "@/components/ui/label";
|
|
235
|
+
|
|
236
|
+
type SignUpValues = {
|
|
237
|
+
name: string;
|
|
238
|
+
email: string;
|
|
239
|
+
password: string;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export default function SignUpPage() {
|
|
243
|
+
const router = useRouter();
|
|
244
|
+
const form = useForm<SignUpValues>({
|
|
245
|
+
defaultValues: { name: "", email: "", password: "" },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
249
|
+
form.clearErrors("root");
|
|
250
|
+
const result = await authClient.signUp.email({
|
|
251
|
+
name: values.name,
|
|
252
|
+
email: values.email,
|
|
253
|
+
password: values.password,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (result.error) {
|
|
257
|
+
form.setError(
|
|
258
|
+
"root",
|
|
259
|
+
rootFormError(
|
|
260
|
+
result.error,
|
|
261
|
+
result.error.message || "Could not create your account.",
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
router.push("/dashboard");
|
|
268
|
+
router.refresh();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Card>
|
|
273
|
+
<CardHeader>
|
|
274
|
+
<CardTitle>Create account</CardTitle>
|
|
275
|
+
<CardDescription>Start building in a few seconds.</CardDescription>
|
|
276
|
+
</CardHeader>
|
|
277
|
+
<CardContent>
|
|
278
|
+
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
|
279
|
+
<div className="flex flex-col gap-2">
|
|
280
|
+
<Label htmlFor="name">Name</Label>
|
|
281
|
+
<Input
|
|
282
|
+
id="name"
|
|
283
|
+
autoComplete="name"
|
|
284
|
+
placeholder="Ada Lovelace"
|
|
285
|
+
required
|
|
286
|
+
{...form.register("name")}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="flex flex-col gap-2">
|
|
290
|
+
<Label htmlFor="email">Email</Label>
|
|
291
|
+
<Input
|
|
292
|
+
id="email"
|
|
293
|
+
type="email"
|
|
294
|
+
autoComplete="email"
|
|
295
|
+
placeholder="you@example.com"
|
|
296
|
+
required
|
|
297
|
+
{...form.register("email")}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="flex flex-col gap-2">
|
|
301
|
+
<Label htmlFor="password">Password</Label>
|
|
302
|
+
<Input
|
|
303
|
+
id="password"
|
|
304
|
+
type="password"
|
|
305
|
+
autoComplete="new-password"
|
|
306
|
+
minLength={8}
|
|
307
|
+
required
|
|
308
|
+
{...form.register("password")}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
{form.formState.errors.root ? (
|
|
312
|
+
<p className="text-sm text-destructive">
|
|
313
|
+
{form.formState.errors.root.message}
|
|
314
|
+
</p>
|
|
315
|
+
) : null}
|
|
316
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
317
|
+
{form.formState.isSubmitting ? "Creating account..." : "Create account"}
|
|
318
|
+
</Button>
|
|
319
|
+
</form>
|
|
320
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
321
|
+
Already have an account?{" "}
|
|
322
|
+
<Link
|
|
323
|
+
className="text-foreground underline underline-offset-4"
|
|
324
|
+
href="/sign-in"
|
|
325
|
+
>
|
|
326
|
+
Sign in
|
|
327
|
+
</Link>
|
|
328
|
+
</p>
|
|
329
|
+
</CardContent>
|
|
330
|
+
</Card>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
const appLayout = `import { headers } from "next/headers";
|
|
335
|
+
import { redirect } from "next/navigation";
|
|
336
|
+
import type { ReactNode } from "react";
|
|
337
|
+
import { AppSidebar } from "@/components/app-sidebar";
|
|
338
|
+
import { auth } from "@/lib/better-auth";
|
|
339
|
+
import { isDevtoolsEnabled } from "@/lib/env";
|
|
340
|
+
|
|
341
|
+
export default async function AppLayout({ children }: { children: ReactNode }) {
|
|
342
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
343
|
+
|
|
344
|
+
if (!session) {
|
|
345
|
+
redirect("/sign-in");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="flex min-h-svh">
|
|
350
|
+
<AppSidebar
|
|
351
|
+
user={{ name: session.user.name, email: session.user.email }}
|
|
352
|
+
showDevtools={isDevtoolsEnabled}
|
|
353
|
+
showOpenapi={true}
|
|
354
|
+
/>
|
|
355
|
+
<main className="flex-1">
|
|
356
|
+
<div className="mx-auto w-full max-w-3xl px-6 py-8">{children}</div>
|
|
357
|
+
</main>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
function dashboardPage(ctx) {
|
|
363
|
+
return `import { headers } from "next/headers";
|
|
364
|
+
import Link from "next/link";
|
|
365
|
+
import {
|
|
366
|
+
Card,
|
|
367
|
+
CardContent,
|
|
368
|
+
CardDescription,
|
|
369
|
+
CardHeader,
|
|
370
|
+
CardTitle,
|
|
371
|
+
} from "@/components/ui/card";
|
|
372
|
+
import { auth } from "@/lib/better-auth";
|
|
373
|
+
|
|
374
|
+
export default async function DashboardPage() {
|
|
375
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
376
|
+
const name = session?.user.name ?? "there";
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="flex flex-col gap-6">
|
|
380
|
+
<div className="flex flex-col gap-1">
|
|
381
|
+
<h1 className="font-heading text-xl font-semibold">Welcome, {name}</h1>
|
|
382
|
+
<p className="text-sm text-muted-foreground">
|
|
383
|
+
Your app is running. Here is where to go next.
|
|
384
|
+
</p>
|
|
385
|
+
</div>
|
|
386
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
387
|
+
<Link href="/todos" className="group">
|
|
388
|
+
<Card className="h-full transition-shadow group-hover:ring-foreground/20">
|
|
389
|
+
<CardHeader>
|
|
390
|
+
<CardTitle>Manage your todos</CardTitle>
|
|
391
|
+
<CardDescription>
|
|
392
|
+
The worked example: a contract-backed API with a typed client,
|
|
393
|
+
React Query, and form validation.
|
|
394
|
+
</CardDescription>
|
|
395
|
+
</CardHeader>
|
|
396
|
+
</Card>
|
|
397
|
+
</Link>
|
|
398
|
+
<Card>
|
|
399
|
+
<CardHeader>
|
|
400
|
+
<CardTitle>Next steps</CardTitle>
|
|
401
|
+
<CardDescription>Make this starter your own.</CardDescription>
|
|
402
|
+
</CardHeader>
|
|
403
|
+
<CardContent className="flex flex-col gap-2 text-sm">
|
|
404
|
+
<code className="rounded-lg bg-muted px-2 py-1.5 font-mono text-xs">
|
|
405
|
+
${packageRunner(ctx)} make feature
|
|
406
|
+
</code>
|
|
407
|
+
<p className="text-muted-foreground">
|
|
408
|
+
Scaffold your next feature, then read the docs at{" "}
|
|
409
|
+
<a
|
|
410
|
+
className="text-foreground underline underline-offset-4"
|
|
411
|
+
href="https://beignetjs.com"
|
|
412
|
+
>
|
|
413
|
+
beignetjs.com
|
|
414
|
+
</a>
|
|
415
|
+
.
|
|
416
|
+
</p>
|
|
417
|
+
</CardContent>
|
|
418
|
+
</Card>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
const todosPage = `import { TodoApp } from "@/features/todos/components/todo-app";
|
|
426
|
+
|
|
427
|
+
export default function TodosPage() {
|
|
428
|
+
return (
|
|
429
|
+
<div className="flex flex-col gap-6">
|
|
430
|
+
<div className="flex flex-col gap-1">
|
|
431
|
+
<h1 className="font-heading text-xl font-semibold">Todos</h1>
|
|
432
|
+
<p className="text-sm text-muted-foreground">
|
|
433
|
+
A contract-backed feature with a typed client, React Query, and form
|
|
434
|
+
validation.
|
|
435
|
+
</p>
|
|
436
|
+
</div>
|
|
437
|
+
<TodoApp />
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
`;
|
|
442
|
+
const settingsLayout = `import type { ReactNode } from "react";
|
|
443
|
+
import { SettingsNav } from "@/components/settings-nav";
|
|
444
|
+
|
|
445
|
+
export default function SettingsLayout({ children }: { children: ReactNode }) {
|
|
446
|
+
return (
|
|
447
|
+
<div className="flex flex-col gap-6">
|
|
448
|
+
<div className="flex flex-col gap-1">
|
|
449
|
+
<h1 className="font-heading text-xl font-semibold">Settings</h1>
|
|
450
|
+
<p className="text-sm text-muted-foreground">
|
|
451
|
+
Manage your account and app preferences.
|
|
452
|
+
</p>
|
|
453
|
+
</div>
|
|
454
|
+
<SettingsNav />
|
|
455
|
+
{children}
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
const settingsProfilePage = `"use client";
|
|
461
|
+
|
|
462
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
463
|
+
import { useState } from "react";
|
|
464
|
+
import { useForm } from "react-hook-form";
|
|
465
|
+
import { authClient } from "@/client/auth-client";
|
|
466
|
+
import { Button } from "@/components/ui/button";
|
|
467
|
+
import {
|
|
468
|
+
Card,
|
|
469
|
+
CardContent,
|
|
470
|
+
CardDescription,
|
|
471
|
+
CardHeader,
|
|
472
|
+
CardTitle,
|
|
473
|
+
} from "@/components/ui/card";
|
|
474
|
+
import { Input } from "@/components/ui/input";
|
|
475
|
+
import { Label } from "@/components/ui/label";
|
|
476
|
+
|
|
477
|
+
type ProfileValues = {
|
|
478
|
+
name: string;
|
|
479
|
+
email: string;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export default function SettingsProfilePage() {
|
|
483
|
+
const session = authClient.useSession();
|
|
484
|
+
const [saved, setSaved] = useState(false);
|
|
485
|
+
const form = useForm<ProfileValues>({
|
|
486
|
+
defaultValues: { name: "", email: "" },
|
|
487
|
+
values: {
|
|
488
|
+
name: session.data?.user.name ?? "",
|
|
489
|
+
email: session.data?.user.email ?? "",
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
494
|
+
form.clearErrors("root");
|
|
495
|
+
setSaved(false);
|
|
496
|
+
|
|
497
|
+
const updated = await authClient.updateUser({ name: values.name });
|
|
498
|
+
if (updated.error) {
|
|
499
|
+
form.setError(
|
|
500
|
+
"root",
|
|
501
|
+
rootFormError(
|
|
502
|
+
updated.error,
|
|
503
|
+
updated.error.message || "Could not update your profile.",
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (values.email !== session.data?.user.email) {
|
|
510
|
+
const changed = await authClient.changeEmail({ newEmail: values.email });
|
|
511
|
+
if (changed.error) {
|
|
512
|
+
form.setError(
|
|
513
|
+
"root",
|
|
514
|
+
rootFormError(
|
|
515
|
+
changed.error,
|
|
516
|
+
changed.error.message || "Could not change your email.",
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await session.refetch();
|
|
524
|
+
setSaved(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<Card>
|
|
529
|
+
<CardHeader>
|
|
530
|
+
<CardTitle>Profile</CardTitle>
|
|
531
|
+
<CardDescription>Update your name and email address.</CardDescription>
|
|
532
|
+
</CardHeader>
|
|
533
|
+
<CardContent>
|
|
534
|
+
<form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
|
|
535
|
+
<div className="flex flex-col gap-2">
|
|
536
|
+
<Label htmlFor="name">Name</Label>
|
|
537
|
+
<Input
|
|
538
|
+
id="name"
|
|
539
|
+
autoComplete="name"
|
|
540
|
+
required
|
|
541
|
+
{...form.register("name")}
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
<div className="flex flex-col gap-2">
|
|
545
|
+
<Label htmlFor="email">Email</Label>
|
|
546
|
+
<Input
|
|
547
|
+
id="email"
|
|
548
|
+
type="email"
|
|
549
|
+
autoComplete="email"
|
|
550
|
+
required
|
|
551
|
+
{...form.register("email")}
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
{form.formState.errors.root ? (
|
|
555
|
+
<p className="text-sm text-destructive">
|
|
556
|
+
{form.formState.errors.root.message}
|
|
557
|
+
</p>
|
|
558
|
+
) : null}
|
|
559
|
+
<div className="flex items-center gap-3">
|
|
560
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
561
|
+
{form.formState.isSubmitting ? "Saving..." : "Save changes"}
|
|
562
|
+
</Button>
|
|
563
|
+
{saved ? (
|
|
564
|
+
<p className="text-sm text-muted-foreground">Saved.</p>
|
|
565
|
+
) : null}
|
|
566
|
+
</div>
|
|
567
|
+
</form>
|
|
568
|
+
</CardContent>
|
|
569
|
+
</Card>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
`;
|
|
573
|
+
const settingsPasswordPage = `"use client";
|
|
574
|
+
|
|
575
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
576
|
+
import { useState } from "react";
|
|
577
|
+
import { useForm } from "react-hook-form";
|
|
578
|
+
import { authClient } from "@/client/auth-client";
|
|
579
|
+
import { Button } from "@/components/ui/button";
|
|
580
|
+
import {
|
|
581
|
+
Card,
|
|
582
|
+
CardContent,
|
|
583
|
+
CardDescription,
|
|
584
|
+
CardHeader,
|
|
585
|
+
CardTitle,
|
|
586
|
+
} from "@/components/ui/card";
|
|
587
|
+
import { Input } from "@/components/ui/input";
|
|
588
|
+
import { Label } from "@/components/ui/label";
|
|
589
|
+
|
|
590
|
+
type PasswordValues = {
|
|
591
|
+
currentPassword: string;
|
|
592
|
+
newPassword: string;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
export default function SettingsPasswordPage() {
|
|
596
|
+
const [saved, setSaved] = useState(false);
|
|
597
|
+
const form = useForm<PasswordValues>({
|
|
598
|
+
defaultValues: { currentPassword: "", newPassword: "" },
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
602
|
+
form.clearErrors("root");
|
|
603
|
+
setSaved(false);
|
|
604
|
+
|
|
605
|
+
const result = await authClient.changePassword({
|
|
606
|
+
currentPassword: values.currentPassword,
|
|
607
|
+
newPassword: values.newPassword,
|
|
608
|
+
revokeOtherSessions: true,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (result.error) {
|
|
612
|
+
form.setError(
|
|
613
|
+
"root",
|
|
614
|
+
rootFormError(
|
|
615
|
+
result.error,
|
|
616
|
+
result.error.message || "Could not change your password.",
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
form.reset();
|
|
623
|
+
setSaved(true);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
<Card>
|
|
628
|
+
<CardHeader>
|
|
629
|
+
<CardTitle>Password</CardTitle>
|
|
630
|
+
<CardDescription>
|
|
631
|
+
Changing your password signs you out everywhere else.
|
|
632
|
+
</CardDescription>
|
|
633
|
+
</CardHeader>
|
|
634
|
+
<CardContent>
|
|
635
|
+
<form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
|
|
636
|
+
<div className="flex flex-col gap-2">
|
|
637
|
+
<Label htmlFor="current-password">Current password</Label>
|
|
638
|
+
<Input
|
|
639
|
+
id="current-password"
|
|
640
|
+
type="password"
|
|
641
|
+
autoComplete="current-password"
|
|
642
|
+
required
|
|
643
|
+
{...form.register("currentPassword")}
|
|
644
|
+
/>
|
|
645
|
+
</div>
|
|
646
|
+
<div className="flex flex-col gap-2">
|
|
647
|
+
<Label htmlFor="new-password">New password</Label>
|
|
648
|
+
<Input
|
|
649
|
+
id="new-password"
|
|
650
|
+
type="password"
|
|
651
|
+
autoComplete="new-password"
|
|
652
|
+
minLength={8}
|
|
653
|
+
required
|
|
654
|
+
{...form.register("newPassword")}
|
|
655
|
+
/>
|
|
656
|
+
</div>
|
|
657
|
+
{form.formState.errors.root ? (
|
|
658
|
+
<p className="text-sm text-destructive">
|
|
659
|
+
{form.formState.errors.root.message}
|
|
660
|
+
</p>
|
|
661
|
+
) : null}
|
|
662
|
+
<div className="flex items-center gap-3">
|
|
663
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
664
|
+
{form.formState.isSubmitting ? "Saving..." : "Change password"}
|
|
665
|
+
</Button>
|
|
666
|
+
{saved ? (
|
|
667
|
+
<p className="text-sm text-muted-foreground">Password changed.</p>
|
|
668
|
+
) : null}
|
|
669
|
+
</div>
|
|
670
|
+
</form>
|
|
671
|
+
</CardContent>
|
|
672
|
+
</Card>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
`;
|
|
676
|
+
const settingsAppearancePage = `import {
|
|
677
|
+
Card,
|
|
678
|
+
CardAction,
|
|
679
|
+
CardDescription,
|
|
680
|
+
CardHeader,
|
|
681
|
+
CardTitle,
|
|
682
|
+
} from "@/components/ui/card";
|
|
683
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
684
|
+
|
|
685
|
+
export default function SettingsAppearancePage() {
|
|
686
|
+
return (
|
|
687
|
+
<Card>
|
|
688
|
+
<CardHeader>
|
|
689
|
+
<CardTitle>Appearance</CardTitle>
|
|
690
|
+
<CardDescription>
|
|
691
|
+
Choose light, dark, or follow your system preference. The choice is
|
|
692
|
+
saved on this device.
|
|
693
|
+
</CardDescription>
|
|
694
|
+
<CardAction>
|
|
695
|
+
<ThemeToggle />
|
|
696
|
+
</CardAction>
|
|
697
|
+
</CardHeader>
|
|
698
|
+
</Card>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
`;
|
|
702
|
+
const settingsAccountPage = `"use client";
|
|
703
|
+
|
|
704
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
705
|
+
import { useRouter } from "next/navigation";
|
|
706
|
+
import { useForm } from "react-hook-form";
|
|
707
|
+
import { authClient } from "@/client/auth-client";
|
|
708
|
+
import { Button } from "@/components/ui/button";
|
|
709
|
+
import {
|
|
710
|
+
Card,
|
|
711
|
+
CardContent,
|
|
712
|
+
CardDescription,
|
|
713
|
+
CardHeader,
|
|
714
|
+
CardTitle,
|
|
715
|
+
} from "@/components/ui/card";
|
|
716
|
+
import { Input } from "@/components/ui/input";
|
|
717
|
+
import { Label } from "@/components/ui/label";
|
|
718
|
+
|
|
719
|
+
type DeleteAccountValues = {
|
|
720
|
+
password: string;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
export default function SettingsAccountPage() {
|
|
724
|
+
const router = useRouter();
|
|
725
|
+
const form = useForm<DeleteAccountValues>({
|
|
726
|
+
defaultValues: { password: "" },
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
730
|
+
form.clearErrors("root");
|
|
731
|
+
const result = await authClient.deleteUser({ password: values.password });
|
|
732
|
+
|
|
733
|
+
if (result.error) {
|
|
734
|
+
form.setError(
|
|
735
|
+
"root",
|
|
736
|
+
rootFormError(
|
|
737
|
+
result.error,
|
|
738
|
+
result.error.message || "Could not delete your account.",
|
|
739
|
+
),
|
|
740
|
+
);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
router.push("/");
|
|
745
|
+
router.refresh();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<Card>
|
|
750
|
+
<CardHeader>
|
|
751
|
+
<CardTitle className="text-destructive">Delete account</CardTitle>
|
|
752
|
+
<CardDescription>
|
|
753
|
+
Permanently delete your account and all of its data. This cannot be
|
|
754
|
+
undone.
|
|
755
|
+
</CardDescription>
|
|
756
|
+
</CardHeader>
|
|
757
|
+
<CardContent>
|
|
758
|
+
<form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
|
|
759
|
+
<div className="flex flex-col gap-2">
|
|
760
|
+
<Label htmlFor="password">Confirm your password</Label>
|
|
761
|
+
<Input
|
|
762
|
+
id="password"
|
|
763
|
+
type="password"
|
|
764
|
+
autoComplete="current-password"
|
|
765
|
+
required
|
|
766
|
+
{...form.register("password")}
|
|
767
|
+
/>
|
|
768
|
+
</div>
|
|
769
|
+
{form.formState.errors.root ? (
|
|
770
|
+
<p className="text-sm text-destructive">
|
|
771
|
+
{form.formState.errors.root.message}
|
|
772
|
+
</p>
|
|
773
|
+
) : null}
|
|
774
|
+
<Button
|
|
775
|
+
type="submit"
|
|
776
|
+
variant="destructive"
|
|
777
|
+
disabled={form.formState.isSubmitting}
|
|
778
|
+
>
|
|
779
|
+
{form.formState.isSubmitting ? "Deleting..." : "Delete account"}
|
|
780
|
+
</Button>
|
|
781
|
+
</form>
|
|
782
|
+
</CardContent>
|
|
783
|
+
</Card>
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
`;
|
|
787
|
+
const clientIndex = `import { createClient } from "@beignet/core/client";
|
|
788
|
+
import { createReactQuery } from "@beignet/react-query";
|
|
789
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
790
|
+
|
|
791
|
+
export const apiClient = createClient({
|
|
792
|
+
validateInput: true,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
export const rq = createReactQuery(apiClient);
|
|
796
|
+
|
|
797
|
+
export function makeQueryClient() {
|
|
798
|
+
return new QueryClient({
|
|
799
|
+
defaultOptions: {
|
|
800
|
+
queries: {
|
|
801
|
+
staleTime: 60 * 1000,
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
`;
|
|
807
|
+
const authClientFile = `import { createAuthClient } from "better-auth/react";
|
|
808
|
+
|
|
809
|
+
export const authClient = createAuthClient();
|
|
810
|
+
`;
|
|
811
|
+
const formsClient = `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
812
|
+
|
|
813
|
+
export const rhf = createReactHookForm();
|
|
814
|
+
`;
|
|
815
|
+
const themeProvider = `"use client";
|
|
816
|
+
|
|
817
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
818
|
+
import type { ComponentProps } from "react";
|
|
819
|
+
|
|
820
|
+
export function ThemeProvider({
|
|
821
|
+
children,
|
|
822
|
+
...props
|
|
823
|
+
}: ComponentProps<typeof NextThemesProvider>) {
|
|
824
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
|
825
|
+
}
|
|
826
|
+
`;
|
|
827
|
+
const themeToggle = `"use client";
|
|
828
|
+
|
|
829
|
+
import { MoonIcon, SunIcon } from "lucide-react";
|
|
830
|
+
import { useTheme } from "next-themes";
|
|
831
|
+
import { Button } from "@/components/ui/button";
|
|
832
|
+
import {
|
|
833
|
+
DropdownMenu,
|
|
834
|
+
DropdownMenuContent,
|
|
835
|
+
DropdownMenuItem,
|
|
836
|
+
DropdownMenuTrigger,
|
|
837
|
+
} from "@/components/ui/dropdown-menu";
|
|
838
|
+
|
|
839
|
+
export function ThemeToggle() {
|
|
840
|
+
const { setTheme } = useTheme();
|
|
841
|
+
|
|
842
|
+
return (
|
|
843
|
+
<DropdownMenu>
|
|
844
|
+
<DropdownMenuTrigger asChild>
|
|
845
|
+
<Button type="button" variant="ghost" size="icon" aria-label="Change theme">
|
|
846
|
+
<SunIcon className="dark:hidden" />
|
|
847
|
+
<MoonIcon className="hidden dark:block" />
|
|
848
|
+
</Button>
|
|
849
|
+
</DropdownMenuTrigger>
|
|
850
|
+
<DropdownMenuContent align="end">
|
|
851
|
+
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
852
|
+
Light
|
|
853
|
+
</DropdownMenuItem>
|
|
854
|
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
855
|
+
Dark
|
|
856
|
+
</DropdownMenuItem>
|
|
857
|
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
858
|
+
System
|
|
859
|
+
</DropdownMenuItem>
|
|
860
|
+
</DropdownMenuContent>
|
|
861
|
+
</DropdownMenu>
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
`;
|
|
865
|
+
const appSidebar = `"use client";
|
|
866
|
+
|
|
867
|
+
import {
|
|
868
|
+
FileJsonIcon,
|
|
869
|
+
LayoutDashboardIcon,
|
|
870
|
+
ListTodoIcon,
|
|
871
|
+
LogOutIcon,
|
|
872
|
+
SettingsIcon,
|
|
873
|
+
WrenchIcon,
|
|
874
|
+
} from "lucide-react";
|
|
875
|
+
import Link from "next/link";
|
|
876
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
877
|
+
import { authClient } from "@/client/auth-client";
|
|
878
|
+
import { Button } from "@/components/ui/button";
|
|
879
|
+
import { Separator } from "@/components/ui/separator";
|
|
880
|
+
import { cn } from "@/lib/utils";
|
|
881
|
+
|
|
882
|
+
const navLinks = [
|
|
883
|
+
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboardIcon },
|
|
884
|
+
{ href: "/todos", label: "Todos", icon: ListTodoIcon },
|
|
885
|
+
{ href: "/settings", label: "Settings", icon: SettingsIcon },
|
|
886
|
+
];
|
|
887
|
+
|
|
888
|
+
const linkClassName =
|
|
889
|
+
"flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground";
|
|
890
|
+
|
|
891
|
+
export function AppSidebar({
|
|
892
|
+
user,
|
|
893
|
+
showDevtools,
|
|
894
|
+
showOpenapi,
|
|
895
|
+
}: {
|
|
896
|
+
user: { name: string; email: string };
|
|
897
|
+
showDevtools: boolean;
|
|
898
|
+
showOpenapi: boolean;
|
|
899
|
+
}) {
|
|
900
|
+
const pathname = usePathname();
|
|
901
|
+
const router = useRouter();
|
|
902
|
+
|
|
903
|
+
async function signOut() {
|
|
904
|
+
await authClient.signOut();
|
|
905
|
+
router.push("/");
|
|
906
|
+
router.refresh();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
<aside className="sticky top-0 flex h-svh w-60 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground">
|
|
911
|
+
<div className="px-5 py-4">
|
|
912
|
+
<Link href="/dashboard" className="font-heading text-sm font-semibold">
|
|
913
|
+
Beignet app
|
|
914
|
+
</Link>
|
|
915
|
+
</div>
|
|
916
|
+
<nav className="flex flex-1 flex-col gap-1 px-3" aria-label="Main">
|
|
917
|
+
{navLinks.map((link) => (
|
|
918
|
+
<Link
|
|
919
|
+
key={link.href}
|
|
920
|
+
href={link.href}
|
|
921
|
+
className={cn(
|
|
922
|
+
linkClassName,
|
|
923
|
+
(pathname === link.href ||
|
|
924
|
+
pathname.startsWith(\`\${link.href}/\`)) &&
|
|
925
|
+
"bg-sidebar-accent text-sidebar-accent-foreground",
|
|
926
|
+
)}
|
|
927
|
+
>
|
|
928
|
+
<link.icon className="size-4" />
|
|
929
|
+
{link.label}
|
|
930
|
+
</Link>
|
|
931
|
+
))}
|
|
932
|
+
{showDevtools || showOpenapi ? <Separator className="my-2" /> : null}
|
|
933
|
+
{showDevtools ? (
|
|
934
|
+
<a href="/api/devtools" className={linkClassName}>
|
|
935
|
+
<WrenchIcon className="size-4" />
|
|
936
|
+
Devtools
|
|
937
|
+
</a>
|
|
938
|
+
) : null}
|
|
939
|
+
{showOpenapi ? (
|
|
940
|
+
<a href="/api/openapi" className={linkClassName}>
|
|
941
|
+
<FileJsonIcon className="size-4" />
|
|
942
|
+
OpenAPI
|
|
943
|
+
</a>
|
|
944
|
+
) : null}
|
|
945
|
+
</nav>
|
|
946
|
+
<div className="flex items-center gap-2 border-t p-3">
|
|
947
|
+
<div className="min-w-0 flex-1">
|
|
948
|
+
<p className="truncate text-sm font-medium">{user.name}</p>
|
|
949
|
+
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
|
950
|
+
</div>
|
|
951
|
+
<Button
|
|
952
|
+
type="button"
|
|
953
|
+
variant="ghost"
|
|
954
|
+
size="icon"
|
|
955
|
+
aria-label="Sign out"
|
|
956
|
+
onClick={signOut}
|
|
957
|
+
>
|
|
958
|
+
<LogOutIcon />
|
|
959
|
+
</Button>
|
|
960
|
+
</div>
|
|
961
|
+
</aside>
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
`;
|
|
965
|
+
const settingsNav = `"use client";
|
|
966
|
+
|
|
967
|
+
import Link from "next/link";
|
|
968
|
+
import { usePathname } from "next/navigation";
|
|
969
|
+
import { cn } from "@/lib/utils";
|
|
970
|
+
|
|
971
|
+
const settingsLinks = [
|
|
972
|
+
{ href: "/settings", label: "Profile" },
|
|
973
|
+
{ href: "/settings/password", label: "Password" },
|
|
974
|
+
{ href: "/settings/appearance", label: "Appearance" },
|
|
975
|
+
{ href: "/settings/account", label: "Account" },
|
|
976
|
+
];
|
|
977
|
+
|
|
978
|
+
export function SettingsNav() {
|
|
979
|
+
const pathname = usePathname();
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<nav className="flex flex-wrap gap-1" aria-label="Settings">
|
|
983
|
+
{settingsLinks.map((link) => (
|
|
984
|
+
<Link
|
|
985
|
+
key={link.href}
|
|
986
|
+
href={link.href}
|
|
987
|
+
className={cn(
|
|
988
|
+
"rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
989
|
+
pathname === link.href && "bg-muted text-foreground",
|
|
990
|
+
)}
|
|
991
|
+
>
|
|
992
|
+
{link.label}
|
|
993
|
+
</Link>
|
|
994
|
+
))}
|
|
995
|
+
</nav>
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
`;
|
|
999
|
+
const todoApp = `"use client";
|
|
1000
|
+
|
|
1001
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
1002
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
1003
|
+
import { Trash2Icon } from "lucide-react";
|
|
1004
|
+
import { rq } from "@/client";
|
|
1005
|
+
import { rhf } from "@/client/forms";
|
|
1006
|
+
import { Button } from "@/components/ui/button";
|
|
1007
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
1008
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
1009
|
+
import { Input } from "@/components/ui/input";
|
|
1010
|
+
import { Label } from "@/components/ui/label";
|
|
1011
|
+
import {
|
|
1012
|
+
createTodo,
|
|
1013
|
+
deleteTodo,
|
|
1014
|
+
listTodos,
|
|
1015
|
+
updateTodo,
|
|
1016
|
+
} from "@/features/todos/contracts";
|
|
1017
|
+
import { cn } from "@/lib/utils";
|
|
1018
|
+
|
|
1019
|
+
const createTodoForm = rhf(createTodo);
|
|
1020
|
+
|
|
1021
|
+
export function TodoApp() {
|
|
1022
|
+
const queryClient = useQueryClient();
|
|
1023
|
+
const todosQuery = useQuery(rq(listTodos).queryOptions());
|
|
1024
|
+
const form = createTodoForm.useForm({
|
|
1025
|
+
defaultValues: { title: "" },
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const invalidateTodos = () => rq(listTodos).invalidate(queryClient);
|
|
1029
|
+
|
|
1030
|
+
const createTodoMutation = useMutation(
|
|
1031
|
+
rq(createTodo).mutationOptions({
|
|
1032
|
+
onSuccess: async () => {
|
|
1033
|
+
form.reset();
|
|
1034
|
+
await invalidateTodos();
|
|
1035
|
+
},
|
|
1036
|
+
onError: (error) => {
|
|
1037
|
+
form.setError("root", rootFormError(error, "Could not create the todo."));
|
|
1038
|
+
},
|
|
1039
|
+
}),
|
|
1040
|
+
);
|
|
1041
|
+
const updateTodoMutation = useMutation(
|
|
1042
|
+
rq(updateTodo).mutationOptions({
|
|
1043
|
+
onSuccess: invalidateTodos,
|
|
1044
|
+
}),
|
|
1045
|
+
);
|
|
1046
|
+
const deleteTodoMutation = useMutation(
|
|
1047
|
+
rq(deleteTodo).mutationOptions({
|
|
1048
|
+
onSuccess: invalidateTodos,
|
|
1049
|
+
}),
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
const onSubmit = form.handleSubmit((body) => {
|
|
1053
|
+
form.clearErrors("root");
|
|
1054
|
+
createTodoMutation.mutate({ body });
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
const todos = todosQuery.data?.items ?? [];
|
|
1058
|
+
|
|
1059
|
+
return (
|
|
1060
|
+
<section className="flex flex-col gap-4">
|
|
1061
|
+
<form className="flex flex-col gap-2" onSubmit={onSubmit}>
|
|
1062
|
+
<Label htmlFor="todo-title">New todo</Label>
|
|
1063
|
+
<div className="flex gap-2">
|
|
1064
|
+
<Input
|
|
1065
|
+
id="todo-title"
|
|
1066
|
+
autoComplete="off"
|
|
1067
|
+
placeholder="Ship something type-safe"
|
|
1068
|
+
{...form.register("title")}
|
|
1069
|
+
/>
|
|
1070
|
+
<Button type="submit" disabled={createTodoMutation.isPending}>
|
|
1071
|
+
{createTodoMutation.isPending ? "Adding..." : "Add"}
|
|
1072
|
+
</Button>
|
|
1073
|
+
</div>
|
|
1074
|
+
{form.formState.errors.title ? (
|
|
1075
|
+
<p className="text-sm text-destructive">
|
|
1076
|
+
{form.formState.errors.title.message}
|
|
1077
|
+
</p>
|
|
1078
|
+
) : null}
|
|
1079
|
+
{form.formState.errors.root ? (
|
|
1080
|
+
<p className="text-sm text-destructive">
|
|
1081
|
+
{form.formState.errors.root.message}
|
|
1082
|
+
</p>
|
|
1083
|
+
) : null}
|
|
1084
|
+
</form>
|
|
1085
|
+
|
|
1086
|
+
<Card className="py-0">
|
|
1087
|
+
<CardContent className="px-0">
|
|
1088
|
+
{todosQuery.isPending ? (
|
|
1089
|
+
<p className="px-4 py-6 text-sm text-muted-foreground">
|
|
1090
|
+
Loading todos...
|
|
1091
|
+
</p>
|
|
1092
|
+
) : todosQuery.isError ? (
|
|
1093
|
+
<p className="px-4 py-6 text-sm text-destructive">
|
|
1094
|
+
Could not load todos.
|
|
1095
|
+
</p>
|
|
1096
|
+
) : todos.length === 0 ? (
|
|
1097
|
+
<p className="px-4 py-6 text-sm text-muted-foreground">
|
|
1098
|
+
Nothing here yet — add your first todo.
|
|
1099
|
+
</p>
|
|
1100
|
+
) : (
|
|
1101
|
+
<ul className="divide-y">
|
|
1102
|
+
{todos.map((todo) => (
|
|
1103
|
+
<li key={todo.id} className="flex items-center gap-3 px-4 py-3">
|
|
1104
|
+
<Checkbox
|
|
1105
|
+
id={\`todo-\${todo.id}\`}
|
|
1106
|
+
checked={todo.completed}
|
|
1107
|
+
disabled={updateTodoMutation.isPending}
|
|
1108
|
+
onCheckedChange={(checked) =>
|
|
1109
|
+
updateTodoMutation.mutate({
|
|
1110
|
+
path: { id: todo.id },
|
|
1111
|
+
body: { completed: checked === true },
|
|
1112
|
+
})
|
|
1113
|
+
}
|
|
1114
|
+
/>
|
|
1115
|
+
<label
|
|
1116
|
+
htmlFor={\`todo-\${todo.id}\`}
|
|
1117
|
+
className={cn(
|
|
1118
|
+
"flex-1 text-sm",
|
|
1119
|
+
todo.completed && "text-muted-foreground line-through",
|
|
1120
|
+
)}
|
|
1121
|
+
>
|
|
1122
|
+
{todo.title}
|
|
1123
|
+
</label>
|
|
1124
|
+
<Button
|
|
1125
|
+
type="button"
|
|
1126
|
+
variant="ghost"
|
|
1127
|
+
size="icon-sm"
|
|
1128
|
+
aria-label="Delete todo"
|
|
1129
|
+
disabled={deleteTodoMutation.isPending}
|
|
1130
|
+
onClick={() =>
|
|
1131
|
+
deleteTodoMutation.mutate({ path: { id: todo.id } })
|
|
1132
|
+
}
|
|
1133
|
+
>
|
|
1134
|
+
<Trash2Icon />
|
|
1135
|
+
</Button>
|
|
1136
|
+
</li>
|
|
1137
|
+
))}
|
|
1138
|
+
</ul>
|
|
1139
|
+
)}
|
|
1140
|
+
</CardContent>
|
|
1141
|
+
</Card>
|
|
1142
|
+
</section>
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
`;
|
|
1146
|
+
export function shellTemplateFiles(ctx) {
|
|
1147
|
+
return [
|
|
1148
|
+
{ path: "app/layout.tsx", content: rootLayout },
|
|
1149
|
+
{ path: "app/providers.tsx", content: appProviders },
|
|
1150
|
+
{ path: "app/page.tsx", content: landingPage },
|
|
1151
|
+
{ path: "app/(auth)/layout.tsx", content: authLayout },
|
|
1152
|
+
{ path: "app/(auth)/sign-in/page.tsx", content: signInPage },
|
|
1153
|
+
{ path: "app/(auth)/sign-up/page.tsx", content: signUpPage },
|
|
1154
|
+
{ path: "app/(app)/layout.tsx", content: appLayout },
|
|
1155
|
+
{ path: "app/(app)/dashboard/page.tsx", content: dashboardPage(ctx) },
|
|
1156
|
+
{ path: "app/(app)/todos/page.tsx", content: todosPage },
|
|
1157
|
+
{ path: "app/(app)/settings/layout.tsx", content: settingsLayout },
|
|
1158
|
+
{ path: "app/(app)/settings/page.tsx", content: settingsProfilePage },
|
|
1159
|
+
{
|
|
1160
|
+
path: "app/(app)/settings/password/page.tsx",
|
|
1161
|
+
content: settingsPasswordPage,
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
path: "app/(app)/settings/appearance/page.tsx",
|
|
1165
|
+
content: settingsAppearancePage,
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
path: "app/(app)/settings/account/page.tsx",
|
|
1169
|
+
content: settingsAccountPage,
|
|
1170
|
+
},
|
|
1171
|
+
{ path: "client/index.ts", content: clientIndex },
|
|
1172
|
+
{ path: "client/auth-client.ts", content: authClientFile },
|
|
1173
|
+
{ path: "client/forms.ts", content: formsClient },
|
|
1174
|
+
{ path: "components/theme-provider.tsx", content: themeProvider },
|
|
1175
|
+
{ path: "components/theme-toggle.tsx", content: themeToggle },
|
|
1176
|
+
{ path: "components/app-sidebar.tsx", content: appSidebar },
|
|
1177
|
+
{ path: "components/settings-nav.tsx", content: settingsNav },
|
|
1178
|
+
{
|
|
1179
|
+
path: "features/todos/components/todo-app.tsx",
|
|
1180
|
+
content: todoApp,
|
|
1181
|
+
},
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
export const shellDependencies = {
|
|
1185
|
+
"better-auth": "1.6.11",
|
|
1186
|
+
"lucide-react": "^1.16.0",
|
|
1187
|
+
"next-themes": "^0.4.6",
|
|
1188
|
+
};
|
|
1189
|
+
export const shellDevDependencies = {};
|
|
1190
|
+
//# sourceMappingURL=shell.js.map
|