Historical F1 qualifying lap comparison. OpenF1-first data, Next.js + TypeScript UI, comparison-first one-page experience. Four phases, ~8 days of solo work.
Each phase blocks the next. Phase 1 locks scope & contracts → Phase 2 builds the data spine → Phase 3 turns payload into product → Phase 4 hardens it.
flowchart LR
classDef phase fill:#1a2331,stroke:#2a3442,color:#d7dee7,rx:10,ry:10
classDef p1 fill:#142031,stroke:#6ec8ff,color:#d7dee7
classDef p2 fill:#142031,stroke:#6ec8ff,color:#d7dee7
classDef p3 fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7
classDef p4 fill:#2a1f10,stroke:#ffb02e,color:#d7dee7
subgraph P1["Phase 1 · Research & scope · 1d"]
direction TB
P1A["Pick stack: Next.js + TS"]
P1B["Define data contract
SessionOption · DriverOption · LapOption
NormalizedTelemetrySample · LapComparisonPayload"]
P1C["Lock v1 scope:
qualifying · 2-lap compare · historical"]
P1D["Design tokens & visual direction"]
end
subgraph P2["Phase 2 · Data access layer · 2d"]
direction TB
P2A["OpenF1 client (typed)"]
P2B["Route handlers
/api/sessions · /api/lap-comparison"]
P2C["Telemetry merge
nearest-sample by timestamp"]
P2D["Lap-progress normalize"]
P2E["Comparison payload mapper"]
P2F["Recorded fixtures"]
end
subgraph P3["Phase 3 · Comparison UI · 3d"]
direction TB
P3A["Page shell + selectors"]
P3B["Summary cards
lap time · sector · top speed"]
P3C["Delta chart (hero)"]
P3D["Telemetry traces
speed · throttle · brake · gear · RPM"]
P3E["Track-progress map
gain/loss segments"]
P3F["Shareable URL state"]
end
subgraph P4["Phase 4 · Test polish & docs · 2d"]
direction TB
P4A["Unit tests · merge math"]
P4B["Integration tests · routes"]
P4C["E2E · one full compare"]
P4D["A11y + perf audit"]
P4E["README · architecture · backlog"]
end
P1 ==> P2 ==> P3 ==> P4
class P1 p1
class P2 p2
class P3 p3
class P4 p4
Browser drives selectors → BFF route handlers fetch & normalize → UI renders charts. URL is the shareable state.
flowchart LR classDef ui fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7,rx:8,ry:8 classDef api fill:#142031,stroke:#6ec8ff,color:#d7dee7,rx:8,ry:8 classDef lib fill:#1a2331,stroke:#2a3442,color:#d7dee7,rx:8,ry:8 classDef ext fill:#2a1f10,stroke:#ffb02e,color:#d7dee7,rx:8,ry:8 U["User · pick session + 2 laps"]:::ui URL["URL search params
?session=...&lapA=...&lapB=..."]:::ui UI["Comparison page
delta · telemetry · track map"]:::ui R1["/api/sessions"]:::api R2["/api/lap-comparison"]:::api C["openf1-client.ts"]:::lib M["merge-lap-samples.ts
nearest by timestamp"]:::lib N["normalize-lap-progress.ts
relative progress + distance"]:::lib B["build-lap-comparison-payload.ts
delta · gain/loss · max/min"]:::lib OF1[("OpenF1 API
sessions · drivers · laps
car_data · location")]:::ext U --> URL --> R2 U --> R1 --> C --> OF1 R2 --> C C --> M --> N --> B --> R2 R2 --> UI UI -.shareable.-> URL
When this is true, MVP ships:
flowchart TB classDef ok fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7,rx:8,ry:8 S1["Open one page · pick session · compare 2 laps"]:::ok S2["Gain/loss zones are obvious without tooltips"]:::ok S3["Shared URL fully restores the comparison"]:::ok S4["UI looks portfolio-grade, not admin-tool"]:::ok S5["Data layer modular enough for “Stint Story” v2"]:::ok S1 --> S2 --> S3 --> S4 --> S5