Compare commits
2 Commits
89a9075391
...
4d838e2fb7
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d838e2fb7 | |||
| 7bad12df68 |
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useMemo, useState, useCallback } from "react";
|
||||||
import { GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
|
import { GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
|
||||||
import { ThirdAssignment, orderedRound } from "@/lib/bracket";
|
import { ThirdAssignment, orderedRound } from "@/lib/bracket";
|
||||||
import { resolveBracket, ResolvedSide, ResolvedTie } from "@/lib/resolve-bracket";
|
import { resolveBracket, ResolvedSide, ResolvedTie } from "@/lib/resolve-bracket";
|
||||||
|
import { STADIUMS, MATCH_STADIUMS, MATCH_DATES } from "@/lib/stadiums";
|
||||||
import Flag from "./Flag";
|
import Flag from "./Flag";
|
||||||
|
|
||||||
interface TipState { x: number; y: number; text: string }
|
interface TipState { x: number; y: number; text: string }
|
||||||
@@ -47,16 +48,29 @@ function Side({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtMatchInfo(utcDate?: string, stadiumId?: string): string {
|
||||||
|
if (!utcDate) return "";
|
||||||
|
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 city = stadiumId ? STADIUMS[stadiumId]?.city : "";
|
||||||
|
const parts = [`${dateStr}`, `${timeStr}`];
|
||||||
|
if (city) parts.push(city);
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
function Tie({
|
function Tie({
|
||||||
tie, teams, isFinal, onShowTip, onHideTip,
|
tie, teams, isFinal, matchInfo, onShowTip, onHideTip,
|
||||||
}: {
|
}: {
|
||||||
tie: ResolvedTie; teams: Team[]; isFinal?: boolean;
|
tie: ResolvedTie; teams: Team[]; isFinal?: boolean;
|
||||||
|
matchInfo?: string;
|
||||||
onShowTip: (text: string, x: number, y: number) => void;
|
onShowTip: (text: string, x: number, y: number) => void;
|
||||||
onHideTip: () => void;
|
onHideTip: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`tie ${isFinal ? "final-tie" : ""}`}>
|
<div className={`tie ${isFinal ? "final-tie" : ""}`}>
|
||||||
<div className="tie-head">Spiel {tie.matchNumber}</div>
|
<div className="tie-head">Spiel {tie.matchNumber}</div>
|
||||||
|
{matchInfo && <div className="tie-meta">{matchInfo}</div>}
|
||||||
<Side s={tie.home} prob={tie.prob?.home} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
<Side s={tie.home} prob={tie.prob?.home} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
||||||
<Side s={tie.away} prob={tie.prob?.away} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
<Side s={tie.away} prob={tie.prob?.away} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
||||||
</div>
|
</div>
|
||||||
@@ -77,6 +91,18 @@ export default function Bracket({
|
|||||||
|
|
||||||
const r32ByNum = new Map(r32Array.map((t) => [t.matchNumber, t]));
|
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);
|
||||||
|
if (info) meta.set(n, info);
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const sfOrder = orderedRound([104]);
|
const sfOrder = orderedRound([104]);
|
||||||
const qfOrder = orderedRound(sfOrder);
|
const qfOrder = orderedRound(sfOrder);
|
||||||
const r16Order = orderedRound(qfOrder);
|
const r16Order = orderedRound(qfOrder);
|
||||||
@@ -109,35 +135,35 @@ export default function Bracket({
|
|||||||
<div className="round">
|
<div className="round">
|
||||||
<div className="round-label">Letzte 32</div>
|
<div className="round-label">Letzte 32</div>
|
||||||
<div className="round-matches">
|
<div className="round-matches">
|
||||||
{r32.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} onShowTip={showTip} onHideTip={hideTip} />)}
|
{r32.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="round">
|
<div className="round">
|
||||||
<div className="round-label">Achtelfinale</div>
|
<div className="round-label">Achtelfinale</div>
|
||||||
<div className="round-matches">
|
<div className="round-matches">
|
||||||
{r16.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} onShowTip={showTip} onHideTip={hideTip} />)}
|
{r16.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="round">
|
<div className="round">
|
||||||
<div className="round-label">Viertelfinale</div>
|
<div className="round-label">Viertelfinale</div>
|
||||||
<div className="round-matches">
|
<div className="round-matches">
|
||||||
{qf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} onShowTip={showTip} onHideTip={hideTip} />)}
|
{qf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="round">
|
<div className="round">
|
||||||
<div className="round-label">Halbfinale</div>
|
<div className="round-label">Halbfinale</div>
|
||||||
<div className="round-matches">
|
<div className="round-matches">
|
||||||
{sf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} onShowTip={showTip} onHideTip={hideTip} />)}
|
{sf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} matchInfo={matchMeta.get(t.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="round">
|
<div className="round">
|
||||||
<div className="round-label">Finale</div>
|
<div className="round-label">Finale</div>
|
||||||
<div className="round-matches">
|
<div className="round-matches">
|
||||||
{fin && <Tie tie={fin} teams={teams} isFinal onShowTip={showTip} onHideTip={hideTip} />}
|
{fin && <Tie tie={fin} teams={teams} isFinal matchInfo={matchMeta.get(fin.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />}
|
||||||
{third && (
|
{third && (
|
||||||
<div style={{ marginTop: 20 }}>
|
<div style={{ marginTop: 20 }}>
|
||||||
<div className="round-label" style={{ marginBottom: 8 }}>Spiel um Platz 3</div>
|
<div className="round-label" style={{ marginBottom: 8 }}>Spiel um Platz 3</div>
|
||||||
<Tie tie={third} teams={teams} onShowTip={showTip} onHideTip={hideTip} />
|
<Tie tie={third} teams={teams} matchInfo={matchMeta.get(third.matchNumber)} onShowTip={showTip} onHideTip={hideTip} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ table.standings { width: 100%; border-collapse: collapse; }
|
|||||||
font-family: var(--font-mono); font-size: 9px; color: var(--ink-faint);
|
font-family: var(--font-mono); font-size: 9px; color: var(--ink-faint);
|
||||||
letter-spacing: 0.04em; padding: 5px 10px 0;
|
letter-spacing: 0.04em; padding: 5px 10px 0;
|
||||||
}
|
}
|
||||||
|
.tie-meta {
|
||||||
|
font-family: var(--font-mono); font-size: 8px; color: var(--ink-dim);
|
||||||
|
padding: 2px 10px 0;
|
||||||
|
}
|
||||||
.side {
|
.side {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 8px 10px; font-size: 13px;
|
padding: 8px 10px; font-size: 13px;
|
||||||
|
|||||||
51
lib/stadiums.ts
Normal file
51
lib/stadiums.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Statische Stadion-Daten für WM 2026.
|
||||||
|
export const STADIUMS: Record<string, { name: string; city: string }> = {
|
||||||
|
"1": { name: "Estadio Azteca", city: "Mexico City" },
|
||||||
|
"2": { name: "Estadio Akron", city: "Guadalajara" },
|
||||||
|
"3": { name: "Estadio BBVA", city: "Monterrey" },
|
||||||
|
"4": { name: "AT&T Stadium", city: "Dallas" },
|
||||||
|
"5": { name: "NRG Stadium", city: "Houston" },
|
||||||
|
"6": { name: "GEHA Field at Arrowhead Stadium", city: "Kansas City" },
|
||||||
|
"7": { name: "Mercedes-Benz Stadium", city: "Atlanta" },
|
||||||
|
"8": { name: "Hard Rock Stadium", city: "Miami" },
|
||||||
|
"9": { name: "Gillette Stadium", city: "Boston" },
|
||||||
|
"10": { name: "Lincoln Financial Field", city: "Philadelphia" },
|
||||||
|
"11": { name: "MetLife Stadium", city: "New York/New Jersey" },
|
||||||
|
"12": { name: "BMO Field", city: "Toronto" },
|
||||||
|
"13": { name: "BC Place", city: "Vancouver" },
|
||||||
|
"14": { name: "Lumen Field", city: "Seattle" },
|
||||||
|
"15": { name: "Levi's Stadium", city: "San Francisco Bay Area" },
|
||||||
|
"16": { name: "SoFi Stadium", city: "Los Angeles" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// K.o.-Spielnummer → ISO-8601 UTC-Anstoßzeit (FIFA-Spielplan, statisch).
|
||||||
|
export const MATCH_DATES: Record<number, string> = {
|
||||||
|
// R32 (28.6.–3.7.)
|
||||||
|
73: "2026-06-28T19:00:00Z", 74: "2026-06-29T21:00:00Z", 75: "2026-06-29T22:00:00Z",
|
||||||
|
76: "2026-06-29T22:00:00Z", 77: "2026-06-30T17:00:00Z", 78: "2026-06-30T21:00:00Z",
|
||||||
|
79: "2026-07-01T19:00:00Z", 80: "2026-07-01T21:00:00Z", 81: "2026-07-01T19:00:00Z",
|
||||||
|
82: "2026-07-02T19:00:00Z", 83: "2026-07-02T21:00:00Z", 84: "2026-07-02T19:00:00Z",
|
||||||
|
85: "2026-07-03T17:00:00Z", 86: "2026-07-03T21:00:00Z", 87: "2026-07-03T22:00:00Z",
|
||||||
|
88: "2026-07-03T22:00:00Z",
|
||||||
|
// R16 (6.7.–9.7.)
|
||||||
|
89: "2026-07-06T21:00:00Z", 90: "2026-07-06T22:00:00Z", 91: "2026-07-07T21:00:00Z",
|
||||||
|
92: "2026-07-07T19:00:00Z", 93: "2026-07-08T22:00:00Z", 94: "2026-07-08T19:00:00Z",
|
||||||
|
95: "2026-07-09T21:00:00Z", 96: "2026-07-09T19:00:00Z",
|
||||||
|
// QF (12.7.–13.7.)
|
||||||
|
97: "2026-07-12T21:00:00Z", 98: "2026-07-12T19:00:00Z", 99: "2026-07-13T21:00:00Z",
|
||||||
|
100: "2026-07-13T22:00:00Z",
|
||||||
|
// SF (16.7.–17.7.)
|
||||||
|
101: "2026-07-16T22:00:00Z", 102: "2026-07-17T21:00:00Z",
|
||||||
|
// 3RD (19.7.)
|
||||||
|
103: "2026-07-19T17:00:00Z",
|
||||||
|
// FINAL (20.7.)
|
||||||
|
104: "2026-07-20T17:00:00Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
// K.o.-Spielnummer → stadium_id (FIFA-Quelle, statisch).
|
||||||
|
export const MATCH_STADIUMS: Record<number, string> = {
|
||||||
|
73: "16", 74: "9", 75: "3", 76: "5", 77: "11", 78: "4", 79: "1", 80: "7",
|
||||||
|
81: "15", 82: "14", 83: "12", 84: "16", 85: "13", 86: "8", 87: "6", 88: "4",
|
||||||
|
89: "10", 90: "5", 91: "11", 92: "1", 93: "4", 94: "14", 95: "7", 96: "13",
|
||||||
|
97: "9", 98: "16", 99: "8", 100: "6", 101: "4", 102: "7", 103: "8", 104: "11",
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user