Session 26: Cross-sport tab counts, scan copy fix, game card visual polish, empty section auto-hide (1579 tests)
This commit is contained in:
@@ -318,6 +318,12 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
|
||||
const [games, setGames] = useState<SlateGame[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
// Session 26 — per-sport schedule counts for the tab labels, fetched
|
||||
// ONCE on mount for every schedule-backed sport (free ESPN, cached 60s).
|
||||
// This makes "MLB (15)" / "WNBA (2)" show on their tabs even while the
|
||||
// user is viewing a different sport — the count was previously only
|
||||
// known for sports loaded by the active tab.
|
||||
const [scheduleCounts, setScheduleCounts] = useState<Partial<Record<SlateSport, number>>>({});
|
||||
// Session 24 — when odds are unavailable but the schedule still has
|
||||
// games, this becomes a soft inline notice instead of a wall-of-error.
|
||||
const [oddsNotice, setOddsNotice] = useState(false);
|
||||
@@ -417,6 +423,33 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
|
||||
// silently blank the MLB panels. Always land back on 'all'.
|
||||
useEffect(() => { setActiveStat('all'); }, [tab]);
|
||||
|
||||
// Session 26 — fetch schedule counts for every schedule-backed sport
|
||||
// once on mount, so all sport tabs show their game count regardless of
|
||||
// which tab is active. Free + cached; failures leave the count absent.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const SPORTS: SlateSport[] = ['nba', 'wnba', 'mlb'];
|
||||
(async () => {
|
||||
const entries = await Promise.all(
|
||||
SPORTS.map(async (sport) => {
|
||||
try {
|
||||
const r = await fetch(`/api/schedule/${sport}`, { cache: 'no-store' });
|
||||
if (!r.ok) return [sport, undefined] as const;
|
||||
const data = (await r.json()) as ScheduleResponse;
|
||||
return [sport, Array.isArray(data?.games) ? data.games.length : undefined] as const;
|
||||
} catch {
|
||||
return [sport, undefined] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
const next: Partial<Record<SlateSport, number>> = {};
|
||||
for (const [sport, count] of entries) if (count != null) next[sport] = count;
|
||||
setScheduleCounts(next);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// Grading call site. Single source of truth so we never have two
|
||||
// PropRows in-flight from the same prop (the loadingKey enforces it).
|
||||
const onGrade = useCallback(async (prop: PropRowProp) => {
|
||||
@@ -496,8 +529,16 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
|
||||
return m;
|
||||
}, [games]);
|
||||
const tabCount = (id: SlateTab): number | null => {
|
||||
if (id === 'all') return games.length || null;
|
||||
return countBySport[id as SlateSport] ?? null;
|
||||
if (id === 'all') {
|
||||
// Prefer the loaded total; fall back to the sum of schedule counts
|
||||
// so the ALL tab reflects every sport even before its games load.
|
||||
if (games.length > 0) return games.length;
|
||||
const sum = Object.values(scheduleCounts).reduce((a, b) => a + (b || 0), 0);
|
||||
return sum || null;
|
||||
}
|
||||
// Loaded games are the most accurate (they include odds-only games);
|
||||
// otherwise fall back to the mount-time schedule count.
|
||||
return countBySport[id as SlateSport] ?? scheduleCounts[id as SlateSport] ?? null;
|
||||
};
|
||||
|
||||
// Manual scan fallback URL — pre-fills /scan with the search query
|
||||
|
||||
Reference in New Issue
Block a user