fix(#76): surface truthful availability across primary routes

This commit is contained in:
2026-07-12 20:28:51 -04:00
parent dafb9b6dc5
commit 5534ebf82c
19 changed files with 1060 additions and 83 deletions

View File

@@ -1,8 +1,8 @@
import type { ReactNode } from 'react'
import { isTimeoutError, userFacingError } from '../lib/fetch'
import { noticeMessage, type DataAvailability } from '../lib/availability'
/** Weekend Context terminology for coverage / availability indicators. */
export type DataAvailability = 'local' | 'partial' | 'stale' | 'archive' | 'limited' | 'missing'
export type { DataAvailability }
export function availabilityLabel(kind: DataAvailability): string {
switch (kind) {
@@ -131,6 +131,7 @@ interface StaleNoticeProps {
availability?: DataAvailability
message?: string
onRetry?: () => void
retrying?: boolean
testId?: string
}
@@ -139,26 +140,22 @@ export function DataNotice({
availability = 'stale',
message,
onRetry,
retrying = false,
testId = 'data-notice',
}: StaleNoticeProps) {
const defaultMessage =
availability === 'stale'
? 'Showing stale cached data. Retry to refresh.'
: availability === 'limited'
? 'Some optional details are unavailable. Core local data is shown.'
: availability === 'partial'
? 'Coverage is partial for this weekend.'
: availability === 'archive'
? 'Showing an archived snapshot.'
: 'Data availability is limited.'
return (
<div className="data-notice" data-testid={testId} role="status">
<AvailabilityBadge kind={availability} />
<span className="data-notice-text">{message ?? defaultMessage}</span>
<span className="data-notice-text">{message ?? noticeMessage(availability)}</span>
{onRetry && (
<button type="button" className="route-state-retry data-notice-retry" onClick={onRetry}>
Retry
<button
type="button"
className="route-state-retry data-notice-retry"
onClick={onRetry}
disabled={retrying}
aria-busy={retrying || undefined}
>
{retrying ? 'Retrying…' : 'Retry'}
</button>
)}
</div>

View File

@@ -10,9 +10,13 @@ import { meetingIdentity } from '../../lib/weekendContext'
* no separate Preview destination. It reuses the existing preview page content and
* frames it with the next-session countdown and compact season navigation. It also
* backs the /preview alias, so saved preview links resolve here instead of looping.
*
* Canonical meeting/session identity is passed into Preview so the nested surface
* cannot independently re-resolve a different current weekend.
*/
export function PreSessionView({ context, now }: { context: WeekendContext; now: Date }) {
const meeting = meetingIdentity(context.next_meeting ?? context.focus_meeting)
const meeting = context.next_meeting ?? context.focus_meeting
const identity = meetingIdentity(meeting)
const next = context.next_session?.session
const nextStart = next?.date_start ?? meeting?.date_start
const nodes = railNodes(
@@ -25,11 +29,11 @@ export function PreSessionView({ context, now }: { context: WeekendContext; now:
<div className="wk-pre" data-testid="weekend-pre-session" data-state="pre_session">
<div className="wk-eyebrow mono" data-testid="wk-eyebrow">Pre-session</div>
{meeting && (
{identity && (
<header className="wk-pre-head" data-testid="wk-pre-head">
<div className="wk-event-id">
<Flag code={meeting.country_code} flag={meeting.country_flag} />
<h1 className="wk-sessions-title">{meeting.meeting_name}</h1>
<Flag code={identity.country_code} flag={identity.country_flag} />
<h1 className="wk-sessions-title">{identity.meeting_name}</h1>
</div>
<div className="wk-pre-countdown">
<span className="wk-next-label mono">{next?.session_name ?? 'Next session'}</span>
@@ -44,7 +48,11 @@ export function PreSessionView({ context, now }: { context: WeekendContext; now:
{nodes.length > 0 && <SessionRail nodes={nodes} />}
<div className="wk-pre-preview" data-testid="wk-pre-preview">
<RacePreviewPage />
<RacePreviewPage
embedded
meeting={meeting}
season={context.season ?? meeting?.year}
/>
</div>
<ChampionshipRoundStrip