import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { fetchDriverSummary, fetchSeasons } from '../api' import { teamColor } from '../utils' import { countryFlag } from '../lib/gpIdentity' import { formatDelta, formatPosition, gridFinishDeltas, type RoundDelta, } from '../lib/driverProfile' import type { DriverSummary } from '../types' import '../styles/driver-profile.css' interface Props { driverNumber: number year?: number } function fmtPts(n: number): string { return Number.isInteger(n) ? String(n) : n.toFixed(1) } export function DriverProfilePage({ driverNumber, year }: Props) { // Without an explicit ?year=, default to the latest ingested season; if the // seasons list is empty/unavailable the backend falls back to the current year. const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: fetchSeasons, enabled: year == null, }) const resolvedYear = year ?? seasonsQuery.data?.[0] const seasonsSettled = year != null || !seasonsQuery.isLoading const summaryQuery = useQuery({ queryKey: ['driver-summary', driverNumber, resolvedYear ?? 'latest'], queryFn: () => fetchDriverSummary(driverNumber, resolvedYear), enabled: seasonsSettled && driverNumber > 0, staleTime: 5 * 60_000, }) if (driverNumber <= 0) { return
Invalid driver number
} if (!seasonsSettled || summaryQuery.isLoading) { return
loading driver profile…
} if (summaryQuery.isError) { return (
{summaryQuery.error instanceof Error ? summaryQuery.error.message : 'Failed to load driver profile'}
) } const summary = summaryQuery.data if (!summary) { return
No driver data
} return } function DriverProfileBody({ summary }: { summary: DriverSummary }) { const color = teamColor(summary.team_colour) const deltas = gridFinishDeltas(summary.rounds) return (
{summary.name_acronym || `#${summary.driver_number}`} #{summary.driver_number}

{summary.full_name || `Driver ${summary.driver_number}`}

{summary.team_name || 'Unknown team'} {summary.season}
{summary.position > 0 ? `P${summary.position}` : '—'}
Championship
{fmtPts(summary.points)}
Points
{summary.wins}
Wins
{summary.podiums}
Podiums
{summary.poles}
Poles
← Championship

Season form

{summary.cumulative.length > 0 ? (
) : (
No completed rounds yet.
)}

Quali vs race

{deltas.some((d) => d.delta != null) ? ( ) : (
No grid-vs-finish data yet.
)}

Track by track

{summary.rounds.length > 0 ? ( ) : (
No completed rounds yet.
)}
) } const LINE_W = 640 const LINE_H = 180 const LINE_PL = 44 const LINE_PR = 16 const LINE_PT = 12 const LINE_PB = 24 function CumulativeLine({ cumulative, labels, color, }: { cumulative: number[] labels: string[] color: string }) { const n = cumulative.length const maxY = Math.max(25, ...cumulative) const plotW = LINE_W - LINE_PL - LINE_PR const plotH = LINE_H - LINE_PT - LINE_PB const x = (i: number) => (n <= 1 ? LINE_PL + plotW / 2 : LINE_PL + (i * plotW) / (n - 1)) const y = (v: number) => LINE_PT + plotH - (v / maxY) * plotH const points = cumulative.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ') const tickStep = Math.max(1, Math.ceil(n / 8)) const ticks: { x: number; label: string }[] = [] for (let i = 0; i < n; i += tickStep) { ticks.push({ x: x(i), label: labels[i] ?? `R${i + 1}` }) } return (
Cumulative points
{[0, 0.5, 1].map((f) => { const v = Math.round(maxY * f) return ( {v} ) })} {ticks.map((t) => ( {t.label} ))}
) } function FormStrip({ form, color }: { form: number[]; color: string }) { const max = Math.max(25, ...form) return (
Last {form.length} races
{form.map((pts, i) => (
{fmtPts(pts)}
))}
) } const DELTA_H = 190 const DELTA_PT = 14 const DELTA_PB = 30 function GridVsRaceChart({ deltas, color }: { deltas: RoundDelta[]; color: string }) { const maxAbs = Math.max(1, ...deltas.map((d) => Math.abs(d.delta ?? 0))) const colW = 34 const w = Math.max(240, deltas.length * colW) const plotH = DELTA_H - DELTA_PT - DELTA_PB const zeroY = DELTA_PT + plotH / 2 const scale = plotH / 2 / maxAbs return (
Positions gained (▲) / lost (▼) from grid to flag, per round
{deltas.map((d, i) => { const cx = i * colW + colW / 2 if (d.delta == null) { return ( · R{d.round} ) } const h = Math.abs(d.delta) * scale const yTop = d.delta >= 0 ? zeroY - h : zeroY return ( {`${d.label}: ${formatPosition(d.grid)} → ${formatPosition(d.finish)} (${formatDelta(d.delta)})`} = 0 ? 0.95 : 0.45} /> = 0 ? yTop - 4 : yTop + h + 11} textAnchor="middle" className="dp-delta-val mono" > {formatDelta(d.delta)} R{d.round} ) })}
) } function statusNote(d: RoundDelta): string { if (d.status === 'dnf') return 'DNF' if (d.status === 'dns') return 'DNS' if (d.status === 'dsq') return 'DSQ' if (d.status === 'absent') return '—' return '' } function RoundsTable({ summary, deltas }: { summary: DriverSummary; deltas: RoundDelta[] }) { return (
{summary.rounds.map((r, i) => { const d = deltas[i] return ( ) })}
Rnd Grand Prix Grid Race Δ Pts
R{i + 1} {r.meeting_name} {formatPosition(d.grid)} {formatPosition(d.finish)} 0 ? 'var(--green)' : d.delta < 0 ? 'var(--red)' : 'var(--text-2)', }} > {formatDelta(d.delta)} {fmtPts(r.points)} {statusNote(d)}
) }