Files
brainwave/docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md
T
2026-04-24 23:39:06 +02:00

113 lines
3.9 KiB
Markdown

# React Router URL Routing — Design Spec
**Date:** 2026-04-24
**Status:** Approved
## Overview
Add React Router v7 to the Brainwave frontend so the URL reflects the active exam and current question number. A user on question 3 of "My Exam" will see `/exam/my-exam/q3` in the address bar. Back/forward browser navigation works naturally, and deep links to a specific question are supported.
## Route Structure
| URL | Screen |
| --- | ------ |
| `/` | Landing |
| `/exam/:slug/q:n` | ExamRunner — question `n` (1-indexed) of exam identified by `slug` |
| `/results` | Results (post-submission) |
| `/review/:slug` | Results in review-only mode |
| `/verify` | Email verification |
| `/reset-password` | Password reset |
| `/admin` | Admin dashboard (admin users only) |
`:slug` is the exam title converted to lowercase kebab-case (e.g. `"My Exam 2024"` → `"my-exam-2024"`). `:n` is the 1-indexed question number.
## Architecture
### New dependency
`react-router-dom` v7 added to `frontend/package.json` dependencies.
### New utility — `frontend/src/utils/slug.ts`
Pure function `slugify(title: string): string`:
- Lowercase
- Replace non-alphanumeric characters with hyphens
- Collapse consecutive hyphens
- Trim leading/trailing hyphens
### `main.tsx`
Wrap `<App>` in `<BrowserRouter>`.
### `App.tsx`
- Remove `screen` state and `AppScreen` type usage.
- Replace the URL-on-mount `useEffect` with React Router's declarative routing.
- Use `<Routes>` / `<Route>` to render screen components.
- Replace all `setScreen()` calls with `useNavigate()`:
- `loadExam()` → `navigate('/exam/${slug}/q1')`
- `submit()` → `navigate('/results')`
- `exit()` → `navigate('/')`
- `reviewAttempt()` → `navigate('/review/${slug}')`
- Admin menu item → `navigate('/admin')`
- `handleAuthFlowDone()` → `navigate('/')`
- `exam` and `examState` remain in React state — no change to storage, grading, or submission logic.
### `ExamRunner.tsx`
- Accept an `onNavigate: (i: number) => void` prop, called from `goTo()` after updating `examState`.
- `App.tsx` passes a handler that calls `navigate('/exam/${slug}/q${i+1}')`.
- ExamRunner has no direct knowledge of React Router — it remains router-agnostic.
### Exam route handler in `App.tsx`
- Reads `:n` and `:slug` from `useParams()`.
- On mount and whenever `:n` changes (back/forward), calls `setExamState` to set `currentIdx = n - 1`.
- Handles the deep-link case: if no active exam in state, checks localStorage for a matching slug; resumes or redirects to `/`.
### Deep linking
When the app loads on `/exam/:slug/q:n`:
- Check localStorage for a saved exam state (`brainwave.exam.state`).
- If the saved exam's slugified title matches `:slug`, resume with `currentIdx = n - 1`.
- If no match, redirect to `/`.
### Vite config
Add `server.historyApiFallback: true` to `vite.config.ts` so the dev server serves `index.html` for all non-asset paths. The production backend already handles this (it serves `index.html` for non-API routes).
## Data Flow
```text
User clicks question 5 in sidebar
→ goTo(4) called in ExamRunner
→ setState updates examState.currentIdx = 4
→ onNavigate(4) called
→ navigate('/exam/my-exam/q5') in App
→ URL bar updates
→ :n param changes
→ useEffect in App exam route calls setExamState [no-op, already 4]
User presses browser Back
→ URL changes to /exam/my-exam/q4
→ :n param changes to '4'
→ useEffect in App exam route calls setExamState({ currentIdx: 3 })
→ ExamRunner re-renders at question 4
```
## What Does Not Change
- `Results.tsx`, `Landing.tsx`, all question components
- `AuthContext`, storage utils, grading utils, API utils
- Backend — no changes required
- `ExamState` shape and localStorage persistence
- `AppScreen` type removed from `types.ts`
## Out of Scope
- Animated route transitions
- Scroll restoration between questions
- Sharing/bookmarking exam links to others (exam data is client-side only)