import { useEffect, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' import { fetchRaceHub, fetchWeekend, fetchWeekendContext } from '../api' import { DatasetStrip } from '../components/DatasetStrip' import { RaceStoryCanvas } from '../components/RaceStoryCanvas' import { TabBar, type Tab } from '../components/TabBar' import { DatasetStatusView } from '../components/DatasetStatusView' import { StrategyView } from '../components/StrategyView' import { LapsView } from '../components/LapsView' import { CompareView } from '../components/CompareView' import { RaceControlView } from '../components/RaceControlView' import { WeatherView } from '../components/WeatherView' import { OverviewView } from '../components/OverviewView' import { WeekendSwitcher } from '../components/WeekendSwitcher' import { SourceBadge } from '../components/SourceBadge' import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity' import { formatCoverageHint, sessionTypeAbbrev } from '../lib/coverage' import { formatCountdown, formatSessionScheduleTime, refreshDeadlineDelay, sortSessionsByStart, } from '../lib/schedule' import type { ContextSession, Weekend } from '../types' interface Props { sessionKey: number } export function RaceHubPage({ sessionKey }: Props) { const navigate = useNavigate() const [activeTab, setActiveTab] = useState('overview') const [switcherOpen, setSwitcherOpen] = useState(false) const [now, setNow] = useState(() => Date.now()) // The server owns bare Race Hub selection so every open tab crosses the // one-hour handoff at the same instant. const contextQuery = useQuery({ queryKey: ['weekend-context'], queryFn: fetchWeekendContext, enabled: sessionKey === 0, }) const { refetch: refetchContext } = contextQuery const context = contextQuery.data const preSession = sessionKey === 0 && context?.race_hub_pre_session === true const preSessionRef = context?.race_hub_default_session const preSessionMeetingKey = preSessionRef?.meeting?.meeting_key const preSessionWeekendQuery = useQuery({ queryKey: ['weekend', preSessionMeetingKey], queryFn: () => fetchWeekend(preSessionMeetingKey!), enabled: preSession && preSessionMeetingKey != null && preSessionMeetingKey > 0, }) useEffect(() => { if (sessionKey !== 0) return const delay = refreshDeadlineDelay(context?.race_hub_refresh_at) if (delay == null) return const timer = window.setTimeout(() => { void refetchContext() }, delay) return () => window.clearTimeout(timer) }, [sessionKey, context?.race_hub_refresh_at, refetchContext]) useEffect(() => { if (!preSession) return const timer = window.setInterval(() => setNow(Date.now()), 1_000) return () => window.clearInterval(timer) }, [preSession]) // A bare route retains canonical context ownership while rendering its // completed analysis selection. Explicit URLs remain user-owned. const selectedSessionKey = sessionKey || context?.race_hub_default_session?.session.session_key || 0 // ─── Active session payload ─── const raceHubQuery = useQuery({ queryKey: ['race-hub', selectedSessionKey], queryFn: () => fetchRaceHub(selectedSessionKey), enabled: selectedSessionKey > 0 && (sessionKey > 0 || !preSession), staleTime: 30_000, }) const meetingKey = raceHubQuery.data?.meeting?.meeting_key const weekendQuery = useQuery({ queryKey: ['weekend', meetingKey], queryFn: () => fetchWeekend(meetingKey!), enabled: meetingKey != null && meetingKey > 0, staleTime: 60_000, }) const data = raceHubQuery.data const weekend = weekendQuery.data const accent = countryAccent(data?.meeting ?? null) const accentStyle = { '--gp-accent': accent } as React.CSSProperties // ─── No session_key: resolve exclusively through canonical Weekend Context ─── if (sessionKey === 0) { if (contextQuery.isLoading || (preSession && preSessionWeekendQuery.isLoading)) { return (
resolving latest local weekend…
) } if (preSession && preSessionRef) { return } if (!selectedSessionKey) { return (
box-box · race hub

No completed local analysis yet

Race Hub opens completed local analysis between weekends. Check Data Health to ingest a completed session.

Open Admin · Data Health Back to Command Center
) } } // ─── Loading / error for the selected session ─── if (raceHubQuery.isLoading) { return (
loading session {selectedSessionKey}…
) } if (raceHubQuery.isError || !data) { return (
{raceHubQuery.error instanceof Error ? raceHubQuery.error.message : `Failed to load session ${selectedSessionKey}.`}
) } const decal = countryDecal(data.meeting ?? null) const sessions = weekend ? sortSessionsByStart(weekend.sessions.map((w) => w.session)) : [] const sessionMeta = weekend ? Object.fromEntries(weekend.sessions.map((w) => [w.session.session_key, w])) : {} const activeSessionMeta = sessionMeta[selectedSessionKey] return (
{/* Topbar */}
box-box · race hub {data.meeting?.year ? ` · ${data.meeting.year}` : ''}
{switcherOpen && ( setSwitcherOpen(false)} /> )} {/* GP Identity band */} {data.meeting && (
)} {/* Session rail */} {sessions.length > 0 && ( )} {/* Active session sub-bar */} {data.session && (
{data.session.session_name} {formatSessionScheduleTime(data.session.date_start)} {activeSessionMeta && ( )} key {selectedSessionKey}
)} {activeTab === 'overview' && } {activeTab === 'race_story' && (
)} {activeTab === 'strategy' && (
Race Strategy
)} {activeTab === 'compare' && (
Driver Compare
)} {activeTab === 'lap_data' && (
Lap Data {data.laps.length > 0 && ( {data.laps.length} samples )}
)} {activeTab === 'conditions' && (
Conditions {data.weather.length > 0 && ( {data.weather.length} samples )}
)} {activeTab === 'race_control' && (
Race Control {data.race_control.length > 0 && ( {data.race_control.length} messages )}
)} {activeTab === 'data_status' && (
Data Status
)}
) } function RaceHubPreSession({ session, weekend, now }: { session: ContextSession; weekend?: Weekend; now: number }) { const meeting = session.meeting const sessions = sortSessionsByStart((weekend?.sessions ?? []).map((entry) => entry.session)) const target = new Date(session.session.date_start) const accent = countryAccent(meeting ?? null) const pendingLiveEvidence = target.getTime() <= now return (
box-box · race hub

{meeting?.meeting_name ?? 'Next race weekend'}

{pendingLiveEvidence ? `${session.session.session_name} is scheduled; awaiting live timing.` : <>{session.session.session_name} begins in {formatCountdown(target, new Date(now))}}

{sessions.length > 0 && (
{sessions.map((scheduled) => (
{scheduled.session_name} {formatSessionScheduleTime(scheduled.date_start)}
))}
)}
) }