import { useEffect, useMemo, useState, type ReactNode } from 'react' import { useQuery } from '@tanstack/react-query' import { fetchChampionshipHub, fetchMeetings, fetchResults, fetchSeasons, fetchSessions, fetchStartingGrid, fetchTrackOutline, } from '../api' import { DataNotice, RouteState } from '../components/RouteState' import { aggregateNotices, noticeFromResponse, noticeMessage, shouldShowEmbeddedNotice, type DataAvailability, } from '../lib/availability' import { userFacingError } from '../lib/fetch' import { countryAccent, countryFlag, formatGpDateRange } from '../lib/gpIdentity' import { buildTitleFightContext, countdownTargetSession, extractPodium, extractPole, findPriorYearMeetingByCircuit, findRaceSession, formatPointsGap, isSprintWeekend, pickPreviewMeeting, RACE_WIN_POINTS, SPRINT_WEEKEND_MAX_POINTS, } from '../lib/preview' import { formatCountdown, formatSessionScheduleTime, sessionStartTime, sortSessionsByStart } from '../lib/schedule' import { buildOutlinePath } from '../lib/trackmap' import { teamColor } from '../utils' import type { Meeting, Session, TrackOutline } from '../types' import '../styles/preview.css' function SectionState({ loading, error, empty, emptyMessage, children, }: { loading?: boolean error?: unknown empty?: boolean emptyMessage?: string children: ReactNode }) { if (loading) { return
Loading…
} if (error) { return (
{userFacingError(error)}
) } if (empty) { return
{emptyMessage ?? 'No data available'}
} return <>{children} } function TrackOutlineCard({ outline, loading, error, accent, }: { outline: TrackOutline | null | undefined loading: boolean error: unknown accent: string }) { const outlinePath = useMemo(() => buildOutlinePath(outline?.points ?? []), [outline]) return (

Circuit

) } function LastYearCard({ year, loading, error, podium, pole, isFirstTime, }: { year: number | null loading: boolean error: unknown podium: ReturnType pole: ReturnType isFirstTime: boolean }) { return (

Last year here

{year != null && !isFirstTime &&

{year} race weekend

}
{podium.map((place) => (
P{place.position} {place.name_acronym} {place.team_name}
))}
{pole && (

Pole: {pole.name_acronym}{' '} ({pole.team_name})

)}
) } function TitleFightCard({ loading, error, drivers, sprintWeekend, season, }: { loading: boolean error: unknown drivers: ReturnType sprintWeekend: boolean season: number | null }) { return (

Title fight context

{season != null &&

{season} drivers' championship · top 3

} {drivers.map((d) => ( ))}
Driver Pts Gap If win (+{RACE_WIN_POINTS})
{d.name_acronym} {d.points} {formatPointsGap(d.gapToLeader)} {d.position === 1 ? 'LEADER' : formatPointsGap(d.gapAfterRaceWin)}
{sprintWeekend && (

Sprint weekend: up to {SPRINT_WEEKEND_MAX_POINTS} pts available (race win {RACE_WIN_POINTS} + sprint win 8). Max gain would reduce gaps by {SPRINT_WEEKEND_MAX_POINTS} vs a standard race win.

)}
) } function PreviewHeader({ meeting, sessions, countdownSession, now, accent, }: { meeting: Meeting sessions: Session[] countdownSession: Session | null now: Date accent: string }) { const countdownTarget = countdownSession ? sessionStartTime(countdownSession) : null return (

{countryFlag(meeting) && {countryFlag(meeting)}} {meeting.meeting_name}

{meeting.circuit_short_name} {meeting.location ? ` · ${meeting.location}` : ''} {formatGpDateRange(meeting) ? ` · ${formatGpDateRange(meeting)}` : ''}

{countdownTarget && (
{countdownSession?.session_name ?? 'Next session'} {formatCountdown(countdownTarget, now)}
)}
{sessions.length > 0 && (
{sessions.map((session) => (
{session.session_name} {formatSessionScheduleTime(session.date_start)}
))}
)}
) } export interface RacePreviewPageProps { /** * Canonical meeting identity from Weekend Context. When set, Preview must not * independently re-select another current meeting/session. */ meeting?: Meeting /** Season for championship supplement; defaults to meeting.year. */ season?: number /** Embedded under Weekend — identity failures stay non-blocking. */ embedded?: boolean /** * Availability already disclosed by the Weekend shell. Embedded Preview * suppresses only an equivalent notice kind; distinct truth stays visible. */ shellAvailability?: DataAvailability | null } export function RacePreviewPage({ meeting: canonicalMeeting, season: canonicalSeason, embedded = false, shellAvailability = null, }: RacePreviewPageProps = {}) { const [now, setNow] = useState(() => Date.now()) useEffect(() => { const timer = window.setInterval(() => setNow(Date.now()), 1000) return () => window.clearInterval(timer) }, []) const nowDate = useMemo(() => new Date(now), [now]) const hasCanonicalMeeting = canonicalMeeting != null && canonicalMeeting.meeting_key > 0 const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: ({ signal }) => fetchSeasons(signal), enabled: !hasCanonicalMeeting, }) const latestSeason = canonicalSeason ?? canonicalMeeting?.year ?? seasonsQuery.data?.[0] ?? null const meetingsQuery = useQuery({ queryKey: ['meetings', latestSeason, 'auto'], queryFn: ({ signal }) => fetchMeetings(latestSeason!, 'auto', signal), enabled: !hasCanonicalMeeting && latestSeason != null, }) const previewMeeting = useMemo(() => { if (hasCanonicalMeeting) return canonicalMeeting return pickPreviewMeeting(meetingsQuery.data ?? [], nowDate) }, [hasCanonicalMeeting, canonicalMeeting, meetingsQuery.data, nowDate]) const sessionsQuery = useQuery({ queryKey: ['sessions', previewMeeting?.meeting_key, 'auto'], queryFn: ({ signal }) => fetchSessions(previewMeeting!.meeting_key, 'auto', signal), enabled: previewMeeting != null, }) const sessions = useMemo( () => sortSessionsByStart(sessionsQuery.data ?? []), [sessionsQuery.data], ) const countdownSession = useMemo(() => countdownTargetSession(sessions, nowDate), [sessions, nowDate]) const sprintWeekend = useMemo(() => isSprintWeekend(sessions), [sessions]) const priorYear = previewMeeting ? previewMeeting.year - 1 : null const priorMeetingsQuery = useQuery({ queryKey: ['meetings', priorYear, 'auto'], queryFn: ({ signal }) => fetchMeetings(priorYear!, 'auto', signal), enabled: priorYear != null && previewMeeting != null, }) const priorMeeting = useMemo( () => findPriorYearMeetingByCircuit(priorMeetingsQuery.data ?? [], previewMeeting?.circuit_key), [priorMeetingsQuery.data, previewMeeting?.circuit_key], ) const priorSessionsQuery = useQuery({ queryKey: ['sessions', priorMeeting?.meeting_key, 'auto'], queryFn: ({ signal }) => fetchSessions(priorMeeting!.meeting_key, 'auto', signal), enabled: priorMeeting != null, }) const priorRaceSession = useMemo( () => findRaceSession(priorSessionsQuery.data ?? []), [priorSessionsQuery.data], ) const priorResultsQuery = useQuery({ queryKey: ['results', priorRaceSession?.session_key, 'auto'], queryFn: ({ signal }) => fetchResults(priorRaceSession!.session_key, 'auto', signal), enabled: priorRaceSession != null, }) const priorGridQuery = useQuery({ queryKey: ['grid', priorRaceSession?.session_key, 'auto'], queryFn: ({ signal }) => fetchStartingGrid(priorRaceSession!.session_key, 'auto', signal), enabled: priorRaceSession != null, }) const trackOutlineQuery = useQuery({ queryKey: ['track-outline', previewMeeting?.circuit_key, previewMeeting?.year], queryFn: ({ signal }) => fetchTrackOutline(previewMeeting!.circuit_key!, previewMeeting!.year, signal), enabled: previewMeeting?.circuit_key != null && previewMeeting.circuit_key > 0, }) const championshipQuery = useQuery({ queryKey: ['championship-hub', latestSeason], queryFn: ({ signal }) => fetchChampionshipHub(latestSeason!, signal), enabled: latestSeason != null, }) const podium = useMemo(() => extractPodium(priorResultsQuery.data ?? []), [priorResultsQuery.data]) const pole = useMemo(() => extractPole(priorGridQuery.data ?? []), [priorGridQuery.data]) const titleFight = useMemo( () => (championshipQuery.data ? buildTitleFightContext(championshipQuery.data) : []), [championshipQuery.data], ) const accent = countryAccent(previewMeeting) const isFirstTimeCircuit = priorMeeting == null && priorYear != null && !priorMeetingsQuery.isLoading const identityLoading = !hasCanonicalMeeting && (seasonsQuery.isLoading || meetingsQuery.isLoading) const identityError = !hasCanonicalMeeting && (seasonsQuery.isError || meetingsQuery.isError) const identityRetrying = seasonsQuery.isFetching || meetingsQuery.isFetching const retryIdentity = () => { if (seasonsQuery.isError && !seasonsQuery.isFetching) void seasonsQuery.refetch() if (meetingsQuery.isError && !meetingsQuery.isFetching) void meetingsQuery.refetch() } // Worst reported truth across primary + supplements — Local must not mask // Sessions/Prior Results Stale/Partial/Limited/Archive. const dataNotice = aggregateNotices([ noticeFromResponse(championshipQuery.data, { includeLocal: true }), noticeFromResponse(sessionsQuery.data, { includeLocal: false }), noticeFromResponse(priorResultsQuery.data, { includeLocal: false }), ]) const showFreshnessNotice = Boolean(dataNotice) && (!embedded || shouldShowEmbeddedNotice(dataNotice, shellAvailability)) const sessionsFailed = sessionsQuery.isError const sessionsRetrying = sessionsQuery.isFetching const retrySessions = () => { if (!sessionsQuery.isFetching) void sessionsQuery.refetch() } if (identityLoading) { return (
) } if (identityError) { const err = seasonsQuery.error ?? meetingsQuery.error return (
{ if (!identityRetrying) retryIdentity() }} retrying={identityRetrying} retryTestId="preview-retry" />
) } if (!previewMeeting) { return (
box-box · race preview

Season complete

No upcoming races on the {latestSeason ?? 'current'} calendar. Check back when the next season schedule is published.

{titleFight.length > 0 && ( )}
) } return (
{showFreshnessNotice && dataNotice && ( )} {sessionsFailed && ( )} {/* Embedded Weekend already shows the canonical countdown header — skip the duplicate. */} {!embedded && ( )} {/* Embedded: still surface recovered schedule once sessions load. */} {embedded && sessions.length > 0 && (
{sessions.map((session) => (
{session.session_name} {formatSessionScheduleTime(session.date_start)}
))}
)}
) }