diff --git a/app/[locale]/components/Fixtures.tsx b/app/[locale]/components/Fixtures.tsx index 92cccc7..4a49b7d 100644 --- a/app/[locale]/components/Fixtures.tsx +++ b/app/[locale]/components/Fixtures.tsx @@ -176,10 +176,13 @@ export default function Fixtures({
- {m.venue && 📍 {m.venue}} - {m.attendance != null && ( - 👥 {m.attendance.toLocaleString(intlLocale)} - )} + {(() => { + const stadium = [m.stadiumName, m.stadiumCity].filter(Boolean).join(", "); + const crowd = m.attendance != null ? m.attendance.toLocaleString(intlLocale) : ""; + const sep = stadium && crowd ? " · " : ""; + const text = stadium + sep + crowd; + return text ? {text} : null; + })()}
); diff --git a/app/[locale]/components/Flag.tsx b/app/[locale]/components/Flag.tsx index d365a9d..4bddde8 100644 --- a/app/[locale]/components/Flag.tsx +++ b/app/[locale]/components/Flag.tsx @@ -1,32 +1,27 @@ "use client"; -import { useState } from "react"; import { Team } from "@/lib/types"; +import { TLA_TO_ISO2 } from "@/lib/flags"; -// Zeigt die Flagge/das Wappen eines Teams. Fällt auf ein neutrales Rund -// zurück, wenn keine crest-URL vorliegt oder das Bild nicht geladen werden kann. +// Zeigt die Flagge eines Teams aus lokalem circle-flags-Bestand. +// Fallback: kein Rendering bei fehlendem Team/Code (kein Broken-Image). export default function Flag({ team, size = 22, }: { team?: Team; size?: number }) { - const [imgFailed, setImgFailed] = useState(false); const style = { width: size, height: size } as const; - if (team?.crest && !imgFailed) { + const iso2 = team?.code ? TLA_TO_ISO2[team.code.toUpperCase()] : undefined; + if (iso2) { return ( {team.code setImgFailed(true)} /> ); } - // Fallback: Kreis mit Ländercode - return ( - - {team?.code?.slice(0, 2) || "··"} - - ); + // Kein Team / kein Code: nichts rendern (kein Broken-Image) + return null; } diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts index 0472c51..4202647 100644 --- a/app/api/matches/route.ts +++ b/app/api/matches/route.ts @@ -1,18 +1,17 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots, attachFifaGoals } from "@/lib/feeds"; +import { fetchMatchesAndTeamsFifa, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, attachFifaGoals } from "@/lib/feeds"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket"; import { securelyQualifiedThirdTeams } from "@/lib/third-place-security"; -// Diese Route wird vom Frontend gepollt. Sie ist der einzige Ort, der die -// Upstream-Feeds berührt — gecacht, damit Rate Limits eingehalten werden. +// Diese Route wird vom Frontend gepollt. FIFA-API als Primärquelle. export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { const locale = request.nextUrl.searchParams.get("locale") || "de"; try { - const { matches: rawMatches, teams } = await fetchMatchesAndTeams(); + const { matches: rawMatches, teams } = await fetchMatchesAndTeamsFifa(locale); // Odds sind optional: fällt der Polymarket-Call aus, liefern wir trotzdem. let matches = rawMatches; @@ -24,13 +23,11 @@ export async function GET(request: NextRequest) { console.error("[polymarket] fetchOdds fehlgeschlagen:", err); } - // FIFA-Live-Scores (additiv, Fallback auf football-data) - assignKONumbersBySlots(matches, teams); - if (odds) { matches = attachKOOdds(matches, teams, odds); } + // FIFA-Live-Scores als häufiger gecachtes Overlay try { const fifaData = await fetchFifaScores(locale); matches = applyFifaScores(matches, teams, fifaData); diff --git a/lib/feeds.ts b/lib/feeds.ts index 76dc665..d0596db 100644 --- a/lib/feeds.ts +++ b/lib/feeds.ts @@ -1,8 +1,9 @@ import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types"; import { venueFor } from "./venues"; -import { localisedTeamName } from "./team-mappings"; +import { localisedTeamName, TEAM_LOCALIZATION } from "./team-mappings"; import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket"; import { computeGroupTables, computeThirdPlaceTable } from "./standings"; +import { FIFA_GROUP_MAP, FIFA_STAGE_MAP } from "./fifa-constants"; // ---------------------------------------------------------------------------- // Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser- @@ -609,6 +610,122 @@ interface FifaScores { idStage: string | null; } +// ---------------------------------------------------------------------------- +// FIFA-API Calendar-Endpoint: Spiele + Teams parallel zur football-data-Quelle +// ---------------------------------------------------------------------------- + +interface FifaCalendarTeamBlock { + IdTeam: string; + Abbreviation: string; + TeamName: Array<{ Locale: string; Description: string }>; +} + +interface FifaCalendarMatch { + IdMatch: string; + IdStage: string; + IdGroup: string | null; + MatchNumber: number; + Date: string; + Home: FifaCalendarTeamBlock | null; + Away: FifaCalendarTeamBlock | null; + HomeTeamScore: number | null; + AwayTeamScore: number | null; + HomeTeamPenaltyScore: number | null; + AwayTeamPenaltyScore: number | null; + MatchStatus: number; + MatchTime: string | null; + ResultType: number | null; + Winner?: string | null; + Attendance: string | null; + Stadium?: { + Name: Array<{ Locale: string; Description: string }>; + CityName: Array<{ Locale: string; Description: string }>; + } | null; +} + +// Holt alle Spiele + Teams von der FIFA-API (parallele Quelle, noch nicht aktiv). +export async function fetchMatchesAndTeamsFifa(locale: string = "de"): Promise<{ matches: Match[]; teams: Team[] }> { + return cached(`fifa:matches:${locale}`, 60_000, async () => { + const lang = locale === "en" ? "en" : "de"; + const url = `${FIFA_BASE}/calendar/matches?language=${lang}&count=500&idSeason=${FIFA_SEASON}`; + const res = await fetch(url, { + cache: "no-store", + headers: { "User-Agent": "wm2026-board/1.0" }, + }); + if (!res.ok) throw new Error(`fifa-calendar ${res.status}`); + const data = (await res.json()) as { Results: FifaCalendarMatch[] }; + + const teamMap = new Map(); + + function teamFromBlock(tb: FifaCalendarTeamBlock, idGroup: string | null): Team | null { + const id = tb.IdTeam; + if (teamMap.has(id)) return teamMap.get(id)!; + const code = fifaCodeToAppCode(tb.Abbreviation); + const group = idGroup ? FIFA_GROUP_MAP[idGroup] : null; + if (!group) return null; + const loc = TEAM_LOCALIZATION[code.toUpperCase()]; + const name = tb.TeamName?.[0]?.Description ?? ""; + const t: Team = { + id, + name, + code, + group, + localisedName: loc ? loc[lang === "en" ? "en" : "de"] : name, + localisedNames: loc ? { de: loc.de, en: loc.en } : { de: name, en: name }, + }; + teamMap.set(id, t); + return t; + } + + function mapFifaStatus(ms: number): MatchStatus { + switch (ms) { + case 0: return "FINISHED"; + case 1: return "SCHEDULED"; + case 10: return "POSTPONED"; + default: return "LIVE"; + } + } + + const matches: Match[] = (data.Results ?? []).map((fm) => { + const group = fm.IdGroup ? FIFA_GROUP_MAP[fm.IdGroup] ?? null : null; + let homeTeam: Team | null = null; + let awayTeam: Team | null = null; + if (fm.Home) homeTeam = teamFromBlock(fm.Home, fm.IdGroup); + if (fm.Away) awayTeam = teamFromBlock(fm.Away, fm.IdGroup); + const attendance = fm.Attendance ? parseInt(fm.Attendance, 10) || null : null; + const minute = fm.MatchTime ? parseInt(fm.MatchTime, 10) || null : null; + const pref = lang === "en" ? "en-GB" : "de-DE"; + const stadiumName = fm.Stadium?.Name?.find(n => n.Locale === pref)?.Description + ?? fm.Stadium?.Name?.[0]?.Description ?? null; + const stadiumCity = fm.Stadium?.CityName?.find(n => n.Locale === pref)?.Description + ?? fm.Stadium?.CityName?.[0]?.Description ?? null; + return { + id: fm.IdMatch, + group, + stage: FIFA_STAGE_MAP[fm.IdStage] ?? "GROUP", + matchNumber: fm.MatchNumber, + utcDate: fm.Date, + status: mapFifaStatus(fm.MatchStatus), + minute, + homeTeamId: fm.Home?.IdTeam ?? null, + awayTeamId: fm.Away?.IdTeam ?? null, + homeScore: fm.HomeTeamScore, + awayScore: fm.AwayTeamScore, + homePenalty: fm.HomeTeamPenaltyScore, + awayPenalty: fm.AwayTeamPenaltyScore, + winnerTeamId: fm.Winner ?? null, + venue: null, + stadiumName, + stadiumCity, + attendance, + }; + }); + + const teams = [...teamMap.values()]; + return { matches, teams }; + }); +} + // Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück. export async function fetchFifaScores(locale: string = "de"): Promise<{ scores: Map; @@ -660,17 +777,13 @@ export async function fetchFifaScores(locale: string = "de"): Promise<{ } // Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel). +// Team-IDs sind jetzt FIFA-IDs → winnerTeamId kann direkt gesetzt werden. export function applyFifaScores( matches: Match[], teams: Team[], fifaData: { scores: Map; fifaIdToAppCode: Map }, ): Match[] { - const { scores: fifaMap, fifaIdToAppCode } = fifaData; + const { scores: fifaMap } = fifaData; if (fifaMap.size === 0) return matches; - // Baue App-Code → App-Team-ID Map - const appCodeToId = new Map(); - for (const t of teams) { - if (t.code) appCodeToId.set(t.code.toLowerCase(), t.id); - } let applied = 0; const result = matches.map((m) => { const fs = fifaMap.get(m.matchNumber); @@ -680,20 +793,7 @@ export function applyFifaScores( if (fs.awayScore != null) r.awayScore = fs.awayScore; if (fs.homePenalty != null) r.homePenalty = fs.homePenalty; if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty; - // FIFA-ID → App-Team-ID auflösen - if (fs.winnerTeamId) { - const appCode = fifaIdToAppCode.get(fs.winnerTeamId); - if (appCode) { - const appTeamId = appCodeToId.get(appCode.toLowerCase()); - if (appTeamId) { - r.winnerTeamId = appTeamId; - } else { - console.warn("[fifa] Winner-Team-Code nicht in App-Teams:", appCode, "| matchNumber:", m.matchNumber); - } - } else { - console.warn("[fifa] FIFA-Winner-ID nicht in Team-Map:", fs.winnerTeamId, "| matchNumber:", m.matchNumber); - } - } + if (fs.winnerTeamId) r.winnerTeamId = fs.winnerTeamId; if (fs.matchTime) { const min = parseInt(fs.matchTime, 10); if (!isNaN(min)) r.minute = min; diff --git a/lib/fifa-constants.ts b/lib/fifa-constants.ts new file mode 100644 index 0000000..5d87c4d --- /dev/null +++ b/lib/fifa-constants.ts @@ -0,0 +1,14 @@ +import { GroupId, Match } from "./types"; + +// FIFA IdGroup → App-GroupId (verifiziert, fortlaufend 289275-289286) +export const FIFA_GROUP_MAP: Record = { + "289275": "A", "289276": "B", "289277": "C", "289278": "D", + "289279": "E", "289280": "F", "289281": "G", "289282": "H", + "289283": "I", "289284": "J", "289285": "K", "289286": "L", +}; + +// FIFA IdStage → App-Stage (verifiziert) +export const FIFA_STAGE_MAP: Record = { + "289273": "GROUP", "289287": "R32", "289288": "R16", + "289289": "QF", "289290": "SF", "289291": "3RD", "289292": "FINAL", +}; diff --git a/lib/flags.ts b/lib/flags.ts new file mode 100644 index 0000000..0e61cb3 --- /dev/null +++ b/lib/flags.ts @@ -0,0 +1,52 @@ +// TLA (3-Buchstaben, wie in TEAM_LOCALIZATION) → circle-flags ISO-2-Dateiname. +// Vollständig für alle Teams aus TEAM_LOCALIZATION. +export const TLA_TO_ISO2: Record = { + ALG: "dz", + ARG: "ar", + AUS: "au", + AUT: "at", + BEL: "be", + BIH: "ba", + BRA: "br", + CAN: "ca", + CHE: "ch", + CIV: "ci", + COD: "cd", + COL: "co", + CPV: "cv", + CZE: "cz", + ECU: "ec", + EGY: "eg", + ENG: "gb-eng", + ESP: "es", + FRA: "fr", + GER: "de", + GHA: "gh", + HAI: "ht", + HRV: "hr", + IRN: "ir", + IRQ: "iq", + JOR: "jo", + JPN: "jp", + KOR: "kr", + KSA: "sa", + MAR: "ma", + MEX: "mx", + NED: "nl", + NOR: "no", + NZL: "nz", + PAN: "pa", + PAR: "py", + PRT: "pt", + QAT: "qa", + RSA: "za", + SCO: "gb-sct", + SEN: "sn", + SWE: "se", + TUN: "tn", + TUR: "tr", + URU: "uy", + USA: "us", + UZB: "uz", + CUW: "cw", +}; diff --git a/lib/types.ts b/lib/types.ts index 13854a0..82612a5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -51,8 +51,10 @@ export interface Match { winnerTeamId?: string | null; // FIFA-Winner (auch bei Elfmeterschießen) // Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden prob?: { home: number; draw: number; away: number } | null; - venue?: string | null; // Austragungsort - attendance?: number | null; // Zuschauerzahl, falls verfügbar + venue?: string | null; // Austragungsort (alt: football-data) + stadiumName?: string | null; // FIFA-Stadionname + stadiumCity?: string | null; // FIFA-Stadt + attendance?: number | null; // Zuschauerzahl goals?: GoalEvent[] | null; // Torereignisse } diff --git a/public/flags/9460.svg b/public/flags/9460.svg new file mode 100644 index 0000000..f2c9853 --- /dev/null +++ b/public/flags/9460.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/flags/ar.svg b/public/flags/ar.svg new file mode 100644 index 0000000..ce73165 --- /dev/null +++ b/public/flags/ar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/at.svg b/public/flags/at.svg new file mode 100644 index 0000000..73a87c8 --- /dev/null +++ b/public/flags/at.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/au.svg b/public/flags/au.svg new file mode 100644 index 0000000..bccaab3 --- /dev/null +++ b/public/flags/au.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ba.svg b/public/flags/ba.svg new file mode 100644 index 0000000..62babc7 --- /dev/null +++ b/public/flags/ba.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/be.svg b/public/flags/be.svg new file mode 100644 index 0000000..3825681 --- /dev/null +++ b/public/flags/be.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/br.svg b/public/flags/br.svg new file mode 100644 index 0000000..44aacbd --- /dev/null +++ b/public/flags/br.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ca.svg b/public/flags/ca.svg new file mode 100644 index 0000000..3348f9e --- /dev/null +++ b/public/flags/ca.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/cd.svg b/public/flags/cd.svg new file mode 100644 index 0000000..17ef2e2 --- /dev/null +++ b/public/flags/cd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ch.svg b/public/flags/ch.svg new file mode 100644 index 0000000..3943b1f --- /dev/null +++ b/public/flags/ch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ci.svg b/public/flags/ci.svg new file mode 100644 index 0000000..1d21f10 --- /dev/null +++ b/public/flags/ci.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/co.svg b/public/flags/co.svg new file mode 100644 index 0000000..ac562e3 --- /dev/null +++ b/public/flags/co.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/cv.svg b/public/flags/cv.svg new file mode 100644 index 0000000..a49dc54 --- /dev/null +++ b/public/flags/cv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/cw.svg b/public/flags/cw.svg new file mode 100644 index 0000000..5e8772d --- /dev/null +++ b/public/flags/cw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/cz.svg b/public/flags/cz.svg new file mode 100644 index 0000000..70668af --- /dev/null +++ b/public/flags/cz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/de.svg b/public/flags/de.svg new file mode 100644 index 0000000..efde341 --- /dev/null +++ b/public/flags/de.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/dz.svg b/public/flags/dz.svg new file mode 100644 index 0000000..4916175 --- /dev/null +++ b/public/flags/dz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ec.svg b/public/flags/ec.svg new file mode 100644 index 0000000..c0a0b22 --- /dev/null +++ b/public/flags/ec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/eg.svg b/public/flags/eg.svg new file mode 100644 index 0000000..7c00ce5 --- /dev/null +++ b/public/flags/eg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/es.svg b/public/flags/es.svg new file mode 100644 index 0000000..55d3ab5 --- /dev/null +++ b/public/flags/es.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/fr.svg b/public/flags/fr.svg new file mode 100644 index 0000000..0fe5619 --- /dev/null +++ b/public/flags/fr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/gb-eng.svg b/public/flags/gb-eng.svg new file mode 100644 index 0000000..3fdf8c5 --- /dev/null +++ b/public/flags/gb-eng.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/gb-sct.svg b/public/flags/gb-sct.svg new file mode 100644 index 0000000..0ccc475 --- /dev/null +++ b/public/flags/gb-sct.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/gh.svg b/public/flags/gh.svg new file mode 100644 index 0000000..fed2c37 --- /dev/null +++ b/public/flags/gh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/hr.svg b/public/flags/hr.svg new file mode 100644 index 0000000..5ddd718 --- /dev/null +++ b/public/flags/hr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ht.svg b/public/flags/ht.svg new file mode 100644 index 0000000..5423c4e --- /dev/null +++ b/public/flags/ht.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/iq.svg b/public/flags/iq.svg new file mode 100644 index 0000000..63ee33e --- /dev/null +++ b/public/flags/iq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ir.svg b/public/flags/ir.svg new file mode 100644 index 0000000..883fe5c --- /dev/null +++ b/public/flags/ir.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/jo.svg b/public/flags/jo.svg new file mode 100644 index 0000000..e725de0 --- /dev/null +++ b/public/flags/jo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/jp.svg b/public/flags/jp.svg new file mode 100644 index 0000000..31f064f --- /dev/null +++ b/public/flags/jp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/kr.svg b/public/flags/kr.svg new file mode 100644 index 0000000..1056107 --- /dev/null +++ b/public/flags/kr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ma.svg b/public/flags/ma.svg new file mode 100644 index 0000000..30cd031 --- /dev/null +++ b/public/flags/ma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/mx.svg b/public/flags/mx.svg new file mode 100644 index 0000000..2671a86 --- /dev/null +++ b/public/flags/mx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/nl.svg b/public/flags/nl.svg new file mode 100644 index 0000000..25b6c91 --- /dev/null +++ b/public/flags/nl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/no.svg b/public/flags/no.svg new file mode 100644 index 0000000..bc702a3 --- /dev/null +++ b/public/flags/no.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/nz.svg b/public/flags/nz.svg new file mode 100644 index 0000000..688bb9d --- /dev/null +++ b/public/flags/nz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/pa.svg b/public/flags/pa.svg new file mode 100644 index 0000000..c29cedd --- /dev/null +++ b/public/flags/pa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/pt.svg b/public/flags/pt.svg new file mode 100644 index 0000000..32ce13f --- /dev/null +++ b/public/flags/pt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/py.svg b/public/flags/py.svg new file mode 100644 index 0000000..537af58 --- /dev/null +++ b/public/flags/py.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/qa.svg b/public/flags/qa.svg new file mode 100644 index 0000000..7ccf5bd --- /dev/null +++ b/public/flags/qa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/sa.svg b/public/flags/sa.svg new file mode 100644 index 0000000..87c4d19 --- /dev/null +++ b/public/flags/sa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/se.svg b/public/flags/se.svg new file mode 100644 index 0000000..c8520bc --- /dev/null +++ b/public/flags/se.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/sn.svg b/public/flags/sn.svg new file mode 100644 index 0000000..f634dca --- /dev/null +++ b/public/flags/sn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/tn.svg b/public/flags/tn.svg new file mode 100644 index 0000000..56d5f3a --- /dev/null +++ b/public/flags/tn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/tr.svg b/public/flags/tr.svg new file mode 100644 index 0000000..6d22c9b --- /dev/null +++ b/public/flags/tr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/us.svg b/public/flags/us.svg new file mode 100644 index 0000000..e3560c6 --- /dev/null +++ b/public/flags/us.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/uy.svg b/public/flags/uy.svg new file mode 100644 index 0000000..93a40d3 --- /dev/null +++ b/public/flags/uy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/uz.svg b/public/flags/uz.svg new file mode 100644 index 0000000..5607ade --- /dev/null +++ b/public/flags/uz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/za.svg b/public/flags/za.svg new file mode 100644 index 0000000..4bb7bd5 --- /dev/null +++ b/public/flags/za.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/download-flags.ts b/scripts/download-flags.ts new file mode 100644 index 0000000..f7b7989 --- /dev/null +++ b/scripts/download-flags.ts @@ -0,0 +1,50 @@ +// download-flags.ts — Lädt circle-flags SVGs für alle Teams aus TLA_TO_ISO2. +// Ausführung: npx tsx scripts/download-flags.ts + +import { TLA_TO_ISO2 } from "../lib/flags"; +import * as fs from "fs"; +import * as path from "path"; + +const BASE = "https://hatscripts.github.io/circle-flags/flags"; +const OUT = path.join(__dirname, "..", "public", "flags"); + +async function main() { + const codes = Object.values(TLA_TO_ISO2); + const unique = [...new Set(codes)]; + console.log(`Lade ${unique.length} Flaggen (${codes.length} Team-Codes)...`); + + if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true }); + + let ok = 0; + let failed = 0; + for (const iso2 of unique) { + const url = `${BASE}/${iso2}.svg`; + try { + const res = await fetch(url, { + headers: { "User-Agent": "circle-flags-download/1.0" }, + }); + if (!res.ok) { + console.warn(` ❌ ${iso2}: HTTP ${res.status}`); + failed++; + continue; + } + const svg = await res.text(); + // Prüfen: keine leere/Fehler-SVG (<10 Bytes = kaputt) + if (svg.trim().length < 10) { + console.warn(` ❌ ${iso2}: leere SVG (${svg.length} Bytes)`); + failed++; + continue; + } + fs.writeFileSync(path.join(OUT, `${iso2}.svg`), svg); + ok++; + } catch (err) { + console.warn(` ❌ ${iso2}: ${err instanceof Error ? err.message : err}`); + failed++; + } + } + + console.log(`\n✅ ${ok} OK, ❌ ${failed} fehlgeschlagen`); + if (failed > 0) process.exit(1); +} + +main(); diff --git a/scripts/fifa-eval.ts b/scripts/fifa-eval.ts new file mode 100644 index 0000000..8ff3467 --- /dev/null +++ b/scripts/fifa-eval.ts @@ -0,0 +1,119 @@ +// fifa-eval.ts — Analyse-Skript: IdGroup, MatchNumber, Attendance, MatchStatus +// Ausführung: npx ts-node --compiler-options '{"module":"commonjs","target":"es2020"}' scripts/fifa-eval.ts +// Oder mit Deno, Bun: bun run scripts/fifa-eval.ts + +const FIFA_BASE = "https://api.fifa.com/api/v3"; +const FIFA_SEASON = "285023"; + +interface RawMatch { + MatchNumber: number; + MatchStatus: number; + IdGroup: string | null; + IdStage: string; + StageName: Array<{ Locale: string; Description: string }>; + GroupName?: Array<{ Locale: string; Description: string }>; + Home: { IdTeam: string; Abbreviation: string; TeamName?: Array<{ Locale: string; Description: string }> } | null; + Away: { IdTeam: string; Abbreviation: string; TeamName?: Array<{ Locale: string; Description: string }> } | null; + HomeTeamScore: number | null; + AwayTeamScore: number | null; + Attendance: string | null; + Date: string; + LocalDate: string; +} + +async function main() { + console.log("📡 Rufe FIFA Calendar-Endpoint ab..."); + const res = await fetch( + `${FIFA_BASE}/calendar/matches?language=de&count=500&idSeason=${FIFA_SEASON}`, + { headers: { "User-Agent": "fifa-eval/1.0" } }, + ); + if (!res.ok) { console.error("Fehler:", res.status, res.statusText); process.exit(1); } + const data = (await res.json()) as { Results: RawMatch[] }; + const matches = data.Results ?? []; + console.log(`✅ ${matches.length} Matches geladen\n`); + + // ── A) IdGroup → GroupName Mapping ── + console.log("═══ A) IdGroup → Gruppe (A-L) ═══"); + const groupMap = new Map(); + for (const m of matches) { + if (!m.IdGroup || !m.GroupName) continue; + const desc = m.GroupName.find(g => g.Locale === "de-DE")?.Description + ?? m.GroupName[0]?.Description + ?? `ID:${m.IdGroup}`; + if (!groupMap.has(m.IdGroup)) groupMap.set(m.IdGroup, desc); + } + console.log(`Anzahl distinct IdGroup-Werte: ${groupMap.size}`); + const sorted = [...groupMap.entries()].sort((a, b) => parseInt(a[0]) - parseInt(b[0])); + for (const [id, name] of sorted) { + console.log(` ${id} → "${name}"`); + } + // Als kopierbare Konstante + console.log("\n// Kopierbare Konstante:"); + console.log("const FIFA_IDGROUP_TO_GROUP: Record = {"); + for (const [id, name] of sorted) { + const letter = name.replace(/^Gruppe\s+/, ""); + console.log(` "${id}": "${letter}", // ${name}`); + } + console.log("};"); + + // ── B) MatchNumber-Konsistenz ── + console.log("\n═══ B) MatchNumber-Konsistenz ═══"); + const groupMatches = matches.filter(m => m.IdGroup != null).sort((a, b) => a.MatchNumber - b.MatchNumber); + const koMatches = matches.filter(m => m.IdGroup == null).sort((a, b) => a.MatchNumber - b.MatchNumber); + console.log(`Gruppenspiele: ${groupMatches.length} (MatchNumber ${groupMatches[0]?.MatchNumber}–${groupMatches[groupMatches.length-1]?.MatchNumber})`); + console.log(`K.o.-Spiele: ${koMatches.length} (MatchNumber ${koMatches[0]?.MatchNumber}–${koMatches[koMatches.length-1]?.MatchNumber})`); + + // Stichproben für K.o.-Slots + const checkNums = [73, 74, 76, 89, 104]; + for (const n of checkNums) { + const m = matches.find(x => x.MatchNumber === n); + if (m) { + const home = m.Home?.Abbreviation ?? "?"; + const away = m.Away?.Abbreviation ?? "?"; + console.log(` #${n}: ${home} vs ${away} | Stage=${m.StageName?.[0]?.Description ?? "?"} | Status=${m.MatchStatus}`); + } else { + console.log(` #${n}: NICHT GEFUNDEN`); + } + } + + // ── C) Attendance + Scheduled ── + console.log("\n═══ C) Attendance + Scheduled-Matches ═══"); + const withAttendance = matches.filter(m => m.Attendance != null); + console.log(`Matches mit Attendance: ${withAttendance.length}`); + if (withAttendance.length > 0) { + const ex = withAttendance[0]; + console.log(` Beispiel: #${ex.MatchNumber} → ${ex.Attendance}`); + } + const scheduled = matches.filter(m => m.MatchStatus === 1); + console.log(`Scheduled-Matches (Status=1): ${scheduled.length}`); + if (scheduled.length > 0) { + const ex = scheduled[0]; + console.log(` Beispiel: #${ex.MatchNumber} ${ex.Home?.Abbreviation ?? "?"} vs ${ex.Away?.Abbreviation ?? "?"} | Date=${ex.Date}`); + } + + // ── D) MatchStatus-Werte ── + console.log("\n═══ D) Distinct MatchStatus-Werte ═══"); + const statuses = new Map(); + for (const m of matches) { + statuses.set(m.MatchStatus, (statuses.get(m.MatchStatus) ?? 0) + 1); + } + const statusLabels: Record = { 0: "finished", 1: "scheduled", 10: "postponed", 3: "live?" }; + for (const [s, c] of [...statuses.entries()].sort((a, b) => a[0] - b[0])) { + console.log(` MatchStatus ${s}: ${c} Matches ${statusLabels[s] ? `(${statusLabels[s]})` : ""}`); + } + + // ── E) Stage-Werte ── + console.log("\n═══ E) Distinct Stages ═══"); + const stages = new Map(); + for (const m of matches) { + const sn = m.StageName?.[0]?.Description ?? String(m.IdStage); + stages.set(sn, (stages.get(sn) ?? 0) + 1); + } + for (const [s, c] of stages.entries()) { + console.log(` "${s}" (IdStage=${matches.find(m => (m.StageName?.[0]?.Description ?? "") === s)?.IdStage}): ${c}`); + } + + console.log("\n✅ Analyse abgeschlossen."); +} + +main().catch(console.error);