@clubedge/create-clubedge-app 0.2.0 → 0.3.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 +16 -0
- package/README.md +50 -35
- package/bin/create.mjs +2 -466
- package/dist/cli/args.js +49 -0
- package/dist/cli/ui.js +85 -0
- package/dist/core/customize.js +56 -0
- package/dist/core/execute.js +60 -0
- package/dist/core/manifest.js +81 -0
- package/dist/core/plan.js +67 -0
- package/dist/core/source.js +107 -0
- package/dist/index.js +125 -0
- package/dist/package-info.js +8 -0
- package/dist/steps/git.js +11 -0
- package/dist/steps/install.js +19 -0
- package/dist/utils/fs.js +97 -0
- package/dist/utils/names.js +14 -0
- package/dist/utils/process.js +49 -0
- package/package.json +21 -5
- package/template/.dockerignore +8 -0
- package/template/.env.example +25 -0
- package/template/.gitattributes +6 -0
- package/template/.github/ISSUE_TEMPLATE/bug_report.yml +35 -0
- package/template/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/template/.github/ISSUE_TEMPLATE/feature_request.yml +24 -0
- package/template/.github/pull_request_template.md +20 -0
- package/template/.github/workflows/ci.yml +49 -0
- package/template/.prettierignore +1 -0
- package/template/.prettierrc.json +1 -0
- package/template/AGENTS.md +31 -0
- package/template/CODE_OF_CONDUCT.md +22 -0
- package/template/CONTRIBUTING.md +53 -0
- package/template/Dockerfile +32 -0
- package/template/LICENSE +201 -0
- package/template/NOTICE +6 -0
- package/template/PUBLISHING.md +27 -0
- package/template/README.md +157 -0
- package/template/SECURITY.md +15 -0
- package/template/SETUP.md +174 -0
- package/template/SUPPORT.md +8 -0
- package/template/_gitignore +13 -0
- package/template/apps/web/AGENTS.md +9 -0
- package/template/apps/web/components.json +21 -0
- package/template/apps/web/e2e/auth.spec.ts +37 -0
- package/template/apps/web/e2e/home.spec.ts +15 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +32 -0
- package/template/apps/web/package.json +33 -0
- package/template/apps/web/postcss.config.mjs +3 -0
- package/template/apps/web/public/.gitkeep +0 -0
- package/template/apps/web/public/.well-known/logo.svg +1 -0
- package/template/apps/web/src/app/actions/auth.ts +85 -0
- package/template/apps/web/src/app/api/health/route.ts +11 -0
- package/template/apps/web/src/app/auth/callback/route.ts +21 -0
- package/template/apps/web/src/app/dashboard/_components/account-button.tsx +34 -0
- package/template/apps/web/src/app/dashboard/_components/app-sidebar.tsx +91 -0
- package/template/apps/web/src/app/dashboard/page.tsx +352 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/layout.tsx +36 -0
- package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +88 -0
- package/template/apps/web/src/app/login/page.tsx +149 -0
- package/template/apps/web/src/app/page.tsx +229 -0
- package/template/apps/web/src/app/theme-provider.tsx +43 -0
- package/template/apps/web/src/app/theme-toggle.tsx +23 -0
- package/template/apps/web/src/components/branding/clubedge-mark.tsx +25 -0
- package/template/apps/web/src/components/landing/copy-command.tsx +48 -0
- package/template/apps/web/src/config/site.json +11 -0
- package/template/apps/web/src/config/site.ts +20 -0
- package/template/apps/web/src/env/client.ts +22 -0
- package/template/apps/web/src/env/server.test.ts +35 -0
- package/template/apps/web/src/env/server.ts +45 -0
- package/template/apps/web/src/lib/login-url.test.ts +16 -0
- package/template/apps/web/src/lib/login-url.ts +16 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/proxy.ts +31 -0
- package/template/apps/web/src/server/auth.ts +69 -0
- package/template/apps/web/src/server/cache.ts +38 -0
- package/template/apps/web/src/server/db.ts +16 -0
- package/template/apps/web/src/server/storage.ts +39 -0
- package/template/apps/web/src/server/users.ts +17 -0
- package/template/apps/web/tsconfig.json +36 -0
- package/template/clubedge.template.json +13 -0
- package/template/docker-compose.yml +8 -0
- package/template/eslint.config.mjs +63 -0
- package/template/package.json +47 -0
- package/template/packages/auth/package.json +23 -0
- package/template/packages/auth/src/index.ts +9 -0
- package/template/packages/auth/src/supabase.test.ts +126 -0
- package/template/packages/auth/src/supabase.ts +65 -0
- package/template/packages/auth/src/types.ts +59 -0
- package/template/packages/auth/tsconfig.json +4 -0
- package/template/packages/cache/package.json +20 -0
- package/template/packages/cache/src/index.test.ts +67 -0
- package/template/packages/cache/src/index.ts +19 -0
- package/template/packages/cache/src/rate-limit.test.ts +52 -0
- package/template/packages/cache/src/rate-limit.ts +74 -0
- package/template/packages/cache/src/redis.ts +106 -0
- package/template/packages/cache/tsconfig.json +4 -0
- package/template/packages/core/package.json +17 -0
- package/template/packages/core/src/client-ip.test.ts +19 -0
- package/template/packages/core/src/client-ip.ts +11 -0
- package/template/packages/core/src/errors.test.ts +20 -0
- package/template/packages/core/src/errors.ts +20 -0
- package/template/packages/core/src/index.ts +4 -0
- package/template/packages/core/src/redirect.test.ts +27 -0
- package/template/packages/core/src/redirect.ts +20 -0
- package/template/packages/core/src/result.ts +10 -0
- package/template/packages/core/tsconfig.json +4 -0
- package/template/packages/db/drizzle/0000_initial.sql +10 -0
- package/template/packages/db/drizzle/meta/_journal.json +13 -0
- package/template/packages/db/drizzle.config.ts +9 -0
- package/template/packages/db/package.json +30 -0
- package/template/packages/db/scripts/load-env.ts +14 -0
- package/template/packages/db/scripts/migrate.ts +11 -0
- package/template/packages/db/scripts/seed.ts +14 -0
- package/template/packages/db/src/client.ts +22 -0
- package/template/packages/db/src/index.ts +4 -0
- package/template/packages/db/src/schema/index.ts +1 -0
- package/template/packages/db/src/schema/users.ts +14 -0
- package/template/packages/db/src/users.ts +16 -0
- package/template/packages/db/tsconfig.json +4 -0
- package/template/packages/storage/package.json +24 -0
- package/template/packages/storage/src/index.ts +1 -0
- package/template/packages/storage/src/s3.test.ts +86 -0
- package/template/packages/storage/src/s3.ts +86 -0
- package/template/packages/storage/src/supabase.ts +69 -0
- package/template/packages/storage/src/types.ts +18 -0
- package/template/packages/storage/tsconfig.json +4 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +32 -0
- package/template/packages/ui/src/components/badge.tsx +48 -0
- package/template/packages/ui/src/components/breadcrumb.tsx +102 -0
- package/template/packages/ui/src/components/button.tsx +57 -0
- package/template/packages/ui/src/components/card.tsx +87 -0
- package/template/packages/ui/src/components/input.tsx +19 -0
- package/template/packages/ui/src/components/label.tsx +19 -0
- package/template/packages/ui/src/components/separator.tsx +20 -0
- package/template/packages/ui/src/components/sheet.tsx +125 -0
- package/template/packages/ui/src/components/sidebar.tsx +692 -0
- package/template/packages/ui/src/components/skeleton.tsx +13 -0
- package/template/packages/ui/src/components/switch.tsx +31 -0
- package/template/packages/ui/src/components/table.tsx +88 -0
- package/template/packages/ui/src/components/tooltip.tsx +53 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/lib/utils.ts +1 -0
- package/template/packages/ui/src/styles/globals.css +126 -0
- package/template/packages/ui/tsconfig.json +19 -0
- package/template/playwright.config.ts +19 -0
- package/template/pnpm-lock.yaml +3624 -0
- package/template/pnpm-workspace.yaml +3 -0
- package/template/scripts/start-standalone.mjs +37 -0
- package/template/tsconfig.base.json +16 -0
- package/template/turbo.json +17 -0
- package/template/vitest.config.ts +8 -0
- package/template.lock.json +142 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
ArrowRight,
|
|
4
|
+
ArrowUpRight,
|
|
5
|
+
Blocks,
|
|
6
|
+
Check,
|
|
7
|
+
CircleDashed,
|
|
8
|
+
Database,
|
|
9
|
+
Github,
|
|
10
|
+
LockKeyhole,
|
|
11
|
+
Settings2,
|
|
12
|
+
ShieldCheck,
|
|
13
|
+
Workflow,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import Link from "next/link";
|
|
16
|
+
|
|
17
|
+
import { Badge } from "@clubedge/ui/components/badge";
|
|
18
|
+
import { Button } from "@clubedge/ui/components/button";
|
|
19
|
+
import {
|
|
20
|
+
Card,
|
|
21
|
+
CardContent,
|
|
22
|
+
CardDescription,
|
|
23
|
+
CardHeader,
|
|
24
|
+
CardTitle,
|
|
25
|
+
} from "@clubedge/ui/components/card";
|
|
26
|
+
import { Separator } from "@clubedge/ui/components/separator";
|
|
27
|
+
import {
|
|
28
|
+
Table,
|
|
29
|
+
TableBody,
|
|
30
|
+
TableCell,
|
|
31
|
+
TableHead,
|
|
32
|
+
TableHeader,
|
|
33
|
+
TableRow,
|
|
34
|
+
} from "@clubedge/ui/components/table";
|
|
35
|
+
import {
|
|
36
|
+
Breadcrumb,
|
|
37
|
+
BreadcrumbItem,
|
|
38
|
+
BreadcrumbLink,
|
|
39
|
+
BreadcrumbList,
|
|
40
|
+
BreadcrumbPage,
|
|
41
|
+
BreadcrumbSeparator,
|
|
42
|
+
} from "@clubedge/ui/components/breadcrumb";
|
|
43
|
+
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@clubedge/ui/components/sidebar";
|
|
44
|
+
import { siteConfig } from "@/config/site";
|
|
45
|
+
import { getPageUser } from "@/server/auth";
|
|
46
|
+
import { AccountButton } from "./_components/account-button";
|
|
47
|
+
import { AppSidebar } from "./_components/app-sidebar";
|
|
48
|
+
import { ThemeToggle } from "../theme-toggle";
|
|
49
|
+
|
|
50
|
+
const modules = [
|
|
51
|
+
{
|
|
52
|
+
name: "PostgreSQL + Drizzle",
|
|
53
|
+
description: "Typed schema and migrations for application data.",
|
|
54
|
+
icon: Database,
|
|
55
|
+
status: "Configure",
|
|
56
|
+
variant: "secondary" as const,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "Supabase Auth",
|
|
60
|
+
description: "Cookie-based sessions and server-validated users.",
|
|
61
|
+
icon: LockKeyhole,
|
|
62
|
+
status: "Optional setup",
|
|
63
|
+
variant: "outline" as const,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Storage adapters",
|
|
67
|
+
description: "S3-compatible storage or Supabase Storage.",
|
|
68
|
+
icon: Workflow,
|
|
69
|
+
status: "Ready to configure",
|
|
70
|
+
variant: "secondary" as const,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Redis cache",
|
|
74
|
+
description: "Optional cache and distributed rate limiting.",
|
|
75
|
+
icon: CircleDashed,
|
|
76
|
+
status: "Optional",
|
|
77
|
+
variant: "outline" as const,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const setupRows = [
|
|
82
|
+
{
|
|
83
|
+
item: "Shared UI workspace",
|
|
84
|
+
detail: "shadcn/ui components with Base UI primitives",
|
|
85
|
+
status: "Ready",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
item: "Database connection",
|
|
89
|
+
detail: "Set DATABASE_URL in apps/web/.env.local",
|
|
90
|
+
status: "Configure",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
item: "Authentication provider",
|
|
94
|
+
detail: "Add Supabase URL and publishable key when needed",
|
|
95
|
+
status: "Optional",
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export default async function DashboardPage() {
|
|
100
|
+
const user = await getPageUser("/dashboard");
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<SidebarProvider defaultOpen>
|
|
104
|
+
<AppSidebar user={user} />
|
|
105
|
+
<SidebarInset>
|
|
106
|
+
<header className="sticky top-0 z-10 border-b bg-background/90 backdrop-blur">
|
|
107
|
+
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
|
108
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
109
|
+
<SidebarTrigger aria-label="Toggle navigation sidebar" />
|
|
110
|
+
<Breadcrumb>
|
|
111
|
+
<BreadcrumbList>
|
|
112
|
+
<BreadcrumbItem>
|
|
113
|
+
<BreadcrumbLink render={<Link href="/" />}>Home</BreadcrumbLink>
|
|
114
|
+
</BreadcrumbItem>
|
|
115
|
+
<BreadcrumbSeparator />
|
|
116
|
+
<BreadcrumbItem>
|
|
117
|
+
<BreadcrumbPage>Dashboard</BreadcrumbPage>
|
|
118
|
+
</BreadcrumbItem>
|
|
119
|
+
</BreadcrumbList>
|
|
120
|
+
</Breadcrumb>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="ml-auto flex items-center gap-2">
|
|
123
|
+
<Badge className="hidden sm:inline-flex" variant="outline">
|
|
124
|
+
<span className="size-1.5 rounded-full bg-primary" aria-hidden="true" />
|
|
125
|
+
Starter ready
|
|
126
|
+
</Badge>
|
|
127
|
+
<ThemeToggle />
|
|
128
|
+
<Button
|
|
129
|
+
aria-label="Open GitHub repository"
|
|
130
|
+
render={
|
|
131
|
+
<Link href={siteConfig.links.repository} target="_blank" rel="noreferrer" />
|
|
132
|
+
}
|
|
133
|
+
size="icon"
|
|
134
|
+
variant="ghost"
|
|
135
|
+
>
|
|
136
|
+
<Github aria-hidden="true" />
|
|
137
|
+
</Button>
|
|
138
|
+
<AccountButton user={user} />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</header>
|
|
142
|
+
|
|
143
|
+
<main
|
|
144
|
+
className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8 lg:py-10"
|
|
145
|
+
id="overview"
|
|
146
|
+
>
|
|
147
|
+
<section className="flex flex-col justify-between gap-5 sm:flex-row sm:items-end">
|
|
148
|
+
<div className="max-w-2xl space-y-3">
|
|
149
|
+
<div className="flex items-center gap-2 text-sm font-medium text-primary">
|
|
150
|
+
<ShieldCheck aria-hidden="true" className="size-4" />
|
|
151
|
+
Production-minded application foundation
|
|
152
|
+
</div>
|
|
153
|
+
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
154
|
+
A better place to start building.
|
|
155
|
+
</h1>
|
|
156
|
+
<p className="max-w-xl text-sm leading-6 text-muted-foreground sm:text-base">
|
|
157
|
+
Your workspace is ready. Connect the services you need, then build your product on
|
|
158
|
+
top of a consistent, replaceable foundation.
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
<Button
|
|
162
|
+
render={<Link href={siteConfig.links.setupGuide} target="_blank" rel="noreferrer" />}
|
|
163
|
+
>
|
|
164
|
+
Read setup guide <ArrowRight aria-hidden="true" />
|
|
165
|
+
</Button>
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<section
|
|
169
|
+
aria-label="Starter summary"
|
|
170
|
+
className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4"
|
|
171
|
+
>
|
|
172
|
+
<Card className="gap-4 py-5">
|
|
173
|
+
<CardHeader className="flex-row items-center justify-between gap-2 px-5">
|
|
174
|
+
<CardDescription>Workspace packages</CardDescription>
|
|
175
|
+
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
176
|
+
<Blocks aria-hidden="true" className="size-4" />
|
|
177
|
+
</span>
|
|
178
|
+
</CardHeader>
|
|
179
|
+
<CardContent className="px-5">
|
|
180
|
+
<CardTitle className="text-2xl">3</CardTitle>
|
|
181
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
182
|
+
Web, shared UI, and root tooling
|
|
183
|
+
</p>
|
|
184
|
+
</CardContent>
|
|
185
|
+
</Card>
|
|
186
|
+
<Card className="gap-4 py-5">
|
|
187
|
+
<CardHeader className="flex-row items-center justify-between gap-2 px-5">
|
|
188
|
+
<CardDescription>Database</CardDescription>
|
|
189
|
+
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
190
|
+
<Database aria-hidden="true" className="size-4" />
|
|
191
|
+
</span>
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent className="px-5">
|
|
194
|
+
<CardTitle className="text-2xl">PostgreSQL</CardTitle>
|
|
195
|
+
<p className="mt-1 text-xs text-muted-foreground">Drizzle manages app data</p>
|
|
196
|
+
</CardContent>
|
|
197
|
+
</Card>
|
|
198
|
+
<Card className="gap-4 py-5">
|
|
199
|
+
<CardHeader className="flex-row items-center justify-between gap-2 px-5">
|
|
200
|
+
<CardDescription>Shared UI</CardDescription>
|
|
201
|
+
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
202
|
+
<Blocks aria-hidden="true" className="size-4" />
|
|
203
|
+
</span>
|
|
204
|
+
</CardHeader>
|
|
205
|
+
<CardContent className="px-5">
|
|
206
|
+
<CardTitle className="text-2xl">shadcn/ui</CardTitle>
|
|
207
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
208
|
+
Base UI primitives in @clubedge/ui
|
|
209
|
+
</p>
|
|
210
|
+
</CardContent>
|
|
211
|
+
</Card>
|
|
212
|
+
<Card className="gap-4 py-5">
|
|
213
|
+
<CardHeader className="flex-row items-center justify-between gap-2 px-5">
|
|
214
|
+
<CardDescription>Health endpoint</CardDescription>
|
|
215
|
+
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
216
|
+
<Activity aria-hidden="true" className="size-4" />
|
|
217
|
+
</span>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent className="px-5">
|
|
220
|
+
<CardTitle className="text-2xl">Available</CardTitle>
|
|
221
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
222
|
+
<Link
|
|
223
|
+
className="inline-flex items-center gap-1 text-primary hover:underline"
|
|
224
|
+
href="/api/health"
|
|
225
|
+
>
|
|
226
|
+
Check liveness <ArrowUpRight aria-hidden="true" className="size-3" />
|
|
227
|
+
</Link>
|
|
228
|
+
</p>
|
|
229
|
+
</CardContent>
|
|
230
|
+
</Card>
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]" id="modules">
|
|
234
|
+
<Card className="gap-0 py-0">
|
|
235
|
+
<CardHeader className="flex-row items-start justify-between gap-3 border-b px-5 py-5 sm:px-6">
|
|
236
|
+
<div className="space-y-1.5">
|
|
237
|
+
<CardTitle className="text-base">Application modules</CardTitle>
|
|
238
|
+
<CardDescription>
|
|
239
|
+
Provider boundaries are ready for your project configuration.
|
|
240
|
+
</CardDescription>
|
|
241
|
+
</div>
|
|
242
|
+
<Badge variant="secondary">4 modules</Badge>
|
|
243
|
+
</CardHeader>
|
|
244
|
+
<CardContent className="px-5 py-2 sm:px-6">
|
|
245
|
+
{modules.map(({ name, description, icon: Icon, status, variant }, index) => (
|
|
246
|
+
<div key={name}>
|
|
247
|
+
{index > 0 && <Separator />}
|
|
248
|
+
<div className="flex items-center gap-3 py-4">
|
|
249
|
+
<span className="grid size-9 shrink-0 place-items-center rounded-lg border bg-muted/60 text-muted-foreground">
|
|
250
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
251
|
+
</span>
|
|
252
|
+
<div className="min-w-0 flex-1">
|
|
253
|
+
<p className="truncate text-sm font-medium">{name}</p>
|
|
254
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
255
|
+
{description}
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
<Badge className="shrink-0" variant={variant}>
|
|
259
|
+
{status}
|
|
260
|
+
</Badge>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</CardContent>
|
|
265
|
+
</Card>
|
|
266
|
+
|
|
267
|
+
<Card className="gap-0 py-0" id="activity">
|
|
268
|
+
<CardHeader className="border-b px-5 py-5 sm:px-6">
|
|
269
|
+
<CardTitle className="text-base">Project setup</CardTitle>
|
|
270
|
+
<CardDescription>Recommended first steps for a new application.</CardDescription>
|
|
271
|
+
</CardHeader>
|
|
272
|
+
<CardContent className="p-0">
|
|
273
|
+
<Table>
|
|
274
|
+
<TableHeader>
|
|
275
|
+
<TableRow className="hover:bg-transparent">
|
|
276
|
+
<TableHead className="ps-5 sm:ps-6">Item</TableHead>
|
|
277
|
+
<TableHead className="pe-5 text-end sm:pe-6">Status</TableHead>
|
|
278
|
+
</TableRow>
|
|
279
|
+
</TableHeader>
|
|
280
|
+
<TableBody>
|
|
281
|
+
{setupRows.map((row) => (
|
|
282
|
+
<TableRow key={row.item}>
|
|
283
|
+
<TableCell className="whitespace-normal py-4 ps-5 sm:ps-6">
|
|
284
|
+
<span className="block font-medium">{row.item}</span>
|
|
285
|
+
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
|
286
|
+
{row.detail}
|
|
287
|
+
</span>
|
|
288
|
+
</TableCell>
|
|
289
|
+
<TableCell className="pe-5 text-end sm:pe-6">
|
|
290
|
+
<Badge variant={row.status === "Ready" ? "default" : "outline"}>
|
|
291
|
+
{row.status === "Ready" && <Check aria-hidden="true" />}
|
|
292
|
+
{row.status}
|
|
293
|
+
</Badge>
|
|
294
|
+
</TableCell>
|
|
295
|
+
</TableRow>
|
|
296
|
+
))}
|
|
297
|
+
</TableBody>
|
|
298
|
+
</Table>
|
|
299
|
+
</CardContent>
|
|
300
|
+
<Separator />
|
|
301
|
+
<CardContent className="flex items-center justify-between gap-4 px-5 py-4 sm:px-6">
|
|
302
|
+
<p className="text-xs text-muted-foreground">Need help connecting a provider?</p>
|
|
303
|
+
<Button
|
|
304
|
+
render={
|
|
305
|
+
<Link href={siteConfig.links.setupGuide} target="_blank" rel="noreferrer" />
|
|
306
|
+
}
|
|
307
|
+
size="sm"
|
|
308
|
+
variant="ghost"
|
|
309
|
+
>
|
|
310
|
+
Setup docs <ArrowUpRight aria-hidden="true" />
|
|
311
|
+
</Button>
|
|
312
|
+
</CardContent>
|
|
313
|
+
</Card>
|
|
314
|
+
</section>
|
|
315
|
+
|
|
316
|
+
<Card className="gap-4 border-dashed bg-muted/30 py-5 shadow-none">
|
|
317
|
+
<CardContent className="flex flex-col gap-4 px-5 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
|
318
|
+
<div className="flex items-start gap-3">
|
|
319
|
+
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-background text-primary shadow-sm">
|
|
320
|
+
<Settings2 aria-hidden="true" className="size-4" />
|
|
321
|
+
</span>
|
|
322
|
+
<div>
|
|
323
|
+
<p className="text-sm font-medium">Make this foundation yours</p>
|
|
324
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
325
|
+
Configure environment values, review the architecture, and add only the UI
|
|
326
|
+
components your product needs.
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<Button
|
|
331
|
+
render={
|
|
332
|
+
<Link href={siteConfig.links.setupGuide} target="_blank" rel="noreferrer" />
|
|
333
|
+
}
|
|
334
|
+
size="sm"
|
|
335
|
+
variant="outline"
|
|
336
|
+
>
|
|
337
|
+
Get started <ArrowRight aria-hidden="true" />
|
|
338
|
+
</Button>
|
|
339
|
+
</CardContent>
|
|
340
|
+
</Card>
|
|
341
|
+
|
|
342
|
+
<footer className="flex flex-col gap-2 border-t pt-5 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
|
343
|
+
<span>{siteConfig.name} · Next.js application foundation</span>
|
|
344
|
+
<span className="inline-flex items-center gap-2">
|
|
345
|
+
<Check aria-hidden="true" className="size-3.5 text-primary" /> Built to be extended
|
|
346
|
+
</span>
|
|
347
|
+
</footer>
|
|
348
|
+
</main>
|
|
349
|
+
</SidebarInset>
|
|
350
|
+
</SidebarProvider>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Script from "next/script";
|
|
3
|
+
import "@clubedge/ui/globals.css";
|
|
4
|
+
import { TooltipProvider } from "@clubedge/ui/components/tooltip";
|
|
5
|
+
import { siteConfig } from "@/config/site";
|
|
6
|
+
import { ThemeProvider } from "./theme-provider";
|
|
7
|
+
|
|
8
|
+
const themeBootstrap = `try {
|
|
9
|
+
const saved = localStorage.getItem("clubedge-theme");
|
|
10
|
+
const theme = saved === "light" || saved === "dark"
|
|
11
|
+
? saved
|
|
12
|
+
: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
13
|
+
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
14
|
+
document.documentElement.style.colorScheme = theme;
|
|
15
|
+
} catch {}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const metadata: Metadata = {
|
|
19
|
+
title: { default: siteConfig.name, template: `%s · ${siteConfig.name}` },
|
|
20
|
+
description: siteConfig.description,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
24
|
+
return (
|
|
25
|
+
<html lang="en" suppressHydrationWarning>
|
|
26
|
+
<body>
|
|
27
|
+
<Script id="theme-bootstrap" strategy="beforeInteractive">
|
|
28
|
+
{themeBootstrap}
|
|
29
|
+
</Script>
|
|
30
|
+
<ThemeProvider>
|
|
31
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
32
|
+
</ThemeProvider>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowRight, Eye, EyeOff, Loader2 } from "lucide-react";
|
|
4
|
+
import { useState, type ReactNode } from "react";
|
|
5
|
+
import { useFormStatus } from "react-dom";
|
|
6
|
+
|
|
7
|
+
import { Button } from "@clubedge/ui/components/button";
|
|
8
|
+
import { Input } from "@clubedge/ui/components/input";
|
|
9
|
+
import { Label } from "@clubedge/ui/components/label";
|
|
10
|
+
|
|
11
|
+
export function PasswordField({ isSignUp }: { isSignUp: boolean }) {
|
|
12
|
+
const [visible, setVisible] = useState(false);
|
|
13
|
+
const { pending } = useFormStatus();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="grid gap-2">
|
|
17
|
+
<Label htmlFor="password">Password</Label>
|
|
18
|
+
<div className="relative">
|
|
19
|
+
<Input
|
|
20
|
+
aria-describedby={isSignUp ? "password-hint" : undefined}
|
|
21
|
+
autoComplete={isSignUp ? "new-password" : "current-password"}
|
|
22
|
+
className="h-11 pr-11"
|
|
23
|
+
disabled={pending}
|
|
24
|
+
id="password"
|
|
25
|
+
maxLength={128}
|
|
26
|
+
minLength={8}
|
|
27
|
+
name="password"
|
|
28
|
+
placeholder={isSignUp ? "Create a password" : "Enter your password"}
|
|
29
|
+
required
|
|
30
|
+
type={visible ? "text" : "password"}
|
|
31
|
+
/>
|
|
32
|
+
<button
|
|
33
|
+
aria-label={visible ? "Hide password" : "Show password"}
|
|
34
|
+
aria-pressed={visible}
|
|
35
|
+
className="absolute inset-y-0 right-0 grid w-11 place-items-center rounded-r-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
36
|
+
onClick={() => setVisible((v) => !v)}
|
|
37
|
+
type="button"
|
|
38
|
+
>
|
|
39
|
+
{visible ? (
|
|
40
|
+
<EyeOff aria-hidden="true" className="size-4" />
|
|
41
|
+
) : (
|
|
42
|
+
<Eye aria-hidden="true" className="size-4" />
|
|
43
|
+
)}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
{isSignUp && (
|
|
47
|
+
<p className="text-xs text-muted-foreground" id="password-hint">
|
|
48
|
+
Use at least 8 characters.
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function EmailField() {
|
|
56
|
+
const { pending } = useFormStatus();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="grid gap-2">
|
|
60
|
+
<Label htmlFor="email">Email</Label>
|
|
61
|
+
<Input
|
|
62
|
+
autoComplete="email"
|
|
63
|
+
autoFocus
|
|
64
|
+
className="h-11"
|
|
65
|
+
disabled={pending}
|
|
66
|
+
id="email"
|
|
67
|
+
name="email"
|
|
68
|
+
placeholder="you@example.com"
|
|
69
|
+
required
|
|
70
|
+
type="email"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function SubmitButton({ children, disabled }: { children: ReactNode; disabled?: boolean }) {
|
|
77
|
+
const { pending } = useFormStatus();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Button className="h-11 w-full justify-center" disabled={disabled || pending} type="submit">
|
|
81
|
+
{pending ? (
|
|
82
|
+
<Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" />
|
|
83
|
+
) : null}
|
|
84
|
+
{children}
|
|
85
|
+
{!pending && <ArrowRight aria-hidden="true" />}
|
|
86
|
+
</Button>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { CircleAlert, Info, MailCheck } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { signIn, signUp } from "@/app/actions/auth";
|
|
6
|
+
import { ClubedgeMark } from "@/components/branding/clubedge-mark";
|
|
7
|
+
import { siteConfig } from "@/config/site";
|
|
8
|
+
import { safeRedirectPath } from "@clubedge/core";
|
|
9
|
+
import { loginUrl } from "@/lib/login-url";
|
|
10
|
+
import { hasSupabaseAuthConfig } from "@/env/client";
|
|
11
|
+
import { Card, CardContent } from "@clubedge/ui/components/card";
|
|
12
|
+
|
|
13
|
+
import { EmailField, PasswordField, SubmitButton } from "./_components/auth-form-controls";
|
|
14
|
+
|
|
15
|
+
type LoginPageProps = {
|
|
16
|
+
searchParams: Promise<{
|
|
17
|
+
error?: string;
|
|
18
|
+
mode?: string;
|
|
19
|
+
"check-email"?: string;
|
|
20
|
+
next?: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const errorMessages: Record<string, string> = {
|
|
25
|
+
"invalid-input": "Enter a valid email and a password with at least 8 characters.",
|
|
26
|
+
"invalid-credentials": "Those credentials could not be verified. Check your email and password.",
|
|
27
|
+
"signup-failed": "We could not create your account. Please check your details and try again.",
|
|
28
|
+
"auth-callback": "That sign-in link could not be verified. Request a new one.",
|
|
29
|
+
"rate-limited": "Too many attempts. Wait a minute and try again.",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type NoticeTone = "error" | "info" | "warning";
|
|
33
|
+
|
|
34
|
+
const noticeStyles: Record<NoticeTone, string> = {
|
|
35
|
+
error: "border-destructive/30 bg-destructive/5 text-destructive",
|
|
36
|
+
info: "border-primary/30 bg-primary/5 text-foreground",
|
|
37
|
+
warning: "border-amber-500/30 bg-amber-500/5 text-foreground",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function Notice({
|
|
41
|
+
children,
|
|
42
|
+
icon,
|
|
43
|
+
role,
|
|
44
|
+
tone,
|
|
45
|
+
}: {
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
icon: ReactNode;
|
|
48
|
+
role: "alert" | "status";
|
|
49
|
+
tone: NoticeTone;
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={`flex items-start gap-2.5 rounded-lg border px-3.5 py-3 text-sm leading-5 ${noticeStyles[tone]}`}
|
|
54
|
+
role={role}
|
|
55
|
+
>
|
|
56
|
+
<span aria-hidden="true" className="mt-0.5 shrink-0 [&>svg]:size-4">
|
|
57
|
+
{icon}
|
|
58
|
+
</span>
|
|
59
|
+
<div className="min-w-0">{children}</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|
65
|
+
const params = await searchParams;
|
|
66
|
+
const isSignUp = params.mode === "signup";
|
|
67
|
+
const error = params.error ? errorMessages[params.error] : undefined;
|
|
68
|
+
const checkEmail = Boolean(params["check-email"]);
|
|
69
|
+
const next = safeRedirectPath(params.next);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<main className="flex min-h-svh flex-col items-center bg-background px-4 py-10 sm:justify-center">
|
|
73
|
+
<div className="w-full max-w-[26rem]">
|
|
74
|
+
<header className="mb-8 flex flex-col items-center gap-4 text-center">
|
|
75
|
+
<Link
|
|
76
|
+
aria-label={`${siteConfig.name} home`}
|
|
77
|
+
className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
78
|
+
href="/"
|
|
79
|
+
>
|
|
80
|
+
<ClubedgeMark className="size-12 rounded-xl" />
|
|
81
|
+
</Link>
|
|
82
|
+
<div className="grid gap-1.5">
|
|
83
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
84
|
+
{isSignUp ? "Create your account" : `Sign in to ${siteConfig.shortName}`}
|
|
85
|
+
</h1>
|
|
86
|
+
<p className="text-sm text-muted-foreground">
|
|
87
|
+
{isSignUp
|
|
88
|
+
? "Enter your email and choose a password."
|
|
89
|
+
: "Welcome back. Enter your details to continue."}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</header>
|
|
93
|
+
|
|
94
|
+
<Card className="rounded-xl border-border/70 p-0 shadow-sm">
|
|
95
|
+
<CardContent className="grid gap-5 p-6 sm:p-8">
|
|
96
|
+
{!hasSupabaseAuthConfig && (
|
|
97
|
+
<Notice icon={<Info />} role="status" tone="warning">
|
|
98
|
+
Authentication is not configured yet. Add your Supabase URL and publishable key to{" "}
|
|
99
|
+
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
|
|
100
|
+
apps/web/.env.local
|
|
101
|
+
</code>
|
|
102
|
+
.
|
|
103
|
+
</Notice>
|
|
104
|
+
)}
|
|
105
|
+
{error && (
|
|
106
|
+
<Notice icon={<CircleAlert />} role="alert" tone="error">
|
|
107
|
+
{error}
|
|
108
|
+
</Notice>
|
|
109
|
+
)}
|
|
110
|
+
{checkEmail && (
|
|
111
|
+
<Notice icon={<MailCheck />} role="status" tone="info">
|
|
112
|
+
Account created. Check your inbox for the confirmation link to finish signing up.
|
|
113
|
+
</Notice>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<form action={isSignUp ? signUp : signIn} className="grid gap-5">
|
|
117
|
+
<input name="next" type="hidden" value={next} />
|
|
118
|
+
<EmailField />
|
|
119
|
+
<PasswordField isSignUp={isSignUp} />
|
|
120
|
+
<SubmitButton disabled={!hasSupabaseAuthConfig}>
|
|
121
|
+
{isSignUp ? "Create account" : "Sign in"}
|
|
122
|
+
</SubmitButton>
|
|
123
|
+
</form>
|
|
124
|
+
</CardContent>
|
|
125
|
+
</Card>
|
|
126
|
+
|
|
127
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
128
|
+
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
|
129
|
+
<Link
|
|
130
|
+
className="font-medium text-foreground underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
131
|
+
href={loginUrl({ mode: isSignUp ? "signin" : "signup", next })}
|
|
132
|
+
>
|
|
133
|
+
{isSignUp ? "Sign in" : "Create one"}
|
|
134
|
+
</Link>
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<footer className="mt-10 text-center text-xs text-muted-foreground sm:absolute sm:bottom-6">
|
|
139
|
+
<Link className="underline-offset-4 hover:underline" href="/">
|
|
140
|
+
Back to home
|
|
141
|
+
</Link>
|
|
142
|
+
<span aria-hidden="true" className="mx-2">
|
|
143
|
+
|
|
|
144
|
+
</span>
|
|
145
|
+
{siteConfig.name}, Apache-2.0
|
|
146
|
+
</footer>
|
|
147
|
+
</main>
|
|
148
|
+
);
|
|
149
|
+
}
|