/*
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 `
`;
}
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);