diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fed0196..d2eba33 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tanstack/react-query": "^5.62.0", "@tanstack/react-router": "^1.81.0", + "lucide-react": "^1.23.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -2556,6 +2557,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6599916..e4a4552 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@tanstack/react-query": "^5.62.0", "@tanstack/react-router": "^1.81.0", + "lucide-react": "^1.23.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/frontend/src/components/CliCommands.tsx b/frontend/src/components/CliCommands.tsx index f3fd3e3..52db347 100644 --- a/frontend/src/components/CliCommands.tsx +++ b/frontend/src/components/CliCommands.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { Copy, Check } from 'lucide-react' interface Command { comment?: string @@ -39,8 +40,9 @@ function CliCommandLine({ cmd }: { cmd: string }) { return (
{cmd} -
) diff --git a/frontend/src/components/OverviewView.tsx b/frontend/src/components/OverviewView.tsx index 41601fa..aa9215c 100644 --- a/frontend/src/components/OverviewView.tsx +++ b/frontend/src/components/OverviewView.tsx @@ -1,6 +1,7 @@ import type { RaceHub } from '../types' import { compareFinishPosition, formatDuration, formatGap, formatLapTime } from '../utils' import { countRaceHubDatasets } from '../lib/coverage' +import { Thermometer, Map, Droplets, Wind, CloudRain } from 'lucide-react' interface Props { data: RaceHub @@ -74,7 +75,7 @@ export function OverviewView({ data }: Props) {
-
+
Conditions {latestWeather && ( @@ -83,17 +84,20 @@ export function OverviewView({ data }: Props) {
{latestWeather ? (
- + - + 0 ? 'Yes' : 'No'} accent={latestWeather.rainfall > 0 ? 'wet' : undefined} @@ -104,7 +108,7 @@ export function OverviewView({ data }: Props) { )}
-
+
Race Control · Latest {data.race_control.length} @@ -126,7 +130,7 @@ export function OverviewView({ data }: Props) { )}
-
+
Local Coverage @@ -173,7 +177,7 @@ function StatCard({ }: StatCardProps) { if (placeholder) { return ( -
+
{label}
No data ingested
@@ -181,7 +185,7 @@ function StatCard({ ) } return ( -
+
{label}
{primary} @@ -196,7 +200,7 @@ function StatCard({ function PodiumCard({ podium }: { podium: Array<{ name_acronym: string; team_colour: string; position: number; full_name: string; gap_to_leader: number | string | number[] | null; duration: number | number[] | null; driver_number: number }> }) { if (podium.length === 0) { return ( -
+
Podium
No classified finishers
@@ -204,7 +208,7 @@ function PodiumCard({ podium }: { podium: Array<{ name_acronym: string; team_col ) } return ( -
+
Podium
    {podium.map((r) => ( @@ -232,13 +236,16 @@ function ConditionChip({ label, value, accent, + icon: Icon, }: { label: string value: string accent?: 'wet' + icon?: React.ElementType }) { return ( -
    +
    + {Icon && } {label} {value}
    diff --git a/frontend/src/components/live/TimingTower.tsx b/frontend/src/components/live/TimingTower.tsx index 039ff13..eee5c54 100644 --- a/frontend/src/components/live/TimingTower.tsx +++ b/frontend/src/components/live/TimingTower.tsx @@ -1,3 +1,4 @@ +import { useState, Fragment } from 'react' import { teamColor } from '../../utils' import type { LiveStintData } from '../../types' import type { LiveTimingRow } from '../../lib/live' @@ -11,6 +12,7 @@ import { import type { GapHistoryMap } from '../../lib/gapHistory' import { GapSparkline } from './GapSparkline' import { StintHistory } from './StintHistory' +import { Pin } from 'lucide-react' interface Props { rows: LiveTimingRow[] @@ -19,6 +21,7 @@ interface Props { battleNumbers?: Set pinned?: string[] onTogglePin?: (racingNumber: string) => void + sessionType?: string } function posClass(pos: number): string { @@ -35,7 +38,11 @@ export function TimingTower({ battleNumbers, pinned, onTogglePin, + sessionType = '', }: Props) { + const [expandedRow, setExpandedRow] = useState(null) + const [gapMode, setGapMode] = useState<'interval' | 'leader'>('interval') + if (rows.length === 0) { return (
    @@ -44,21 +51,38 @@ export function TimingTower({ ) } + const sType = sessionType.toLowerCase() + const isRace = sType.includes('race') || sType.includes('sprint') + const isQuali = sType.includes('qualifying') || sType.includes('practice') || !isRace + + const isQ1 = sType === 'qualifying 1' || sType.includes('q1') + const isQ2 = sType === 'qualifying 2' || sType.includes('q2') + return (
    - + {isRace && } - - + + {isRace && } + {isQuali && } + {isQuali && } + {isQuali && } - - + {isRace && } + @@ -68,54 +92,123 @@ export function TimingTower({ const deltaClass = positionDeltaClass(driver) const isPinned = pinned?.includes(row.RacingNumber) ?? false const inBattle = battleNumbers?.has(row.RacingNumber) ?? false + const isExpanded = expandedRow === row.RacingNumber + + // Knockout zone border + let koClass = '' + if (isQuali && (isQ1 || isQ2)) { + if (isQ1 && row.Position === 15) koClass = 'ko-line-p15' + if (isQ2 && row.Position === 10) koClass = 'ko-line-p10' + } else if (isQuali) { + if (row.Position === 15) koClass = 'ko-line-p15' + if (row.Position === 10) koClass = 'ko-line-p10' + } + + const gapText = gapMode === 'interval' && isRace ? (driver.Interval || driver.GapToLeader) : driver.GapToLeader + + // Render micro sectors + const renderSector = (idx: number) => { + const sec = driver.Sectors?.[idx] + if (!sec) return '-' + let sClass = 'mono' + if (sec.OverallFastest) sClass = 'mono txt-purple' + else if (sec.PersonalFastest) sClass = 'mono txt-green' + else if (sec.Value) sClass = 'mono txt-yellow' + return {sec.Value || '-'} + } + return ( - onTogglePin(row.RacingNumber) : undefined} - title={onTogglePin ? (isPinned ? 'Click to unpin' : 'Click to pin (max 3)') : undefined} - > - - - - - - - - - - - + + setExpandedRow(isExpanded ? null : row.RacingNumber)} + style={{ cursor: 'pointer' }} + > + + {isRace && } + + + + + + {isRace && ( + + )} + + {isQuali && } + {isQuali && } + {isQuali && } + + + + {isRace && } + + + + {isExpanded && ( + + + + )} + ) })} diff --git a/frontend/src/pages/CommandCenterPage.tsx b/frontend/src/pages/CommandCenterPage.tsx index 9c3e0b0..05f09d0 100644 --- a/frontend/src/pages/CommandCenterPage.tsx +++ b/frontend/src/pages/CommandCenterPage.tsx @@ -17,6 +17,7 @@ import { import { countryAccent, countryDecal, countryFlag, formatGpDateRange } from '../lib/gpIdentity' import type { Meeting, Session, Weekend, WeekendSession } from '../types' import { PaddockBriefing } from '../components/PaddockBriefing' +import { Play, Activity, Calendar } from 'lucide-react' type WeekendStatusKind = 'live' | 'current' | 'next' | 'recent' | 'fallback' @@ -217,7 +218,7 @@ export function CommandCenterPage() { )} {focusMeeting && ( -
    +
    PosΔΔDriver Tyre Last LapGapTrend setGapMode(g => g === 'interval' ? 'leader' : 'interval')} + title="Click to toggle Gap vs Interval" + style={{ cursor: 'pointer', textDecoration: 'underline' }} + > + {gapMode === 'interval' && isRace ? 'Interval' : 'Gap to P1'} + TrendS1S2S3BestStintsLapsLaps
    {row.Position}{delta} -
    -
    - {driverCode(row)} - {row.RacingNumber} - {isPinned && } - {driver.InPit && PIT} - {driver.PitOut && !driver.InPit && OUT} - {driver.Retired && RET} - {driver.KnockedOut && KO} - {driver.Cutoff && !driver.KnockedOut && CUT} - {driver.OnFlyingLap && FL} -
    -
    - {tyreLabel(row.Tyre)} - - {driver.LastLapTime || '-'} - {driver.GapToLeader || driver.Interval || '-'} - - - {driver.BestLapTime || '-'} - - - {driver.NumberOfLaps || '-'}
    {row.Position}{delta} +
    +
    + {driverCode(row)} + {row.RacingNumber} + {driver.InPit && PIT} + {driver.PitOut && !driver.InPit && OUT} + {driver.Retired && RET} + {driver.KnockedOut && KO} + {driver.Cutoff && !driver.KnockedOut && CUT} + {driver.OnFlyingLap && FL} +
    +
    + {tyreLabel(row.Tyre)} + + {driver.LastLapTime || '-'} + {gapText || '-'} + + {renderSector(0)}{renderSector(1)}{renderSector(2)} + {driver.BestLapTime || '-'} + {driver.NumberOfLaps || '-'} + {onTogglePin && ( + + )} +
    +
    +
    +
    STINTS
    + +
    + {!isRace && ( +
    +
    LAPS
    +
    {driver.NumberOfLaps || 0}
    +
    + )} + {driver.SpeedTrap && ( +
    +
    SPEED TRAP
    +
    {driver.SpeedTrap} km/h
    +
    + )} +
    +