---
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` (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 `<Layout>` at the top |
