Starters-on-pitch, chart movers sort, pre-filled what-if bracket, vs/@ fix

- Finished matches show the STARTING XIs on the pitch (red markers with
  the minute on everyone subbed off) with 'Subbed on' / 'Subbed off'
  lists below, minutes and ratings included; live matches keep the
  current-XI view that swaps substitutes in
- Title-race chart gets a sort toggle, defaulting to 'Biggest movers'
  (the teams whose odds changed most at the latest update — what's
  relevant now) with 'Title favorites' as the alternative
- Entering what-if on the bracket now pre-fills the model's most likely
  tournament: group slots seeded from win-group/advance odds (best-
  thirds assigned to their allowed slots via backtracking), every
  pairing picked by the model — a complete overview to edit by tapping;
  Reset returns to the projection
- Team fixtures drop the US 'vs/@' convention: always 'vs' plus an H/A
  badge with a tooltip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:46:27 +02:00
parent f78f599ed2
commit 15d955e65c
9 changed files with 303 additions and 59 deletions
+35 -5
View File
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from 'recharts';
import { teamFlag } from '@/lib/teams';
import { useFormat } from '@/lib/i18n';
import { useFormat, useT } from '@/lib/i18n';
import type { OddsHistoryPoint } from '@/lib/types';
const PALETTE = [
@@ -15,11 +15,25 @@ const PALETTE = [
/** Championship odds over time — one line per top contender, a point per result
* that moved the model. */
type ChartSort = 'movers' | 'leaders';
export function OddsChart({ history }: { history: OddsHistoryPoint[] }) {
const { locale } = useFormat();
const t = useT();
// Default to the teams whose odds moved most at the latest update — what
// changed just now is more interesting than a static favorites list.
const [sort, setSort] = useState<ChartSort>('movers');
const { data, teams } = useMemo(() => {
const last = history[history.length - 1];
const teams = (last?.top ?? []).slice(0, 6).map((t) => t.team);
const prev = history[history.length - 2];
const latest = last?.top ?? [];
let picked = latest.slice(0, 6);
if (sort === 'movers' && prev) {
const before = new Map(prev.top.map((x) => [x.team, x.champion]));
const delta = (x: { team: string; champion: number }) => Math.abs(x.champion - (before.get(x.team) ?? x.champion));
picked = [...latest].sort((a, b) => delta(b) - delta(a) || b.champion - a.champion).slice(0, 6);
}
const teams = picked.map((x) => x.team);
const tag = locale === 'de' ? 'de-DE' : 'en-GB';
const data = history.map((h, i) => {
const day = new Date(h.t).toLocaleDateString(tag, { day: 'numeric', month: 'short' });
@@ -28,10 +42,25 @@ export function OddsChart({ history }: { history: OddsHistoryPoint[] }) {
return row;
});
return { data, teams };
}, [history, locale]);
}, [history, locale, sort]);
return (
<div className="h-72 w-full">
<div className="w-full">
<div className="mb-2 flex justify-end">
<div className="flex overflow-hidden rounded-lg border border-line text-xs font-medium">
{([['movers', t.predict.titleRace.sortMovers], ['leaders', t.predict.titleRace.sortLeaders]] as const).map(([v, label]) => (
<button
key={v}
type="button"
onClick={() => setSort(v)}
className={v === sort ? 'bg-accent-glow px-3 py-1 font-bold text-accent' : 'px-3 py-1 text-muted hover:bg-elevated hover:text-ink'}
>
{label}
</button>
))}
</div>
</div>
<div className="h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8, right: 16, bottom: 4, left: -8 }}>
<CartesianGrid stroke="var(--app-line)" strokeDasharray="3 3" vertical={false} />
@@ -74,6 +103,7 @@ export function OddsChart({ history }: { history: OddsHistoryPoint[] }) {
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}