Slim match summary + per-deploy "What's new" modal
- Removed the Videos feed and the "Market movement vs the model" card from the match Summary tab (and the now-orphaned OddsMovement component, the odds-history fetch, and the dead i18n keys). - New WhatsNew modal (src/components/WhatsNew.tsx, mounted in RootLayout): on first visit after a deploy it shows the changelog entries the visitor hasn't seen yet, then records the version in localStorage. Bilingual, driven by public/data/changelog.json (en+de per entry); silent once the visitor is current. Bump changelog.json's top entry each deploy. Gates: 170 tests, build, typecheck:server — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useParams } from '@tanstack/react-router';
|
||||
import { ArrowLeft, ArrowRight, BadgeCheck, Ban, Clapperboard, Goal, Shield, Shirt, Stethoscope } from 'lucide-react';
|
||||
import { ArrowLeft, ArrowRight, BadgeCheck, Ban, Goal, Shield, Shirt, Stethoscope } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { WinProbBar } from '@/components/WinProbBar';
|
||||
import { WinProbTimeline, type TimelinePoint } from '@/components/WinProbTimeline';
|
||||
import { OddsMovement } from '@/components/OddsMovement';
|
||||
import { FormChips } from '@/components/FormChips';
|
||||
import { XgRace } from '@/components/XgRace';
|
||||
import { ShotMap } from '@/components/ShotMap';
|
||||
@@ -19,7 +18,7 @@ import { redCards } from '@/lib/discipline';
|
||||
import { inPlayProbs } from '@/lib/model/inplay';
|
||||
import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
import { PlayerLink } from '@/components/PlayerLink';
|
||||
import type { Fixture, KeyPlayer, MatchPreview, MatchRich, OddsPoint } from '@/lib/types';
|
||||
import type { Fixture, KeyPlayer, MatchPreview, MatchRich } from '@/lib/types';
|
||||
|
||||
const num = (s: string | undefined) => (s ? Number(s.replace('%', '')) || 0 : 0);
|
||||
|
||||
@@ -98,7 +97,6 @@ export function MatchPreviewPage() {
|
||||
const { t, locale, kickoffDay, kickoffTime } = useFormat();
|
||||
const [preview, setPreview] = useState<MatchPreview | null>(null);
|
||||
const [timeline, setTimeline] = useState<TimelinePoint[]>([]);
|
||||
const [oddsHistory, setOddsHistory] = useState<OddsPoint[]>([]);
|
||||
const [rich, setRich] = useState<MatchRich | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('summary');
|
||||
@@ -133,11 +131,6 @@ export function MatchPreviewPage() {
|
||||
void load();
|
||||
void loadTimeline();
|
||||
void loadRich();
|
||||
// line history changes on a 3h sweep — once per visit is plenty
|
||||
fetch(`/api/odds/${numParam}`)
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
||||
.then((d: { history: OddsPoint[] }) => alive && setOddsHistory(d.history))
|
||||
.catch(() => {});
|
||||
const id = setInterval(() => {
|
||||
if (useTournamentStore.getState().snapshot?.fixtures.find((f) => f.num === Number(numParam))?.status === 'live') {
|
||||
void load();
|
||||
@@ -175,12 +168,6 @@ export function MatchPreviewPage() {
|
||||
const homeName = f.home.label;
|
||||
const awayName = f.away.label;
|
||||
const finished = f.status === 'finished';
|
||||
const koT = new Date(f.kickoff).getTime();
|
||||
const hasOddsChart = oddsHistory.filter((o) => o.home_ml != null && o.draw_ml != null && o.away_ml != null && o.captured_at <= koT).length >= 2;
|
||||
// Latest pre-kickoff totals/handicap line (display only, like all odds here).
|
||||
const closingLine = oddsHistory
|
||||
.filter((o) => o.captured_at <= koT && (o.over_under != null || o.spread != null))
|
||||
.sort((a, b) => b.captured_at - a.captured_at)[0] ?? null;
|
||||
|
||||
// Hero extras from the rich capture: kickoff-hour weather (pre-match only)
|
||||
// and post-match official chips (attendance, referee, player of the match).
|
||||
@@ -430,54 +417,6 @@ export function MatchPreviewPage() {
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
{preview.prediction && (hasOddsChart || closingLine) && (
|
||||
<Card>
|
||||
<CardHeader><span className="font-display font-bold text-ink">{t.match.oddsMovement}</span></CardHeader>
|
||||
<CardBody>
|
||||
{hasOddsChart && <OddsMovement history={oddsHistory} model={preview.prediction.probs} kickoff={f.kickoff} />}
|
||||
{closingLine && (
|
||||
<p className={`text-xs text-faint${hasOddsChart ? ' mt-2' : ''}`}>
|
||||
{[
|
||||
closingLine.over_under != null ? fmt(t.match.oddsExtras.ou, { n: closingLine.over_under }) : null,
|
||||
closingLine.spread != null ? fmt(t.match.oddsExtras.spread, { n: `${closingLine.spread > 0 ? '+' : ''}${closingLine.spread}` }) : null,
|
||||
].filter(Boolean).join(' · ')} · {closingLine.provider}
|
||||
</p>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
{preview.videos.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<Clapperboard size={16} className="text-accent" />
|
||||
<span className="font-display font-bold text-ink">{t.match.videos}</span>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{preview.videos.map((v) => (
|
||||
<a
|
||||
key={v.href}
|
||||
href={v.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group flex gap-3 rounded-lg border border-line p-2 transition-colors hover:border-line-strong hover:bg-panel-2"
|
||||
>
|
||||
{v.thumbnail && <img src={v.thumbnail} alt="" loading="lazy" className="h-14 w-24 shrink-0 rounded object-cover" />}
|
||||
<span className="min-w-0">
|
||||
<span className="line-clamp-2 text-sm font-medium text-ink group-hover:text-accent">{v.headline}</span>
|
||||
{v.duration != null && (
|
||||
<span className="mt-0.5 block text-[11px] text-faint">
|
||||
{Math.floor(v.duration / 60)}:{String(v.duration % 60).padStart(2, '0')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-faint">{t.match.videosNote}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user