mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
feat(#73): Adaptive Weekend shell and responsive navigation
Implemented by claude via .agents/dev dispatch.
This commit is contained in:
148
frontend/src/hooks/useWeekendContext.ts
Normal file
148
frontend/src/hooks/useWeekendContext.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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'
|
||||
|
||||
export interface UseWeekendContextResult {
|
||||
context: WeekendContext
|
||||
/** True when the payload came from the canonical /api/v1/weekend-context endpoint. */
|
||||
fromEndpoint: boolean
|
||||
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.
|
||||
*/
|
||||
export function useWeekendContext(): UseWeekendContextResult {
|
||||
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 contextQuery = useQuery({
|
||||
queryKey: ['weekend-context'],
|
||||
queryFn: fetchWeekendContext,
|
||||
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 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,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const news: NewsItem[] = newsQuery.data ?? []
|
||||
|
||||
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,
|
||||
],
|
||||
)
|
||||
|
||||
if (contextQuery.data) {
|
||||
return { context: contextQuery.data, fromEndpoint: true, 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