diff --git a/src/components/PredictionList.tsx b/src/components/PredictionList.tsx
new file mode 100644
index 0000000..c430ef4
--- /dev/null
+++ b/src/components/PredictionList.tsx
@@ -0,0 +1,37 @@
+import { Card, CardBody } from '@/components/ui/Card';
+import { WinProbBar } from '@/components/WinProbBar';
+import { fmt, useFormat } from '@/lib/i18n';
+import type { Fixture, MatchPrediction } from '@/lib/types';
+
+export interface PredictionDay {
+ key: string;
+ firstKickoff: string;
+ items: { f: Fixture; m: MatchPrediction }[];
+}
+
+/** Day-grouped prediction cards: a localized day header, then one card per match. */
+export function PredictionList({ days }: { days: PredictionDay[] }) {
+ const { t, kickoffDay, kickoffTime } = useFormat();
+ return (
+
+ {days.map((d) => (
+
+ {kickoffDay(d.firstKickoff)}
+
+ {d.items.map(({ m, f }) => (
+
+
+
+ {f.group ? fmt(t.common.group, { group: f.group }) : t.stage[f.stage]}
+ {kickoffTime(f.kickoff)}
+
+
+
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/src/features/predictions/PredictionsPage.tsx b/src/features/predictions/PredictionsPage.tsx
index 74c4cf1..e279738 100644
--- a/src/features/predictions/PredictionsPage.tsx
+++ b/src/features/predictions/PredictionsPage.tsx
@@ -1,17 +1,46 @@
-import { lazy, Suspense, useMemo } from 'react';
+import { lazy, Suspense, useMemo, useState } from 'react';
import { Link } from '@tanstack/react-router';
import { Gauge, TrendingUp, Trophy } from 'lucide-react';
import { PageHeader } from '@/components/ui/PageHeader';
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
import { OddsTable } from '@/components/OddsTable';
-import { WinProbBar } from '@/components/WinProbBar';
+import { PredictionList, type PredictionDay } from '@/components/PredictionList';
// Recharts is heavy and only used here — load it as a separate on-demand chunk.
const OddsChart = lazy(() => import('@/components/OddsChart').then((m) => ({ default: m.OddsChart })));
import { teamFlag } from '@/lib/teams';
+import { groupByDay } from '@/lib/fixtures';
+import { cn } from '@/lib/cn';
import { fmt, useFormat } from '@/lib/i18n';
import { useTournamentStore } from '@/stores/tournamentStore';
+type Range = 'next3' | 'all';
+type StageFilter = 'all' | 'group' | 'ko';
+
+function Chips({ value, onChange, options }: {
+ value: T;
+ onChange: (v: T) => void;
+ options: { v: T; label: string }[];
+}) {
+ return (
+
+ {options.map((o) => (
+
+ ))}
+
+ );
+}
+
function ChampionBoard({ odds }: { odds: { team: string; champion: number }[] }) {
const top = odds.slice(0, 8);
const max = Math.max(...top.map((t) => t.champion), 0.01);
@@ -38,19 +67,29 @@ function ChampionBoard({ odds }: { odds: { team: string; champion: number }[] })
}
export function PredictionsPage() {
- const { t, locale, kickoffDay, kickoffTime } = useFormat();
+ const { t, locale } = useFormat();
const model = useTournamentStore((s) => s.model);
const snapshot = useTournamentStore((s) => s.snapshot);
+ const [range, setRange] = useState('next3');
+ const [stage, setStage] = useState('all');
- const upcoming = useMemo(() => {
- if (!model || !snapshot) return [];
- const fxByNum = new Map(snapshot.fixtures.map((f) => [f.num, f]));
- return model.matches
- .map((m) => ({ m, f: fxByNum.get(m.num)! }))
- .filter(({ f }) => f && f.status !== 'finished')
- .sort((a, b) => a.f.kickoff.localeCompare(b.f.kickoff))
- .slice(0, 8);
- }, [model, snapshot]);
+ const { days, shown } = useMemo(() => {
+ if (!model || !snapshot) return { days: [] as PredictionDay[], shown: 0 };
+ const preds = new Map(model.matches.map((m) => [m.num, m]));
+ const fixtures = snapshot.fixtures.filter(
+ (f) =>
+ f.status !== 'finished' &&
+ preds.has(f.num) &&
+ (stage === 'all' || (stage === 'group' ? f.stage === 'group' : f.stage !== 'group')),
+ );
+ const grouped = groupByDay(fixtures, range === 'next3' ? 3 : Infinity);
+ const days = grouped.map((g) => ({
+ key: g.key,
+ firstKickoff: g.firstKickoff,
+ items: g.fixtures.map((f) => ({ f, m: preds.get(f.num)! })),
+ }));
+ return { days, shown: days.reduce((s, d) => s + d.items.length, 0) };
+ }, [model, snapshot, range, stage]);
if (!model) {
return (
@@ -108,24 +147,29 @@ export function PredictionsPage() {
- {upcoming.length > 0 && (
- <>
- {t.predict.nextMatchesHeading}
-
- {upcoming.map(({ m, f }) => (
-
-
-
- {f.group ? fmt(t.common.group, { group: f.group }) : f.round}
- {kickoffDay(f.kickoff)} · {kickoffTime(f.kickoff)}
-
-
-
-
- ))}
-
- >
- )}
+
+
{t.predict.nextMatchesHeading} ({shown})
+
+
+
+
+
+
);
}