Files
wm-projekt/app/page.tsx
2026-06-24 09:52:03 -05:00

154 lines
5.5 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 { 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 &amp; Tabellen) · Polymarket Gamma API (Wahrscheinlichkeiten) ·
Annex-C-Zuordnung nach den FIFA-Wettbewerbsregeln WM 2026. Kein offizielles FIFA-Produkt.
</div>
</footer>
</>
);
}