Files
wm-projekt/app/components/Fixtures.tsx
2026-06-22 22:11:04 -05:00

191 lines
6.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 { GROUP_IDS, GroupId, Match, Team } from "@/lib/types";
import { computeGroupTables } from "@/lib/standings";
import Flag from "./Flag";
function teamById(teams: Team[], id: string | null) {
return id ? teams.find((t) => t.id === id) : undefined;
}
// Kürzel der lokalen Browser-Zeitzone, z.B. "MEZ"/"GMT+1" einmal ermittelt.
const TZ_LABEL = (() => {
try {
const parts = new Intl.DateTimeFormat("de-DE", { timeZoneName: "short" })
.formatToParts(new Date());
return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
} catch {
return "";
}
})();
function fmtDate(iso: string): string {
const d = new Date(iso);
const s = d.toLocaleString("de-DE", {
weekday: "short", day: "2-digit", month: "2-digit",
hour: "2-digit", minute: "2-digit",
});
return TZ_LABEL ? `${s} ${TZ_LABEL}` : s;
}
function statusText(m: Match): { text: string; live: boolean } {
switch (m.status) {
case "LIVE":
case "IN_PLAY":
return { text: m.minute != null ? `${m.minute}'` : "läuft", live: true };
case "PAUSED":
return { text: "Halbzeit", live: true };
case "FINISHED":
return { text: "Beendet", live: false };
default:
return { text: fmtDate(m.utcDate), live: false };
}
}
// Mitte: Ergebnis (wenn vorhanden) oder ein schlichtes "" bei ungespielten
// Spielen. Datum/Uhrzeit steht bereits links im Status und wird hier NICHT
// wiederholt.
function ScoreCell({ m }: { m: Match }) {
const hasScore = m.homeScore != null && m.awayScore != null;
const st = statusText(m);
if (hasScore) {
return (
<span className={`fx-score ${st.live ? "score-live" : ""}`}>
{m.homeScore} : {m.awayScore}
</span>
);
}
return <span className="fx-vs"></span>;
}
function GroupStandings({
group, teams, matches,
}: { group: GroupId; teams: Team[]; matches: Match[] }) {
// Live-Tabelle: laufende Spiele werden mit Zwischenstand eingerechnet.
const table = computeGroupTables(teams, matches, true).find((t) => t.group === group);
if (!table) return null;
// Teams, die gerade ein laufendes Spiel haben -> Zeile markieren.
const liveTeamIds = new Set<string>();
let hasLive = false;
for (const m of matches) {
if (m.group !== group) continue;
if (m.status === "LIVE" || m.status === "IN_PLAY" || m.status === "PAUSED") {
hasLive = true;
if (m.homeTeamId) liveTeamIds.add(m.homeTeamId);
if (m.awayTeamId) liveTeamIds.add(m.awayTeamId);
}
}
return (
<div className="fx-standings">
<div className="fx-standings-title">
Tabelle Gruppe {group}
{hasLive && <span className="fx-standings-live"> LIVE</span>}
</div>
<table className="standings">
<thead>
<tr>
<th className="team">Mannschaft</th>
<th>Sp</th><th>S</th><th>U</th><th>N</th>
<th>Tore</th><th>±</th><th>Pkt</th>
</tr>
</thead>
<tbody>
{table.rows.map((r) => {
const team = teamById(teams, r.teamId);
const cls = r.rank <= 2 ? `q${r.rank}` : r.rank === 3 ? "q3" : "";
const isLive = liveTeamIds.has(r.teamId);
return (
<tr key={r.teamId} className={isLive ? "row-live" : ""}>
<td className="team">
<span className={`rankdot ${cls}`}>{r.rank}</span>
<Flag team={team} size={20} />
<span className="team-name">{team?.name ?? r.teamId}</span>
{isLive && <span className="row-live-dot" title="läuft gerade" />}
</td>
<td>{r.played}</td>
<td>{r.won}</td>
<td>{r.drawn}</td>
<td>{r.lost}</td>
<td>{r.goalsFor}:{r.goalsAgainst}</td>
<td>{r.goalDiff > 0 ? `+${r.goalDiff}` : r.goalDiff}</td>
<td className="pts">{r.points}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
export default function Fixtures({
group, teams, matches, onSelectGroup,
}: {
group: GroupId; teams: Team[]; matches: Match[];
onSelectGroup: (g: GroupId) => void;
}) {
const list = matches
.filter((m) => m.group === group)
.sort((a, b) => +new Date(a.utcDate) - +new Date(b.utcDate));
return (
<div>
<div className="grp-switch">
{GROUP_IDS.map((g) => (
<button
key={g}
className={`grp-chip ${g === group ? "active" : ""}`}
onClick={() => onSelectGroup(g)}
>
{g}
</button>
))}
</div>
{list.length === 0 ? (
<div className="notice">Für Gruppe {group} liegen noch keine Spiele im Feed vor.</div>
) : (
<div className="fixtures">
{list.map((m) => {
const home = teamById(teams, m.homeTeamId);
const away = teamById(teams, m.awayTeamId);
const st = statusText(m);
const homeWin = m.homeScore != null && m.awayScore != null && m.homeScore > m.awayScore;
const awayWin = m.homeScore != null && m.awayScore != null && m.awayScore > m.homeScore;
return (
<div className={`fx ${st.live ? "fx-live" : ""}`} key={m.id}>
<div className="fx-status">
{st.live && <span className="dot live" />}
<span>{st.text}</span>
</div>
<div className="fx-teams">
<div className={`fx-team home ${homeWin ? "win" : ""}`}>
<Flag team={home} size={22} />
<span className="fx-name">{home?.name ?? "—"}</span>
</div>
<ScoreCell m={m} />
<div className={`fx-team away ${awayWin ? "win" : ""}`}>
<Flag team={away} size={22} />
<span className="fx-name">{away?.name ?? "—"}</span>
</div>
</div>
<div className="fx-meta">
{m.venue && <span className="fx-venue">📍 {m.venue}</span>}
{m.attendance != null && (
<span className="fx-att">👥 {m.attendance.toLocaleString("de-DE")}</span>
)}
</div>
</div>
);
})}
</div>
)}
{list.length > 0 && (
<GroupStandings group={group} teams={teams} matches={matches} />
)}
</div>
);
}