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

198 lines
7.4 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback, useMemo, use } from "react";
import { GroupId, GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
import { ThirdAssignment } from "@/lib/bracket";
import { getDictionary, Locale, Dictionary } from "@/lib/i18n";
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";
import KoFixtures from "./components/KoFixtures";
interface ApiData {
updatedAt: string;
teams: Team[];
matches: Match[];
groupTables: GroupTable[];
groupTablesLive: GroupTable[];
thirdTable: ThirdPlaceRow[];
secureThirdTeams: string[];
annexAssignment: ThirdAssignment | null;
annexResolved: boolean;
}
type Tab = "groups" | "groupfixtures" | "kofixtures" | "thirds" | "bracket" | "sim";
export default function Home({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = use(params) as { locale: Locale };
const dict = useMemo(() => getDictionary(locale), [locale]);
const [data, setData] = useState<ApiData | null>(null);
const [error, setError] = useState<string | null>(null);
const [tab, setTab] = useState<Tab>("kofixtures");
// 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?locale=${locale}`, { 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");
}
}, [locale]);
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("groupfixtures");
}, []);
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">{dict.header.hostCountries}</span>
</div>
<span className="status-pill">
<span className={`dot ${anyLive ? "live" : data ? "on" : ""}`} />
{error
? dict.status.feedOffline
: data
? anyLive ? dict.status.live : dict.status.updated(new Date(data.updatedAt).toLocaleTimeString(locale === "en" ? "en-US" : "de-DE"))
: dict.status.loading}
</span>
<nav className="tabs masthead-tabs">
<button className={`tab ${tab === "kofixtures" ? "active" : ""}`} onClick={() => setTab("kofixtures")}>
{dict.nav.koFixtures}
</button>
<button className={`tab ${tab === "groups" ? "active" : ""}`} onClick={() => setTab("groups")}>
{dict.nav.groups}
</button>
<button
className={`tab ${tab === "groupfixtures" ? "active" : ""}`}
onClick={() => { if (!fixturesGroup) setFixturesGroup("A"); setTab("groupfixtures"); }}
>
{fixturesGroup ? dict.nav.groupFixtures(fixturesGroup) : dict.nav.groupFixturesNoGroup}
</button>
<button className={`tab ${tab === "thirds" ? "active" : ""}`} onClick={() => setTab("thirds")}>
{dict.nav.thirdPlace}
</button>
<button className={`tab ${tab === "bracket" ? "active" : ""}`} onClick={() => setTab("bracket")}>
{dict.nav.bracket}
</button>
<button className={`tab ${tab === "sim" ? "active" : ""}`} onClick={() => setTab("sim")}>
{dict.nav.simulation}
</button>
</nav>
</div>
</header>
<main className="wrap">
<section className="section">
{error && (
<div className="notice err">
{dict.error.feedUnreachable}
</div>
)}
{!data && !error && (
<div className="group-grid">
{Array.from({ length: 6 }).map((_, i) => <div className="skel" key={i} />)}
</div>
)}
{data && tab === "kofixtures" && (
<KoFixtures teams={data.teams} matches={data.matches} dict={dict} locale={locale} />
)}
{data && tab === "groups" && (
<Groups
tables={data.groupTablesLive} teams={data.teams} matches={data.matches}
onOpenGroup={openGroupFixtures} dict={dict} locale={locale}
/>
)}
{data && tab === "groupfixtures" && fixturesGroup && (
<Fixtures
group={fixturesGroup} teams={data.teams} matches={data.matches}
onSelectGroup={setFixturesGroup} dict={dict} locale={locale}
/>
)}
{data && tab === "thirds" && (
<ThirdPlace rows={data.thirdTable} teams={data.teams} secureTeams={data.secureThirdTeams} dict={dict} />
)}
{data && tab === "bracket" && (
<Bracket
matches={data.matches} teams={data.teams} tables={data.groupTablesLive}
thirds={data.thirdTable} assignment={data.annexAssignment}
annexResolved={data.annexResolved} dict={dict} locale={locale}
/>
)}
{data && tab === "sim" && (
<Simulation teams={data.teams} matches={data.matches} dict={dict} locale={locale} />
)}
</section>
</main>
<footer style={{
borderTop: "1px solid var(--line-soft)",
padding: "24px 0",
marginTop: 40,
}}>
<div className="wrap" style={{
display: "flex", flexDirection: "column", alignItems: "center", gap: 8,
}}>
<div style={{
display: "flex", alignItems: "center", gap: 12,
}}>
<span style={{
display: "inline-flex", alignItems: "center", justifyContent: "center",
width: 28, height: 28,
fontFamily: "var(--font-display)", fontSize: 13, fontWeight: 700,
color: "var(--ink-dim)", background: "var(--bg-card)",
border: "1px solid var(--line-soft)", borderRadius: "var(--radius-sm)",
}}>
AK
</span>
<a href="#" style={{
fontFamily: "var(--font-mono)", fontSize: 12,
color: "var(--ink-faint)", textDecoration: "none",
}}>
Andreas Knuth
</a>
</div>
<span style={{
fontFamily: "var(--font-mono)", fontSize: 10,
color: "var(--ink-faint)",
}}>
{dict.footer.dashboard}
</span>
<span style={{
fontFamily: "var(--font-mono)", fontSize: 9,
color: "var(--ink-faint)", opacity: 0.5, marginTop: 8,
}}>
{dict.footer.dataSources}
</span>
</div>
</footer>
</>
);
}