diff --git a/src/components/DailyDigest.tsx b/src/components/DailyDigest.tsx new file mode 100644 index 0000000..5cf9e47 --- /dev/null +++ b/src/components/DailyDigest.tsx @@ -0,0 +1,92 @@ +import { useMemo } from 'react'; +import { Link } from '@tanstack/react-router'; +import { Newspaper, TrendingDown, TrendingUp } from 'lucide-react'; +import { Card, CardBody, CardHeader } from '@/components/ui/Card'; +import { teamFlag } from '@/lib/teams'; +import { dayKeyOf } from '@/lib/format'; +import { useT } from '@/lib/i18n'; +import { useTournamentStore } from '@/stores/tournamentStore'; + +/** The day in brief: yesterday's results + the model's biggest title-odds moves. */ +export function DailyDigest() { + const t = useT(); + const snapshot = useTournamentStore((s) => s.snapshot); + const model = useTournamentStore((s) => s.model); + + const digest = useMemo(() => { + if (!snapshot) return null; + const now = new Date(); + const yesterdayKey = dayKeyOf(new Date(now.getTime() - 24 * 3600_000).toISOString()); + const yesterday = snapshot.fixtures + .filter((f) => f.status === 'finished' && dayKeyOf(f.kickoff) === yesterdayKey) + .sort((a, b) => a.kickoff.localeCompare(b.kickoff)); + + let movers: { team: string; delta: number }[] = []; + const hist = model?.history ?? []; + if (hist.length >= 2) { + const latest = hist[hist.length - 1]!; + const cutoff = new Date(latest.t).getTime() - 20 * 3600_000; + const base = [...hist].reverse().find((h) => new Date(h.t).getTime() <= cutoff) ?? hist[0]!; + if (base !== latest) { + const before = new Map(base.top.map((x) => [x.team, x.champion])); + movers = latest.top + .map((x) => ({ team: x.team, delta: x.champion - (before.get(x.team) ?? 0) })) + .filter((m) => Math.abs(m.delta) >= 0.005) + .sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta)) + .slice(0, 3); + } + } + if (yesterday.length === 0 && movers.length === 0) return null; + return { yesterday, movers }; + }, [snapshot, model]); + + if (!digest) return null; + + return ( + + + + {t.live.digestTitle} + + + {digest.yesterday.length > 0 && ( +
+
{t.live.digestYesterday}
+
    + {digest.yesterday.map((f) => ( +
  • + + {f.home.team && teamFlag(f.home.team)} + {f.home.label} + {f.homeScore}–{f.awayScore} + {f.away.label} + {f.away.team && teamFlag(f.away.team)} + +
  • + ))} +
+
+ )} + {digest.movers.length > 0 && ( +
+
{t.live.digestMovers}
+
    + {digest.movers.map((m) => ( +
  • + + {teamFlag(m.team)} + {m.team} + 0 ? 'text-accent' : 'text-danger'}`}> + {m.delta > 0 ? : } + {m.delta > 0 ? '+' : ''}{(m.delta * 100).toFixed(1)} pp + + +
  • + ))} +
+
+ )} +
+
+ ); +} diff --git a/src/features/live/LivePage.tsx b/src/features/live/LivePage.tsx index acf6d43..a4af72e 100644 --- a/src/features/live/LivePage.tsx +++ b/src/features/live/LivePage.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { Radio } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { MatchCard } from '@/components/MatchCard'; +import { DailyDigest } from '@/components/DailyDigest'; import { useTournamentStore } from '@/stores/tournamentStore'; import { useFavoriteStore } from '@/stores/favoriteStore'; import { isToday } from '@/lib/format'; @@ -76,6 +77,8 @@ export function LivePage() { subtitle={fmt(t.live.subtitle, { season: snapshot.season })} /> + + {favMatch && (

★ {t.live.myTeam}

diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index 691f809..f311462 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -68,6 +68,9 @@ export const de: Dict = { comingUp: "Demnächst", myTeam: "Mein Team", latestResults: "Letzte Ergebnisse", + digestTitle: "Der Tag in Kürze", + digestYesterday: "Gestern", + digestMovers: "Bewegung bei den Titelchancen", empty: "Der Spielplan ist geladen. Sobald das Turnier läuft, tauchen die Spiele hier auf.", }, match: { diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index cd1e826..4c0cf24 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -67,6 +67,9 @@ export const en = { comingUp: "Coming up", myTeam: "My team", latestResults: "Latest results", + digestTitle: "The day in brief", + digestYesterday: "Yesterday", + digestMovers: "Title-odds movers", empty: "The schedule is ready. Matches will appear here once the tournament kicks off.", }, match: {