Public Access
docs: add React Router URL routing design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
40fe1479f9
commit
1c13f4849a
1 file changed
+112
@@ -0,0 +1,112 @@
|
||||
# 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)
|
||||
Reference in new issue
Block a user