@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.
- package/CHANGELOG.md +8 -0
- package/README.md +29 -13
- package/dist/cli/args.js +5 -0
- package/dist/cli/ui.js +2 -0
- package/dist/core/customize.js +40 -1
- package/dist/core/manifest.js +41 -3
- package/dist/core/plan.js +68 -3
- package/dist/index.js +42 -7
- package/dist/utils/fs.js +3 -0
- package/package.json +5 -4
- package/template/.dockerignore +3 -0
- package/template/.github/workflows/ci.yml +51 -4
- package/template/.prettierignore +1 -0
- package/template/AGENTS.md +3 -2
- package/template/CONTRIBUTING.md +2 -2
- package/template/README.md +18 -5
- package/template/SETUP.md +23 -1
- package/template/_gitignore +4 -0
- package/template/apps/start/.env.example +27 -0
- package/template/apps/start/AGENTS.md +11 -0
- package/template/apps/start/Dockerfile +38 -0
- package/template/apps/start/package.json +37 -0
- package/template/apps/start/public/.well-known/logo.svg +1 -0
- package/template/apps/start/public/favicon.ico +0 -0
- package/template/apps/start/src/components/auth/auth-form-controls.tsx +108 -0
- package/template/apps/start/src/components/branding/clubedge-mark.tsx +24 -0
- package/template/apps/start/src/components/dashboard/account-button.tsx +33 -0
- package/template/apps/start/src/components/dashboard/app-sidebar.tsx +90 -0
- package/template/apps/start/src/components/landing/copy-command.tsx +46 -0
- package/template/apps/start/src/components/theme/theme-provider.tsx +41 -0
- package/template/apps/start/src/components/theme/theme-toggle.tsx +21 -0
- package/template/apps/start/src/config/site.json +11 -0
- package/template/apps/start/src/config/site.ts +20 -0
- package/template/apps/start/src/env/server.test.ts +48 -0
- package/template/apps/start/src/env/server.ts +67 -0
- package/template/apps/start/src/lib/login-url.test.ts +16 -0
- package/template/apps/start/src/lib/login-url.ts +16 -0
- package/template/apps/start/src/routeTree.gen.ts +220 -0
- package/template/apps/start/src/router.tsx +27 -0
- package/template/apps/start/src/routes/__root.tsx +59 -0
- package/template/apps/start/src/routes/api/health.ts +15 -0
- package/template/apps/start/src/routes/auth/callback.ts +10 -0
- package/template/apps/start/src/routes/auth/sign-in.ts +11 -0
- package/template/apps/start/src/routes/auth/sign-out.ts +11 -0
- package/template/apps/start/src/routes/auth/sign-up.ts +11 -0
- package/template/apps/start/src/routes/dashboard.tsx +351 -0
- package/template/apps/start/src/routes/index.tsx +233 -0
- package/template/apps/start/src/routes/login.tsx +164 -0
- package/template/apps/start/src/server/auth-forms.test.ts +169 -0
- package/template/apps/start/src/server/auth-forms.ts +118 -0
- package/template/apps/start/src/server/auth.functions.ts +27 -0
- package/template/apps/start/src/server/auth.ts +51 -0
- package/template/apps/start/src/server/cache.ts +38 -0
- package/template/apps/start/src/server/cookies.test.ts +41 -0
- package/template/apps/start/src/server/cookies.ts +31 -0
- package/template/apps/start/src/server/db.ts +16 -0
- package/template/apps/start/src/server/storage.ts +39 -0
- package/template/apps/start/src/server/users.ts +17 -0
- package/template/apps/start/src/start.ts +40 -0
- package/template/apps/start/src/styles.css +2 -0
- package/template/apps/start/tsconfig.json +19 -0
- package/template/apps/start/vite.config.ts +27 -0
- package/template/apps/web/package.json +2 -2
- package/template/{scripts → apps/web/scripts}/start-standalone.mjs +1 -2
- package/template/clubedge.template.json +17 -1
- package/template/e2e/app.ts +12 -0
- package/template/{apps/web/e2e → e2e}/auth.spec.ts +2 -2
- package/template/eslint.config.mjs +4 -0
- package/template/package.json +3 -3
- package/template/packages/auth/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/playwright.config.ts +6 -2
- package/template/pnpm-lock.yaml +1382 -19
- package/template/turbo.json +1 -1
- package/template/vitest.config.ts +2 -0
- package/template.lock.json +50 -5
- /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
|
+
});
|