"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) => )}
Sieger / weiter
Finale
fix qualifiziert
vorläufig (≈, nach aktueller Tabelle)
Platzhalter offen
%-Werte: Polymarket-Wahrscheinlichkeit (falls verfügbar)
{tip && (
{tip.text}
)}
);
}