110 lines
3.7 KiB
Markdown
110 lines
3.7 KiB
Markdown
|
|
---
|
||
|
|
name: add-page
|
||
|
|
description: >-
|
||
|
|
Add a new Next.js page under pages/. Use when you need a new route, a new
|
||
|
|
view for an existing resource, or an admin-only screen. Covers Layout
|
||
|
|
wiring, auth, theme tokens, and the public/authenticated split pattern.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Add a page
|
||
|
|
|
||
|
|
`pages/<file>.js` becomes a route. Pages router conventions:
|
||
|
|
|
||
|
|
| File | Route |
|
||
|
|
| --- | --- |
|
||
|
|
| `pages/about.js` | `/about` |
|
||
|
|
| `pages/cards/[id].js` | `/cards/:id` (but use `pages/card/[id].js` per existing naming) |
|
||
|
|
| `pages/admin/index.js` | `/admin` |
|
||
|
|
|
||
|
|
## Step 1: Decide auth shape
|
||
|
|
|
||
|
|
| Mode | Template |
|
||
|
|
| --- | --- |
|
||
|
|
| Public-only | Render without `ProtectedRoute`; no auth check |
|
||
|
|
| Auth-required | Wrap top-level export with `<ProtectedRoute>` |
|
||
|
|
| Admin-only | Wrap with `<AdminProtected>` from `components/AdminProtected.js` |
|
||
|
|
| Public + auth-enhanced (e.g. `/cards`) | Inline split: render `<PublicView>` if not logged in, `<AuthedView>` if logged in. Copy the pattern from `pages/cards.js`. |
|
||
|
|
|
||
|
|
## Step 2: Skeleton
|
||
|
|
|
||
|
|
```js
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import Layout from '../components/Layout';
|
||
|
|
import ProtectedRoute from '../components/ProtectedRoute';
|
||
|
|
import { useAuth } from '../lib/use-auth';
|
||
|
|
|
||
|
|
export default function MyPage() {
|
||
|
|
return (
|
||
|
|
<ProtectedRoute>
|
||
|
|
<MyPageInner />
|
||
|
|
</ProtectedRoute>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function MyPageInner() {
|
||
|
|
const { user, loading } = useAuth();
|
||
|
|
const [items, setItems] = useState([]);
|
||
|
|
const [fetching, setFetching] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (loading || !user) return;
|
||
|
|
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
setFetching(true);
|
||
|
|
fetch('/api/my-resource', {
|
||
|
|
headers: { Authorization: `Bearer ${token}` },
|
||
|
|
})
|
||
|
|
.then((r) => r.json())
|
||
|
|
.then((data) => setItems(data.items || []))
|
||
|
|
.catch((err) => console.error('fetch failed', err))
|
||
|
|
.finally(() => setFetching(false));
|
||
|
|
}, [user, loading]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Layout user={user} showSearch={false}>
|
||
|
|
<div className="p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||
|
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
My Page
|
||
|
|
</h1>
|
||
|
|
{fetching ? <p>Loading…</p> : items.map((i) => <div key={i.id}>{i.name}</div>)}
|
||
|
|
</div>
|
||
|
|
</Layout>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Step 3: Theme tokens (not hex)
|
||
|
|
|
||
|
|
- Backgrounds → `var(--bg-primary)`, `var(--bg-secondary)`, `var(--bg-tertiary)`
|
||
|
|
- Text → `var(--text-primary)`, `var(--text-secondary)`
|
||
|
|
- Accents → `var(--accent-ember)`, `var(--accent-flame)`
|
||
|
|
- Borders → `var(--border)`
|
||
|
|
|
||
|
|
Use Tailwind for layout, spacing, sizing, hover/focus states. Use CSS vars (inline `style={{ ... }}`) for colors that need to switch with theme.
|
||
|
|
|
||
|
|
## Step 4: Always pass `user` to Layout
|
||
|
|
|
||
|
|
`<Layout user={user}>` — never let the default kick in (it's a hardcoded maintainer email; see `AGENTS.md` Gotcha #8).
|
||
|
|
|
||
|
|
## Step 5: Mobile
|
||
|
|
|
||
|
|
`components/Layout.js` already handles the mobile drawer + bottom nav. To add the page to nav, edit `NavigationContent` in `Layout.js`. Use existing icon names from the `getIcon` registry; add new ones to that registry before referencing.
|
||
|
|
|
||
|
|
## Step 6: Check
|
||
|
|
|
||
|
|
- [ ] Auth wrapper chosen (ProtectedRoute / AdminProtected / public).
|
||
|
|
- [ ] `useAuth()` from `lib/use-auth.js` (not the legacy `lib/auth-context.js`).
|
||
|
|
- [ ] `user` passed to Layout explicitly.
|
||
|
|
- [ ] Colors come from theme tokens, not hex.
|
||
|
|
- [ ] All interactive elements have `aria-label` or visible text.
|
||
|
|
- [ ] Mobile: confirm the page renders in the mobile drawer.
|
||
|
|
|
||
|
|
## Anti-patterns
|
||
|
|
|
||
|
|
| Don't | Do |
|
||
|
|
| --- | --- |
|
||
|
|
| Hardcode hex colors | Use CSS variables |
|
||
|
|
| Default `user = { … }` to a real email | Default to `null` |
|
||
|
|
| Pull from `lib/auth-context` for new code | Use `lib/use-auth` |
|
||
|
|
| Render Layout twice on the same page | Single `<Layout>` at the top |
|