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