@clubedge/create-clubedge-app 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +29 -13
  3. package/dist/cli/args.js +5 -0
  4. package/dist/cli/ui.js +2 -0
  5. package/dist/core/customize.js +40 -1
  6. package/dist/core/manifest.js +41 -3
  7. package/dist/core/plan.js +68 -3
  8. package/dist/index.js +42 -7
  9. package/dist/utils/fs.js +3 -0
  10. package/package.json +5 -4
  11. package/template/.dockerignore +3 -0
  12. package/template/.github/workflows/ci.yml +51 -4
  13. package/template/.prettierignore +1 -0
  14. package/template/AGENTS.md +3 -2
  15. package/template/CONTRIBUTING.md +2 -2
  16. package/template/README.md +18 -5
  17. package/template/SETUP.md +23 -1
  18. package/template/_gitignore +4 -0
  19. package/template/apps/start/.env.example +27 -0
  20. package/template/apps/start/AGENTS.md +11 -0
  21. package/template/apps/start/Dockerfile +38 -0
  22. package/template/apps/start/package.json +37 -0
  23. package/template/apps/start/public/.well-known/logo.svg +1 -0
  24. package/template/apps/start/public/favicon.ico +0 -0
  25. package/template/apps/start/src/components/auth/auth-form-controls.tsx +108 -0
  26. package/template/apps/start/src/components/branding/clubedge-mark.tsx +24 -0
  27. package/template/apps/start/src/components/dashboard/account-button.tsx +33 -0
  28. package/template/apps/start/src/components/dashboard/app-sidebar.tsx +90 -0
  29. package/template/apps/start/src/components/landing/copy-command.tsx +46 -0
  30. package/template/apps/start/src/components/theme/theme-provider.tsx +41 -0
  31. package/template/apps/start/src/components/theme/theme-toggle.tsx +21 -0
  32. package/template/apps/start/src/config/site.json +11 -0
  33. package/template/apps/start/src/config/site.ts +20 -0
  34. package/template/apps/start/src/env/server.test.ts +48 -0
  35. package/template/apps/start/src/env/server.ts +67 -0
  36. package/template/apps/start/src/lib/login-url.test.ts +16 -0
  37. package/template/apps/start/src/lib/login-url.ts +16 -0
  38. package/template/apps/start/src/routeTree.gen.ts +220 -0
  39. package/template/apps/start/src/router.tsx +27 -0
  40. package/template/apps/start/src/routes/__root.tsx +59 -0
  41. package/template/apps/start/src/routes/api/health.ts +15 -0
  42. package/template/apps/start/src/routes/auth/callback.ts +10 -0
  43. package/template/apps/start/src/routes/auth/sign-in.ts +11 -0
  44. package/template/apps/start/src/routes/auth/sign-out.ts +11 -0
  45. package/template/apps/start/src/routes/auth/sign-up.ts +11 -0
  46. package/template/apps/start/src/routes/dashboard.tsx +351 -0
  47. package/template/apps/start/src/routes/index.tsx +233 -0
  48. package/template/apps/start/src/routes/login.tsx +164 -0
  49. package/template/apps/start/src/server/auth-forms.test.ts +169 -0
  50. package/template/apps/start/src/server/auth-forms.ts +118 -0
  51. package/template/apps/start/src/server/auth.functions.ts +27 -0
  52. package/template/apps/start/src/server/auth.ts +51 -0
  53. package/template/apps/start/src/server/cache.ts +38 -0
  54. package/template/apps/start/src/server/cookies.test.ts +41 -0
  55. package/template/apps/start/src/server/cookies.ts +31 -0
  56. package/template/apps/start/src/server/db.ts +16 -0
  57. package/template/apps/start/src/server/storage.ts +39 -0
  58. package/template/apps/start/src/server/users.ts +17 -0
  59. package/template/apps/start/src/start.ts +40 -0
  60. package/template/apps/start/src/styles.css +2 -0
  61. package/template/apps/start/tsconfig.json +19 -0
  62. package/template/apps/start/vite.config.ts +27 -0
  63. package/template/apps/web/package.json +2 -2
  64. package/template/{scripts → apps/web/scripts}/start-standalone.mjs +1 -2
  65. package/template/clubedge.template.json +17 -1
  66. package/template/e2e/app.ts +12 -0
  67. package/template/{apps/web/e2e → e2e}/auth.spec.ts +2 -2
  68. package/template/eslint.config.mjs +4 -0
  69. package/template/package.json +3 -3
  70. package/template/packages/auth/package.json +1 -1
  71. package/template/packages/cache/package.json +1 -1
  72. package/template/packages/core/package.json +1 -1
  73. package/template/packages/db/package.json +1 -1
  74. package/template/packages/storage/package.json +1 -1
  75. package/template/packages/ui/package.json +1 -1
  76. package/template/playwright.config.ts +6 -2
  77. package/template/pnpm-lock.yaml +1382 -19
  78. package/template/turbo.json +1 -1
  79. package/template/vitest.config.ts +2 -0
  80. package/template.lock.json +50 -5
  81. /package/template/{apps/web/e2e → e2e}/home.spec.ts +0 -0
@@ -0,0 +1,48 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ async function loadFreshModule() {
4
+ vi.resetModules();
5
+ return import("./server");
6
+ }
7
+
8
+ describe("server env", () => {
9
+ afterEach(() => {
10
+ vi.unstubAllEnvs();
11
+ });
12
+
13
+ it("does not validate at import time", async () => {
14
+ vi.stubEnv("DATABASE_URL", "");
15
+ await expect(loadFreshModule()).resolves.toBeDefined();
16
+ });
17
+
18
+ it("throws on first use when required variables are missing", async () => {
19
+ vi.stubEnv("DATABASE_URL", "");
20
+ const { getServerEnv } = await loadFreshModule();
21
+ vi.spyOn(console, "error").mockImplementation(() => {});
22
+ expect(() => getServerEnv()).toThrow("Server environment validation failed");
23
+ });
24
+
25
+ it("reports Supabase Auth only when both values are present", async () => {
26
+ const { getSupabaseAuthEnv } = await loadFreshModule();
27
+ vi.stubEnv("SUPABASE_URL", "https://project.supabase.co");
28
+ vi.stubEnv("SUPABASE_PUBLISHABLE_KEY", "");
29
+ expect(getSupabaseAuthEnv()).toBeNull();
30
+
31
+ vi.stubEnv("SUPABASE_PUBLISHABLE_KEY", "sb_publishable_123");
32
+ expect(getSupabaseAuthEnv()).toEqual({
33
+ url: "https://project.supabase.co",
34
+ publishableKey: "sb_publishable_123",
35
+ });
36
+
37
+ vi.stubEnv("SUPABASE_URL", "not a url");
38
+ expect(() => getSupabaseAuthEnv()).toThrow("SUPABASE_URL must be an absolute URL.");
39
+ });
40
+
41
+ it("defaults APP_URL for local development", async () => {
42
+ const { getAppUrl } = await loadFreshModule();
43
+ vi.stubEnv("APP_URL", "");
44
+ expect(getAppUrl()).toBe("http://localhost:3000");
45
+ vi.stubEnv("APP_URL", "https://app.example");
46
+ expect(getAppUrl()).toBe("https://app.example");
47
+ });
48
+ });
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+
3
+ const optional = <T extends z.ZodType>(schema: T) => schema.optional().or(z.literal(""));
4
+
5
+ const serverSchema = z.object({
6
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
7
+ APP_URL: z.url().default("http://localhost:3000"),
8
+ DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
9
+ SUPABASE_URL: optional(z.url()),
10
+ SUPABASE_PUBLISHABLE_KEY: optional(z.string()),
11
+ REDIS_URL: optional(z.url({ protocol: /^rediss?$/ })),
12
+ STORAGE_PROVIDER: z.enum(["s3", "supabase"]).default("s3"),
13
+ STORAGE_BUCKET: optional(z.string()),
14
+ STORAGE_REGION: z.string().default("auto"),
15
+ STORAGE_ENDPOINT: optional(z.url()),
16
+ STORAGE_ACCESS_KEY_ID: optional(z.string()),
17
+ STORAGE_SECRET_ACCESS_KEY: optional(z.string()),
18
+ STORAGE_PUBLIC_URL: optional(z.url()),
19
+ SUPABASE_STORAGE_BUCKET: optional(z.string()),
20
+ });
21
+
22
+ export type ServerEnv = z.infer<typeof serverSchema>;
23
+
24
+ let cached: ServerEnv | undefined;
25
+
26
+ /**
27
+ * Validates server variables on first use rather than at import, so pages that never touch
28
+ * the database or providers can build and render without them.
29
+ */
30
+ export function getServerEnv(): ServerEnv {
31
+ if (cached) return cached;
32
+
33
+ const parsed = serverSchema.safeParse(process.env);
34
+ if (!parsed.success) {
35
+ console.error("Invalid server environment:", z.treeifyError(parsed.error));
36
+ throw new Error("Server environment validation failed. Check your .env.local file.");
37
+ }
38
+ cached = parsed.data;
39
+ return cached;
40
+ }
41
+
42
+ /** The public origin used in emailed links. Validates only APP_URL. */
43
+ export function getAppUrl(): string {
44
+ const parsed = serverSchema.shape.APP_URL.safeParse(process.env.APP_URL || undefined);
45
+ if (!parsed.success) throw new Error("APP_URL must be an absolute URL.");
46
+ return parsed.data;
47
+ }
48
+
49
+ export interface SupabaseAuthEnv {
50
+ url: string;
51
+ publishableKey: string;
52
+ }
53
+
54
+ /** Supabase Auth settings, or null while they are not configured. Validates only those keys. */
55
+ export function getSupabaseAuthEnv(): SupabaseAuthEnv | null {
56
+ const url = serverSchema.shape.SUPABASE_URL.safeParse(process.env.SUPABASE_URL);
57
+ if (!url.success) throw new Error("SUPABASE_URL must be an absolute URL.");
58
+ const publishableKey = process.env.SUPABASE_PUBLISHABLE_KEY;
59
+ return url.data && publishableKey ? { url: url.data, publishableKey } : null;
60
+ }
61
+
62
+ /** Validates only REDIS_URL, so cache and rate limiting work without the rest of the env. */
63
+ export function getRedisUrl(): string | undefined {
64
+ const parsed = serverSchema.shape.REDIS_URL.safeParse(process.env.REDIS_URL);
65
+ if (!parsed.success) throw new Error("REDIS_URL must be a redis:// or rediss:// URL.");
66
+ return parsed.data || undefined;
67
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { loginUrl } from "./login-url";
3
+
4
+ describe("loginUrl", () => {
5
+ it("builds the plain login path", () => {
6
+ expect(loginUrl()).toBe("/login");
7
+ });
8
+
9
+ it("omits the default destination and encodes everything else", () => {
10
+ expect(loginUrl({ next: "/dashboard" })).toBe("/login");
11
+ expect(loginUrl({ mode: "signup", error: "invalid-input", next: "/a?b=c" })).toBe(
12
+ "/login?mode=signup&error=invalid-input&next=%2Fa%3Fb%3Dc",
13
+ );
14
+ expect(loginUrl({ mode: "signup", checkEmail: true })).toBe("/login?mode=signup&check-email=1");
15
+ });
16
+ });
@@ -0,0 +1,16 @@
1
+ type LoginUrlOptions = {
2
+ mode?: "signin" | "signup";
3
+ error?: string;
4
+ next?: string;
5
+ checkEmail?: boolean;
6
+ };
7
+
8
+ export function loginUrl({ mode = "signin", error, next, checkEmail }: LoginUrlOptions = {}) {
9
+ const params = new URLSearchParams();
10
+ if (mode === "signup") params.set("mode", "signup");
11
+ if (error) params.set("error", error);
12
+ if (checkEmail) params.set("check-email", "1");
13
+ if (next && next !== "/dashboard") params.set("next", next);
14
+ const query = params.toString();
15
+ return query ? `/login?${query}` : "/login";
16
+ }
@@ -0,0 +1,220 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+ import { Route as DashboardRouteImport } from './routes/dashboard'
14
+ import { Route as LoginRouteImport } from './routes/login'
15
+ import { Route as ApiHealthRouteImport } from './routes/api/health'
16
+ import { Route as AuthCallbackRouteImport } from './routes/auth/callback'
17
+ import { Route as AuthSignInRouteImport } from './routes/auth/sign-in'
18
+ import { Route as AuthSignOutRouteImport } from './routes/auth/sign-out'
19
+ import { Route as AuthSignUpRouteImport } from './routes/auth/sign-up'
20
+
21
+ const IndexRoute = IndexRouteImport.update({
22
+ id: '/',
23
+ path: '/',
24
+ getParentRoute: () => rootRouteImport,
25
+ } as any)
26
+ const DashboardRoute = DashboardRouteImport.update({
27
+ id: '/dashboard',
28
+ path: '/dashboard',
29
+ getParentRoute: () => rootRouteImport,
30
+ } as any)
31
+ const LoginRoute = LoginRouteImport.update({
32
+ id: '/login',
33
+ path: '/login',
34
+ getParentRoute: () => rootRouteImport,
35
+ } as any)
36
+ const ApiHealthRoute = ApiHealthRouteImport.update({
37
+ id: '/api/health',
38
+ path: '/api/health',
39
+ getParentRoute: () => rootRouteImport,
40
+ } as any)
41
+ const AuthCallbackRoute = AuthCallbackRouteImport.update({
42
+ id: '/auth/callback',
43
+ path: '/auth/callback',
44
+ getParentRoute: () => rootRouteImport,
45
+ } as any)
46
+ const AuthSignInRoute = AuthSignInRouteImport.update({
47
+ id: '/auth/sign-in',
48
+ path: '/auth/sign-in',
49
+ getParentRoute: () => rootRouteImport,
50
+ } as any)
51
+ const AuthSignOutRoute = AuthSignOutRouteImport.update({
52
+ id: '/auth/sign-out',
53
+ path: '/auth/sign-out',
54
+ getParentRoute: () => rootRouteImport,
55
+ } as any)
56
+ const AuthSignUpRoute = AuthSignUpRouteImport.update({
57
+ id: '/auth/sign-up',
58
+ path: '/auth/sign-up',
59
+ getParentRoute: () => rootRouteImport,
60
+ } as any)
61
+
62
+ export interface FileRoutesByFullPath {
63
+ '/': typeof IndexRoute
64
+ '/dashboard': typeof DashboardRoute
65
+ '/login': typeof LoginRoute
66
+ '/api/health': typeof ApiHealthRoute
67
+ '/auth/callback': typeof AuthCallbackRoute
68
+ '/auth/sign-in': typeof AuthSignInRoute
69
+ '/auth/sign-out': typeof AuthSignOutRoute
70
+ '/auth/sign-up': typeof AuthSignUpRoute
71
+ }
72
+ export interface FileRoutesByTo {
73
+ '/': typeof IndexRoute
74
+ '/dashboard': typeof DashboardRoute
75
+ '/login': typeof LoginRoute
76
+ '/api/health': typeof ApiHealthRoute
77
+ '/auth/callback': typeof AuthCallbackRoute
78
+ '/auth/sign-in': typeof AuthSignInRoute
79
+ '/auth/sign-out': typeof AuthSignOutRoute
80
+ '/auth/sign-up': typeof AuthSignUpRoute
81
+ }
82
+ export interface FileRoutesById {
83
+ __root__: typeof rootRouteImport
84
+ '/': typeof IndexRoute
85
+ '/dashboard': typeof DashboardRoute
86
+ '/login': typeof LoginRoute
87
+ '/api/health': typeof ApiHealthRoute
88
+ '/auth/callback': typeof AuthCallbackRoute
89
+ '/auth/sign-in': typeof AuthSignInRoute
90
+ '/auth/sign-out': typeof AuthSignOutRoute
91
+ '/auth/sign-up': typeof AuthSignUpRoute
92
+ }
93
+ export interface FileRouteTypes {
94
+ fileRoutesByFullPath: FileRoutesByFullPath
95
+ fullPaths:
96
+ | '/'
97
+ | '/dashboard'
98
+ | '/login'
99
+ | '/api/health'
100
+ | '/auth/callback'
101
+ | '/auth/sign-in'
102
+ | '/auth/sign-out'
103
+ | '/auth/sign-up'
104
+ fileRoutesByTo: FileRoutesByTo
105
+ to:
106
+ | '/'
107
+ | '/dashboard'
108
+ | '/login'
109
+ | '/api/health'
110
+ | '/auth/callback'
111
+ | '/auth/sign-in'
112
+ | '/auth/sign-out'
113
+ | '/auth/sign-up'
114
+ id:
115
+ | '__root__'
116
+ | '/'
117
+ | '/dashboard'
118
+ | '/login'
119
+ | '/api/health'
120
+ | '/auth/callback'
121
+ | '/auth/sign-in'
122
+ | '/auth/sign-out'
123
+ | '/auth/sign-up'
124
+ fileRoutesById: FileRoutesById
125
+ }
126
+ export interface RootRouteChildren {
127
+ IndexRoute: typeof IndexRoute
128
+ DashboardRoute: typeof DashboardRoute
129
+ LoginRoute: typeof LoginRoute
130
+ ApiHealthRoute: typeof ApiHealthRoute
131
+ AuthCallbackRoute: typeof AuthCallbackRoute
132
+ AuthSignInRoute: typeof AuthSignInRoute
133
+ AuthSignOutRoute: typeof AuthSignOutRoute
134
+ AuthSignUpRoute: typeof AuthSignUpRoute
135
+ }
136
+
137
+ declare module '@tanstack/react-router' {
138
+ interface FileRoutesByPath {
139
+ '/': {
140
+ id: '/'
141
+ path: '/'
142
+ fullPath: '/'
143
+ preLoaderRoute: typeof IndexRouteImport
144
+ parentRoute: typeof rootRouteImport
145
+ }
146
+ '/dashboard': {
147
+ id: '/dashboard'
148
+ path: '/dashboard'
149
+ fullPath: '/dashboard'
150
+ preLoaderRoute: typeof DashboardRouteImport
151
+ parentRoute: typeof rootRouteImport
152
+ }
153
+ '/login': {
154
+ id: '/login'
155
+ path: '/login'
156
+ fullPath: '/login'
157
+ preLoaderRoute: typeof LoginRouteImport
158
+ parentRoute: typeof rootRouteImport
159
+ }
160
+ '/api/health': {
161
+ id: '/api/health'
162
+ path: '/api/health'
163
+ fullPath: '/api/health'
164
+ preLoaderRoute: typeof ApiHealthRouteImport
165
+ parentRoute: typeof rootRouteImport
166
+ }
167
+ '/auth/callback': {
168
+ id: '/auth/callback'
169
+ path: '/auth/callback'
170
+ fullPath: '/auth/callback'
171
+ preLoaderRoute: typeof AuthCallbackRouteImport
172
+ parentRoute: typeof rootRouteImport
173
+ }
174
+ '/auth/sign-in': {
175
+ id: '/auth/sign-in'
176
+ path: '/auth/sign-in'
177
+ fullPath: '/auth/sign-in'
178
+ preLoaderRoute: typeof AuthSignInRouteImport
179
+ parentRoute: typeof rootRouteImport
180
+ }
181
+ '/auth/sign-out': {
182
+ id: '/auth/sign-out'
183
+ path: '/auth/sign-out'
184
+ fullPath: '/auth/sign-out'
185
+ preLoaderRoute: typeof AuthSignOutRouteImport
186
+ parentRoute: typeof rootRouteImport
187
+ }
188
+ '/auth/sign-up': {
189
+ id: '/auth/sign-up'
190
+ path: '/auth/sign-up'
191
+ fullPath: '/auth/sign-up'
192
+ preLoaderRoute: typeof AuthSignUpRouteImport
193
+ parentRoute: typeof rootRouteImport
194
+ }
195
+ }
196
+ }
197
+
198
+ const rootRouteChildren: RootRouteChildren = {
199
+ IndexRoute: IndexRoute,
200
+ DashboardRoute: DashboardRoute,
201
+ LoginRoute: LoginRoute,
202
+ ApiHealthRoute: ApiHealthRoute,
203
+ AuthCallbackRoute: AuthCallbackRoute,
204
+ AuthSignInRoute: AuthSignInRoute,
205
+ AuthSignOutRoute: AuthSignOutRoute,
206
+ AuthSignUpRoute: AuthSignUpRoute,
207
+ }
208
+ export const routeTree = rootRouteImport
209
+ ._addFileChildren(rootRouteChildren)
210
+ ._addFileTypes<FileRouteTypes>()
211
+
212
+ import type { getRouter } from './router.tsx'
213
+ import type { startInstance } from './start.ts'
214
+ declare module '@tanstack/react-start' {
215
+ interface Register {
216
+ ssr: true
217
+ router: Awaited<ReturnType<typeof getRouter>>
218
+ config: Awaited<ReturnType<typeof startInstance.getOptions>>
219
+ }
220
+ }
@@ -0,0 +1,27 @@
1
+ import { createRouter } from "@tanstack/react-router";
2
+ import { routeTree } from "./routeTree.gen";
3
+
4
+ // Plain ?key=value search params, as in the Next.js app and the URLs built by loginUrl().
5
+ // The router's default JSON encoding would rewrite ?check-email=1 to ?check-email=%221%22.
6
+ function parseSearch(search: string): Record<string, string> {
7
+ return Object.fromEntries(new URLSearchParams(search));
8
+ }
9
+
10
+ function stringifySearch(search: Record<string, unknown>): string {
11
+ const params = new URLSearchParams();
12
+ for (const [key, value] of Object.entries(search)) {
13
+ if (value !== undefined && value !== null) params.set(key, String(value));
14
+ }
15
+ const query = params.toString();
16
+ return query ? `?${query}` : "";
17
+ }
18
+
19
+ export function getRouter() {
20
+ return createRouter({
21
+ routeTree,
22
+ parseSearch,
23
+ stringifySearch,
24
+ scrollRestoration: true,
25
+ defaultPreload: "intent",
26
+ });
27
+ }
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from "react";
2
+ import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
3
+ import { TooltipProvider } from "@clubedge/ui/components/tooltip";
4
+ import { ThemeProvider } from "@/components/theme/theme-provider";
5
+ import { siteConfig } from "@/config/site";
6
+ import appCss from "../styles.css?url";
7
+
8
+ // Applies the saved or preferred theme before first paint to avoid a flash.
9
+ const themeBootstrap = `try {
10
+ const saved = localStorage.getItem("clubedge-theme");
11
+ const theme = saved === "light" || saved === "dark"
12
+ ? saved
13
+ : window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
14
+ document.documentElement.classList.toggle("dark", theme === "dark");
15
+ document.documentElement.style.colorScheme = theme;
16
+ } catch {}
17
+ `;
18
+
19
+ export const Route = createRootRoute({
20
+ head: () => ({
21
+ meta: [
22
+ { charSet: "utf-8" },
23
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
24
+ { title: siteConfig.name },
25
+ { name: "description", content: siteConfig.description },
26
+ ],
27
+ links: [
28
+ { rel: "stylesheet", href: appCss },
29
+ { rel: "icon", href: "/favicon.ico" },
30
+ ],
31
+ scripts: [{ children: themeBootstrap }],
32
+ }),
33
+ shellComponent: RootDocument,
34
+ component: RootComponent,
35
+ });
36
+
37
+ function RootComponent() {
38
+ return (
39
+ <ThemeProvider>
40
+ <TooltipProvider>
41
+ <Outlet />
42
+ </TooltipProvider>
43
+ </ThemeProvider>
44
+ );
45
+ }
46
+
47
+ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
48
+ return (
49
+ <html lang="en" suppressHydrationWarning>
50
+ <head>
51
+ <HeadContent />
52
+ </head>
53
+ <body>
54
+ {children}
55
+ <Scripts />
56
+ </body>
57
+ </html>
58
+ );
59
+ }
@@ -0,0 +1,15 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { siteConfig } from "@/config/site";
3
+
4
+ export const Route = createFileRoute("/api/health")({
5
+ server: {
6
+ handlers: {
7
+ GET: () =>
8
+ Response.json({
9
+ status: "ok",
10
+ service: siteConfig.serviceId,
11
+ timestamp: new Date().toISOString(),
12
+ }),
13
+ },
14
+ },
15
+ });
@@ -0,0 +1,10 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { handleAuthCallback } from "@/server/auth-forms";
3
+
4
+ export const Route = createFileRoute("/auth/callback")({
5
+ server: {
6
+ handlers: {
7
+ GET: ({ request }) => handleAuthCallback(request),
8
+ },
9
+ },
10
+ });
@@ -0,0 +1,11 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { handleFormEndpointVisit, handleSignIn } from "@/server/auth-forms";
3
+
4
+ export const Route = createFileRoute("/auth/sign-in")({
5
+ server: {
6
+ handlers: {
7
+ GET: () => handleFormEndpointVisit(),
8
+ POST: ({ request }) => handleSignIn(request),
9
+ },
10
+ },
11
+ });
@@ -0,0 +1,11 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { handleFormEndpointVisit, handleSignOut } from "@/server/auth-forms";
3
+
4
+ export const Route = createFileRoute("/auth/sign-out")({
5
+ server: {
6
+ handlers: {
7
+ GET: () => handleFormEndpointVisit(),
8
+ POST: () => handleSignOut(),
9
+ },
10
+ },
11
+ });
@@ -0,0 +1,11 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { handleFormEndpointVisit, handleSignUp } from "@/server/auth-forms";
3
+
4
+ export const Route = createFileRoute("/auth/sign-up")({
5
+ server: {
6
+ handlers: {
7
+ GET: () => handleFormEndpointVisit(),
8
+ POST: ({ request }) => handleSignUp(request),
9
+ },
10
+ },
11
+ });