diff --git a/src/features/live/LivePage.tsx b/src/features/live/LivePage.tsx index 86b7a28..f7f6452 100644 --- a/src/features/live/LivePage.tsx +++ b/src/features/live/LivePage.tsx @@ -3,10 +3,13 @@ import { Radio } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { MatchCard } from '@/components/MatchCard'; import { useTournamentStore } from '@/stores/tournamentStore'; -import { dayKeyOf, isToday, kickoffDay } from '@/lib/format'; +import { isToday } from '@/lib/format'; +import { upcomingByDay, type DayGroup } from '@/lib/fixtures'; import { fmt, useFormat } from '@/lib/i18n'; import type { Fixture } from '@/lib/types'; +const UPCOMING_DAYS = 3; + function Section({ title, fixtures }: { title: string; fixtures: Fixture[] }) { if (!fixtures.length) return null; return ( @@ -21,7 +24,7 @@ function Section({ title, fixtures }: { title: string; fixtures: Fixture[] }) { export function LivePage() { const snapshot = useTournamentStore((s) => s.snapshot); - const { t, locale } = useFormat(); + const { t, kickoffDay } = useFormat(); const groups = useMemo(() => { const fixtures = snapshot?.fixtures ?? []; @@ -30,20 +33,16 @@ export function LivePage() { .filter((f) => f.status !== 'live' && isToday(f.kickoff)) .sort((a, b) => a.kickoff.localeCompare(b.kickoff)); - // If nothing is on today, surface the next match day in full. - const upcoming = fixtures - .filter((f) => f.status === 'scheduled' && !isToday(f.kickoff) && new Date(f.kickoff).getTime() > Date.now()) - .sort((a, b) => a.kickoff.localeCompare(b.kickoff)); - const nextDayKey = upcoming[0] ? dayKeyOf(upcoming[0].kickoff) : null; - const nextDay = nextDayKey ? upcoming.filter((f) => dayKeyOf(f.kickoff) === nextDayKey) : []; + // The next few match days, each under its own day header. + const upcoming: DayGroup[] = upcomingByDay(fixtures, UPCOMING_DAYS); const recent = fixtures .filter((f) => f.status === 'finished') .sort((a, b) => b.kickoff.localeCompare(a.kickoff)) .slice(0, 6); - return { live, today, nextDay, nextDayLabel: upcoming[0] ? kickoffDay(upcoming[0].kickoff, locale) : '', recent }; - }, [snapshot, locale]); + return { live, today, upcoming, recent }; + }, [snapshot]); if (!snapshot) { return ( @@ -58,7 +57,7 @@ export function LivePage() { ); } - const nothingToday = !groups.live.length && !groups.today.length; + const nothingNow = !groups.live.length && !groups.today.length; return (
{t.live.empty}
diff --git a/src/lib/fixtures.test.ts b/src/lib/fixtures.test.ts new file mode 100644 index 0000000..99d81d4 --- /dev/null +++ b/src/lib/fixtures.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { upcomingByDay } from './fixtures'; +import type { Fixture } from './types'; + +const fx = (num: number, kickoff: string, status: Fixture['status'] = 'scheduled'): Fixture => ({ + num, stage: 'group', group: 'A', round: 'Matchday 1', groupRound: 1, + kickoff, venue: 'Somewhere', + home: { team: 'A', placeholder: null, label: 'A' }, + away: { team: 'B', placeholder: null, label: 'B' }, + status, homeScore: null, awayScore: null, minute: null, +} as Fixture); + +// fixed "now": midday UTC so local-day edges don't flip the assertions +const NOW = new Date('2026-06-14T12:00:00Z'); + +describe('upcomingByDay', () => { + it('groups scheduled fixtures after today into at most maxDays days', () => { + const groups = upcomingByDay([ + fx(1, '2026-06-15T18:00:00Z'), + fx(2, '2026-06-15T21:00:00Z'), + fx(3, '2026-06-16T18:00:00Z'), + fx(4, '2026-06-17T18:00:00Z'), + fx(5, '2026-06-18T18:00:00Z'), + ], 3, NOW); + expect(groups).toHaveLength(3); + expect(groups[0]!.fixtures.map((f) => f.num)).toEqual([1, 2]); + expect(groups[2]!.fixtures.map((f) => f.num)).toEqual([4]); + }); + + it('excludes today, live and finished fixtures', () => { + const groups = upcomingByDay([ + fx(1, '2026-06-14T20:00:00Z'), // today → excluded + fx(2, '2026-06-15T18:00:00Z', 'live'), // live → excluded + fx(3, '2026-06-15T21:00:00Z', 'finished'), // finished → excluded + fx(4, '2026-06-15T15:00:00Z'), + ], 3, NOW); + expect(groups).toHaveLength(1); + expect(groups[0]!.fixtures.map((f) => f.num)).toEqual([4]); + }); + + it('sorts within a day by kickoff and keeps day order', () => { + const groups = upcomingByDay([ + fx(2, '2026-06-15T21:00:00Z'), + fx(1, '2026-06-15T15:00:00Z'), + ], 2, NOW); + expect(groups[0]!.fixtures.map((f) => f.num)).toEqual([1, 2]); + expect(groups[0]!.firstKickoff).toBe('2026-06-15T15:00:00Z'); + }); + + it('returns fewer groups when fewer days remain', () => { + expect(upcomingByDay([fx(1, '2026-06-15T18:00:00Z')], 3, NOW)).toHaveLength(1); + }); + + it('handles the tournament-over case', () => { + expect(upcomingByDay([fx(1, '2026-06-10T18:00:00Z', 'finished')], 3, NOW)).toEqual([]); + }); +}); diff --git a/src/lib/fixtures.ts b/src/lib/fixtures.ts new file mode 100644 index 0000000..d37a94e --- /dev/null +++ b/src/lib/fixtures.ts @@ -0,0 +1,39 @@ +// Pure fixture grouping helpers (no store/DOM imports — vitest-friendly). +import { dayKeyOf } from './format'; +import type { Fixture } from './types'; + +export interface DayGroup { + /** Stable local-day key (YYYY-MM-DD). */ + key: string; + /** Kickoff ISO of the day's first match (for localized day headers). */ + firstKickoff: string; + fixtures: Fixture[]; +} + +/** Group fixtures (as given) by local day, first `maxDays` distinct days, + * sorted by kickoff within and across days. Pass Infinity for all days. */ +export function groupByDay(fixtures: Fixture[], maxDays: number): DayGroup[] { + const sorted = [...fixtures].sort((a, b) => a.kickoff.localeCompare(b.kickoff)); + const groups: DayGroup[] = []; + for (const f of sorted) { + const key = dayKeyOf(f.kickoff); + const last = groups[groups.length - 1]; + if (last && last.key === key) { + last.fixtures.push(f); + } else { + if (groups.length >= maxDays) break; + groups.push({ key, firstKickoff: f.kickoff, fixtures: [f] }); + } + } + return groups; +} + +/** Scheduled fixtures after today, grouped by local day, first `maxDays` + * distinct days. Days and fixtures are sorted by kickoff. */ +export function upcomingByDay(fixtures: Fixture[], maxDays: number, now = new Date()): DayGroup[] { + const todayKey = dayKeyOf(now.toISOString()); + return groupByDay( + fixtures.filter((f) => f.status === 'scheduled' && dayKeyOf(f.kickoff) > todayKey), + maxDays, + ); +}