154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import { GroupId, GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
|
||
import { ThirdAssignment } from "@/lib/bracket";
|
||
import Groups from "./components/Groups";
|
||
import ThirdPlace from "./components/ThirdPlace";
|
||
import Bracket from "./components/Bracket";
|
||
import Fixtures from "./components/Fixtures";
|
||
import Simulation from "./components/Simulation";
|
||
|
||
interface ApiData {
|
||
updatedAt: string;
|
||
teams: Team[];
|
||
matches: Match[];
|
||
groupTables: GroupTable[];
|
||
groupTablesLive: GroupTable[];
|
||
thirdTable: ThirdPlaceRow[];
|
||
annexAssignment: ThirdAssignment | null;
|
||
annexResolved: boolean;
|
||
}
|
||
|
||
type Tab = "groups" | "fixtures" | "thirds" | "bracket" | "sim";
|
||
|
||
export default function Home() {
|
||
const [data, setData] = useState<ApiData | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [tab, setTab] = useState<Tab>("groups");
|
||
// Zuletzt gewählte Gruppe für den Spiele-Tab. null = noch keine gewählt.
|
||
const [fixturesGroup, setFixturesGroup] = useState<GroupId | null>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
try {
|
||
const res = await fetch("/api/matches", { cache: "no-store" });
|
||
if (!res.ok) {
|
||
const body = await res.json().catch(() => ({}));
|
||
throw new Error(body.detail || `Fehler ${res.status}`);
|
||
}
|
||
setData(await res.json());
|
||
setError(null);
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : "Verbindung fehlgeschlagen");
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
const id = setInterval(load, 30_000); // alle 30 s aktualisieren
|
||
return () => clearInterval(id);
|
||
}, [load]);
|
||
|
||
// Klick auf einen Gruppen-Header: Gruppe merken und zum Spiele-Tab wechseln.
|
||
const openGroupFixtures = useCallback((g: GroupId) => {
|
||
setFixturesGroup(g);
|
||
setTab("fixtures");
|
||
}, []);
|
||
|
||
const anyLive = data?.matches.some(
|
||
(m) => m.status === "LIVE" || m.status === "IN_PLAY" || m.status === "PAUSED",
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<header className="masthead">
|
||
<div className="wrap masthead-inner">
|
||
<div className="brand">
|
||
<span className="brand-mark">WM <span className="accent">26</span></span>
|
||
<span className="brand-sub">USA · Kanada · Mexiko</span>
|
||
</div>
|
||
<span className="status-pill">
|
||
<span className={`dot ${anyLive ? "live" : data ? "on" : ""}`} />
|
||
{error
|
||
? "Feed offline"
|
||
: data
|
||
? anyLive ? "Live" : `Aktualisiert ${new Date(data.updatedAt).toLocaleTimeString("de-DE")}`
|
||
: "Lade Daten…"}
|
||
</span>
|
||
<nav className="tabs masthead-tabs">
|
||
<button className={`tab ${tab === "groups" ? "active" : ""}`} onClick={() => setTab("groups")}>
|
||
Gruppen
|
||
</button>
|
||
<button
|
||
className={`tab ${tab === "fixtures" ? "active" : ""}`}
|
||
onClick={() => { if (!fixturesGroup) setFixturesGroup("A"); setTab("fixtures"); }}
|
||
>
|
||
{fixturesGroup ? `Spiele – Gruppe ${fixturesGroup}` : "Spiele"}
|
||
</button>
|
||
<button className={`tab ${tab === "thirds" ? "active" : ""}`} onClick={() => setTab("thirds")}>
|
||
Drittplatzierte
|
||
</button>
|
||
<button className={`tab ${tab === "bracket" ? "active" : ""}`} onClick={() => setTab("bracket")}>
|
||
K.o.-Baum
|
||
</button>
|
||
<button className={`tab ${tab === "sim" ? "active" : ""}`} onClick={() => setTab("sim")}>
|
||
Simulation
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="wrap">
|
||
|
||
<section className="section">
|
||
{error && (
|
||
<div className="notice err">
|
||
Die Live-Feeds sind gerade nicht erreichbar: {error}.
|
||
Prüfe den <code>FOOTBALL_DATA_TOKEN</code> und die Netzwerkfreigabe des Servers.
|
||
Die Seite versucht es automatisch erneut.
|
||
</div>
|
||
)}
|
||
|
||
{!data && !error && (
|
||
<div className="group-grid">
|
||
{Array.from({ length: 6 }).map((_, i) => <div className="skel" key={i} />)}
|
||
</div>
|
||
)}
|
||
|
||
{data && tab === "groups" && (
|
||
<Groups
|
||
tables={data.groupTablesLive} teams={data.teams} matches={data.matches}
|
||
onOpenGroup={openGroupFixtures}
|
||
/>
|
||
)}
|
||
{data && tab === "fixtures" && fixturesGroup && (
|
||
<Fixtures
|
||
group={fixturesGroup} teams={data.teams} matches={data.matches}
|
||
onSelectGroup={setFixturesGroup}
|
||
/>
|
||
)}
|
||
{data && tab === "thirds" && (
|
||
<ThirdPlace rows={data.thirdTable} teams={data.teams} />
|
||
)}
|
||
{data && tab === "bracket" && (
|
||
<Bracket
|
||
matches={data.matches} teams={data.teams} tables={data.groupTablesLive}
|
||
thirds={data.thirdTable} assignment={data.annexAssignment}
|
||
annexResolved={data.annexResolved}
|
||
/>
|
||
)}
|
||
{data && tab === "sim" && (
|
||
<Simulation teams={data.teams} matches={data.matches} />
|
||
)}
|
||
</section>
|
||
</main>
|
||
|
||
<footer className="foot">
|
||
<div className="wrap">
|
||
Daten: football-data.org (Spiele & Tabellen) · Polymarket Gamma API (Wahrscheinlichkeiten) ·
|
||
Annex-C-Zuordnung nach den FIFA-Wettbewerbsregeln WM 2026. Kein offizielles FIFA-Produkt.
|
||
</div>
|
||
</footer>
|
||
</>
|
||
);
|
||
} |