"use client"; import { useMemo, useState, useCallback } from "react"; import { GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types"; import { ThirdAssignment, orderedRound } from "@/lib/bracket"; import { resolveBracket, ResolvedSide, ResolvedTie } from "@/lib/resolve-bracket"; import { STADIUMS, MATCH_STADIUMS } from "@/lib/stadiums"; import Flag from "./Flag"; interface TipState { x: number; y: number; text: string } function Side({ s, prob, teams, onShowTip, onHideTip, }: { s: ResolvedSide; prob?: number | null; teams: Team[]; onShowTip: (text: string, x: number, y: number) => void; onHideTip: () => void; }) { const team = s.teamId ? teams.find((t) => t.id === s.teamId) : undefined; const fix = s.teamId != null && !s.provisional; const hasTip = !!s.tooltip; return (
) => onShowTip(s.tooltip!, e.clientX, e.clientY), onMouseMove: (e: React.MouseEvent) => onShowTip(s.tooltip!, e.clientX, e.clientY), onMouseLeave: onHideTip, } : {})} > {s.teamId ? ( <> {s.label} {s.provisional && } ) : ( {s.label} )} {prob != null && prob > 0 && ( {Math.round(prob * 100)}% )} {s.score != null ? s.score : "–"}
); } function fmtMatchInfo(utcDate?: string, stadiumId?: string): string { const city = stadiumId ? STADIUMS[stadiumId]?.city : ""; if (!utcDate) return city; // Stadt auch ohne Datum anzeigen const d = new Date(utcDate); const dateStr = d.toLocaleDateString("de-DE", { day: "numeric", month: "numeric" }); const timeStr = d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); const parts = [`${dateStr}`, `${timeStr}`]; if (city) parts.push(city); return parts.join(" · "); } function Tie({ tie, teams, isFinal, matchInfo, onShowTip, onHideTip, }: { tie: ResolvedTie; teams: Team[]; isFinal?: boolean; matchInfo?: string; onShowTip: (text: string, x: number, y: number) => void; onHideTip: () => void; }) { return (
Spiel {tie.matchNumber}
{matchInfo &&
{matchInfo}
}
); } export default function Bracket({ matches, teams, tables, thirds, assignment, annexResolved, preResolved, }: { matches: Match[]; teams: Team[]; tables: GroupTable[]; thirds: ThirdPlaceRow[]; assignment: ThirdAssignment | null; annexResolved: boolean; preResolved?: { r32: ResolvedTie[]; later: Record }; }) { const { r32: r32Array, later } = preResolved ?? resolveBracket( matches, teams, tables, thirds, assignment, annexResolved, ); const r32ByNum = new Map(r32Array.map((t) => [t.matchNumber, t])); // Baue slotNumber → formatted info (Stadt aus MATCH_STADIUMS, Datum/Uhrzeit aus Feed-Match) const feedByNum = useMemo(() => { const m = new Map(); for (const fm of matches) { if (fm.group == null) m.set(fm.matchNumber, fm); } return m; }, [matches]); const matchMeta = useMemo(() => { const meta = new Map(); for (let n = 73; n <= 104; n++) { const stadiumId = MATCH_STADIUMS[n]; const feedMatch = feedByNum.get(n); const info = fmtMatchInfo(feedMatch?.utcDate, stadiumId); if (info) meta.set(n, info); if (feedMatch?.matchNumber === 0 || !feedMatch) { console.log("[KO-SLOT-DATE]", "slot:", n, "| match.matchNumber:", feedMatch?.matchNumber ?? "—", "| match.utcDate:", feedMatch?.utcDate ?? "—", "| stadt:", STADIUMS[MATCH_STADIUMS[n]]?.city ?? "—"); } } return meta; }, [feedByNum]); const sfOrder = orderedRound([104]); const qfOrder = orderedRound(sfOrder); const r16Order = orderedRound(qfOrder); const r32Order = orderedRound(r16Order); const r32 = r32Order.map((n) => r32ByNum.get(n)).filter(Boolean) as ResolvedTie[]; const r16 = r16Order.map((n) => later[n]).filter(Boolean); const qf = qfOrder.map((n) => later[n]).filter(Boolean); const sf = sfOrder.map((n) => later[n]).filter(Boolean); const fin = later[104]; const third = later[103]; const [tip, setTip] = useState(null); const showTip = useCallback((text: string, x: number, y: number) => setTip({ x, y, text }), []); const hideTip = useCallback(() => setTip(null), []); return (
Annex-C-Zuordnung der Drittplatzierten: {annexResolved ? "aufgelöst — die acht Dritten sind den Gruppensiegern fest zugeteilt" : "noch offen — sobald die 8 besten Dritten feststehen, verbindet sich der Baum automatisch"}
Letzte 32
{r32.map((t) => )}
Achtelfinale
{r16.map((t) => )}
Viertelfinale
{qf.map((t) => )}
Halbfinale
{sf.map((t) => )}
Finale
{fin && } {third && (
Spiel um Platz 3
)}
Sieger / weiter Finale fix qualifiziert vorläufig (≈, nach aktueller Tabelle) Platzhalter offen %-Werte: Polymarket-Wahrscheinlichkeit (falls verfügbar)
{tip && (
{tip.text}
)}
); }