create-bw-app 0.12.0 → 0.13.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 (47) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/add.mjs +2 -1
  4. package/src/app-manifest.mjs +1 -0
  5. package/src/constants.mjs +4 -9
  6. package/src/generator.mjs +64 -56
  7. package/src/remove.mjs +2 -0
  8. package/src/update.mjs +3 -0
  9. package/template/base/AGENTS.md +16 -20
  10. package/template/base/app/globals.css +5 -569
  11. package/template/base/app/theme.css +5 -0
  12. package/template/base/config/modules.ts +3 -5
  13. package/template/base/docs/ai/README.md +15 -41
  14. package/template/base/docs/ai/examples.md +13 -34
  15. package/template/base/public/brand/logo-dark.svg +2 -2
  16. package/template/base/public/brand/logo-light.svg +2 -2
  17. package/template/modules/admin/app/admin/users/page.tsx +2 -0
  18. package/template/modules/admin/app/api/admin/users/roles/route.ts +1 -6
  19. package/template/modules/admin/app/api/admin/users/route.ts +1 -6
  20. package/template/modules/crm/app/api/crm/contacts/route.ts +3 -18
  21. package/template/modules/crm/app/api/crm/organizations/route.ts +1 -8
  22. package/template/modules/crm/app/api/crm/owners/route.ts +1 -8
  23. package/template/modules/crm/app/api/crm/stats/route.ts +1 -8
  24. package/template/modules/crm/app/api/crm/timeline/route.ts +1 -8
  25. package/template/site/base/AGENTS.md +6 -19
  26. package/template/site/base/docs/ai/README.md +6 -33
  27. package/template/site/base/docs/ai/examples.md +6 -28
  28. package/template/base/app/bootstrap/page.tsx +0 -75
  29. package/template/base/app/page.tsx +0 -145
  30. package/template/base/app/playground/auth/page.tsx +0 -5
  31. package/template/base/app/playground/layout.tsx +0 -41
  32. package/template/base/app/preview/app-shell/page.tsx +0 -11
  33. package/template/base/components/app-shell-preview.tsx +0 -185
  34. package/template/base/components/auth-playground.tsx +0 -112
  35. package/template/base/config/bootstrap.ts +0 -131
  36. package/template/base/config/client.ts +0 -18
  37. package/template/base/config/env.ts +0 -100
  38. package/template/base/lib/email/resend-base.ts +0 -13
  39. package/template/modules/admin/app/playground/admin/page.tsx +0 -102
  40. package/template/modules/crm/app/api/crm/_shared/create-module-route-handler.ts +0 -13
  41. package/template/modules/projects/app/playground/projects/page.tsx +0 -93
  42. package/template/site/base/app/page.tsx +0 -165
  43. package/template/site/base/components/ui/badge.tsx +0 -28
  44. package/template/site/base/components/ui/button.tsx +0 -52
  45. package/template/site/base/components/ui/card.tsx +0 -28
  46. package/template/site/base/components.json +0 -17
  47. package/template/site/base/lib/utils.ts +0 -6
@@ -1,185 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import {
5
- AccountMenu,
6
- AppHeader,
7
- DesktopSidebar,
8
- MobileNav,
9
- computeInitials,
10
- getShellNavGroup,
11
- type NavGroupConfig,
12
- type ResolvedClientAppShellConfig,
13
- } from "@brightweblabs/app-shell";
14
- import { LayoutTemplate, Sparkles, Users } from "lucide-react";
15
- import { starterBrandConfig } from "../config/brand";
16
- import { getStarterShellConfig } from "../config/shell";
17
-
18
- const mockUser = {
19
- email: "admin@starter-client.test",
20
- user_metadata: {
21
- first_name: "Starter",
22
- last_name: "Admin",
23
- },
24
- };
25
-
26
- const mockSurfaceCards = [
27
- {
28
- title: "Module activation",
29
- description: "Nav, admin access, and tools placement are resolved from the starter client config.",
30
- icon: LayoutTemplate,
31
- },
32
- {
33
- title: "Shared updates",
34
- description: "The shell primitives come from `@brightweblabs/app-shell`, so new clients keep the same integration contract.",
35
- icon: Sparkles,
36
- },
37
- {
38
- title: "User governance",
39
- description: "Admin placement stays visible because the preview assumes an admin/staff viewer.",
40
- icon: Users,
41
- },
42
- ];
43
-
44
- export function AppShellPreview() {
45
- const { shellPreview: config } = getStarterShellConfig() as { shellPreview: ResolvedClientAppShellConfig };
46
- const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
47
- const [toolsExpanded, setToolsExpanded] = useState(true);
48
- const [crmExpanded, setCrmExpanded] = useState(true);
49
- const [activeHref, setActiveHref] = useState(config.primaryNav[0]?.href ?? "/dashboard");
50
-
51
- const crmNavGroup = useMemo(
52
- () =>
53
- getShellNavGroup(config, "crm")
54
- ?? ({
55
- label: "CRM",
56
- icon: Users,
57
- children: [{ href: "/crm", label: "CRM", icon: Users }],
58
- } satisfies NavGroupConfig),
59
- [config],
60
- );
61
- const collapsedToolsHref = config.toolsSection.items[0]?.href ?? "/";
62
- const displayName = "Starter Admin";
63
- const userInitials = computeInitials(displayName);
64
- const isNavItemActive = (href: string) => activeHref === href;
65
- const isToolLinkActive = (href: string) => activeHref === href;
66
- const isCrmChildActive = (href: string) => activeHref === href;
67
- const isCrmGroupActive = crmNavGroup.children.some((item) => activeHref === item.href);
68
- const isToolActive = config.toolsSection.items.some((item) => activeHref === item.href);
69
-
70
- return (
71
- <div className="app-preview-shell">
72
- <DesktopSidebar
73
- className="app-preview-sidebar"
74
- brand={config.brand}
75
- collapsedToolsHref={collapsedToolsHref}
76
- isSidebarCollapsed={isSidebarCollapsed}
77
- isToolActive={isToolActive}
78
- toolsExpanded={toolsExpanded}
79
- visiblePrimaryNav={config.primaryNav}
80
- adminNavItem={config.adminNavItem}
81
- visibleToolNav={config.toolsSection.items}
82
- crmNavGroup={crmNavGroup}
83
- crmGroupExpanded={crmExpanded}
84
- isCrmGroupActive={isCrmGroupActive}
85
- isNavItemActive={isNavItemActive}
86
- isToolLinkActive={isToolLinkActive}
87
- isCrmChildActive={isCrmChildActive}
88
- onToggleSidebar={() => setIsSidebarCollapsed((current) => !current)}
89
- onToggleTools={() => setToolsExpanded((current) => !current)}
90
- onToggleCrmGroup={() => setCrmExpanded((current) => !current)}
91
- />
92
-
93
- <div className="app-preview-main">
94
- <AppHeader className="app-preview-header">
95
- <div className="app-preview-header-copy">
96
- <span className="eyebrow">{starterBrandConfig.companyName}</span>
97
- <h1>Logged-in shell preview</h1>
98
- <p className="muted">This is the packaged shell running with the starter client registration.</p>
99
- </div>
100
- <div className="app-preview-header-actions">
101
- <div className="status ok">Admin preview</div>
102
- <AccountMenu
103
- displayName={displayName}
104
- isStaff
105
- onSignOut={async () => {}}
106
- user={mockUser}
107
- userInitials={userInitials}
108
- />
109
- </div>
110
- </AppHeader>
111
-
112
- <div className="app-preview-mobile-nav">
113
- <MobileNav
114
- className="panel"
115
- toolsExpanded={toolsExpanded}
116
- visiblePrimaryNav={config.primaryNav}
117
- visibleToolNav={config.toolsSection.items}
118
- isNavItemActive={isNavItemActive}
119
- isToolLinkActive={isToolLinkActive}
120
- onToggleTools={() => setToolsExpanded((current) => !current)}
121
- />
122
- </div>
123
-
124
- <section className="app-preview-content">
125
- <div className="app-preview-stage">
126
- <div className="grid">
127
- {mockSurfaceCards.map((card) => (
128
- <article key={card.title} className="panel preview-glass-card">
129
- <div className="panel-inner">
130
- <div className="preview-card-icon">
131
- <card.icon className="size-4" />
132
- </div>
133
- <h2>{card.title}</h2>
134
- <p className="muted">{card.description}</p>
135
- </div>
136
- </article>
137
- ))}
138
- </div>
139
-
140
- <article className="panel preview-stage-panel">
141
- <div className="panel-inner">
142
- <div className="preview-stage-head">
143
- <div>
144
- <span className="eyebrow">Active path</span>
145
- <h2>{activeHref}</h2>
146
- </div>
147
- <div className="actions">
148
- {[...config.primaryNav, ...crmNavGroup.children, ...(config.adminNavItem ? [config.adminNavItem] : []), ...config.toolsSection.items].map((item) => (
149
- <button
150
- key={item.href}
151
- type="button"
152
- className={`nav-chip ${activeHref === item.href ? "active" : ""}`}
153
- onClick={() => setActiveHref(item.href)}
154
- >
155
- {item.label}
156
- </button>
157
- ))}
158
- </div>
159
- </div>
160
-
161
- <div className="preview-surface-grid">
162
- <div className="preview-surface-card">
163
- <p className="preview-label">Primary navigation</p>
164
- <strong>{config.primaryNav.length} item(s)</strong>
165
- <span>{config.primaryNav.map((item) => item.label).join(" · ")}</span>
166
- </div>
167
- <div className="preview-surface-card">
168
- <p className="preview-label">CRM group</p>
169
- <strong>{crmNavGroup.children.length} item(s)</strong>
170
- <span>{crmNavGroup.children.map((item) => item.label).join(" · ")}</span>
171
- </div>
172
- <div className="preview-surface-card">
173
- <p className="preview-label">Tools section</p>
174
- <strong>{config.toolsSection.items.length} item(s)</strong>
175
- <span>{config.toolsSection.items.map((item) => item.label).join(" · ") || "No tools enabled"}</span>
176
- </div>
177
- </div>
178
- </div>
179
- </article>
180
- </div>
181
- </section>
182
- </div>
183
- </div>
184
- );
185
- }
@@ -1,112 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import {
5
- AUTH_RESEND_COOLDOWN_SECONDS,
6
- buildResetPasswordRedirectUrl,
7
- buildSignupCallbackUrl,
8
- validateEmail,
9
- validatePassword,
10
- } from "@brightweblabs/core-auth/shared";
11
- import { useCooldownTimer } from "@brightweblabs/core-auth/client";
12
-
13
- export function AuthPlayground() {
14
- const [email, setEmail] = useState("hello@brightweblabs.pt");
15
- const [password, setPassword] = useState("Brightweb2026");
16
- const passwordResult = useMemo(() => validatePassword(password), [password]);
17
- const emailIsValid = useMemo(() => validateEmail(email), [email]);
18
- const cooldown = useCooldownTimer(AUTH_RESEND_COOLDOWN_SECONDS);
19
- const authUrls = useMemo(() => {
20
- try {
21
- return {
22
- signupCallbackUrl: buildSignupCallbackUrl(),
23
- resetPasswordUrl: buildResetPasswordRedirectUrl(),
24
- error: null,
25
- };
26
- } catch (error) {
27
- return {
28
- signupCallbackUrl: null,
29
- resetPasswordUrl: null,
30
- error: error instanceof Error ? error.message : "Unknown error",
31
- };
32
- }
33
- }, []);
34
-
35
- return (
36
- <>
37
- <article className="panel">
38
- <div className="panel-inner">
39
- <p className="eyebrow">Core Auth</p>
40
- <h1>Auth package playground</h1>
41
- <p className="muted">
42
- This page exercises the external `@brightweblabs/core-auth` shared and client entrypoints directly.
43
- </p>
44
- </div>
45
- </article>
46
-
47
- <article className="panel">
48
- <div className="panel-inner form-grid">
49
- <div className="field">
50
- <label htmlFor="email">Email validator</label>
51
- <input id="email" value={email} onChange={(event) => setEmail(event.target.value)} />
52
- </div>
53
-
54
- <p className={`status ${emailIsValid ? "ok" : "error"}`}>
55
- {emailIsValid ? "Valid email" : "Invalid email"}
56
- </p>
57
-
58
- <div className="field">
59
- <label htmlFor="password">Password validator</label>
60
- <input id="password" value={password} onChange={(event) => setPassword(event.target.value)} />
61
- </div>
62
-
63
- <div className="result-box">
64
- <strong>{passwordResult.isValid ? "Password accepted" : "Password rejected"}</strong>
65
- <ul>
66
- {passwordResult.errors.length === 0 ? <li>No validation errors.</li> : null}
67
- {passwordResult.errors.map((error) => <li key={error}>{error}</li>)}
68
- </ul>
69
- </div>
70
- </div>
71
- </article>
72
-
73
- <article className="panel">
74
- <div className="panel-inner stack">
75
- <div>
76
- <h2>Derived URLs</h2>
77
- <p className="muted">These values come from the auth package and depend on `NEXT_PUBLIC_APP_URL`.</p>
78
- </div>
79
- <div className="code-box">
80
- {authUrls.error ? (
81
- <div>
82
- <strong>Configuration required:</strong> {authUrls.error}
83
- </div>
84
- ) : (
85
- <>
86
- <div><strong>Signup callback:</strong> {authUrls.signupCallbackUrl}</div>
87
- <div><strong>Reset password:</strong> {authUrls.resetPasswordUrl}</div>
88
- </>
89
- )}
90
- </div>
91
- </div>
92
- </article>
93
-
94
- <article className="panel">
95
- <div className="panel-inner stack">
96
- <div>
97
- <h2>Cooldown hook</h2>
98
- <p className="muted">Useful for resend-email and rate-limited auth actions.</p>
99
- </div>
100
- <div className="actions">
101
- <button className="action" onClick={() => cooldown.start()} disabled={cooldown.isCoolingDown}>
102
- {cooldown.isCoolingDown ? `Cooling down: ${cooldown.remaining}s` : "Start cooldown"}
103
- </button>
104
- <button className="action secondary" onClick={() => cooldown.reset()}>
105
- Reset
106
- </button>
107
- </div>
108
- </div>
109
- </article>
110
- </>
111
- );
112
- }
@@ -1,131 +0,0 @@
1
- import { getStarterClientConfig } from "./client";
2
-
3
- export type StarterChecklistItem = {
4
- label: string;
5
- done: boolean;
6
- detail?: string;
7
- };
8
-
9
- export type StarterChecklistSection = {
10
- key: string;
11
- title: string;
12
- items: StarterChecklistItem[];
13
- };
14
-
15
- function hasModule(moduleKeys: string[], key: string) {
16
- return moduleKeys.includes(key);
17
- }
18
-
19
- export function getStarterBootstrapChecklist() {
20
- const config = getStarterClientConfig();
21
- const moduleKeys = config.enabledModules.map((moduleConfig) => moduleConfig.key);
22
-
23
- const identitySection: StarterChecklistSection = {
24
- key: "identity",
25
- title: "Client identity",
26
- items: [
27
- {
28
- label: "Define company and product name",
29
- done: config.brand.companyName !== "Starter Client" && config.brand.productName !== "Operations Platform",
30
- detail: `${config.brand.companyName} / ${config.brand.productName}`,
31
- },
32
- {
33
- label: "Set client slug",
34
- done: config.brand.slug !== "starter-client",
35
- detail: config.brand.slug,
36
- },
37
- {
38
- label: "Set client contact and support inboxes",
39
- done: config.brand.contactEmail !== "hello@example.com" && config.brand.supportEmail !== "support@example.com",
40
- detail: `${config.brand.contactEmail} · ${config.brand.supportEmail}`,
41
- },
42
- ],
43
- };
44
-
45
- const envSection: StarterChecklistSection = {
46
- key: "environment",
47
- title: "Environment and infrastructure",
48
- items: config.envStatus.map((item) => ({
49
- label: `Configure ${item.key}`,
50
- done: item.present,
51
- detail: `${item.scope} · ${item.requiredFor.join(", ")}`,
52
- })),
53
- };
54
-
55
- const dataSection: StarterChecklistSection = {
56
- key: "services",
57
- title: "Client services",
58
- items: [
59
- {
60
- label: "Provision dedicated Supabase project",
61
- done: config.envStatus.filter((item) => item.key.includes("SUPABASE")).every((item) => item.present),
62
- detail: "Database, auth, storage, and RPC functions should be client-specific.",
63
- },
64
- {
65
- label: "Configure Resend for the client sender domain",
66
- done: [
67
- "RESEND_API_KEY",
68
- "RESEND_FROM_TRANSACTIONAL",
69
- "RESEND_FROM_MARKETING",
70
- "CONTACT_TO_EMAIL",
71
- "RESEND_WEBHOOK_SECRET",
72
- ].every((key) => Boolean(config.envStatus.find((item) => item.key === key)?.present)),
73
- detail: "Use client-owned sender identities and configure webhook signature verification.",
74
- },
75
- {
76
- label: "Create per-client environment variables",
77
- done: config.envReadiness.allReady,
78
- detail: "Fill `.env.local` with the real service values for this client.",
79
- },
80
- ],
81
- };
82
-
83
- const modulesSection: StarterChecklistSection = {
84
- key: "modules",
85
- title: "Enabled modules",
86
- items: config.enabledModules.map((moduleConfig) => ({
87
- label: `Validate ${moduleConfig.label}`,
88
- done: Boolean(moduleConfig.playgroundHref),
89
- detail: moduleConfig.playgroundHref
90
- ? `Preview at ${moduleConfig.playgroundHref}`
91
- : `Package ${moduleConfig.packageName}`,
92
- })),
93
- };
94
-
95
- const rolloutSection: StarterChecklistSection = {
96
- key: "rollout",
97
- title: "Launch readiness",
98
- items: [
99
- {
100
- label: "Preview Auth flows",
101
- done: true,
102
- detail: "/playground/auth",
103
- },
104
- {
105
- label: "Preview CRM module",
106
- done: hasModule(moduleKeys, "crm"),
107
- detail: hasModule(moduleKeys, "crm") ? "/crm" : "CRM not enabled",
108
- },
109
- {
110
- label: "Preview Projects module",
111
- done: hasModule(moduleKeys, "projects"),
112
- detail: hasModule(moduleKeys, "projects") ? "/playground/projects" : "Projects not enabled",
113
- },
114
- {
115
- label: "Preview Admin module",
116
- done: hasModule(moduleKeys, "admin"),
117
- detail: hasModule(moduleKeys, "admin") ? "/playground/admin" : "Admin not enabled",
118
- },
119
- {
120
- label: "Deploy only after env readiness is green",
121
- done: config.envReadiness.allReady,
122
- detail: config.envReadiness.allReady ? "Ready for deploy validation." : `${config.envReadiness.missing.length} env key(s) still missing.`,
123
- },
124
- ],
125
- };
126
-
127
- return {
128
- client: config,
129
- sections: [identitySection, envSection, dataSection, modulesSection, rolloutSection],
130
- };
131
- }
@@ -1,18 +0,0 @@
1
- import { starterBrandConfig } from "./brand";
2
- import { getStarterEnvStatus, isStarterEnvReady } from "./env";
3
- import { getStarterShellConfig } from "./shell";
4
-
5
- export function getStarterClientConfig() {
6
- const shell = getStarterShellConfig();
7
- const enabledModules = shell.enabledModules;
8
- const moduleKeys = enabledModules.map((moduleConfig) => moduleConfig.key);
9
-
10
- return {
11
- brand: starterBrandConfig,
12
- enabledModules,
13
- envStatus: getStarterEnvStatus(),
14
- envReadiness: isStarterEnvReady(moduleKeys),
15
- shellPreview: shell.shellPreview,
16
- toolbarRoutes: shell.toolbarRoutes,
17
- };
18
- }
@@ -1,100 +0,0 @@
1
- export type StarterEnvScope = "public" | "server";
2
-
3
- export type StarterEnvRequirement = {
4
- key: string;
5
- scope: StarterEnvScope;
6
- description: string;
7
- requiredFor: string[];
8
- };
9
-
10
- export type StarterEnvStatus = StarterEnvRequirement & {
11
- present: boolean;
12
- };
13
-
14
- export const starterEnvRequirements: StarterEnvRequirement[] = [
15
- {
16
- key: "NEXT_PUBLIC_APP_URL",
17
- scope: "public",
18
- description: "Canonical base URL used by auth callbacks and platform links.",
19
- requiredFor: ["core-auth"],
20
- },
21
- {
22
- key: "NEXT_PUBLIC_SUPABASE_URL",
23
- scope: "public",
24
- description: "Supabase project URL for client-side and server-side API access.",
25
- requiredFor: ["crm", "projects", "admin"],
26
- },
27
- {
28
- key: "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY",
29
- scope: "public",
30
- description: "Supabase publishable key for browser auth and RPC calls.",
31
- requiredFor: ["crm", "projects", "admin"],
32
- },
33
- {
34
- key: "SUPABASE_SECRET_DEFAULT_KEY",
35
- scope: "server",
36
- description: "Supabase secret key for privileged admin, CRM, and project actions.",
37
- requiredFor: ["crm", "projects", "admin"],
38
- },
39
- {
40
- key: "RESEND_API_KEY",
41
- scope: "server",
42
- description: "Email delivery key for app-owned transactional and marketing server flows.",
43
- requiredFor: ["admin"],
44
- },
45
- {
46
- key: "RESEND_FROM_TRANSACTIONAL",
47
- scope: "server",
48
- description: "Default transactional sender used by app-owned invite/contact email flows.",
49
- requiredFor: ["admin"],
50
- },
51
- {
52
- key: "RESEND_FROM_MARKETING",
53
- scope: "server",
54
- description: "Default marketing sender used by campaign and workflow email flows.",
55
- requiredFor: ["admin"],
56
- },
57
- {
58
- key: "CONTACT_TO_EMAIL",
59
- scope: "server",
60
- description: "Destination inbox for app-owned contact notifications.",
61
- requiredFor: ["admin"],
62
- },
63
- {
64
- key: "RESEND_WEBHOOK_SECRET",
65
- scope: "server",
66
- description: "Webhook signing secret used to verify inbound Resend event payloads.",
67
- requiredFor: ["admin"],
68
- },
69
- {
70
- key: "MARKETING_WORKER_SECRET",
71
- scope: "server",
72
- description: "Shared secret required to trigger internal marketing worker endpoints.",
73
- requiredFor: ["admin"],
74
- },
75
- {
76
- key: "MARKETING_TEST_EMAIL",
77
- scope: "server",
78
- description: "Inbox used by admin marketing test send actions.",
79
- requiredFor: ["admin"],
80
- },
81
- ];
82
-
83
- export function getStarterEnvStatus() {
84
- return starterEnvRequirements.map(
85
- (requirement) =>
86
- ({
87
- ...requirement,
88
- present: typeof process.env[requirement.key] === "string" && process.env[requirement.key]!.trim().length > 0,
89
- }) satisfies StarterEnvStatus,
90
- );
91
- }
92
-
93
- export function isStarterEnvReady(requiredModules: string[]) {
94
- const relevant = getStarterEnvStatus().filter((item) => item.requiredFor.some((moduleKey) => requiredModules.includes(moduleKey)));
95
- return {
96
- allReady: relevant.every((item) => item.present),
97
- items: relevant,
98
- missing: relevant.filter((item) => !item.present),
99
- };
100
- }
@@ -1,13 +0,0 @@
1
- import "server-only";
2
-
3
- export {
4
- getContactDestination,
5
- getMarketingSender,
6
- getResendApiKey,
7
- getResendWebhookSecret,
8
- getTransactionalSender,
9
- resendApiRequest,
10
- verifyResendWebhookSignature,
11
- ResendApiError,
12
- ResendConfigError,
13
- } from "@brightweblabs/infra/server";
@@ -1,102 +0,0 @@
1
- export const dynamic = "force-dynamic";
2
-
3
- function formatError(error: unknown) {
4
- if (error instanceof Error) {
5
- return error.message;
6
- }
7
-
8
- return "Unknown error";
9
- }
10
-
11
- export default async function AdminPlaygroundPage() {
12
- try {
13
- const { getAdminUsersPageData } = await import("@brightweblabs/module-admin");
14
- const { users } = await getAdminUsersPageData();
15
-
16
- return (
17
- <>
18
- <article className="panel">
19
- <div className="panel-inner">
20
- <p className="eyebrow">Admin Module</p>
21
- <h1>Admin users and roles playground</h1>
22
- <p className="muted">
23
- This route previews the shared admin governance module without relying on any client-specific app.
24
- </p>
25
- </div>
26
- </article>
27
-
28
- <article className="grid">
29
- <div className="panel">
30
- <div className="panel-inner">
31
- <p className="status ok">Connected</p>
32
- <h2>User governance snapshot</h2>
33
- <ul className="list">
34
- <li>Total users in page: {users.data.length}</li>
35
- <li>Total users overall: {users.pagination.total}</li>
36
- <li>Current page: {users.pagination.page}</li>
37
- <li>Total pages: {users.pagination.totalPages}</li>
38
- </ul>
39
- </div>
40
- </div>
41
-
42
- <div className="panel">
43
- <div className="panel-inner">
44
- <h2>Role mix</h2>
45
- <div className="code-box">
46
- <pre>
47
- {JSON.stringify(
48
- users.data.reduce<Record<string, number>>((acc, row) => {
49
- acc[row.role] = (acc[row.role] ?? 0) + 1;
50
- return acc;
51
- }, {}),
52
- null,
53
- 2,
54
- )}
55
- </pre>
56
- </div>
57
- </div>
58
- </div>
59
- </article>
60
-
61
- <article className="panel">
62
- <div className="panel-inner">
63
- <h2>First admin users</h2>
64
- <div className="code-box">
65
- <pre>{JSON.stringify(users.data.slice(0, 5), null, 2)}</pre>
66
- </div>
67
- </div>
68
- </article>
69
- </>
70
- );
71
- } catch (error) {
72
- return (
73
- <>
74
- <article className="panel">
75
- <div className="panel-inner">
76
- <p className="eyebrow">Admin Module</p>
77
- <h1>Admin users and roles playground</h1>
78
- <p className="muted">
79
- The admin package is wired, but this starter app still needs a configured client database and admin-ready
80
- auth environment.
81
- </p>
82
- </div>
83
- </article>
84
-
85
- <article className="panel">
86
- <div className="panel-inner stack">
87
- <p className="status warn">Configuration required</p>
88
- <div className="result-box">
89
- <strong>Current error</strong>
90
- <p>{formatError(error)}</p>
91
- </div>
92
- <ul className="list">
93
- <li>Set the Supabase public URL and publishable key.</li>
94
- <li>Set the Supabase secret key.</li>
95
- <li>Ensure the client database includes the admin role assignment tables and RPCs.</li>
96
- </ul>
97
- </div>
98
- </article>
99
- </>
100
- );
101
- }
102
- }
@@ -1,13 +0,0 @@
1
- type RouteHandler = (request: Request) => Response | Promise<Response>;
2
-
3
- type ModuleWithHandler<THandlerName extends string> = Record<THandlerName, RouteHandler>;
4
-
5
- export function createModuleRouteHandler<THandlerName extends string>(
6
- loadModule: () => Promise<ModuleWithHandler<THandlerName>>,
7
- handlerName: THandlerName,
8
- ): RouteHandler {
9
- return async function moduleRouteHandler(request: Request) {
10
- const module = await loadModule();
11
- return module[handlerName](request);
12
- };
13
- }