/* Football-Data API v4 integration for a static HTML/CSS/JS site. Important security note: On a fully static site the API token is visible in the browser. For real token protection, use a server proxy, Cloudflare Worker, Yandex Cloud Function, or another backend layer. This file follows the current task requirement: no server side, browser-side requests only. */ const API_TOKEN = 'b4e186e0a1c448e9a3eb21040313d6ac'; const API_BASE = 'https://api.football-data.org/v4'; const CACHE_VERSION = 'v3'; const CACHE_TTL = 1000 * 60 * 60 * 4; const REFRESH_INTERVAL = CACHE_TTL; const COMPETITIONS = ['PL', 'PD', 'SA', 'BL1', 'FL1', 'DED', 'PPL', 'ELC', 'CL', 'BSA']; const STANDINGS_COMPETITIONS = ['PL', 'PD', 'SA', 'BL1', 'FL1', 'CL', 'DED', 'PPL', 'ELC', 'BSA']; const HOME_STANDINGS_COMPETITIONS = ['PL', 'PD', 'SA', 'BL1', 'FL1', 'CL']; const COMPETITION_NAMES = { PL: 'Premier League', PD: 'La Liga', SA: 'Serie A', BL1: 'Bundesliga', FL1: 'Ligue 1', DED: 'Eredivisie', PPL: 'Primeira Liga', ELC: 'Championship', CL: 'Champions League', BSA: 'Brazil Serie A' }; const LIVE_STATUSES = ['LIVE', 'IN_PLAY', 'PAUSED']; const FINISHED_STATUSES = ['FINISHED']; const SCHEDULED_STATUSES = ['SCHEDULED', 'TIMED']; const KNOWN_STATUSES = ['SCHEDULED', 'TIMED', 'LIVE', 'IN_PLAY', 'PAUSED', 'FINISHED', 'POSTPONED', 'SUSPENDED', 'CANCELLED']; let allMatches = []; let currentStatusFilter = 'all'; let currentLeagueFilter = 'all'; const TRANSLATIONS = { az: { all: 'Hamısı', live: 'Canlı', finished: 'Bitib', scheduled: 'Gözlənilir', statusAll: 'Hamısı', loadingMatches: 'Matçlar yüklənir...', loadingStandings: 'Turnir cədvəlləri yüklənir...', noMatches: 'Bu bölmə üçün hazırda matç tapılmadı.', noStandings: 'Bu liqa üçün hazırda turnir cədvəli tapılmadı.', lastUpdated: 'Son yenilənmə', team: 'Komanda', teams: 'Komandalar', league: 'Liqa', date: 'Tarix', time: 'Saat', status: 'Status', score: 'Hesab', tablePosition: '#', tableTeam: 'Komanda', tablePlayed: 'Oyun', tableWon: 'Qələbə', tableDraw: 'Heç-heçə', tableLost: 'Məğlubiyyət', tablePoints: 'Xal', statuses: { SCHEDULED: 'Gözlənilir', TIMED: 'Vaxt təyin olunub', LIVE: 'Canlı', IN_PLAY: 'Canlı', PAUSED: 'Fasilə', FINISHED: 'Bitib', POSTPONED: 'Təxirə salınıb', SUSPENDED: 'Dayandırılıb', CANCELLED: 'Ləğv edilib' } }, ru: { all: 'Все', live: 'Live', finished: 'Завершены', scheduled: 'Запланированы', statusAll: 'Все', loadingMatches: 'Матчи загружаются...', loadingStandings: 'Турнирные таблицы загружаются...', noMatches: 'Для этого блока пока нет матчей.', noStandings: 'Для этой лиги пока нет турнирной таблицы.', lastUpdated: 'Последнее обновление', team: 'Команда', teams: 'Команды', league: 'Лига', date: 'Дата', time: 'Время', status: 'Статус', score: 'Счёт', tablePosition: '#', tableTeam: 'Команда', tablePlayed: 'И', tableWon: 'В', tableDraw: 'Н', tableLost: 'П', tablePoints: 'Очки', statuses: { SCHEDULED: 'Запланирован', TIMED: 'Запланирован', LIVE: 'Live', IN_PLAY: 'Live', PAUSED: 'Пауза', FINISHED: 'Завершён', POSTPONED: 'Перенесён', SUSPENDED: 'Приостановлен', CANCELLED: 'Отменён' } } }; const FALLBACK_MATCHES = buildFallbackMatches(); const FALLBACK_STANDINGS = buildFallbackStandings(); function getLang() { return document.documentElement.lang === 'ru' ? 'ru' : 'az'; } function t() { return TRANSLATIONS[getLang()]; } function getDateOffset(days) { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } function getDateRange() { return { from: getDateOffset(-14), to: getDateOffset(30) }; } function apiFetch(endpoint) { return fetch(`${API_BASE}${endpoint}`, { method: 'GET', headers: { 'X-Auth-Token': API_TOKEN }, cache: 'no-store' }).then(async response => { if (!response.ok) { let message = ''; try { message = await response.text(); } catch (error) { message = ''; } throw new Error(`Football-Data API error ${response.status}: ${message || endpoint}`); } return response.json(); }); } function getCache(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed.timestamp !== 'number') return null; const isFresh = Date.now() - parsed.timestamp < CACHE_TTL; if (!isFresh) return null; return parsed.payload; } catch (error) { console.warn('Failed to read localStorage cache.', error); return null; } } function setCache(key, payload) { try { localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), payload })); } catch (error) { console.warn('Failed to write localStorage cache.', error); } } function clearDynamicCache() { try { Object.keys(localStorage).forEach(key => { if (key.startsWith('soccerstand_matches_') || key.startsWith('soccerstand_standings_')) { localStorage.removeItem(key); } }); } catch (error) { console.warn('Failed to clear dynamic cache.', error); } } function ensureCacheVersion() { try { const versionKey = 'soccerstand_cache_version'; const savedVersion = localStorage.getItem(versionKey); if (savedVersion !== CACHE_VERSION) { clearDynamicCache(); localStorage.setItem(versionKey, CACHE_VERSION); } } catch (error) { console.warn('Failed to check cache version.', error); } } function mapApiMatch(match) { const fullTime = match.score && match.score.fullTime ? match.score.fullTime : {}; const homeScore = fullTime.home; const awayScore = fullTime.away; let score = '-'; if (homeScore !== null && homeScore !== undefined && awayScore !== null && awayScore !== undefined) { score = `${homeScore} : ${awayScore}`; } const competitionCode = match.competition && match.competition.code ? match.competition.code : 'GEN'; return { competition: match.competition && match.competition.name ? match.competition.name : COMPETITION_NAMES[competitionCode] || competitionCode, competitionCode, home: match.homeTeam && (match.homeTeam.shortName || match.homeTeam.name) ? (match.homeTeam.shortName || match.homeTeam.name) : 'Home', away: match.awayTeam && (match.awayTeam.shortName || match.awayTeam.name) ? (match.awayTeam.shortName || match.awayTeam.name) : 'Away', date: match.utcDate, status: match.status || 'SCHEDULED', score }; } async function loadMatchesRange() { const { from, to } = getDateRange(); const cacheKey = `soccerstand_matches_${CACHE_VERSION}_${from}_${to}`; const cached = getCache(cacheKey); if (Array.isArray(cached) && cached.length) { return cached; } const collected = []; const errors = []; for (const code of COMPETITIONS) { try { const data = await apiFetch(`/competitions/${code}/matches?dateFrom=${from}&dateTo=${to}`); const matches = Array.isArray(data.matches) ? data.matches : []; matches.forEach(match => collected.push(mapApiMatch(match))); } catch (error) { errors.push({ code, error }); console.warn(`Failed to load matches for ${code}`, error); } } if (collected.length) { const sorted = collected.sort((a, b) => new Date(a.date) - new Date(b.date)); setCache(cacheKey, sorted); return sorted; } console.warn('Football-Data API did not return matches. Fallback matches are used.', errors); return getFallbackMatchesForRange(from, to); } async function loadStandings(code) { const cacheKey = `soccerstand_standings_${CACHE_VERSION}_${code}`; const cached = getCache(cacheKey); if (Array.isArray(cached) && cached.length) { return cached; } try { const data = await apiFetch(`/competitions/${code}/standings`); const standings = Array.isArray(data.standings) ? data.standings : []; const preferred = standings.find(item => item.type === 'TOTAL') || standings[0]; const table = preferred && Array.isArray(preferred.table) ? preferred.table : []; if (table.length) { setCache(cacheKey, table); return table; } } catch (error) { console.warn(`Failed to load standings for ${code}`, error); } return FALLBACK_STANDINGS[code] || []; } function applyMatchFilters(matches = allMatches) { let filtered = Array.isArray(matches) ? [...matches] : []; if (currentLeagueFilter !== 'all') { filtered = filtered.filter(match => match.competitionCode === currentLeagueFilter); } if (currentStatusFilter === 'live') { filtered = filtered.filter(match => LIVE_STATUSES.includes(match.status)); } if (currentStatusFilter === 'finished') { filtered = filtered.filter(match => FINISHED_STATUSES.includes(match.status)); } if (currentStatusFilter === 'scheduled') { filtered = filtered.filter(match => SCHEDULED_STATUSES.includes(match.status)); } return filtered; } function groupMatchesByLeague(matches) { return matches.reduce((acc, match) => { const code = match.competitionCode || 'GEN'; const name = match.competition || COMPETITION_NAMES[code] || code; if (!acc[code]) { acc[code] = { code, name, items: [] }; } acc[code].items.push(match); return acc; }, {}); } function renderGroupedMatches(container, matches) { if (!container) return; const items = Array.isArray(matches) ? matches : []; if (!items.length) { container.innerHTML = `
${escapeHtml(t().noMatches)}
`; return; } const groups = Object.values(groupMatchesByLeague(items)); container.innerHTML = groups.map(group => { const rows = group.items .sort((a, b) => new Date(a.date) - new Date(b.date)) .map(renderMatchRow) .join(''); return `
${escapeHtml(group.name)} ${group.items.length}
${rows}
`; }).join(''); } function renderMatchRow(match) { const lang = getLang() === 'ru' ? 'ru-RU' : 'az-AZ'; const date = new Date(match.date); const status = t().statuses[match.status] || match.status; const statusClass = getStatusClass(match.status); const dateText = new Intl.DateTimeFormat(lang, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date); const timeText = new Intl.DateTimeFormat(lang, { hour: '2-digit', minute: '2-digit' }).format(date); return `
${escapeHtml(timeText)} ${escapeHtml(dateText)}
${escapeHtml(match.home)}
${escapeHtml(match.away)}
${escapeHtml(COMPETITION_NAMES[match.competitionCode] || match.competition)} ${escapeHtml(status)}
${escapeHtml(match.score || '-')}
`; } function renderStandings(container, standingsMap) { if (!container) return; const entries = normalizeStandingsEntries(standingsMap); if (!entries.length) { container.innerHTML = `
${escapeHtml(t().noStandings)}
`; return; } container.innerHTML = `
${entries.map(entry => renderStandingsCard(entry.code, entry.rows)).join('')}
`; } function normalizeStandingsEntries(standingsMap) { if (Array.isArray(standingsMap)) { return [{ code: 'GEN', rows: standingsMap }].filter(entry => entry.rows.length); } return Object.entries(standingsMap || {}) .map(([code, rows]) => ({ code, rows: Array.isArray(rows) ? rows : [] })) .filter(entry => entry.rows.length); } function renderStandingsCard(code, rows) { const title = COMPETITION_NAMES[code] || code; const limitedRows = rows.slice(0, 20); return `

${escapeHtml(title)}

${limitedRows.map(row => ` `).join('')}
${escapeHtml(t().tablePosition)} ${escapeHtml(t().tableTeam)} ${escapeHtml(t().tablePlayed)} ${escapeHtml(t().tableWon)} ${escapeHtml(t().tableDraw)} ${escapeHtml(t().tableLost)} ${escapeHtml(t().tablePoints)}
${escapeHtml(row.position)} ${escapeHtml(getTeamName(row))} ${escapeHtml(row.playedGames ?? row.played ?? 0)} ${escapeHtml(row.won ?? 0)} ${escapeHtml(row.draw ?? 0)} ${escapeHtml(row.lost ?? 0)} ${escapeHtml(row.points ?? 0)}
`; } async function initHome() { const todayContainer = findContainer(['[data-matches-container="today"]', '#today-matches']); const recentContainer = findContainer(['[data-matches-container="recent"]', '#recent-results']); const upcomingContainer = findContainer(['[data-matches-container="upcoming"]', '#upcoming-matches']); const standingsContainer = findContainer(['[data-home-standings]', '#home-standings-list', '#home-standings-table']); setLoading(todayContainer, t().loadingMatches); setLoading(recentContainer, t().loadingMatches); setLoading(upcomingContainer, t().loadingMatches); setLoading(standingsContainer, t().loadingStandings); allMatches = await loadMatchesRange(); const todayKey = getDateOffset(0); const todayMatches = allMatches.filter(match => getIsoDate(match.date) === todayKey); const recentResults = allMatches .filter(match => FINISHED_STATUSES.includes(match.status)) .sort((a, b) => new Date(b.date) - new Date(a.date)) .slice(0, 40); const upcomingMatches = allMatches .filter(match => SCHEDULED_STATUSES.includes(match.status)) .sort((a, b) => new Date(a.date) - new Date(b.date)) .slice(0, 60); renderGroupedMatches(todayContainer, todayMatches); renderGroupedMatches(recentContainer, recentResults); renderGroupedMatches(upcomingContainer, upcomingMatches); if (standingsContainer) { const map = {}; for (const code of HOME_STANDINGS_COMPETITIONS) { map[code] = await loadStandings(code); } renderStandings(standingsContainer, map); } setLastUpdated(); } async function initMatchesPage() { const container = findContainer(['[data-matches-container="all"]', '#all-matches-box', '#matches-list']); setLoading(container, t().loadingMatches); const params = new URLSearchParams(window.location.search); currentStatusFilter = normalizeStatusFilter(params.get('status') || currentStatusFilter); currentLeagueFilter = normalizeLeagueFilter(params.get('league') || currentLeagueFilter); allMatches = await loadMatchesRange(); setupMatchFilterControls(container); syncFilterButtons(); renderGroupedMatches(container, applyMatchFilters()); setLastUpdated(); } async function initStandingsPage() { const container = findContainer(['[data-standings-list]', '#standings-list', '#full-standings-table']); setLoading(container, t().loadingStandings); const standingsMap = {}; for (const code of STANDINGS_COMPETITIONS) { standingsMap[code] = await loadStandings(code); } renderStandings(container, standingsMap); setupStandingsFilterControls(standingsMap, container); setLastUpdated(); } function setupMatchFilterControls(container) { document.querySelectorAll('[data-match-filter]').forEach(button => { button.addEventListener('click', event => { event.preventDefault(); currentStatusFilter = normalizeStatusFilter(button.dataset.matchFilter || 'all'); syncFilterButtons(); renderGroupedMatches(container, applyMatchFilters()); updateMatchesPageUrl(); }); }); document.querySelectorAll('[data-league-filter]').forEach(button => { button.addEventListener('click', event => { event.preventDefault(); currentLeagueFilter = normalizeLeagueFilter(button.dataset.leagueFilter || 'all'); syncFilterButtons(); renderGroupedMatches(container, applyMatchFilters()); updateMatchesPageUrl(); }); }); } function setupStandingsFilterControls(standingsMap, container) { const controls = document.querySelectorAll('[data-standings-filter], [data-full-standings-tab], [data-standings-tab]'); if (!controls.length) return; controls.forEach(button => { button.addEventListener('click', event => { event.preventDefault(); const code = button.dataset.standingsFilter || button.dataset.fullStandingsTab || button.dataset.standingsTab || 'all'; controls.forEach(item => item.classList.remove('active')); button.classList.add('active'); if (code === 'all') { renderStandings(container, standingsMap); return; } renderStandings(container, { [code]: standingsMap[code] || [] }); }); }); } function syncFilterButtons() { document.querySelectorAll('[data-match-filter]').forEach(button => { const value = normalizeStatusFilter(button.dataset.matchFilter || 'all'); button.classList.toggle('active', value === currentStatusFilter); }); document.querySelectorAll('[data-league-filter]').forEach(button => { const value = normalizeLeagueFilter(button.dataset.leagueFilter || 'all'); button.classList.toggle('active', value === currentLeagueFilter); }); } function updateMatchesPageUrl() { if (!window.history || !window.history.replaceState) return; const params = new URLSearchParams(); if (currentStatusFilter !== 'all') params.set('status', currentStatusFilter); if (currentLeagueFilter !== 'all') params.set('league', currentLeagueFilter); const query = params.toString(); const nextUrl = query ? `${window.location.pathname}?${query}` : window.location.pathname; window.history.replaceState(null, '', nextUrl); } function reloadCurrentPageData() { const page = document.body.dataset.page || detectPageType(); if (page === 'matches') { return initMatchesPage(); } if (page === 'standings') { return initStandingsPage(); } return initHome(); } function setupAutoRefresh() { window.setInterval(async () => { clearDynamicCache(); try { await reloadCurrentPageData(); } catch (error) { console.warn('Failed to auto-refresh football data.', error); } }, REFRESH_INTERVAL); } function setLastUpdated() { const lang = getLang() === 'ru' ? 'ru-RU' : 'az-AZ'; const value = new Intl.DateTimeFormat(lang, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date()); document.querySelectorAll('[data-last-updated]').forEach(element => { element.textContent = `${t().lastUpdated}: ${value}`; }); } function setStaticUiLabels() { document.querySelectorAll('[data-status-label="all"]').forEach(item => item.textContent = t().all); document.querySelectorAll('[data-status-label="live"]').forEach(item => item.textContent = t().live); document.querySelectorAll('[data-status-label="finished"]').forEach(item => item.textContent = t().finished); document.querySelectorAll('[data-status-label="scheduled"]').forEach(item => item.textContent = t().scheduled); } function setupCurrentYear() { document.querySelectorAll('[data-current-year]').forEach(element => { element.textContent = new Date().getFullYear(); }); } async function init() { ensureCacheVersion(); setStaticUiLabels(); setupCurrentYear(); setupAutoRefresh(); try { await reloadCurrentPageData(); } catch (error) { console.warn('Failed to initialize football data.', error); renderEmergencyFallback(); } } function renderEmergencyFallback() { const page = document.body.dataset.page || detectPageType(); if (page === 'standings') { const container = findContainer(['[data-standings-list]', '#standings-list', '#full-standings-table']); const map = {}; HOME_STANDINGS_COMPETITIONS.forEach(code => { map[code] = FALLBACK_STANDINGS[code] || []; }); renderStandings(container, map); return; } const container = findContainer(['[data-matches-container="all"]', '#all-matches-box', '#matches-list', '[data-matches-container="today"]', '#today-matches']); renderGroupedMatches(container, FALLBACK_MATCHES.slice(0, 60)); } function detectPageType() { if (document.querySelector('[data-standings-list], #standings-list, #full-standings-table')) return 'standings'; if (document.querySelector('[data-matches-container="all"], #all-matches-box, #matches-list')) return 'matches'; return 'home'; } function normalizeStatusFilter(value) { return ['all', 'live', 'finished', 'scheduled'].includes(value) ? value : 'all'; } function normalizeLeagueFilter(value) { return value === 'all' || COMPETITIONS.includes(value) ? value : 'all'; } function getStatusClass(status) { if (LIVE_STATUSES.includes(status)) return 'live'; if (FINISHED_STATUSES.includes(status)) return 'finished'; if (SCHEDULED_STATUSES.includes(status)) return 'scheduled'; if (['POSTPONED', 'SUSPENDED', 'CANCELLED'].includes(status)) return 'cancelled'; return 'default'; } function getTeamName(row) { if (row && row.team && row.team.name) return row.team.name; if (row && row.teamName) return row.teamName; return 'Team'; } function getIsoDate(iso) { return new Date(iso).toISOString().slice(0, 10); } function setLoading(container, text) { if (!container) return; container.innerHTML = `
${escapeHtml(text)}
`; } function findContainer(selectors) { for (const selector of selectors) { const element = document.querySelector(selector); if (element) return element; } return null; } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); } function getFallbackMatchesForRange(from, to) { const start = new Date(`${from}T00:00:00Z`); const end = new Date(`${to}T23:59:59Z`); return FALLBACK_MATCHES.filter(match => { const date = new Date(match.date); return date >= start && date <= end; }); } function buildFallbackMatches() { const fixtures = { PL: [ ['Arsenal', 'Chelsea'], ['Liverpool', 'Everton'], ['Manchester City', 'Tottenham'], ['Newcastle', 'Aston Villa'] ], PD: [ ['Real Madrid', 'Valencia'], ['Barcelona', 'Sevilla'], ['Atletico Madrid', 'Villarreal'], ['Real Sociedad', 'Celta Vigo'] ], SA: [ ['Inter', 'Roma'], ['Milan', 'Lazio'], ['Juventus', 'Atalanta'], ['Napoli', 'Fiorentina'] ], BL1: [ ['Bayern München', 'Borussia Dortmund'], ['Bayer Leverkusen', 'RB Leipzig'], ['Stuttgart', 'Mainz'], ['Eintracht Frankfurt', 'Freiburg'] ], FL1: [ ['Paris Saint-Germain', 'Monaco'], ['Marseille', 'Lille'], ['Lyon', 'Nice'], ['Rennes', 'Lens'] ], DED: [ ['Ajax', 'PSV'], ['Feyenoord', 'AZ'], ['Twente', 'Utrecht'], ['Heerenveen', 'Sparta Rotterdam'] ], PPL: [ ['Benfica', 'Porto'], ['Sporting CP', 'Braga'], ['Boavista', 'Famalicão'], ['Vitória SC', 'Casa Pia'] ], ELC: [ ['Leeds United', 'Norwich City'], ['Southampton', 'Hull City'], ['West Bromwich Albion', 'Sunderland'], ['Middlesbrough', 'Watford'] ], CL: [ ['Real Madrid', 'Bayern München'], ['Inter', 'Barcelona'], ['Paris Saint-Germain', 'Arsenal'], ['Borussia Dortmund', 'Atletico Madrid'] ], BSA: [ ['Flamengo', 'Palmeiras'], ['São Paulo', 'Corinthians'], ['Botafogo', 'Fluminense'], ['Atlético Mineiro', 'Grêmio'] ] }; const matches = []; Object.entries(fixtures).forEach(([code, pairs], competitionIndex) => { pairs.forEach(([home, away], index) => { const finishedHome = 1 + ((competitionIndex + index) % 4); const finishedAway = (competitionIndex + index * 2) % 3; const pastOffset = -13 + ((competitionIndex * 2 + index) % 12); const futureOffset = 1 + ((competitionIndex + index * 4) % 28); matches.push({ competition: COMPETITION_NAMES[code], competitionCode: code, home, away, date: fallbackIso(pastOffset, 17 + (index % 5)), status: 'FINISHED', score: `${finishedHome} : ${finishedAway}` }); matches.push({ competition: COMPETITION_NAMES[code], competitionCode: code, home: away, away: home, date: fallbackIso(futureOffset, 18 + (index % 4)), status: index % 2 === 0 ? 'SCHEDULED' : 'TIMED', score: '-' }); }); }); matches.push({ competition: COMPETITION_NAMES.PL, competitionCode: 'PL', home: 'Arsenal', away: 'Liverpool', date: fallbackIso(0, 21), status: 'IN_PLAY', score: '1 : 0' }); matches.push({ competition: COMPETITION_NAMES.CL, competitionCode: 'CL', home: 'Qarabağ FK', away: 'Galatasaray', date: fallbackIso(0, 20), status: 'SCHEDULED', score: '-' }); return matches.sort((a, b) => new Date(a.date) - new Date(b.date)); } function buildFallbackStandings() { return { PL: makeStandingRows(['Arsenal', 'Liverpool', 'Manchester City', 'Aston Villa', 'Tottenham', 'Chelsea', 'Newcastle', 'Brighton']), PD: makeStandingRows(['Real Madrid', 'Barcelona', 'Atletico Madrid', 'Athletic Club', 'Girona', 'Real Sociedad', 'Valencia', 'Sevilla']), SA: makeStandingRows(['Inter', 'Milan', 'Juventus', 'Atalanta', 'Roma', 'Napoli', 'Lazio', 'Fiorentina']), BL1: makeStandingRows(['Bayern München', 'Bayer Leverkusen', 'Borussia Dortmund', 'RB Leipzig', 'Stuttgart', 'Eintracht Frankfurt', 'Freiburg', 'Wolfsburg']), FL1: makeStandingRows(['Paris Saint-Germain', 'Monaco', 'Lille', 'Marseille', 'Nice', 'Lyon', 'Rennes', 'Lens']), DED: makeStandingRows(['PSV', 'Ajax', 'Feyenoord', 'AZ', 'Twente', 'Utrecht', 'Sparta Rotterdam', 'Heerenveen']), PPL: makeStandingRows(['Sporting CP', 'Benfica', 'Porto', 'Braga', 'Vitória SC', 'Boavista', 'Famalicão', 'Casa Pia']), ELC: makeStandingRows(['Leeds United', 'Southampton', 'Leicester City', 'Ipswich Town', 'West Bromwich Albion', 'Norwich City', 'Hull City', 'Sunderland']), CL: makeStandingRows(['Real Madrid', 'Bayern München', 'Inter', 'Arsenal', 'Paris Saint-Germain', 'Barcelona', 'Borussia Dortmund', 'Atletico Madrid']), BSA: makeStandingRows(['Flamengo', 'Palmeiras', 'São Paulo', 'Botafogo', 'Atlético Mineiro', 'Fluminense', 'Grêmio', 'Corinthians']) }; } function makeStandingRows(teamNames) { return teamNames.map((name, index) => { const playedGames = 24 + ((name.length + index * 3) % 10); const won = Math.max(7, Math.round(playedGames * (0.72 - index * 0.045))); const draw = Math.max(2, Math.min(playedGames - won, 3 + ((name.length + index) % 5))); const lost = Math.max(0, playedGames - won - draw); const points = won * 3 + draw; return { position: 0, team: { name }, playedGames, won, draw, lost, points }; }) .sort((a, b) => b.points - a.points || b.won - a.won || a.team.name.localeCompare(b.team.name)) .map((row, index) => ({ ...row, position: index + 1 })); } function fallbackIso(dayOffset, hour) { const d = new Date(); d.setDate(d.getDate() + dayOffset); d.setHours(hour, 0, 0, 0); return d.toISOString(); } document.addEventListener('DOMContentLoaded', init);