@mars-stack/cli 3.0.2 → 4.0.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/dist/index.js +350 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/skills/mars-upgrade-scaffold/SKILL.md +70 -0
- package/template/AGENTS.md +5 -1
- package/template/scripts/ensure-db.mjs +15 -9
- package/template/src/app/(auth)/verify/page.tsx +9 -8
- package/template/src/app/(protected)/dashboard/page.tsx +228 -11
- package/template/src/app/(protected)/files/page.tsx +30 -0
- package/template/src/app/(protected)/layout.tsx +14 -1
- package/template/src/app/(protected)/settings/billing/page.tsx +262 -0
- package/template/src/app/api/auth/signup/route.test.ts +118 -0
- package/template/src/app/api/auth/signup/route.ts +29 -5
- package/template/src/app/api/protected/billing/checkout/route.ts +2 -2
- package/template/src/app/api/protected/billing/portal/route.ts +1 -1
- package/template/src/app/api/protected/billing/subscription/route.ts +13 -0
- package/template/src/app/api/protected/files/list/route.ts +22 -0
- package/template/src/app/pricing/page.tsx +276 -0
- package/template/src/config/routes.ts +3 -0
- package/template/src/features/uploads/components/FileList.tsx +202 -0
- package/template/src/features/uploads/components/FileUploader.tsx +225 -0
- package/template/src/features/uploads/components/index.ts +2 -0
- package/template/src/features/uploads/index.ts +2 -0
- package/template/src/proxy.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Skill: Upgrade Mars scaffold (template sync)
|
|
2
|
+
|
|
3
|
+
Apply upstream template fixes to an existing Mars project without regenerating the app. Use when the user wants to pull in fixes for ensure-db, auth verify page, signup route, or other scaffold plumbing.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Use this skill when:
|
|
8
|
+
- The user asks to **upgrade mars template**, **sync scaffold with upstream**, **apply template fixes**, or **pull in ensure-db / verify page / signup fixes**
|
|
9
|
+
- The project was scaffolded earlier and is missing fixes that landed in the Mars template (e.g. Strict Mode verify page, AUTO_VERIFY_EMAIL for localhost, ensure-db portable DB check)
|
|
10
|
+
- The user says they cannot upgrade via CLI because `mars upgrade` only updates npm packages
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
- Confirm the project is a Mars scaffold: `package.json` has `"mars": true` and dependencies on `@mars-stack/core` and/or `@mars-stack/ui`.
|
|
15
|
+
- **Source of truth for the plan:** [docs/exec-plans/active/scaffold-template-sync-upgrade.md](https://github.com/greaveselliott/mars/blob/main/docs/exec-plans/active/scaffold-template-sync-upgrade.md) (or the same path in the Mars monorepo if you have it).
|
|
16
|
+
|
|
17
|
+
## Allowlisted “plumbing” paths (safe to overwrite with template version)
|
|
18
|
+
|
|
19
|
+
Only these paths are candidates for **verbatim replace** from the upstream template. Do **not** overwrite user-owned files (e.g. `src/config/app.config.ts`, feature code the user added, or files outside this list) without explicit user approval.
|
|
20
|
+
|
|
21
|
+
| Path (relative to project root) | Purpose |
|
|
22
|
+
|---------------------------------|---------|
|
|
23
|
+
| `scripts/ensure-db.mjs` | Dev DB provisioning; portable DB ping, prisma generate + push |
|
|
24
|
+
| `src/app/(auth)/verify/page.tsx` | Verify email page; dev link must survive Strict Mode |
|
|
25
|
+
| `src/app/api/auth/signup/route.ts` | Signup; AUTO_VERIFY_EMAIL + authLogger |
|
|
26
|
+
|
|
27
|
+
If the exec plan or a future manifest adds more paths, follow that list. When in doubt, **propose a diff** and let the user approve before writing.
|
|
28
|
+
|
|
29
|
+
## Step-by-Step
|
|
30
|
+
|
|
31
|
+
1. **Confirm Mars project** — Check `package.json` for `"mars": true` and `@mars-stack/*` deps. If not present, stop and tell the user this skill applies only to Mars scaffolds.
|
|
32
|
+
|
|
33
|
+
2. **Obtain upstream template** — User must provide the source:
|
|
34
|
+
- **Option A:** Path to the Mars monorepo `template/` (e.g. `../mars/template` or a branch they have checked out).
|
|
35
|
+
- **Option B:** They run `npx @mars-stack/cli create temp-sync --skip-install` in a temp dir and you copy from `temp-sync/` (then they can delete it).
|
|
36
|
+
- Do not assume a path; ask or use the path they give.
|
|
37
|
+
|
|
38
|
+
3. **For each allowlisted path:**
|
|
39
|
+
- If the file does not exist in the project, copy from template.
|
|
40
|
+
- If the file exists, **diff** template version vs project version. If the project file has only template-like content (or user has not clearly customized it), propose replacing with the template version. If the project file has substantial customizations, **show the diff** and ask the user whether to overwrite or merge manually.
|
|
41
|
+
|
|
42
|
+
4. **Never overwrite without user approval** when:
|
|
43
|
+
- The file is not in the allowlist above.
|
|
44
|
+
- The file contains user-specific config (e.g. `app.config.ts` feature flags, theme, URLs).
|
|
45
|
+
- You are unsure; in that case, output the diff and ask.
|
|
46
|
+
|
|
47
|
+
5. **After applying changes:**
|
|
48
|
+
- Run `yarn db:push` (or `npm run db:push`) if `scripts/ensure-db.mjs` or any Prisma-related path was updated.
|
|
49
|
+
- Suggest running `yarn dev` to confirm the app starts and auth flow works.
|
|
50
|
+
|
|
51
|
+
## Verification
|
|
52
|
+
|
|
53
|
+
- [ ] Project is a Mars scaffold
|
|
54
|
+
- [ ] Only allowlisted plumbing paths were modified (or user explicitly approved others)
|
|
55
|
+
- [ ] Diff was shown for any overwrite of an existing file
|
|
56
|
+
- [ ] `yarn build` or at least `yarn dev` runs after changes (user can confirm)
|
|
57
|
+
- [ ] If ensure-db was updated, user knows to use `yarn dev` (not raw `next dev`) so the script runs
|
|
58
|
+
|
|
59
|
+
## Checklist
|
|
60
|
+
|
|
61
|
+
- [ ] Confirmed `package.json` has `mars: true` and `@mars-stack/*` deps
|
|
62
|
+
- [ ] Obtained template source path from user or instructions
|
|
63
|
+
- [ ] For each allowlisted file: compared, proposed replace or left as-is with reason
|
|
64
|
+
- [ ] No overwrite of app.config, user features, or unlisted paths without approval
|
|
65
|
+
- [ ] Post-change: suggested `yarn db:push` if needed and `yarn dev` smoke test
|
|
66
|
+
|
|
67
|
+
## Related
|
|
68
|
+
|
|
69
|
+
- **Exec plan:** [scaffold-template-sync-upgrade](https://github.com/greaveselliott/mars/blob/main/docs/exec-plans/active/scaffold-template-sync-upgrade.md) — full problem statement, CLI track (future), and task list. In the Mars monorepo the path is `docs/exec-plans/active/scaffold-template-sync-upgrade.md`.
|
|
70
|
+
- **Local-first dev:** Template `AGENTS.md` and [local-first-development](https://github.com/greaveselliott/mars/blob/main/docs/design-docs/local-first-development.md): embedded DB, ensure-db, AUTO_VERIFY_EMAIL.
|
package/template/AGENTS.md
CHANGED
|
@@ -76,7 +76,7 @@ src/
|
|
|
76
76
|
## How to Run
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
yarn dev #
|
|
79
|
+
yarn dev # ensure-db (embedded Postgres) + Next.js — use this, not raw next dev
|
|
80
80
|
yarn build # Production build
|
|
81
81
|
yarn test # Run Vitest unit tests
|
|
82
82
|
yarn test:e2e # Run Playwright e2e tests
|
|
@@ -87,6 +87,10 @@ yarn db:seed # Seed database
|
|
|
87
87
|
yarn db:studio # Open Prisma Studio
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
**Database (local):** `yarn dev` runs `scripts/ensure-db.mjs` first: embedded Postgres via `prisma dev` (PGlite), `DATABASE_URL` written to `.env.development.local`. No Docker. Same zero-setup idea as an in-memory DB; data lives under `.prisma/dev` until you reset it. Use a cloud `DATABASE_URL` in `.env` only when you intentionally dev against Neon/Supabase.
|
|
91
|
+
|
|
92
|
+
**Email verification (local):** With console email, after register you land on `/verify` and use **“Click here to verify your email”** (no inbox needed). Set `AUTO_VERIFY_EMAIL=true` to skip that for **new** signups instead. `AUTO_VERIFY_EMAIL` applies when `yarn dev` runs or `APP_URL` points at `localhost` / `127.0.0.1`; ignored on Vercel production. Older accounts stay unverified until you use the link or Prisma Studio.
|
|
93
|
+
|
|
90
94
|
## Working Discipline
|
|
91
95
|
|
|
92
96
|
1. **Commit after every step.** When executing a plan, commit after each completed task — not at the end. Version control is the progress log. Each commit message should reference the plan and step (e.g., `feat(billing): add Subscription model (billing-plan step 1.1)`).
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Ensures a local database is available before starting development.
|
|
5
5
|
*
|
|
6
|
+
* Default dev datastore: embedded Postgres via `prisma dev` (PGlite-backed, no Docker).
|
|
7
|
+
* Same “zero setup” goal as an in-memory DB — data persists under .prisma/dev until reset.
|
|
8
|
+
*
|
|
6
9
|
* Strategy (in order):
|
|
7
|
-
* 1. If DATABASE_URL connects
|
|
8
|
-
* 2. Otherwise
|
|
9
|
-
*
|
|
10
|
-
* 3. Sync the Prisma schema automatically.
|
|
10
|
+
* 1. If DATABASE_URL connects (verified with a real query), use it and sync schema.
|
|
11
|
+
* 2. Otherwise start embedded Postgres, write DATABASE_URL to .env.development.local
|
|
12
|
+
* (overrides commented/placeholder .env), then sync schema.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { execSync } from 'node:child_process';
|
|
@@ -58,11 +60,14 @@ function loadEnvFile() {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function canConnectToDb() {
|
|
63
|
+
if (!process.env.DATABASE_URL?.trim()) return false;
|
|
61
64
|
try {
|
|
62
|
-
execSync('npx prisma db execute --stdin
|
|
65
|
+
execSync('npx prisma db execute --stdin', {
|
|
63
66
|
cwd: ROOT,
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
input: 'SELECT 1;\n',
|
|
68
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
69
|
+
timeout: 15000,
|
|
70
|
+
env: { ...process.env },
|
|
66
71
|
});
|
|
67
72
|
return true;
|
|
68
73
|
} catch {
|
|
@@ -72,11 +77,12 @@ function canConnectToDb() {
|
|
|
72
77
|
|
|
73
78
|
function pushSchema() {
|
|
74
79
|
try {
|
|
75
|
-
execSync('npx prisma db push --
|
|
80
|
+
execSync('npx prisma generate && npx prisma db push --accept-data-loss', {
|
|
76
81
|
cwd: ROOT,
|
|
77
82
|
stdio: 'inherit',
|
|
78
|
-
timeout:
|
|
83
|
+
timeout: 120000,
|
|
79
84
|
env: { ...process.env },
|
|
85
|
+
shell: true,
|
|
80
86
|
});
|
|
81
87
|
return true;
|
|
82
88
|
} catch {
|
|
@@ -11,17 +11,16 @@ function VerifyContent() {
|
|
|
11
11
|
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
const stored = sessionStorage.getItem('verify-email');
|
|
14
|
-
if (stored)
|
|
15
|
-
setEmail(stored);
|
|
16
|
-
sessionStorage.removeItem('verify-email');
|
|
17
|
-
}
|
|
14
|
+
if (stored) setEmail(stored);
|
|
18
15
|
const storedDevLink = sessionStorage.getItem('verify-dev-link');
|
|
19
|
-
if (storedDevLink)
|
|
20
|
-
setDevLink(storedDevLink);
|
|
21
|
-
sessionStorage.removeItem('verify-dev-link');
|
|
22
|
-
}
|
|
16
|
+
if (storedDevLink) setDevLink(storedDevLink);
|
|
23
17
|
}, []);
|
|
24
18
|
|
|
19
|
+
const clearVerifyStorage = () => {
|
|
20
|
+
sessionStorage.removeItem('verify-email');
|
|
21
|
+
sessionStorage.removeItem('verify-dev-link');
|
|
22
|
+
};
|
|
23
|
+
|
|
25
24
|
return (
|
|
26
25
|
<div className="text-center">
|
|
27
26
|
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-success-muted">
|
|
@@ -50,6 +49,7 @@ function VerifyContent() {
|
|
|
50
49
|
</Text.Paragraph>
|
|
51
50
|
<a
|
|
52
51
|
href={devLink}
|
|
52
|
+
onClick={clearVerifyStorage}
|
|
53
53
|
className="mt-1 inline-block text-sm font-medium text-text-link hover:text-text-link-hover hover:underline"
|
|
54
54
|
>
|
|
55
55
|
Click here to verify your email →
|
|
@@ -58,6 +58,7 @@ function VerifyContent() {
|
|
|
58
58
|
)}
|
|
59
59
|
<Link
|
|
60
60
|
href={routes.signIn}
|
|
61
|
+
onClick={clearVerifyStorage}
|
|
61
62
|
className="mt-8 inline-block text-sm text-text-link hover:text-text-link-hover hover:underline"
|
|
62
63
|
>
|
|
63
64
|
Back to sign in
|
|
@@ -1,22 +1,239 @@
|
|
|
1
1
|
import { verifySession } from '@/lib/mars';
|
|
2
|
-
import {
|
|
2
|
+
import { appConfig } from '@/config/app.config';
|
|
3
|
+
import { routes } from '@/config/routes';
|
|
4
|
+
import { Card, Badge, LinkButton } from '@mars-stack/ui';
|
|
5
|
+
|
|
6
|
+
type FeatureKey = keyof typeof appConfig.features;
|
|
7
|
+
|
|
8
|
+
interface FeatureMeta {
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
route?: string;
|
|
12
|
+
category: 'core' | 'engagement' | 'monetization' | 'developer';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FEATURE_REGISTRY: Partial<Record<FeatureKey, FeatureMeta>> = {
|
|
16
|
+
auth: {
|
|
17
|
+
label: 'Authentication',
|
|
18
|
+
description: 'Email/password auth, sessions, and password reset.',
|
|
19
|
+
route: routes.settings,
|
|
20
|
+
category: 'core',
|
|
21
|
+
},
|
|
22
|
+
admin: {
|
|
23
|
+
label: 'Admin Panel',
|
|
24
|
+
description: 'Role-based admin dashboard for user management.',
|
|
25
|
+
route: routes.admin,
|
|
26
|
+
category: 'core',
|
|
27
|
+
},
|
|
28
|
+
darkMode: {
|
|
29
|
+
label: 'Dark Mode',
|
|
30
|
+
description: 'Automatic theme switching with system preference detection.',
|
|
31
|
+
category: 'core',
|
|
32
|
+
},
|
|
33
|
+
notifications: {
|
|
34
|
+
label: 'Notifications',
|
|
35
|
+
description: 'In-app notifications with real-time delivery.',
|
|
36
|
+
category: 'engagement',
|
|
37
|
+
},
|
|
38
|
+
onboarding: {
|
|
39
|
+
label: 'Onboarding',
|
|
40
|
+
description: 'Guided setup flow for new users.',
|
|
41
|
+
route: routes.onboarding,
|
|
42
|
+
category: 'engagement',
|
|
43
|
+
},
|
|
44
|
+
billing: {
|
|
45
|
+
label: 'Billing',
|
|
46
|
+
description: 'Stripe subscriptions, pricing page, and customer portal.',
|
|
47
|
+
route: routes.billing,
|
|
48
|
+
category: 'monetization',
|
|
49
|
+
},
|
|
50
|
+
blog: {
|
|
51
|
+
label: 'Blog',
|
|
52
|
+
description: 'Content management with markdown posts.',
|
|
53
|
+
route: routes.blog,
|
|
54
|
+
category: 'engagement',
|
|
55
|
+
},
|
|
56
|
+
seo: {
|
|
57
|
+
label: 'SEO',
|
|
58
|
+
description: 'Meta tags, Open Graph, structured data, and sitemaps.',
|
|
59
|
+
category: 'developer',
|
|
60
|
+
},
|
|
61
|
+
analytics: {
|
|
62
|
+
label: 'Analytics',
|
|
63
|
+
description: 'Page views, events, and user behavior tracking.',
|
|
64
|
+
category: 'developer',
|
|
65
|
+
},
|
|
66
|
+
ai: {
|
|
67
|
+
label: 'AI Assistant',
|
|
68
|
+
description: 'Chat interface with streaming AI completions.',
|
|
69
|
+
route: '/ai',
|
|
70
|
+
category: 'engagement',
|
|
71
|
+
},
|
|
72
|
+
fileUpload: {
|
|
73
|
+
label: 'File Upload',
|
|
74
|
+
description: 'Drag-and-drop uploads with progress and file management.',
|
|
75
|
+
route: routes.files,
|
|
76
|
+
category: 'engagement',
|
|
77
|
+
},
|
|
78
|
+
search: {
|
|
79
|
+
label: 'Search',
|
|
80
|
+
description: 'Full-text search across your application data.',
|
|
81
|
+
category: 'engagement',
|
|
82
|
+
},
|
|
83
|
+
commandPalette: {
|
|
84
|
+
label: 'Command Palette',
|
|
85
|
+
description: 'Keyboard-first navigation with Cmd+K shortcut.',
|
|
86
|
+
category: 'developer',
|
|
87
|
+
},
|
|
88
|
+
cookieConsent: {
|
|
89
|
+
label: 'Cookie Consent',
|
|
90
|
+
description: 'GDPR-compliant cookie consent banner.',
|
|
91
|
+
category: 'core',
|
|
92
|
+
},
|
|
93
|
+
realtime: {
|
|
94
|
+
label: 'Realtime',
|
|
95
|
+
description: 'Live updates via server-sent events or WebSockets.',
|
|
96
|
+
category: 'developer',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
101
|
+
core: 'Core',
|
|
102
|
+
engagement: 'Engagement',
|
|
103
|
+
monetization: 'Monetization',
|
|
104
|
+
developer: 'Developer Tools',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function FeatureIcon({ category }: { category: string }) {
|
|
108
|
+
const iconClass = 'h-5 w-5 text-text-muted';
|
|
109
|
+
|
|
110
|
+
switch (category) {
|
|
111
|
+
case 'core':
|
|
112
|
+
return (
|
|
113
|
+
<svg className={iconClass} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
114
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
|
115
|
+
</svg>
|
|
116
|
+
);
|
|
117
|
+
case 'engagement':
|
|
118
|
+
return (
|
|
119
|
+
<svg className={iconClass} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
120
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
|
121
|
+
</svg>
|
|
122
|
+
);
|
|
123
|
+
case 'monetization':
|
|
124
|
+
return (
|
|
125
|
+
<svg className={iconClass} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
126
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
case 'developer':
|
|
130
|
+
default:
|
|
131
|
+
return (
|
|
132
|
+
<svg className={iconClass} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
133
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function FeatureCard({ featureKey: _featureKey, meta }: { featureKey: string; meta: FeatureMeta }) {
|
|
140
|
+
const hasRoute = !!meta.route;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Card className="flex flex-col justify-between">
|
|
144
|
+
<div>
|
|
145
|
+
<div className="flex items-start justify-between gap-2">
|
|
146
|
+
<div className="flex items-center gap-2.5">
|
|
147
|
+
<FeatureIcon category={meta.category} />
|
|
148
|
+
<h3 className="text-sm font-semibold text-text-primary">{meta.label}</h3>
|
|
149
|
+
</div>
|
|
150
|
+
<Badge variant={hasRoute ? 'success' : 'neutral'}>
|
|
151
|
+
{hasRoute ? 'Active' : 'Enabled'}
|
|
152
|
+
</Badge>
|
|
153
|
+
</div>
|
|
154
|
+
<p className="mt-2 text-sm text-text-secondary leading-relaxed">{meta.description}</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{hasRoute && (
|
|
158
|
+
<div className="mt-4">
|
|
159
|
+
<LinkButton href={meta.route!} variant="subtle" size="sm">
|
|
160
|
+
Open {meta.label}
|
|
161
|
+
</LinkButton>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</Card>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function QuickLinks({ isAdmin }: { isAdmin: boolean }) {
|
|
169
|
+
return (
|
|
170
|
+
<Card>
|
|
171
|
+
<h3 className="text-sm font-semibold text-text-primary">Quick Links</h3>
|
|
172
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
173
|
+
<LinkButton href={routes.settings} variant="subtle" size="sm">
|
|
174
|
+
Settings
|
|
175
|
+
</LinkButton>
|
|
176
|
+
{isAdmin && (
|
|
177
|
+
<LinkButton href={routes.admin} variant="subtle" size="sm">
|
|
178
|
+
Admin Panel
|
|
179
|
+
</LinkButton>
|
|
180
|
+
)}
|
|
181
|
+
<LinkButton href={routes.home} variant="subtle" size="sm">
|
|
182
|
+
Home
|
|
183
|
+
</LinkButton>
|
|
184
|
+
</div>
|
|
185
|
+
</Card>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
3
188
|
|
|
4
189
|
export default async function Dashboard() {
|
|
5
190
|
const session = await verifySession();
|
|
6
191
|
|
|
192
|
+
const enabledFeatures = Object.entries(appConfig.features)
|
|
193
|
+
.filter(([, enabled]) => enabled)
|
|
194
|
+
.map(([key]) => key as FeatureKey)
|
|
195
|
+
.filter((key) => FEATURE_REGISTRY[key])
|
|
196
|
+
.map((key) => ({ key, meta: FEATURE_REGISTRY[key]! }));
|
|
197
|
+
|
|
198
|
+
const categories = [...new Set(enabledFeatures.map((f) => f.meta.category))];
|
|
199
|
+
|
|
200
|
+
const activeCount = enabledFeatures.filter((f) => f.meta.route).length;
|
|
201
|
+
const totalEnabled = enabledFeatures.length;
|
|
202
|
+
|
|
7
203
|
return (
|
|
8
204
|
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</Card>
|
|
205
|
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
|
206
|
+
<div>
|
|
207
|
+
<h1 className="text-3xl font-bold text-text-primary">Dashboard</h1>
|
|
208
|
+
<p className="mt-1 text-text-secondary">Welcome back, {session.name}.</p>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="mt-2 flex items-center gap-3 sm:mt-0">
|
|
211
|
+
<Badge variant="success">{activeCount} active</Badge>
|
|
212
|
+
<Badge variant="neutral">{totalEnabled} enabled</Badge>
|
|
213
|
+
</div>
|
|
19
214
|
</div>
|
|
215
|
+
|
|
216
|
+
{categories.map((category) => {
|
|
217
|
+
const categoryFeatures = enabledFeatures.filter((f) => f.meta.category === category);
|
|
218
|
+
if (categoryFeatures.length === 0) return null;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<section key={category} className="mt-8">
|
|
222
|
+
<h2 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-4">
|
|
223
|
+
{CATEGORY_LABELS[category] ?? category}
|
|
224
|
+
</h2>
|
|
225
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
226
|
+
{categoryFeatures.map(({ key, meta }) => (
|
|
227
|
+
<FeatureCard key={key} featureKey={key} meta={meta} />
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
|
|
234
|
+
<section className="mt-8">
|
|
235
|
+
<QuickLinks isAdmin={session.role === 'admin'} />
|
|
236
|
+
</section>
|
|
20
237
|
</div>
|
|
21
238
|
);
|
|
22
239
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { FileUploader, FileList } from '@/features/uploads';
|
|
5
|
+
|
|
6
|
+
export default function FilesPage() {
|
|
7
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
8
|
+
|
|
9
|
+
const handleUploadComplete = useCallback(() => {
|
|
10
|
+
setRefreshKey((prev) => prev + 1);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-8">
|
|
15
|
+
<div>
|
|
16
|
+
<h1 className="text-2xl font-bold text-text-primary">Files</h1>
|
|
17
|
+
<p className="mt-1 text-text-secondary">Upload, manage, and download your files.</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<FileUploader onUploadComplete={handleUploadComplete} />
|
|
21
|
+
|
|
22
|
+
<section>
|
|
23
|
+
<h2 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-4">
|
|
24
|
+
Your Files
|
|
25
|
+
</h2>
|
|
26
|
+
<FileList refreshKey={refreshKey} />
|
|
27
|
+
</section>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -8,11 +8,24 @@ import { useAuth } from '@/features/auth/context/AuthContext';
|
|
|
8
8
|
import { routes } from '@/config/routes';
|
|
9
9
|
import { appConfig } from '@/config/app.config';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const STATIC_NAV_ITEMS = [
|
|
12
12
|
{ label: 'Dashboard', href: routes.dashboard },
|
|
13
13
|
{ label: 'Settings', href: routes.settings },
|
|
14
14
|
] as const;
|
|
15
15
|
|
|
16
|
+
function buildNavItems() {
|
|
17
|
+
const items: Array<{ label: string; href: string }> = [...STATIC_NAV_ITEMS];
|
|
18
|
+
if (appConfig.features.billing) {
|
|
19
|
+
items.push({ label: 'Billing', href: routes.billing });
|
|
20
|
+
}
|
|
21
|
+
if (appConfig.features.fileUpload) {
|
|
22
|
+
items.push({ label: 'Files', href: routes.files });
|
|
23
|
+
}
|
|
24
|
+
return items;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const NAV_ITEMS = buildNavItems();
|
|
28
|
+
|
|
16
29
|
function NavLink({ href, label, active }: { href: string; label: string; active: boolean }) {
|
|
17
30
|
return (
|
|
18
31
|
<Link
|