diff --git a/src/components/ScheduleDisplay.tsx b/src/components/ScheduleDisplay.tsx new file mode 100644 index 00000000..d5adbdb8 --- /dev/null +++ b/src/components/ScheduleDisplay.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +interface ScheduleDisplayProps { + schedule: T | null; + scheduleLoaded: boolean; + renderSchedule: (schedule: T) => React.ReactNode; + /** Optional custom message shown when the schedule is not yet released. */ + notReleasedMessage?: string; +} + +/** + * ScheduleDisplay + * + * Renders the match schedule, a loading spinner while fetching, or an + * informative message when the schedule has not been released yet. + * + * Resolves issue #200: previously an infinite spinner was shown when the + * schedule simply hadn't been published yet. + */ +export function ScheduleDisplay({ + schedule, + scheduleLoaded, + renderSchedule, + notReleasedMessage = 'The match schedule has not been released yet. Check back closer to the event!', +}: ScheduleDisplayProps) { + if (!scheduleLoaded) { + return ( +
+
+
+ ); + } + + if (!schedule) { + return ( +
+

{notReleasedMessage}

+
+ ); + } + + return <>{renderSchedule(schedule)}; +} diff --git a/src/hooks/useMatchSchedule.ts b/src/hooks/useMatchSchedule.ts new file mode 100644 index 00000000..793c6b02 --- /dev/null +++ b/src/hooks/useMatchSchedule.ts @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; + +export interface MatchScheduleResult { + schedule: T | null; + /** True once the fetch has completed, even if no data was returned. */ + scheduleLoaded: boolean; + error: Error | null; +} + +/** + * useMatchSchedule + * + * Fetches the match schedule for a given team and returns both the data and + * a `scheduleLoaded` flag so callers can distinguish between + * "still loading" and "loaded but no schedule released yet" (issue #200). + * + * @param fetchFn - Async function that fetches the schedule data. + * @param deps - Dependency array (passed to useEffect). + */ +export function useMatchSchedule( + fetchFn: () => Promise, + deps: unknown[] = [], +): MatchScheduleResult { + const [schedule, setSchedule] = useState(null); + const [scheduleLoaded, setScheduleLoaded] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setScheduleLoaded(false); + setError(null); + + fetchFn() + .then((data) => { + if (!cancelled) { + setSchedule(data); + setScheduleLoaded(true); + } + }) + .catch((err: unknown) => { + if (!cancelled) { + setError(err instanceof Error ? err : new Error(String(err))); + setScheduleLoaded(true); // stop spinner even on error + } + }); + + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); + + return { schedule, scheduleLoaded, error }; +}