import { useState, Fragment } from 'react' import { teamColor } from '../../utils' import { useAutoAnimate } from '@formkit/auto-animate/react' import type { LiveStintData } from '../../types' import type { LiveTimingRow } from '../../lib/live' import { driverCode, positionDelta, positionDeltaClass, tyreClass, tyreLabel, } from '../../lib/live' import type { GapHistoryMap } from '../../lib/gapHistory' import { GapSparkline } from './GapSparkline' import { StintHistory } from './StintHistory' import { Pin } from 'lucide-react' interface Props { rows: LiveTimingRow[] stints?: Record history?: GapHistoryMap battleNumbers?: Set pinned?: string[] onTogglePin?: (racingNumber: string) => void sessionType?: string } function posClass(pos: number): string { if (pos === 1) return 'pos-p1' if (pos === 2) return 'pos-p2' if (pos === 3) return 'pos-p3' return 'pos-n' } export function TimingTower({ rows, stints, history, battleNumbers, pinned, onTogglePin, sessionType = '', }: Props) { const [expandedRow, setExpandedRow] = useState(null) const [gapMode, setGapMode] = useState<'interval' | 'leader'>('interval') const [listRef] = useAutoAnimate({ duration: 300, easing: 'ease-out' }) if (rows.length === 0) { return (
Live timing is connected, but no driver timing rows have arrived yet.
) } 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 && } {rows.map((row) => { const driver = row.Driver const delta = positionDelta(driver) 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 ( setExpandedRow(isExpanded ? null : row.RacingNumber)} style={{ cursor: 'pointer' }} > {isRace && } {isRace && ( )} {isQuali && } {isQuali && } {isQuali && } {isRace && } {isExpanded && ( )} ) })}
PosΔDriver Tyre Last Lap 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'} TrendS1S2S3BestLaps
{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
)}
) }