Files
wm-projekt/app/[locale]/components/Bracket.tsx
2026-07-01 11:21:29 -05:00

202 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 (
<div
className={`side ${s.isWinner ? "win" : ""} ${s.provisional ? "prov" : ""} ${fix ? "fix" : ""}`}
{...(hasTip ? {
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => onShowTip(s.tooltip!, e.clientX, e.clientY),
onMouseMove: (e: React.MouseEvent<HTMLDivElement>) => onShowTip(s.tooltip!, e.clientX, e.clientY),
onMouseLeave: onHideTip,
} : {})}
>
<span className="nm">
{s.teamId ? (
<>
<Flag team={team} size={18} />
<span className="tn">{teamName(team, locale)}</span>
{s.provisional && <span className="prov-mark" title={dict.bracket.provisionalTooltip}></span>}
</>
) : (
<span className="lbl">{s.label}</span>
)}
{prob != null && prob > 0 && (
<span className="prob">{Math.round(prob * 100)}%</span>
)}
</span>
<span className="sc">{s.score != null ? s.score : ""}</span>
</div>
);
}
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 (
<div className={`tie ${isFinal ? "final-tie" : ""}`}>
<div className="match-badge">{tie.matchNumber}</div>
<div className="tie-meta">
{matchInfo || dict.bracket.match(tie.matchNumber)}
</div>
<Side s={tie.home} prob={tie.prob?.home} teams={teams} locale={locale} dict={dict} onShowTip={onShowTip} onHideTip={onHideTip} />
<Side s={tie.away} prob={tie.prob?.away} teams={teams} locale={locale} dict={dict} onShowTip={onShowTip} onHideTip={onHideTip} />
{tie.homePenalty != null && tie.awayPenalty != null && (
<div className="tie-meta" style={{ padding: "2px 10px 4px", textAlign: "center" }}>
({tie.homePenalty}:{tie.awayPenalty} {dict.bracket.penaltyShootout})
</div>
)}
</div>
);
}
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<number, ResolvedTie> };
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<number, string>();
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<TipState | null>(null);
const showTip = useCallback((text: string, x: number, y: number) => setTip({ x, y, text }), []);
const hideTip = useCallback(() => setTip(null), []);
return (
<div>
<div className="bracket-banner">
<span className="k">{dict.bracket.annexHeader}</span>
<span className="v">
{annexResolved
? dict.bracket.annexResolved
: dict.bracket.annexPending}
</span>
</div>
<div className="bracket-scroll">
<div className="bracket">
<div className="round">
<div className="round-label">{dict.round.r32}</div>
<div className="round-matches">
{r32.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} locale={locale} dict={dict} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
</div>
</div>
<div className="round">
<div className="round-label">{dict.round.r16}</div>
<div className="round-matches">
{r16.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} locale={locale} dict={dict} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
</div>
</div>
<div className="round">
<div className="round-label">{dict.round.qf}</div>
<div className="round-matches">
{qf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} locale={locale} dict={dict} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
</div>
</div>
<div className="round">
<div className="round-label">{dict.round.sf}</div>
<div className="round-matches">
{sf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} locale={locale} dict={dict} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
</div>
</div>
<div className="round">
<div className="round-label">{dict.round.final}</div>
<div className="round-matches">
{fin && <Tie tie={fin} teams={teams} isFinal locale={locale} dict={dict} matchInfo={matchMeta.get(fin.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />}
{third && (
<div style={{ marginTop: 20 }}>
<div className="round-label" style={{ marginBottom: 8 }}>{dict.round.thirdPlace}</div>
<Tie tie={third} teams={teams} locale={locale} dict={dict} matchInfo={matchMeta.get(third.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />
</div>
)}
</div>
</div>
</div>
</div>
<div className="legend">
<span><i style={{ background: "var(--turf)" }} />{dict.legend.winner}</span>
<span><i style={{ background: "var(--gold)" }} />{dict.legend.final}</span>
<span><i className="leg-fix" />{dict.legend.fixed}</span>
<span><i className="leg-prov" />{dict.legend.provisional}</span>
<span><i style={{ background: "var(--ink-faint)" }} />{dict.legend.placeholder}</span>
<span>{dict.legend.probExplanation}</span>
</div>
{tip && (
<div className="kobaum-tip" style={{ left: tip.x + 12, top: tip.y + 12 }}>
{tip.text}
</div>
)}
</div>
);
}