A spaced repetition learning app for memorizing racetrack corner names. Primary use case: learning Spa-Francorchamps and Nürburgring Nordschleife before driving them. Built for web first, then mobile.
The Nordschleife has 42 named sections covering 73 corners over 20.8km. Spa has ~14 named corners. You're driving the Nordschleife this summer. Passive watching of "learn the Ring" videos doesn't encode names reliably. Active recall via spaced repetition does.
| Layer | Choice | Why |
|---|---|---|
| Frontend | React + Vite + Tailwind | You know React; Vite is the current standard CRA replacement — it's just a fast dev server + bundler, nothing exotic |
| Storage (now) | localStorage | Zero setup, works offline immediately |
| Storage (later) | Supabase | Postgres + auth + REST API. Free tier. Swap in by changing one file. |
| Mobile | PWA first | Wraps the same React app, works offline, installs to home screen. No rewrite. |
| App Store (optional) | Capacitor | Wraps the PWA in a native shell if App Store distribution is ever needed. |
All progress read/write goes through a single module — src/storage.js.
Today it wraps localStorage. Later it wraps Supabase. The rest of the app
never knows or cares which backend is active.
// src/storage.js
export async function saveProgress(cornerId, trackId, smData) { ... }
export async function loadAllProgress(trackId) { ... }
export async function resetProgress(trackId) { ... }Corner data lives in static JSON files in public/data/.
Progress/SRS state lives in storage (localStorage → Supabase).
Images live in public/images/corners/{trackId}/{cornerId}.jpg.
SM-2 is the classic spaced repetition algorithm powering Anki. ~40 lines of code. Battle-tested since 1987.
After each flashcard, user rates 0–5:
- 0–2 (Again/Hard): card comes back in minutes/hours
- 3 (Good): interval stays or slightly increases
- 4–5 (Easy): interval multiplies by "easiness factor"
Each card stores:
{
"cornerId": "eau-rouge",
"trackId": "spa",
"interval": 6,
"repetitions": 3,
"easeFactor": 2.5,
"nextReview": "2026-03-28T00:00:00Z"
}Cards due today are surfaced for review. New cards are introduced at a configurable rate (e.g. 5 new cards/day) to avoid overwhelming.
Walk through all corners in lap order. See the corner name + notes. No pressure — just absorbing before testing begins. Use this first when starting a new track.
The core learning loop:
- Show: driver's-perspective image of corner approach
- User thinks: "what corner is this?"
- Tap to reveal: corner name, lap position, character notes
- Rate: Again / Hard / Good / Easy
- SM-2 schedules next review
Also supports reverse: show corner name → recall position in lap / what it looks like.
Interactive SVG track map. Tap a corner to see its name and drill it. Pin corners you're struggling with.
{
"id": "eau-rouge",
"name": "Eau Rouge",
"order": 2,
"type": "fast",
"gps": { "lat": 50.4351, "lng": 5.9717 },
"heading": 28,
"verified": false,
"notes": "High-speed left-hander at the bottom of the valley, leads immediately into Raidillon."
}verified: false means GPS coords and/or name needs cross-checking against
a primary source before the Street View image download script is run.
Do not use AI-estimated GPS. Use OpenStreetMap / Overpass API instead.
Both circuits are fully mapped in OSM. The script scripts/fetch-gps.js
queries the Overpass API for the circuit track geometry (a GPS polyline),
then maps each named section to its position along that polyline.
Overpass query for Nordschleife:
[out:json];
way["name"="Nürburgring Nordschleife"];
out geom;
Once we have consecutive GPS points, heading is calculated as:
Math.atan2(lng2 - lng1, lat2 - lat1) * (180 / Math.PI)This gives the bearing the driver faces approaching each corner entry.
Run once. Never again. Script: scripts/download-images.js
For each corner:
GET https://maps.googleapis.com/maps/api/streetview
?size=800x500
&location={lat},{lng}
&heading={heading}
&pitch=5
&fov=90
&key={GOOGLE_MAPS_API_KEY}
Output: public/images/corners/{trackId}/{cornerId}.jpg
Requires a Google Maps Static API key. Free up to 28,000 requests/month. Run this script once → images become local static assets forever. No API key needed at runtime.
Your Assetto Corsa sim screenshots. Drive to the entry of each corner,
screenshot, save as public/images/corners/spa/{cornerId}.jpg.
Filenames must match corner IDs in spa.json.
- Define corner JSON schema
- Compile
spa.json— corner names verified against primary sources - Compile
nordschleife.json— all 42 named sections verified - Mark all entries
verified: falseuntil GPS + name confirmed - Write
scripts/fetch-gps.js— queries Overpass API, updates JSON with real coords - Write
scripts/download-images.js— bulk Street View image download
- Scaffold:
npm create vite@latest cornerflash -- --template react - Add Tailwind:
npm install -D tailwindcss @tailwindcss/vite - Implement
src/storage.js— localStorage abstraction - Implement
src/sm2.js— SM-2 algorithm (pure functions, no side effects) - Build Study Mode — sequential walkthrough, corner name + notes
- Build Flashcard Mode — image → reveal → rate → next
- Build Progress View — per-track stats, due today, mastered, struggling
- Track selector — switch between Spa and Nordschleife
- Create Supabase project (free tier)
- Schema:
users,progresstables - Swap
src/storage.jsto use Supabase client - Add auth (email/Google OAuth)
- Test cross-device sync
- Add
vite-plugin-pwa - Configure service worker + cache strategy (cache-first for images)
- Add web manifest (name, icon, theme color)
- Test install on iOS and Android
- Source or create SVG track maps for Spa and Nordschleife
- Overlay corner tap targets
- Link taps to flashcard drill for that corner
cornerflash/
├── public/
│ ├── data/
│ │ ├── spa.json
│ │ └── nordschleife.json
│ └── images/
│ └── corners/
│ ├── spa/ ← your AC screenshots
│ └── nordschleife/ ← Street View downloads
├── scripts/
│ ├── fetch-gps.js ← one-time: queries Overpass, writes GPS to JSON
│ └── download-images.js ← one-time: downloads Street View images
├── src/
│ ├── storage.js ← THE abstraction layer (swap localStorage ↔ Supabase here)
│ ├── sm2.js ← pure SM-2 algorithm
│ ├── components/
│ │ ├── StudyMode.jsx
│ │ ├── FlashcardMode.jsx
│ │ ├── ProgressView.jsx
│ │ └── TrackSelector.jsx
│ ├── App.jsx
│ └── main.jsx
├── PLAN.md ← this file
└── package.json
| Decision | Choice | Rationale |
|---|---|---|
| Images hosted vs API | Static hosted | Offline PWA support; no API key at runtime; no rate limits; no future cost |
| Image source — Spa | Assetto Corsa screenshots | You sim race it; you own the screenshots; perfect driver POV |
| Image source — Nordschleife | Google Street View (downloaded once) | Full circuit coverage; real road; one-time API use |
| GPS source | OpenStreetMap / Overpass API | Real surveyed data; not AI estimates |
| Mobile | PWA first, Capacitor if needed | Reuses 100% of web code; no App Store friction for personal use |
| Backend | Supabase | Postgres-based; generous free tier; self-hostable; not Firebase |
| Cross-device sync | Yes (it's 2026) | localStorage for local dev; Supabase replaces it in Phase 3 |
| Learning method | SM-2 SRS + active recall | Decades of research; proven for vocabulary-scale memorization tasks |
- Verify Nordschleife section names against nring.info or oversteer48.com before building — don't want to memorize wrong names before your drive.
- Current Spa layout — circuit was modified post-2022. Confirm corner names reflect the current racing layout, not the pre-2022 one.
- Google Maps API key — needed to run
download-images.jsonce. Create at console.cloud.google.com. Enable "Street View Static API". - Supabase project — can be deferred to Phase 3 but create early to reserve a project name.
- Nordschleife direction — confirm lap direction for heading calculations (clockwise when viewed from above, going east out of the pits).