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 { noticeFromResponse, noticeMessage } 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 (
)
}
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
}
| Driver |
Pts |
Gap |
If win (+{RACE_WIN_POINTS}) |
{drivers.map((d) => (
|
{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 (
)
}
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
}
export function RacePreviewPage({
meeting: canonicalMeeting,
season: canonicalSeason,
embedded = false,
}: 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()
}
const dataNotice =
noticeFromResponse(championshipQuery.data, { includeLocal: true }) ??
noticeFromResponse(sessionsQuery.data, { includeLocal: false }) ??
noticeFromResponse(priorResultsQuery.data, { includeLocal: false })
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 (
{/* Weekend shell owns availability disclosure when Preview is embedded. */}
{!embedded && dataNotice && (
)}
{/* Embedded Weekend already shows the canonical countdown header — skip the duplicate. */}
{!embedded && (
)}
)
}