flags
This commit is contained in:
@@ -3,15 +3,20 @@
|
||||
import { GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
|
||||
import { ThirdAssignment } from "@/lib/bracket";
|
||||
import { resolveBracket, ResolvedSide, ResolvedTie } from "@/lib/resolve-bracket";
|
||||
import Flag from "./Flag";
|
||||
|
||||
function Side({ s, prob }: { s: ResolvedSide; prob?: number | null }) {
|
||||
function Side({
|
||||
s, prob, teams,
|
||||
}: { s: ResolvedSide; prob?: number | null; teams: Team[] }) {
|
||||
const team = s.teamId ? teams.find((t) => t.id === s.teamId) : undefined;
|
||||
return (
|
||||
<div className={`side ${s.isWinner ? "win" : ""}`}>
|
||||
<div className={`side ${s.isWinner ? "win" : ""} ${s.provisional ? "prov" : ""}`}>
|
||||
<span className="nm">
|
||||
{s.teamId ? (
|
||||
<>
|
||||
<Flag team={team} size={18} />
|
||||
<span>{s.label}</span>
|
||||
{s.code && <span className="c">{s.code}</span>}
|
||||
{s.provisional && <span className="prov-mark" title="vorläufig – Gruppe/Zuordnung noch nicht fix">≈</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className="lbl">{s.label}</span>
|
||||
@@ -25,12 +30,12 @@ function Side({ s, prob }: { s: ResolvedSide; prob?: number | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Tie({ tie, isFinal }: { tie: ResolvedTie; isFinal?: boolean }) {
|
||||
function Tie({ tie, teams, isFinal }: { tie: ResolvedTie; teams: Team[]; isFinal?: boolean }) {
|
||||
return (
|
||||
<div className={`tie ${isFinal ? "final-tie" : ""}`}>
|
||||
<span className="tie-num">#{tie.matchNumber}</span>
|
||||
<Side s={tie.home} prob={tie.prob?.home} />
|
||||
<Side s={tie.away} prob={tie.prob?.away} />
|
||||
<span className="tie-num">Spiel {tie.matchNumber}</span>
|
||||
<Side s={tie.home} prob={tie.prob?.home} teams={teams} />
|
||||
<Side s={tie.away} prob={tie.prob?.away} teams={teams} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +46,9 @@ export default function Bracket({
|
||||
matches: Match[]; teams: Team[]; tables: GroupTable[];
|
||||
thirds: ThirdPlaceRow[]; assignment: ThirdAssignment | null; annexResolved: boolean;
|
||||
}) {
|
||||
const { r32, later } = resolveBracket(matches, teams, tables, thirds, assignment);
|
||||
const { r32, later } = resolveBracket(
|
||||
matches, teams, tables, thirds, assignment, annexResolved,
|
||||
);
|
||||
|
||||
const pick = (nums: number[]) => nums.map((n) => later[n]).filter(Boolean);
|
||||
const r16 = pick([89, 90, 91, 92, 93, 94, 95, 96]);
|
||||
@@ -66,35 +73,35 @@ export default function Bracket({
|
||||
<div className="round">
|
||||
<div className="round-label">Letzte 32</div>
|
||||
<div className="round-matches">
|
||||
{r32.map((t) => <Tie key={t.matchNumber} tie={t} />)}
|
||||
{r32.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="round">
|
||||
<div className="round-label">Achtelfinale</div>
|
||||
<div className="round-matches">
|
||||
{r16.map((t) => <Tie key={t.matchNumber} tie={t} />)}
|
||||
{r16.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="round">
|
||||
<div className="round-label">Viertelfinale</div>
|
||||
<div className="round-matches">
|
||||
{qf.map((t) => <Tie key={t.matchNumber} tie={t} />)}
|
||||
{qf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="round">
|
||||
<div className="round-label">Halbfinale</div>
|
||||
<div className="round-matches">
|
||||
{sf.map((t) => <Tie key={t.matchNumber} tie={t} />)}
|
||||
{sf.map((t) => <Tie key={t.matchNumber} tie={t} teams={teams} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="round">
|
||||
<div className="round-label">Finale</div>
|
||||
<div className="round-matches">
|
||||
{fin && <Tie tie={fin} isFinal />}
|
||||
{fin && <Tie tie={fin} teams={teams} isFinal />}
|
||||
{third && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div className="round-label" style={{ marginBottom: 8 }}>Spiel um Platz 3</div>
|
||||
<Tie tie={third} />
|
||||
<Tie tie={third} teams={teams} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -105,6 +112,8 @@ export default function Bracket({
|
||||
<div className="legend">
|
||||
<span><i style={{ background: "var(--turf)" }} />Sieger / weiter</span>
|
||||
<span><i style={{ background: "var(--gold)" }} />Finale</span>
|
||||
<span><i className="leg-fix" />fix qualifiziert</span>
|
||||
<span><i className="leg-prov" />vorläufig (≈, nach aktueller Tabelle)</span>
|
||||
<span><i style={{ background: "var(--ink-faint)" }} />Platzhalter offen</span>
|
||||
<span>%-Werte: Polymarket-Wahrscheinlichkeit (falls verfügbar)</span>
|
||||
</div>
|
||||
|
||||
109
app/components/Fixtures.tsx
Normal file
109
app/components/Fixtures.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { GROUP_IDS, GroupId, Match, Team } from "@/lib/types";
|
||||
import Flag from "./Flag";
|
||||
|
||||
function teamById(teams: Team[], id: string | null) {
|
||||
return id ? teams.find((t) => t.id === id) : undefined;
|
||||
}
|
||||
|
||||
function fmtDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString("de-DE", {
|
||||
weekday: "short", day: "2-digit", month: "2-digit",
|
||||
hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
function ScoreOrTime({ 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-time">{fmtDate(m.utcDate)}</span>;
|
||||
}
|
||||
|
||||
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 ${homeWin ? "win" : ""}`}>
|
||||
<Flag team={home} size={22} />
|
||||
<span className="fx-name">{home?.name ?? "—"}</span>
|
||||
</div>
|
||||
<ScoreOrTime m={m} />
|
||||
<div className={`fx-team away ${awayWin ? "win" : ""}`}>
|
||||
<span className="fx-name">{away?.name ?? "—"}</span>
|
||||
<Flag team={away} size={22} />
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
app/components/Flag.tsx
Normal file
29
app/components/Flag.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { Team } from "@/lib/types";
|
||||
|
||||
// Zeigt die Flagge/das Wappen eines Teams. Fällt auf ein neutrales Rund
|
||||
// zurück, wenn keine crest-URL vorliegt. SVG-Crests (football-data) skalieren
|
||||
// sauber; das Rendern als rundes Bild gibt den Flaggen-Look aus dem Screenshot.
|
||||
export default function Flag({
|
||||
team, size = 22,
|
||||
}: { team?: Team; size?: number }) {
|
||||
const style = { width: size, height: size } as const;
|
||||
if (team?.crest) {
|
||||
return (
|
||||
<img
|
||||
src={team.crest}
|
||||
alt={team.code || team.name}
|
||||
className="flag"
|
||||
style={style}
|
||||
loading="lazy"
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Fallback: Kreis mit Ländercode
|
||||
return (
|
||||
<span className="flag flag-fallback" style={style} aria-hidden>
|
||||
{team?.code?.slice(0, 2) || "··"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { GroupTable, Match, Team } from "@/lib/types";
|
||||
import { GroupId, GroupTable, Match, Team } from "@/lib/types";
|
||||
import Flag from "./Flag";
|
||||
|
||||
function teamById(teams: Team[], id: string) {
|
||||
return teams.find((t) => t.id === id);
|
||||
@@ -13,24 +14,31 @@ function liveMatchFor(group: string, matches: Match[]) {
|
||||
}
|
||||
|
||||
export default function Groups({
|
||||
tables, teams, matches,
|
||||
}: { tables: GroupTable[]; teams: Team[]; matches: Match[] }) {
|
||||
tables, teams, matches, onOpenGroup,
|
||||
}: {
|
||||
tables: GroupTable[]; teams: Team[]; matches: Match[];
|
||||
onOpenGroup: (g: GroupId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="group-grid">
|
||||
{tables.map((t) => {
|
||||
const live = liveMatchFor(t.group, matches);
|
||||
return (
|
||||
<div className="group-card" key={t.group}>
|
||||
<div className="group-head">
|
||||
<button
|
||||
className="group-head group-head-btn"
|
||||
onClick={() => onOpenGroup(t.group)}
|
||||
title={`Spiele der Gruppe ${t.group} ansehen`}
|
||||
>
|
||||
<span className="group-name">Gruppe {t.group}</span>
|
||||
<span className="group-tag">
|
||||
{live ? "● LIVE" : `${t.rows.reduce((a, r) => a + r.played, 0)} Spiele`}
|
||||
{live ? "● LIVE" : "Spiele ansehen →"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<table className="standings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="team">Team</th>
|
||||
<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>
|
||||
@@ -43,8 +51,8 @@ export default function Groups({
|
||||
<tr key={r.teamId}>
|
||||
<td className="team">
|
||||
<span className={`rankdot ${cls}`}>{r.rank}</span>
|
||||
<Flag team={team} size={20} />
|
||||
<span className="team-name">{team?.name ?? r.teamId}</span>
|
||||
{team?.code && <span className="team-code">{team.code}</span>}
|
||||
</td>
|
||||
<td>{r.played}</td>
|
||||
<td>{r.won}</td>
|
||||
|
||||
Reference in New Issue
Block a user