Files
wm-projekt/app/components/KoFixtures.tsx
2026-06-28 17:03:21 -05:00

224 lines
8.4 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 { useMemo, useState, useEffect, useRef } from "react";
import { Match, Team } from "@/lib/types";
import { STADIUMS, MATCH_STADIUMS } from "@/lib/stadiums";
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]);
const targetMatchId = useMemo(() => {
const live = koMatches.find(m => isLive(m));
if (live) return live.id;
const upcoming = koMatches.find(m => m.status !== "FINISHED");
return upcoming?.id ?? null;
}, [koMatches]);
const targetRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (mounted && targetRef.current) {
targetRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, [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",
};
const city = STADIUMS[MATCH_STADIUMS[m.matchNumber]]?.city;
return (
<div
key={m.id}
ref={m.id === targetMatchId ? targetRef : undefined}
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 || "—"}
{city ? ` · ${city}` : ""}
</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}&apos;
</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>
);
}