KO round
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots } from "@/lib/feeds";
|
import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots, fetchKOLiveData, attachKOLiveData } from "@/lib/feeds";
|
||||||
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
||||||
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
||||||
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
||||||
@@ -39,6 +39,14 @@ export async function GET() {
|
|||||||
matches = attachKOOdds(matches, teams, odds);
|
matches = attachKOOdds(matches, teams, odds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KO-Live-Daten (Tore, Minute) von worldcup26
|
||||||
|
try {
|
||||||
|
const koGames = await fetchKOLiveData();
|
||||||
|
matches = attachKOLiveData(matches, teams, koGames);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[worldcup26] KO-Live-Daten fehlgeschlagen:", err instanceof Error ? err.message : err);
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
||||||
|
|
||||||
const thirdTable = computeThirdPlaceTable(groupTablesLive);
|
const thirdTable = computeThirdPlaceTable(groupTablesLive);
|
||||||
|
|||||||
201
app/components/KoFixtures.tsx
Normal file
201
app/components/KoFixtures.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
import { Match, Team } from "@/lib/types";
|
||||||
|
import Flag from "./Flag";
|
||||||
|
|
||||||
|
function fmtShortDate(iso: string): string {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleDateString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(iso: string): string {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreDisplay(m: Match): string {
|
||||||
|
if (m.status === "SCHEDULED" || (m.homeScore == null && m.awayScore == null)) return "– : –";
|
||||||
|
return `${m.homeScore ?? 0} : ${m.awayScore ?? 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(m: Match): string {
|
||||||
|
switch (m.status) {
|
||||||
|
case "LIVE": case "IN_PLAY": return m.minute != null ? `${m.minute}'` : "Live";
|
||||||
|
case "PAUSED": return "Halbzeit";
|
||||||
|
case "FINISHED": return "Beendet";
|
||||||
|
default: return fmtTime(m.utcDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLive(m: Match): boolean {
|
||||||
|
return m.status === "LIVE" || m.status === "IN_PLAY" || m.status === "PAUSED";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KoFixtures({ teams, matches }: { teams: Team[]; matches: Match[] }) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => { setMounted(true); }, []);
|
||||||
|
|
||||||
|
const koMatches = useMemo(() => {
|
||||||
|
return matches
|
||||||
|
.filter((m) =>
|
||||||
|
m.group == null &&
|
||||||
|
m.homeTeamId != null && m.awayTeamId != null &&
|
||||||
|
// Nur Spiele mit bekannten Teams (keine TBD)
|
||||||
|
teams.some(t => t.id === m.homeTeamId) &&
|
||||||
|
teams.some(t => t.id === m.awayTeamId),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Live zuerst
|
||||||
|
const aLive = isLive(a) ? 1 : 0;
|
||||||
|
const bLive = isLive(b) ? 1 : 0;
|
||||||
|
if (aLive !== bLive) return bLive - aLive;
|
||||||
|
// Innerhalb Live/Upcoming/Finished: Datum aufsteigend
|
||||||
|
return +new Date(a.utcDate) - +new Date(b.utcDate);
|
||||||
|
});
|
||||||
|
}, [matches, teams]);
|
||||||
|
|
||||||
|
// Lokale Datums-Extraktion (vermeidet UTC-Tagesverschiebung bei US-Spielen)
|
||||||
|
function localDateKey(iso: string): string {
|
||||||
|
const d = new Date(iso);
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach Datum gruppieren (lokale Zeitzone nach Hydration, sonst UTC)
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const map = new Map<string, Match[]>();
|
||||||
|
for (const m of koMatches) {
|
||||||
|
const key = mounted ? localDateKey(m.utcDate) : m.utcDate.slice(0, 10);
|
||||||
|
if (!map.has(key)) map.set(key, []);
|
||||||
|
map.get(key)!.push(m);
|
||||||
|
}
|
||||||
|
return [...map.entries()];
|
||||||
|
}, [koMatches, mounted]);
|
||||||
|
|
||||||
|
if (koMatches.length === 0) {
|
||||||
|
return <div className="notice">Keine K.o.-Spiele mit bekannten Teams verfügbar.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{groups.map(([date, groupMatches]) => (
|
||||||
|
<div key={date} style={{ marginBottom: 24 }}>
|
||||||
|
<h3 style={{
|
||||||
|
fontFamily: "var(--font-display)", fontSize: 13,
|
||||||
|
textTransform: "uppercase", letterSpacing: "0.06em",
|
||||||
|
color: "var(--ink-dim)", margin: "0 0 10px",
|
||||||
|
}}>
|
||||||
|
{fmtShortDate(groupMatches[0].utcDate)}
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{groupMatches.map((m) => {
|
||||||
|
const home = m.homeTeamId ? teams.find(t => t.id === m.homeTeamId) : undefined;
|
||||||
|
const away = m.awayTeamId ? teams.find(t => t.id === m.awayTeamId) : undefined;
|
||||||
|
const live = isLive(m);
|
||||||
|
const stageMap: Record<string, string> = {
|
||||||
|
R32: "R32", R16: "Achtelfinale", QF: "Viertelfinale",
|
||||||
|
SF: "Halbfinale", "3RD": "Platz 3", FINAL: "Finale",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card)", border: `1px solid ${live ? "var(--turf)" : "var(--line-soft)"}`,
|
||||||
|
borderRadius: "var(--radius-sm)", padding: "12px 14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Kopfzeile */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||||
|
marginBottom: 8, fontSize: 11,
|
||||||
|
fontFamily: "var(--font-mono)", color: "var(--ink-faint)",
|
||||||
|
}}>
|
||||||
|
<span>{stageMap[m.stage] ?? m.stage} · Spiel {m.matchNumber || "—"}</span>
|
||||||
|
<span style={{ color: live ? "var(--turf)" : undefined, fontWeight: live ? 700 : undefined }}>
|
||||||
|
{statusLabel(m)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Teams + Score */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<TeamLabel team={home} />
|
||||||
|
<span style={{
|
||||||
|
fontFamily: "var(--font-mono)", fontSize: 22, fontWeight: 700,
|
||||||
|
color: live ? "var(--turf)" : "var(--ink)",
|
||||||
|
padding: "0 16px",
|
||||||
|
}}>
|
||||||
|
{scoreDisplay(m)}
|
||||||
|
</span>
|
||||||
|
<TeamLabel team={away} reverse />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Torabfolge */}
|
||||||
|
{m.goals && m.goals.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 8, padding: "8px 10px",
|
||||||
|
background: "var(--bg-raised)", borderRadius: "var(--radius-sm)",
|
||||||
|
fontSize: 12, fontFamily: "var(--font-mono)",
|
||||||
|
}}>
|
||||||
|
<div style={{ color: "var(--ink-faint)", fontSize: 10, marginBottom: 4 }}>
|
||||||
|
Tore
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||||
|
{m.goals.map((g, i) => (
|
||||||
|
<span key={i} style={{
|
||||||
|
display: "inline-flex", alignItems: "center", gap: 3,
|
||||||
|
color: g.team === "home" ? "var(--ink)" : "var(--ink)",
|
||||||
|
}}>
|
||||||
|
{g.team === "home" ? "⬆" : "⬇"}
|
||||||
|
<span style={{ color: "var(--turf)", fontWeight: 700 }}>
|
||||||
|
{g.scorer}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: "var(--ink-faint)", fontSize: 10 }}>
|
||||||
|
{g.minute}'
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Prob (falls vorhanden) */}
|
||||||
|
{m.prob && m.prob.home > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 6, fontSize: 10, color: "var(--ink-faint)",
|
||||||
|
fontFamily: "var(--font-mono)",
|
||||||
|
}}>
|
||||||
|
Polymarket: {Math.round(m.prob.home * 100)}% / {Math.round(m.prob.draw * 100)}% / {Math.round(m.prob.away * 100)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TeamLabel({ team, reverse }: { team?: Team; reverse?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 8,
|
||||||
|
flexDirection: reverse ? "row-reverse" : "row",
|
||||||
|
flex: 1, minWidth: 0,
|
||||||
|
}}>
|
||||||
|
<Flag team={team} size={20} />
|
||||||
|
<span style={{
|
||||||
|
fontWeight: 600, fontSize: 14,
|
||||||
|
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
|
||||||
|
maxWidth: 120,
|
||||||
|
}}>
|
||||||
|
{team?.localisedName ?? team?.name ?? "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/page.tsx
21
app/page.tsx
@@ -8,6 +8,7 @@ import ThirdPlace from "./components/ThirdPlace";
|
|||||||
import Bracket from "./components/Bracket";
|
import Bracket from "./components/Bracket";
|
||||||
import Fixtures from "./components/Fixtures";
|
import Fixtures from "./components/Fixtures";
|
||||||
import Simulation from "./components/Simulation";
|
import Simulation from "./components/Simulation";
|
||||||
|
import KoFixtures from "./components/KoFixtures";
|
||||||
|
|
||||||
interface ApiData {
|
interface ApiData {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -21,12 +22,12 @@ interface ApiData {
|
|||||||
annexResolved: boolean;
|
annexResolved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = "groups" | "fixtures" | "thirds" | "bracket" | "sim";
|
type Tab = "groups" | "groupfixtures" | "kofixtures" | "thirds" | "bracket" | "sim";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [data, setData] = useState<ApiData | null>(null);
|
const [data, setData] = useState<ApiData | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [tab, setTab] = useState<Tab>("groups");
|
const [tab, setTab] = useState<Tab>("kofixtures");
|
||||||
// Zuletzt gewählte Gruppe für den Spiele-Tab. null = noch keine gewählt.
|
// Zuletzt gewählte Gruppe für den Spiele-Tab. null = noch keine gewählt.
|
||||||
const [fixturesGroup, setFixturesGroup] = useState<GroupId | null>(null);
|
const [fixturesGroup, setFixturesGroup] = useState<GroupId | null>(null);
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ export default function Home() {
|
|||||||
// Klick auf einen Gruppen-Header: Gruppe merken und zum Spiele-Tab wechseln.
|
// Klick auf einen Gruppen-Header: Gruppe merken und zum Spiele-Tab wechseln.
|
||||||
const openGroupFixtures = useCallback((g: GroupId) => {
|
const openGroupFixtures = useCallback((g: GroupId) => {
|
||||||
setFixturesGroup(g);
|
setFixturesGroup(g);
|
||||||
setTab("fixtures");
|
setTab("groupfixtures");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const anyLive = data?.matches.some(
|
const anyLive = data?.matches.some(
|
||||||
@@ -77,14 +78,17 @@ export default function Home() {
|
|||||||
: "Lade Daten…"}
|
: "Lade Daten…"}
|
||||||
</span>
|
</span>
|
||||||
<nav className="tabs masthead-tabs">
|
<nav className="tabs masthead-tabs">
|
||||||
|
<button className={`tab ${tab === "kofixtures" ? "active" : ""}`} onClick={() => setTab("kofixtures")}>
|
||||||
|
K.O.-Spiele
|
||||||
|
</button>
|
||||||
<button className={`tab ${tab === "groups" ? "active" : ""}`} onClick={() => setTab("groups")}>
|
<button className={`tab ${tab === "groups" ? "active" : ""}`} onClick={() => setTab("groups")}>
|
||||||
Gruppen
|
Gruppen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab ${tab === "fixtures" ? "active" : ""}`}
|
className={`tab ${tab === "groupfixtures" ? "active" : ""}`}
|
||||||
onClick={() => { if (!fixturesGroup) setFixturesGroup("A"); setTab("fixtures"); }}
|
onClick={() => { if (!fixturesGroup) setFixturesGroup("A"); setTab("groupfixtures"); }}
|
||||||
>
|
>
|
||||||
{fixturesGroup ? `Spiele – Gruppe ${fixturesGroup}` : "Spiele"}
|
{fixturesGroup ? `Gruppenspiele – ${fixturesGroup}` : "Gruppenspiele"}
|
||||||
</button>
|
</button>
|
||||||
<button className={`tab ${tab === "thirds" ? "active" : ""}`} onClick={() => setTab("thirds")}>
|
<button className={`tab ${tab === "thirds" ? "active" : ""}`} onClick={() => setTab("thirds")}>
|
||||||
Drittplatzierte
|
Drittplatzierte
|
||||||
@@ -116,13 +120,16 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{data && tab === "kofixtures" && (
|
||||||
|
<KoFixtures teams={data.teams} matches={data.matches} />
|
||||||
|
)}
|
||||||
{data && tab === "groups" && (
|
{data && tab === "groups" && (
|
||||||
<Groups
|
<Groups
|
||||||
tables={data.groupTablesLive} teams={data.teams} matches={data.matches}
|
tables={data.groupTablesLive} teams={data.teams} matches={data.matches}
|
||||||
onOpenGroup={openGroupFixtures}
|
onOpenGroup={openGroupFixtures}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{data && tab === "fixtures" && fixturesGroup && (
|
{data && tab === "groupfixtures" && fixturesGroup && (
|
||||||
<Fixtures
|
<Fixtures
|
||||||
group={fixturesGroup} teams={data.teams} matches={data.matches}
|
group={fixturesGroup} teams={data.teams} matches={data.matches}
|
||||||
onSelectGroup={setFixturesGroup}
|
onSelectGroup={setFixturesGroup}
|
||||||
|
|||||||
80
lib/feeds.ts
80
lib/feeds.ts
@@ -568,6 +568,8 @@ interface Wc26Game {
|
|||||||
home_score?: string;
|
home_score?: string;
|
||||||
away_score?: string;
|
away_score?: string;
|
||||||
time_elapsed?: string;
|
time_elapsed?: string;
|
||||||
|
goals?: Array<{ name?: string; minute?: string; team?: string }>;
|
||||||
|
current_minute?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LiveScore {
|
interface LiveScore {
|
||||||
@@ -668,3 +670,81 @@ export function applyLiveScores(
|
|||||||
if (applied > 0) console.log("[worldcup26] auf matches angewandt:", applied);
|
if (applied > 0) console.log("[worldcup26] auf matches angewandt:", applied);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Holt KO-Live-Daten von worldcup26 (Tore, Minute) und hängt sie an die Feed-Matches.
|
||||||
|
export async function fetchKOLiveData(): Promise<Wc26Game[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${WC26_BASE}/get/games`, {
|
||||||
|
cache: "no-store",
|
||||||
|
headers: { "User-Agent": "wm2026-board/1.0" },
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`worldcup26 ${res.status}`);
|
||||||
|
const data = (await res.json()) as { games: Wc26Game[] };
|
||||||
|
return (data.games ?? []).filter(g => {
|
||||||
|
const grp = (g.group ?? "").toUpperCase();
|
||||||
|
return grp === "" || grp === "R32" || grp === "R16" || grp === "QF" || grp === "SF" || grp === "3RD" || grp === "FINAL" || grp === "FINALIST";
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hängt worldcup26-KO-Live-Daten an die Matches an (Tore, Minute, Scores).
|
||||||
|
export function attachKOLiveData(matches: Match[], teams: Team[], koGames: Wc26Game[]): Match[] {
|
||||||
|
if (koGames.length === 0) return matches;
|
||||||
|
|
||||||
|
const nameById = new Map<string, string>();
|
||||||
|
for (const t of teams) {
|
||||||
|
const nn = normName(t.name);
|
||||||
|
if (!nameById.has(nn)) nameById.set(nn, t.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.map((m) => {
|
||||||
|
if (m.group != null) return m; // nur K.o.-Spiele
|
||||||
|
|
||||||
|
const hId = m.homeTeamId;
|
||||||
|
const aId = m.awayTeamId;
|
||||||
|
const hName = hId ? teams.find(t => t.id === hId)?.name : null;
|
||||||
|
const aName = aId ? teams.find(t => t.id === aId)?.name : null;
|
||||||
|
|
||||||
|
// Finde worldcup26-Spiel über Teamnamen
|
||||||
|
const wm = koGames.find(g => {
|
||||||
|
if (!hName || !aName) return false;
|
||||||
|
const gh = normName(g.home_team_name_en ?? "");
|
||||||
|
const ga = normName(g.away_team_name_en ?? "");
|
||||||
|
return (gh === normName(hName) && ga === normName(aName)) ||
|
||||||
|
(gh === normName(aName) && ga === normName(hName));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wm) return m;
|
||||||
|
|
||||||
|
const result = { ...m };
|
||||||
|
|
||||||
|
// Live-Score + Status
|
||||||
|
if (wm.time_elapsed === "live" || wm.time_elapsed === "finished") {
|
||||||
|
result.status = wm.time_elapsed === "finished" ? "FINISHED" : "IN_PLAY";
|
||||||
|
const hs = parseScore(wm.home_score);
|
||||||
|
const as = parseScore(wm.away_score);
|
||||||
|
if (hs != null) result.homeScore = hs;
|
||||||
|
if (as != null) result.awayScore = as;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spielminute
|
||||||
|
if (wm.current_minute) {
|
||||||
|
const min = parseInt(wm.current_minute, 10);
|
||||||
|
if (!isNaN(min)) result.minute = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Torereignisse
|
||||||
|
if (wm.goals && wm.goals.length > 0) {
|
||||||
|
result.goals = wm.goals.map(g => ({
|
||||||
|
scorer: g.name ?? "?",
|
||||||
|
minute: parseInt(g.minute ?? "0", 10) || 0,
|
||||||
|
team: g.team?.toLowerCase() === "away" ? "away" as const : "home" as const,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,6 +21,12 @@ export interface Team {
|
|||||||
export type MatchStatus =
|
export type MatchStatus =
|
||||||
| "SCHEDULED" | "LIVE" | "IN_PLAY" | "PAUSED" | "FINISHED";
|
| "SCHEDULED" | "LIVE" | "IN_PLAY" | "PAUSED" | "FINISHED";
|
||||||
|
|
||||||
|
export interface GoalEvent {
|
||||||
|
scorer: string; // Torschützen-Name
|
||||||
|
minute: number; // Spielminute
|
||||||
|
team: "home" | "away";
|
||||||
|
}
|
||||||
|
|
||||||
export interface Match {
|
export interface Match {
|
||||||
id: string;
|
id: string;
|
||||||
group: GroupId | null; // null = K.o.-Spiel
|
group: GroupId | null; // null = K.o.-Spiel
|
||||||
@@ -37,6 +43,7 @@ export interface Match {
|
|||||||
prob?: { home: number; draw: number; away: number } | null;
|
prob?: { home: number; draw: number; away: number } | null;
|
||||||
venue?: string | null; // Austragungsort
|
venue?: string | null; // Austragungsort
|
||||||
attendance?: number | null; // Zuschauerzahl, falls verfügbar
|
attendance?: number | null; // Zuschauerzahl, falls verfügbar
|
||||||
|
goals?: GoalEvent[] | null; // Torereignisse von worldcup26.ir
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eine berechnete Tabellenzeile innerhalb einer Gruppe.
|
// Eine berechnete Tabellenzeile innerhalb einer Gruppe.
|
||||||
|
|||||||
Reference in New Issue
Block a user