mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
feat(#76): bound primary-route fetches and local-first driver summary
Add shared apiFetch timeouts/abort/typed errors with in-flight dedupe, a RouteState UI for loading/empty/timeout/error+retry, and wire it through Weekend (Data Health), Championship, Driver Profile, Briefing, Live, and Race Hub. Driver summary is local-first with bounded optional OpenF1 enrichment so a hung remote call cannot block the profile. Spike note: parseSourceMode defaults to openf1; driver summary now treats omitted ?source= as auto so local season data is preferred. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
} from '../lib/digest'
|
||||
import { stripHtml, timeAgo } from '../utils'
|
||||
import type { ArticleContent, NewsItem } from '../types'
|
||||
import { RouteState } from '../components/RouteState'
|
||||
import '../styles/digest.css'
|
||||
|
||||
type Category = 'all' | 'official' | 'news' | 'video'
|
||||
@@ -406,27 +407,27 @@ export function BriefingPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const now = useMemo(() => new Date(), [])
|
||||
|
||||
const { data: allNews = [], isLoading, isError } = useQuery({
|
||||
const { data: allNews = [], isLoading, isError, error, refetch, isFetching } = useQuery({
|
||||
queryKey: ['news'],
|
||||
queryFn: () => fetchNews(100),
|
||||
queryFn: ({ signal }) => fetchNews(100, undefined, signal),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
queryFn: fetchSeasons,
|
||||
queryFn: ({ signal }) => fetchSeasons(signal),
|
||||
})
|
||||
const latestSeason = seasonsQuery.data?.[0] ?? null
|
||||
|
||||
const meetingsQuery = useQuery({
|
||||
queryKey: ['season-meetings', latestSeason],
|
||||
queryFn: () => fetchSeasonMeetings(latestSeason!),
|
||||
queryFn: ({ signal }) => fetchSeasonMeetings(latestSeason!, signal),
|
||||
enabled: latestSeason != null,
|
||||
})
|
||||
|
||||
const hubQuery = useQuery({
|
||||
queryKey: ['championship-hub', latestSeason],
|
||||
queryFn: () => fetchChampionshipHub(latestSeason!),
|
||||
queryFn: ({ signal }) => fetchChampionshipHub(latestSeason!, signal),
|
||||
enabled: latestSeason != null,
|
||||
})
|
||||
|
||||
@@ -524,8 +525,21 @@ export function BriefingPage() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isLoading && <div className="loading-state">loading briefing…</div>}
|
||||
{isError && <div className="error-box">Failed to load paddock briefing.</div>}
|
||||
{isLoading && (
|
||||
<RouteState kind="loading" title="loading briefing…" testId="briefing-loading" />
|
||||
)}
|
||||
{isError && (
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Briefing unavailable"
|
||||
error={error}
|
||||
onRetry={() => {
|
||||
if (!isFetching) void refetch()
|
||||
}}
|
||||
retrying={isFetching}
|
||||
testId="briefing-error"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ChampHubDriver, ChampionshipHub } from '../types'
|
||||
import { ChampionshipSimulator } from '../components/ChampionshipSimulator'
|
||||
import { RivalryCompare } from '../components/RivalryCompare'
|
||||
import { Meaning } from '../components/Meaning'
|
||||
import { RouteState } from '../components/RouteState'
|
||||
import { TeammateH2H } from '../components/TeammateH2H'
|
||||
import { teammatePairs } from '../lib/h2h'
|
||||
import { pointsGapMeaning } from '../lib/meaning'
|
||||
@@ -81,23 +82,55 @@ function teamSplit(teamName: string, drivers: ChampHubDriver[]): TeamSplit {
|
||||
export function ChampionshipPage() {
|
||||
const [view, setView] = useState<View>('drivers')
|
||||
|
||||
const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: fetchSeasons })
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
queryFn: ({ signal }) => fetchSeasons(signal),
|
||||
})
|
||||
const latestSeason = seasonsQuery.data?.[0] ?? null
|
||||
|
||||
const hubQuery = useQuery({
|
||||
queryKey: ['championship-hub', latestSeason],
|
||||
queryFn: () => fetchChampionshipHub(latestSeason ?? undefined),
|
||||
queryFn: ({ signal }) => fetchChampionshipHub(latestSeason ?? undefined, signal),
|
||||
enabled: latestSeason != null,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
if (seasonsQuery.isLoading || hubQuery.isLoading) {
|
||||
return <div className="page loading-state">loading championship…</div>
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState kind="loading" title="loading championship…" testId="championship-loading" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (seasonsQuery.isError) {
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Championship unavailable"
|
||||
error={seasonsQuery.error}
|
||||
onRetry={() => {
|
||||
if (!seasonsQuery.isFetching) void seasonsQuery.refetch()
|
||||
}}
|
||||
retrying={seasonsQuery.isFetching}
|
||||
testId="championship-error"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (hubQuery.isError) {
|
||||
return (
|
||||
<div className="page error-box">
|
||||
{hubQuery.error instanceof Error ? hubQuery.error.message : 'Failed to load championship'}
|
||||
<div className="page">
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Championship unavailable"
|
||||
error={hubQuery.error}
|
||||
onRetry={() => {
|
||||
if (!hubQuery.isFetching) void hubQuery.refetch()
|
||||
}}
|
||||
retrying={hubQuery.isFetching}
|
||||
testId="championship-error"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { SourceBadge, weekendStatusLabel } from '../components/SourceBadge'
|
||||
import { CliCommands, ingestYearCommands } from '../components/CliCommands'
|
||||
import { MeetingDetailPanel } from '../components/MeetingDetailPanel'
|
||||
import { RouteState } from '../components/RouteState'
|
||||
import type { Meeting, Weekend } from '../types'
|
||||
|
||||
function formatMeetingDate(meeting: Meeting): string {
|
||||
@@ -27,12 +28,12 @@ export function DataLibraryPage() {
|
||||
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
queryFn: fetchSeasons,
|
||||
queryFn: ({ signal }) => fetchSeasons(signal),
|
||||
})
|
||||
|
||||
const meetingsQuery = useQuery({
|
||||
queryKey: ['meetings', selectedYear],
|
||||
queryFn: () => fetchLocalMeetings(selectedYear!),
|
||||
queryFn: ({ signal }) => fetchLocalMeetings(selectedYear!, signal),
|
||||
enabled: selectedYear != null,
|
||||
})
|
||||
|
||||
@@ -41,7 +42,7 @@ export function DataLibraryPage() {
|
||||
const weekendQueries = useQueries({
|
||||
queries: meetings.map((meeting) => ({
|
||||
queryKey: ['weekend', meeting.meeting_key],
|
||||
queryFn: () => fetchWeekend(meeting.meeting_key),
|
||||
queryFn: ({ signal }: { signal: AbortSignal }) => fetchWeekend(meeting.meeting_key, signal),
|
||||
enabled: meetings.length > 0,
|
||||
staleTime: 60_000,
|
||||
})),
|
||||
@@ -78,13 +79,26 @@ export function DataLibraryPage() {
|
||||
const weekendsLoading = weekendQueries.some((q) => q.isLoading)
|
||||
|
||||
if (seasonsQuery.isLoading) {
|
||||
return <div className="page loading-state">loading local data library…</div>
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState kind="loading" title="loading local data library…" testId="weekend-loading" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (seasonsQuery.isError) {
|
||||
return (
|
||||
<div className="page error-box">
|
||||
{seasonsQuery.error instanceof Error ? seasonsQuery.error.message : 'Failed to load seasons'}
|
||||
<div className="page">
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Weekend data unavailable"
|
||||
error={seasonsQuery.error}
|
||||
onRetry={() => {
|
||||
if (!seasonsQuery.isFetching) void seasonsQuery.refetch()
|
||||
}}
|
||||
retrying={seasonsQuery.isFetching}
|
||||
testId="weekend-error"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -222,15 +236,20 @@ export function DataLibraryPage() {
|
||||
</div>
|
||||
|
||||
{meetingsQuery.isLoading && (
|
||||
<div className="loading-state">loading meetings…</div>
|
||||
<RouteState kind="loading" title="loading meetings…" testId="weekend-meetings-loading" />
|
||||
)}
|
||||
|
||||
{meetingsQuery.isError && (
|
||||
<div className="error-box">
|
||||
{meetingsQuery.error instanceof Error
|
||||
? meetingsQuery.error.message
|
||||
: 'Failed to load meetings'}
|
||||
</div>
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Weekend meetings unavailable"
|
||||
error={meetingsQuery.error}
|
||||
onRetry={() => {
|
||||
if (!meetingsQuery.isFetching) void meetingsQuery.refetch()
|
||||
}}
|
||||
retrying={meetingsQuery.isFetching}
|
||||
testId="weekend-error"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!meetingsQuery.isLoading && !meetingsQuery.isError && meetings.length === 0 && (
|
||||
@@ -316,11 +335,16 @@ export function DataLibraryPage() {
|
||||
|
||||
<div className="dl-detail-wrap">
|
||||
{weekendsLoading && selectedWeekend == null && (
|
||||
<div className="loading-state">loading weekend details…</div>
|
||||
<RouteState kind="loading" title="loading weekend details…" testId="weekend-detail-loading" />
|
||||
)}
|
||||
{selectedWeekend && <MeetingDetailPanel weekend={selectedWeekend} />}
|
||||
{selectedMeetingKey != null && !weekendsLoading && selectedWeekend == null && (
|
||||
<div className="missing-notice">Could not load weekend details.</div>
|
||||
<RouteState
|
||||
kind="empty"
|
||||
title="Weekend details missing"
|
||||
message="Could not load weekend details for this meeting."
|
||||
testId="weekend-detail-empty"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { fetchDriverSummary, fetchSeasons } from '../api'
|
||||
import { DataNotice, RouteState } from '../components/RouteState'
|
||||
import { teamColor } from '../utils'
|
||||
import { countryFlag } from '../lib/gpIdentity'
|
||||
import {
|
||||
@@ -26,7 +27,7 @@ export function DriverProfilePage({ driverNumber, year }: Props) {
|
||||
// seasons list is empty/unavailable the backend falls back to the current year.
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
queryFn: fetchSeasons,
|
||||
queryFn: ({ signal }) => fetchSeasons(signal),
|
||||
enabled: year == null,
|
||||
})
|
||||
const resolvedYear = year ?? seasonsQuery.data?.[0]
|
||||
@@ -34,39 +35,86 @@ export function DriverProfilePage({ driverNumber, year }: Props) {
|
||||
|
||||
const summaryQuery = useQuery({
|
||||
queryKey: ['driver-summary', driverNumber, resolvedYear ?? 'latest'],
|
||||
queryFn: () => fetchDriverSummary(driverNumber, resolvedYear),
|
||||
queryFn: ({ signal }) => fetchDriverSummary(driverNumber, resolvedYear, signal),
|
||||
enabled: seasonsSettled && driverNumber > 0,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
if (driverNumber <= 0) {
|
||||
return <div className="page error-box">Invalid driver number</div>
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState kind="empty" title="Invalid driver number" message="Pick a driver from the championship standings." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!seasonsSettled || summaryQuery.isLoading) {
|
||||
return <div className="page loading-state">loading driver profile…</div>
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState kind="loading" title="loading driver profile…" testId="driver-profile-loading" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (summaryQuery.isError) {
|
||||
return (
|
||||
<div className="page error-box">
|
||||
{summaryQuery.error instanceof Error
|
||||
? summaryQuery.error.message
|
||||
: 'Failed to load driver profile'}
|
||||
<div className="page">
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Driver profile unavailable"
|
||||
error={summaryQuery.error}
|
||||
onRetry={() => {
|
||||
if (!summaryQuery.isFetching) void summaryQuery.refetch()
|
||||
}}
|
||||
retrying={summaryQuery.isFetching}
|
||||
testId="driver-profile-error"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const summary = summaryQuery.data
|
||||
if (!summary) {
|
||||
return <div className="page error-box">No driver data</div>
|
||||
return (
|
||||
<div className="page">
|
||||
<RouteState
|
||||
kind="empty"
|
||||
title="No driver data"
|
||||
message="This driver is not in the local season standings yet."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <DriverProfileBody summary={summary} />
|
||||
return (
|
||||
<DriverProfileBody
|
||||
summary={summary}
|
||||
onRetry={() => {
|
||||
if (!summaryQuery.isFetching) void summaryQuery.refetch()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DriverProfileBody({ summary }: { summary: DriverSummary }) {
|
||||
function DriverProfileBody({
|
||||
summary,
|
||||
onRetry,
|
||||
}: {
|
||||
summary: DriverSummary
|
||||
onRetry?: () => void
|
||||
}) {
|
||||
const color = teamColor(summary.team_colour)
|
||||
const deltas = gridFinishDeltas(summary.rounds)
|
||||
|
||||
return (
|
||||
<div className="dp-page" data-testid="driver-profile">
|
||||
{summary.enrichment === 'limited' && (
|
||||
<DataNotice
|
||||
availability="limited"
|
||||
message="Optional remote details are unavailable. Showing local season identity and results."
|
||||
onRetry={onRetry}
|
||||
testId="driver-profile-limited"
|
||||
/>
|
||||
)}
|
||||
{summary.source === 'local' && summary.enrichment !== 'limited' && (
|
||||
<DataNotice availability="local" message="Loaded from local season data." testId="driver-profile-local" />
|
||||
)}
|
||||
<header className="dp-header" data-testid="dp-header" style={{ borderLeftColor: color }}>
|
||||
<div className="dp-identity">
|
||||
<div className="dp-title-row">
|
||||
|
||||
@@ -36,6 +36,7 @@ import { EventRail } from '../components/live/EventRail'
|
||||
import { TeamRadioTicker } from '../components/live/TeamRadioTicker'
|
||||
import { TyreDegPanel } from '../components/live/TyreDegPanel'
|
||||
import { LiveHandoff } from '../components/live/LiveHandoff'
|
||||
import { RouteState } from '../components/RouteState'
|
||||
import '../styles/live-state.css'
|
||||
|
||||
/** How often to re-check weekend-context while analysis is still ingesting. */
|
||||
@@ -67,12 +68,15 @@ export function LiveTimingPage() {
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
isFetching,
|
||||
isFetched: liveStateFetched,
|
||||
} = useQuery({
|
||||
queryKey: ['live-state'],
|
||||
queryFn: fetchLiveState,
|
||||
queryFn: ({ signal }) => fetchLiveState(signal),
|
||||
staleTime: 5_000,
|
||||
})
|
||||
|
||||
@@ -109,7 +113,7 @@ export function LiveTimingPage() {
|
||||
// analysis-ready without a manual refresh.
|
||||
const contextQuery = useQuery({
|
||||
queryKey: ['weekend-context'],
|
||||
queryFn: fetchWeekendContext,
|
||||
queryFn: ({ signal }) => fetchWeekendContext(signal),
|
||||
enabled: !isLive,
|
||||
staleTime: WEEKEND_CONTEXT_STALE_MS,
|
||||
refetchInterval: (query) => {
|
||||
@@ -296,9 +300,16 @@ export function LiveTimingPage() {
|
||||
return (
|
||||
<div className="page live-page" data-testid="live-page" data-phase={phase}>
|
||||
{isError && (
|
||||
<div className="error-box">
|
||||
{error instanceof Error ? error.message : 'Failed to load live timing state'}
|
||||
</div>
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Live timing unavailable"
|
||||
error={error}
|
||||
onRetry={() => {
|
||||
if (!isFetching) void refetch()
|
||||
}}
|
||||
retrying={isFetching}
|
||||
testId="live-initial-error"
|
||||
/>
|
||||
)}
|
||||
|
||||
{phase === 'disconnected' && (
|
||||
@@ -316,8 +327,12 @@ export function LiveTimingPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === 'connecting' && (
|
||||
<div className="loading-state">connecting to live timing…</div>
|
||||
{phase === 'connecting' && (isLoading || !liveStateFetched) && (
|
||||
<RouteState
|
||||
kind="loading"
|
||||
title="connecting to live timing…"
|
||||
testId="live-initial-loading"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(phase === 'settling' || phase === 'inactive') && (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../components/PreSessionView'
|
||||
import { WeekendSwitcher } from '../components/WeekendSwitcher'
|
||||
import { SourceBadge } from '../components/SourceBadge'
|
||||
import { RouteState } from '../components/RouteState'
|
||||
import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity'
|
||||
import { sessionTypeAbbrev } from '../lib/coverage'
|
||||
import { formatSessionScheduleTime, sortSessionsByStart } from '../lib/schedule'
|
||||
@@ -55,7 +56,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
|
||||
const contextQuery = useQuery({
|
||||
queryKey: ['weekend-context'],
|
||||
queryFn: fetchWeekendContext,
|
||||
queryFn: ({ signal }) => fetchWeekendContext(signal),
|
||||
staleTime: 15_000,
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
@@ -73,7 +74,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
|
||||
const raceHubQuery = useQuery({
|
||||
queryKey: ['race-hub', sessionKey],
|
||||
queryFn: () => fetchRaceHub(sessionKey),
|
||||
queryFn: ({ signal }) => fetchRaceHub(sessionKey, signal),
|
||||
enabled: sessionKey > 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
@@ -81,7 +82,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
const meetingKey = raceHubQuery.data?.meeting?.meeting_key
|
||||
const weekendQuery = useQuery({
|
||||
queryKey: ['weekend', meetingKey],
|
||||
queryFn: () => fetchWeekend(meetingKey!),
|
||||
queryFn: ({ signal }) => fetchWeekend(meetingKey!, signal),
|
||||
enabled: meetingKey != null && meetingKey > 0,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
@@ -102,33 +103,32 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
if (contextQuery.isLoading) {
|
||||
return (
|
||||
<div className="rh-page" style={accentStyle}>
|
||||
<div className="loading-state">resolving weekend context…</div>
|
||||
<RouteState
|
||||
kind="loading"
|
||||
title="resolving weekend context…"
|
||||
testId="race-hub-loading"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (contextQuery.isError) {
|
||||
return (
|
||||
<div className="rh-page" data-testid="race-hub-error" style={accentStyle}>
|
||||
<div className="rh-recover">
|
||||
<div className="error-box">
|
||||
{contextQuery.error instanceof Error
|
||||
? contextQuery.error.message
|
||||
: 'Failed to load weekend context.'}
|
||||
</div>
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Weekend unavailable"
|
||||
error={contextQuery.error}
|
||||
onRetry={() => {
|
||||
if (!contextQuery.isFetching) void contextQuery.refetch()
|
||||
}}
|
||||
retrying={contextQuery.isFetching}
|
||||
>
|
||||
<div className="rh-recover-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="rh-recover-btn primary"
|
||||
onClick={() => contextQuery.refetch()}
|
||||
data-testid="rh-retry"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<Link to="/" search={{}} className="rh-recover-btn" data-testid="rh-back-weekend">
|
||||
Back to Weekend
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</RouteState>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -170,7 +170,11 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
|
||||
return (
|
||||
<div className="rh-page" style={accentStyle}>
|
||||
<div className="loading-state">resolving weekend context…</div>
|
||||
<RouteState
|
||||
kind="loading"
|
||||
title="resolving weekend context…"
|
||||
testId="race-hub-loading"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -179,7 +183,11 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
if (raceHubQuery.isLoading) {
|
||||
return (
|
||||
<div className="rh-page" style={accentStyle}>
|
||||
<div className="loading-state">loading session {sessionKey}…</div>
|
||||
<RouteState
|
||||
kind="loading"
|
||||
title={`loading session ${sessionKey}…`}
|
||||
testId="race-hub-loading"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -191,21 +199,21 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
|
||||
return (
|
||||
<div className="rh-page" data-testid="race-hub-error" style={accentStyle}>
|
||||
<div className="rh-recover">
|
||||
<div className="error-box">
|
||||
{raceHubQuery.error instanceof Error
|
||||
? raceHubQuery.error.message
|
||||
: `Failed to load session ${sessionKey}.`}
|
||||
</div>
|
||||
<RouteState
|
||||
kind="error"
|
||||
title="Session unavailable"
|
||||
error={raceHubQuery.error}
|
||||
message={
|
||||
raceHubQuery.error
|
||||
? undefined
|
||||
: `Session ${sessionKey} could not be loaded from local data.`
|
||||
}
|
||||
onRetry={() => {
|
||||
if (!raceHubQuery.isFetching) void raceHubQuery.refetch()
|
||||
}}
|
||||
retrying={raceHubQuery.isFetching}
|
||||
>
|
||||
<div className="rh-recover-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="rh-recover-btn primary"
|
||||
onClick={() => raceHubQuery.refetch()}
|
||||
data-testid="rh-retry"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
search={backSearch}
|
||||
@@ -217,7 +225,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
||||
Back to Weekend
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</RouteState>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ export function RacePreviewPage() {
|
||||
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
queryFn: fetchSeasons,
|
||||
queryFn: ({ signal }) => fetchSeasons(signal),
|
||||
})
|
||||
|
||||
const latestSeason = seasonsQuery.data?.[0] ?? null
|
||||
|
||||
@@ -71,7 +71,8 @@ export function WeekendPage({
|
||||
/** Restored from `/?session_key=` when returning from Race Hub analysis. */
|
||||
focusSessionKey?: number
|
||||
}) {
|
||||
const { context, loadState, error, championship, briefing, now } = useWeekendContext()
|
||||
const { context, loadState, error, championship, briefing, now, refetch, isFetching } =
|
||||
useWeekendContext()
|
||||
const hasFocus =
|
||||
(focusMeetingKey != null && focusMeetingKey > 0) ||
|
||||
(focusSessionKey != null && focusSessionKey > 0)
|
||||
@@ -87,7 +88,7 @@ export function WeekendPage({
|
||||
if (loadState === 'error' || context == null) {
|
||||
return (
|
||||
<main className="wk-page" data-testid="weekend-page" data-state="error">
|
||||
<WeekendError message={error?.message} />
|
||||
<WeekendError error={error} onRetry={refetch} retrying={isFetching} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user