"use client"; import { useMemo, useState, useCallback } from "react"; import { GroupTable, Match, Team, ThirdPlaceRow, teamName } from "@/lib/types"; import { ThirdAssignment, orderedRound } from "@/lib/bracket"; import { resolveBracket, ResolvedSide, ResolvedTie } from "@/lib/resolve-bracket"; import { Dictionary } from "@/lib/i18n"; import { STADIUMS, MATCH_STADIUMS, MATCH_DATES } from "@/lib/stadiums"; import Flag from "./Flag"; interface TipState { x: number; y: number; text: string } function Side({ s, prob, teams, locale, dict, onShowTip, onHideTip, }: { s: ResolvedSide; prob?: number | null; teams: Team[]; locale: string; dict: Dictionary; 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 ? ( <> {teamName(team, locale)} {s.provisional && } ) : ( {s.label} )} {prob != null && prob > 0 && ( {Math.round(prob * 100)}% )} {s.score != null ? s.score : "–"}
); } function fmtMatchInfo(utcDate?: string, stadiumId?: string, locale: string = "de"): string { if (!utcDate) return ""; const intlLocale = locale === "en" ? "en-US" : "de-DE"; const d = new Date(utcDate); const dateStr = d.toLocaleDateString(intlLocale, { day: "numeric", month: "numeric" }); const timeStr = d.toLocaleTimeString(intlLocale, { hour: "2-digit", minute: "2-digit" }); const city = stadiumId ? STADIUMS[stadiumId]?.city : ""; const parts = [`${dateStr}`, `${timeStr}`]; if (city) parts.push(city); return parts.join(" "); } function Tie({ tie, teams, isFinal, matchInfo, locale, dict, onShowTip, onHideTip, }: { tie: ResolvedTie; teams: Team[]; isFinal?: boolean; matchInfo?: string; locale: string; dict: Dictionary; onShowTip: (text: string, x: number, y: number) => void; onHideTip: () => void; }) { return (
{tie.matchNumber}
{matchInfo || dict.bracket.match(tie.matchNumber)}
{tie.homePenalty != null && tie.awayPenalty != null && (
({tie.homePenalty}:{tie.awayPenalty} {dict.bracket.penaltyShootout})
)}
); } export default function Bracket({ matches, teams, tables, thirds, assignment, annexResolved, preResolved, dict, locale, }: { matches: Match[]; teams: Team[]; tables: GroupTable[]; thirds: ThirdPlaceRow[]; assignment: ThirdAssignment | null; annexResolved: boolean; preResolved?: { r32: ResolvedTie[]; later: Record }; dict: Dictionary; locale: string; }) { const { r32: r32Array, later } = preResolved ?? resolveBracket( matches, teams, tables, thirds, assignment, annexResolved, undefined, dict, ); const r32ByNum = new Map(r32Array.map((t) => [t.matchNumber, t])); // Slot-Nummer → formatierte Anzeige (Datum/Uhrzeit aus MATCH_DATES, Stadt aus MATCH_STADIUMS) const matchMeta = useMemo(() => { const meta = new Map(); for (let n = 73; n <= 104; n++) { const utcDate = MATCH_DATES[n]; const stadiumId = MATCH_STADIUMS[n]; const info = fmtMatchInfo(utcDate, stadiumId, locale); if (info) meta.set(n, info); } return meta; }, [locale]); 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 (
{dict.bracket.annexHeader} {annexResolved ? dict.bracket.annexResolved : dict.bracket.annexPending}
{dict.round.r32}
{r32.map((t) => )}
{dict.round.r16}
{r16.map((t) => )}
{dict.round.qf}
{qf.map((t) => )}
{dict.round.sf}
{sf.map((t) => )}
{dict.round.final}
{fin && } {third && (
{dict.round.thirdPlace}
)}
{dict.legend.winner} {dict.legend.final} {dict.legend.fixed} {dict.legend.provisional} {dict.legend.placeholder} {dict.legend.probExplanation}
{tip && (
{tip.text}
)}
); }