mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
fix(#73): consume canonical Weekend Context contract and repair navigation
Address the independent review blockers on PR #80 after rebasing onto the authoritative #72 Weekend Context API. - Replace the invented frontend WeekendContext with the exact backend contract (temporal_state, previous/focus/next meetings, previous_completed/active/next/ default_analysis sessions with availability). Every valid canonical payload now maps to a designed state via a total resolveViewState; a well-formed response can never fall through to the limited-data placeholder. - Make the canonical read the single source of truth: useWeekendContext no longer fans out to season/meetings/per-weekend/OpenF1/live queries. Only supplementary championship + news reads run, and only once the canonical context resolves. - Fix the Prepare/analysis flow: /preview is a stable alias that renders the preparation surface (PreSessionView) instead of redirecting back to the same between-races screen. - One primary navigation system per breakpoint: the mobile top-bar links are hidden so the bottom bar is the sole primary nav, and Admin is moved out of every Primary landmark into an operator-utilities toolbar. - Add Vitest coverage for the contract mapping, every temporal state, loading/ error/limited surfaces, the no-fanout guarantee, the /preview CTA, and the nav hierarchy; add hermetic Playwright journeys (seeded + injected canonical payloads), 390/768/1440 overflow checks, and Weekend visual snapshots. Retire the stale Command Center specs/snapshots. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,32 +1,38 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useQueries, useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
fetchChampionshipHub,
|
||||
fetchLiveState,
|
||||
fetchLocalMeetings,
|
||||
fetchNews,
|
||||
fetchSeasonMeetings,
|
||||
fetchSeasons,
|
||||
fetchSessions,
|
||||
fetchWeekend,
|
||||
fetchWeekendContext,
|
||||
} from '../api'
|
||||
import { pickFocusMeeting } from '../lib/schedule'
|
||||
import { deriveWeekendContext } from '../lib/weekendContext'
|
||||
import type { NewsItem, Weekend, WeekendContext } from '../types'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchChampionshipHub, fetchNews, fetchWeekendContext } from '../api'
|
||||
import { championshipImpact, briefingItems } from '../lib/weekendContext'
|
||||
import type {
|
||||
WeekendChampionshipImpact,
|
||||
WeekendBriefingItem,
|
||||
WeekendContext,
|
||||
} from '../types'
|
||||
|
||||
export type WeekendLoadState = 'loading' | 'error' | 'ready'
|
||||
|
||||
export interface UseWeekendContextResult {
|
||||
context: WeekendContext
|
||||
/** True when the payload came from the canonical /api/v1/weekend-context endpoint. */
|
||||
fromEndpoint: boolean
|
||||
/** Canonical context, present only when loadState === 'ready'. */
|
||||
context: WeekendContext | null
|
||||
loadState: WeekendLoadState
|
||||
error?: Error
|
||||
/** Supplementary championship movers (not part of the #72 contract). */
|
||||
championship?: WeekendChampionshipImpact
|
||||
/** Supplementary briefing items (not part of the #72 contract). */
|
||||
briefing: WeekendBriefingItem[]
|
||||
now: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* useWeekendContext prefers the canonical /api/v1/weekend-context endpoint and
|
||||
* falls back to deriving the same contract client-side from existing endpoints
|
||||
* so the Weekend home works even when the backend handler (sibling story #72) is
|
||||
* not yet deployed.
|
||||
* useWeekendContext reads the canonical /api/v1/weekend-context endpoint as the
|
||||
* single source of truth for the Weekend home. When the canonical read succeeds
|
||||
* it layers on two pieces of supplementary data that the contract intentionally
|
||||
* omits — championship movers and the paddock briefing — and nothing else.
|
||||
*
|
||||
* It deliberately does NOT fan out to season / meetings / per-weekend / OpenF1
|
||||
* session / live-state queries: those would defeat the local-first canonical read
|
||||
* model and can hit the rate-limited OpenF1 REST surface during active sessions.
|
||||
* Supplementary queries are gated on a successful canonical read so a failing or
|
||||
* pending endpoint issues no extra requests.
|
||||
*/
|
||||
export function useWeekendContext(): UseWeekendContextResult {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
@@ -42,107 +48,42 @@ export function useWeekendContext(): UseWeekendContextResult {
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: fetchSeasons })
|
||||
const season = seasonsQuery.data?.[0] ?? null
|
||||
|
||||
const localMeetingsQuery = useQuery({
|
||||
queryKey: ['meetings', season, 'local'],
|
||||
queryFn: () => fetchLocalMeetings(season!),
|
||||
enabled: season != null,
|
||||
})
|
||||
|
||||
const seasonMeetingsQuery = useQuery({
|
||||
queryKey: ['season-meetings', season],
|
||||
queryFn: () => fetchSeasonMeetings(season!),
|
||||
enabled: season != null,
|
||||
})
|
||||
const canonical = contextQuery.data ?? null
|
||||
const canonicalReady = canonical != null
|
||||
const season = canonical?.season
|
||||
|
||||
// Supplementary data — only fetched once the canonical context has resolved,
|
||||
// so a pending/failed canonical read never triggers a request fan-out.
|
||||
const championshipQuery = useQuery({
|
||||
queryKey: ['championship-hub', season],
|
||||
queryFn: () => fetchChampionshipHub(season!),
|
||||
enabled: season != null,
|
||||
})
|
||||
|
||||
const liveQuery = useQuery({ queryKey: ['live-state'], queryFn: fetchLiveState, staleTime: 5_000 })
|
||||
|
||||
const newsQuery = useQuery({ queryKey: ['news', 6], queryFn: () => fetchNews(6) })
|
||||
|
||||
const localMeetings = useMemo(() => localMeetingsQuery.data ?? [], [localMeetingsQuery.data])
|
||||
const seasonMeetings = seasonMeetingsQuery.data?.length ? seasonMeetingsQuery.data : localMeetings
|
||||
|
||||
const weekendQueries = useQueries({
|
||||
queries: localMeetings.map((meeting) => ({
|
||||
queryKey: ['weekend', meeting.meeting_key],
|
||||
queryFn: () => fetchWeekend(meeting.meeting_key),
|
||||
enabled: localMeetings.length > 0,
|
||||
staleTime: 60_000,
|
||||
})),
|
||||
})
|
||||
|
||||
const weekendsByKey = useMemo(() => {
|
||||
const map = new Map<number, Weekend>()
|
||||
localMeetings.forEach((meeting, i) => {
|
||||
const data = weekendQueries[i]?.data
|
||||
if (data) map.set(meeting.meeting_key, data)
|
||||
})
|
||||
return map
|
||||
}, [localMeetings, weekendQueries])
|
||||
|
||||
const focusMeeting = useMemo(() => pickFocusMeeting(seasonMeetings, nowDate), [seasonMeetings, nowDate])
|
||||
const focusHasLocal = focusMeeting ? weekendsByKey.has(focusMeeting.meeting_key) : false
|
||||
|
||||
const focusSessionsQuery = useQuery({
|
||||
queryKey: ['sessions', focusMeeting?.meeting_key, 'openf1'],
|
||||
queryFn: () => fetchSessions(focusMeeting!.meeting_key, 'openf1'),
|
||||
enabled: focusMeeting != null && !focusHasLocal,
|
||||
queryKey: ['championship-hub', season ?? 'current'],
|
||||
queryFn: () => fetchChampionshipHub(season),
|
||||
enabled: canonicalReady,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const news: NewsItem[] = newsQuery.data ?? []
|
||||
const newsQuery = useQuery({
|
||||
queryKey: ['news', 6],
|
||||
queryFn: () => fetchNews(6),
|
||||
enabled: canonicalReady,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const derived = useMemo(
|
||||
() =>
|
||||
deriveWeekendContext({
|
||||
season,
|
||||
meetings: seasonMeetings,
|
||||
weekendsByKey,
|
||||
championship: championshipQuery.data,
|
||||
liveActive: liveQuery.data?.is_live === true,
|
||||
news,
|
||||
focusSessions: focusSessionsQuery.data,
|
||||
now: nowDate,
|
||||
}),
|
||||
[
|
||||
season,
|
||||
seasonMeetings,
|
||||
weekendsByKey,
|
||||
championshipQuery.data,
|
||||
liveQuery.data,
|
||||
news,
|
||||
focusSessionsQuery.data,
|
||||
nowDate,
|
||||
],
|
||||
const championship = useMemo(
|
||||
() => championshipImpact(championshipQuery.data),
|
||||
[championshipQuery.data],
|
||||
)
|
||||
const briefing = useMemo(() => briefingItems(newsQuery.data ?? []), [newsQuery.data])
|
||||
|
||||
if (contextQuery.data) {
|
||||
return { context: contextQuery.data, fromEndpoint: true, now: nowDate }
|
||||
let loadState: WeekendLoadState = 'loading'
|
||||
if (contextQuery.isError) loadState = 'error'
|
||||
else if (canonicalReady) loadState = 'ready'
|
||||
|
||||
return {
|
||||
context: canonical,
|
||||
loadState,
|
||||
error: contextQuery.error instanceof Error ? contextQuery.error : undefined,
|
||||
championship,
|
||||
briefing,
|
||||
now: nowDate,
|
||||
}
|
||||
|
||||
if (seasonsQuery.isLoading || (season != null && seasonMeetingsQuery.isLoading && localMeetingsQuery.isLoading)) {
|
||||
return { context: { state: 'loading', season: season ?? 0 }, fromEndpoint: false, now: nowDate }
|
||||
}
|
||||
|
||||
if (seasonsQuery.isError) {
|
||||
return {
|
||||
context: {
|
||||
state: 'error',
|
||||
season: 0,
|
||||
message: seasonsQuery.error instanceof Error ? seasonsQuery.error.message : 'Failed to load Weekend',
|
||||
},
|
||||
fromEndpoint: false,
|
||||
now: nowDate,
|
||||
}
|
||||
}
|
||||
|
||||
return { context: derived, fromEndpoint: false, now: nowDate }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user