import { Link } from '@tanstack/react-router'
import { Activity, ArrowRight, Play } from 'lucide-react'
import { sessionTypeAbbrev } from '../lib/coverage'
import { countryDecal, countryFlag, formatGpDateRange } from '../lib/gpIdentity'
import { classifySessionStatus, type HeroStateKind } from '../lib/hero'
import { sortLiveTimingRows, trackStatusInfo } from '../lib/live'
import {
formatCountdown,
formatSessionScheduleTime,
meetingStartTime,
sessionStartTime,
type FocusMeetingKind,
} from '../lib/schedule'
import type { EnrichedResult, LiveStreamData, Meeting, Session, WeekendSession } from '../types'
import '../styles/hero.css'
export interface CommandCenterHeroProps {
state: HeroStateKind
now: Date
accent: string
liveActive: boolean
liveData: LiveStreamData | null | undefined
focusMeeting: Meeting
focusKind: FocusMeetingKind
sessions: WeekendSession[]
currentSession: Session | null
nextSession: Session | null
analysisSessionKey?: number
analysisSessionName?: string
lastRaceName: string
lastRacePodium: EnrichedResult[]
lastRaceSessionKey?: number
nextMeeting: Meeting | null
}
export function CommandCenterHero({
state,
now,
accent,
liveActive,
liveData,
focusMeeting,
focusKind,
sessions,
currentSession,
nextSession,
analysisSessionKey,
analysisSessionName,
lastRaceName,
lastRacePodium,
lastRaceSessionKey,
nextMeeting,
}: CommandCenterHeroProps) {
const accentStyle = {
'--gp-accent': accent,
'--hero-accent': accent,
} as React.CSSProperties
return (
{state === 'live' && (
)}
{state === 'upcoming' && (
)}
{state === 'between' && (
)}
)
}
function LiveHero({
liveActive,
liveData,
currentSession,
focusMeeting,
}: {
liveActive: boolean
liveData: LiveStreamData | null | undefined
currentSession: Session | null
focusMeeting: Meeting
}) {
const sessionName =
liveData?.Session?.SessionName ?? currentSession?.session_name ?? 'Live session'
const trackStatus = trackStatusInfo(liveData?.TrackStatus)
const topThree = sortLiveTimingRows(liveData).filter((r) => r.Position > 0).slice(0, 3)
const decal = countryDecal(focusMeeting)
return (
<>
{decal}
● Live now
{sessionName}
{[focusMeeting.meeting_name, focusMeeting.circuit_short_name].filter(Boolean).join(' · ')}
{trackStatus.label}
{topThree.length > 0 && (
{topThree.map((row) => (
P{row.Position}
{row.Info?.Tla ?? row.RacingNumber}
{row.Position === 1 ? 'LEAD' : row.Driver.Interval || row.Driver.GapToLeader || '—'}
))}
)}
{liveActive ? 'SignalR' : 'On track'}
LIVE
{sessionName}
>
)
}
function UpcomingHero({
now,
focusMeeting,
focusKind,
sessions,
currentSession,
nextSession,
liveActive,
analysisSessionKey,
analysisSessionName,
}: {
now: Date
focusMeeting: Meeting
focusKind: FocusMeetingKind
sessions: WeekendSession[]
currentSession: Session | null
nextSession: Session | null
liveActive: boolean
analysisSessionKey?: number
analysisSessionName?: string
}) {
const decal = countryDecal(focusMeeting)
const countdownTarget =
nextSession && sessionStartTime(nextSession)
? sessionStartTime(nextSession)!
: meetingStartTime(focusMeeting)
const kindLabel =
focusKind === 'current'
? 'Current weekend'
: focusKind === 'next'
? 'Next weekend'
: 'Weekend'
const sortedSessions = [...sessions].sort((a, b) => {
const left = sessionStartTime(a.session)?.getTime() ?? 0
const right = sessionStartTime(b.session)?.getTime() ?? 0
return left - right
})
return (
<>
{decal}
{kindLabel}
{focusMeeting.meeting_name}
{[focusMeeting.location, focusMeeting.circuit_short_name].filter(Boolean).join(' · ')}
{formatGpDateRange(focusMeeting)}
{nextSession ? (
<>
Next · {nextSession.session_name}
{countdownTarget && (
{formatCountdown(countdownTarget, now)}
)}
{formatSessionScheduleTime(nextSession.date_start)}
>
) : (
<>
Status
Complete
Weekend finished
>
)}
{sortedSessions.length > 0 && (
{sortedSessions.map(({ session }) => {
const status = classifySessionStatus(session, now)
const isNext = nextSession?.session_key === session.session_key
const isCurrent = currentSession?.session_key === session.session_key
const isLive = isCurrent && liveActive
return (
{sessionTypeAbbrev(session.session_type, session.session_name)}
{session.session_name}
{formatSessionScheduleTime(session.date_start)}
{isCurrent ? 'On track' : status === 'done' ? 'Done' : isNext ? 'Next' : 'Upcoming'}
)
})}
)}
Watch Live
{analysisSessionKey != null && (
Open Analysis
{analysisSessionName ? ` · ${analysisSessionName}` : ''}
)}
>
)
}
function BetweenHero({
now,
lastRaceName,
lastRacePodium,
lastRaceSessionKey,
nextMeeting,
analysisSessionKey,
analysisSessionName,
}: {
now: Date
lastRaceName: string
lastRacePodium: EnrichedResult[]
lastRaceSessionKey?: number
nextMeeting: Meeting | null
analysisSessionKey?: number
analysisSessionName?: string
}) {
const nextStart = nextMeeting ? meetingStartTime(nextMeeting) : null
const podium = lastRacePodium
.filter((r) => r.position >= 1 && r.position <= 3)
.sort((a, b) => a.position - b.position)
return (
<>
Between race weekends
{lastRaceName && (
After {lastRaceName}
)}
{!lastRaceName &&
Season pause
}
{podium.length > 0 && (
{podium.map((r) => (
P{r.position}
{r.name_acronym}
))}
)}
{nextMeeting && (
{countryFlag(nextMeeting) && (
{countryFlag(nextMeeting)}
)}
Next GP
{nextMeeting.meeting_name}
{nextStart && (
<>
{formatCountdown(nextStart, now)}
{formatGpDateRange(nextMeeting)}
>
)}
)}
{lastRaceSessionKey != null && (
View {lastRaceName || 'last race'} results →
)}
{analysisSessionKey != null && analysisSessionKey !== lastRaceSessionKey && (
Open Analysis{analysisSessionName ? ` · ${analysisSessionName}` : ''}
)}
Live Timing →
>
)
}