--- 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/.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 `` | | Admin-only | Wrap with `` from `components/AdminProtected.js` | | Public + auth-enhanced (e.g. `/cards`) | Inline split: render `` if not logged in, `` 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 ( ); } 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 (

My Page

{fetching ?

Loading…

: items.map((i) =>
{i.name}
)}
); } ``` ## 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 `` — 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` (the only client auth hook; `lib/auth-context.js` and `lib/admin-auth.js` were deleted by the `single-auth-provider` convoy). - [ ] `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` | | Reintroduce `lib/auth-context` or `lib/admin-auth` (deleted) | Use `lib/use-auth` | | Render Layout twice on the same page | Single `` at the top |