const API_TOKEN = 'b4e186e0a1c448e9a3eb21040313d6ac'; const API_BASE = 'https://api.football-data.org/v4'; const CACHE_TTL = 1000 * 60 * 60 * 4; const REFRESH_INTERVAL = 1000 * 60 * 60 * 4; const CACHE_VERSION = 'soccerstand-v16-standings-fixed'; const COMPETITIONS = ['PL', 'PD', 'SA', 'BL1', 'FL1', 'DED', 'PPL', 'ELC', 'CL', 'WC', 'BSA']; const TOP_STANDINGS = ['PL', 'PD', 'SA', 'BL1', 'FL1', 'CL']; const COMP_LABELS = { 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', WC: 'World Cup', EC: 'European Championship' }; const FALLBACK_MATCHES = buildFallbackMatches(); function buildFallbackMatches() { const fixtures = { PL: ['Arsenal|Chelsea', 'Liverpool|Everton', 'Manchester City|Tottenham', 'Newcastle|Aston Villa', 'West Ham|Brighton'], PD: ['Real Madrid|Valencia', 'Barcelona|Sevilla', 'Atletico Madrid|Villarreal', 'Real Sociedad|Celta Vigo', 'Girona|Osasuna'], SA: ['Inter|Roma', 'Milan|Lazio', 'Juventus|Atalanta', 'Napoli|Fiorentina', 'Bologna|Torino'], BL1: ['Bayern|Dortmund', 'Leverkusen|Leipzig', 'Stuttgart|Mainz', 'Frankfurt|Freiburg', 'Wolfsburg|Augsburg'], FL1: ['PSG|Monaco', 'Marseille|Lille', 'Lyon|Nice', 'Rennes|Lens', 'Nantes|Toulouse'], DED: ['Ajax|PSV', 'Feyenoord|AZ', 'Twente|Utrecht', 'Heerenveen|Sparta Rotterdam'], PPL: ['Benfica|Porto', 'Sporting CP|Braga', 'Boavista|Famalicao', 'Vitoria SC|Casa Pia'], ELC: ['Leeds|Norwich', 'Southampton|Hull', 'West Brom|Sunderland', 'Middlesbrough|Watford'], CL: ['Real Madrid|Bayern', 'Inter|Barcelona', 'PSG|Arsenal', 'Dortmund|Atletico Madrid'], WC: ['Brazil|Argentina', 'France|Germany', 'Spain|Portugal', 'England|Netherlands'] }; const out = []; Object.entries(fixtures).forEach(([code, list], cidx) => { list.forEach((pair, idx) => { const [home, away] = pair.split('|'); const pastDay = -14 + ((idx + cidx * 2) % 13); const futureDay = 1 + ((idx * 3 + cidx) % 30); const pastHome = 1 + ((idx + cidx) % 4); const pastAway = (idx + cidx * 2) % 3; out.push({ competition: COMP_LABELS[code] || code, competitionCode: code, home, away, date: offsetISO(pastDay, 18 + (idx % 5)), status: 'FINISHED', score: `${pastHome} : ${pastAway}` }); out.push({ competition: COMP_LABELS[code] || code, competitionCode: code, home: away, away: home, date: offsetISO(futureDay, 17 + (idx % 6)), status: 'SCHEDULED', score: '-' }); }); }); out.push({ competition: COMP_LABELS.PL, competitionCode: 'PL', home: 'Arsenal', away: 'Liverpool', date: offsetISO(0, 21), status: 'IN_PLAY', score: '1 : 0' }); out.push({ competition: COMP_LABELS.PD, competitionCode: 'PD', home: 'Barcelona', away: 'Girona', date: offsetISO(0, 20), status: 'SCHEDULED', score: '-' }); return out.sort((a, b) => new Date(a.date) - new Date(b.date)); } const FALLBACK_STANDINGS = { PL: fakeStandings(['Arsenal','Liverpool','Manchester City','Aston Villa','Tottenham']), PD: fakeStandings(['Real Madrid','Barcelona','Atletico Madrid','Athletic Club','Girona']), SA: fakeStandings(['Inter','Milan','Juventus','Atalanta','Roma']), BL1: fakeStandings(['Bayern','Leverkusen','Dortmund','Leipzig','Stuttgart']), FL1: fakeStandings(['PSG','Monaco','Lille','Marseille','Nice']), CL: fakeStandings(['Real Madrid','Bayern','Inter','Arsenal','PSG']) }; const UI = { az: { lastUpdated: 'Son yenilənmə', homeMatchesTitle: 'Bugünkü futbol matçları', liveTab: 'Hamısı', noMatches: 'Bu bölmə üçün hazırda matç tapılmadı.', noStandings: 'Hazırda cədvəl görünmür.', standingsTitles: { PL: 'Premier League cədvəli', PD: 'La Liga cədvəli', SA: 'Serie A cədvəli', BL1: 'Bundesliga cədvəli', FL1: 'Ligue 1 cədvəli', CL: 'Champions League cədvəli' }, statusMap: { SCHEDULED: 'Gözlənilir', TIMED: 'Vaxt təyin olunub', IN_PLAY: 'Canlı', LIVE: 'Canlı', PAUSED: 'Fasilə', FINISHED: 'Bitib', POSTPONED: 'Təxirə salınıb', CANCELLED: 'Ləğv edilib' }, leagueMap: COMP_LABELS, matchLabels: { team: 'Komandalar', league: 'Liqa', date: 'Tarix', status: 'Status' } }, ru: { lastUpdated: 'Последнее обновление', homeMatchesTitle: 'Футбольные матчи', liveTab: 'Все', noMatches: 'Для этого блока пока нет матчей.', noStandings: 'Турнирная таблица пока недоступна.', standingsTitles: { PL: 'Таблица Premier League', PD: 'Таблица La Liga', SA: 'Таблица Serie A', BL1: 'Таблица Bundesliga', FL1: 'Таблица Ligue 1', CL: 'Таблица Champions League' }, statusMap: { SCHEDULED: 'Запланирован', TIMED: 'Запланирован', IN_PLAY: 'Live', LIVE: 'Live', PAUSED: 'Пауза', FINISHED: 'Завершён', POSTPONED: 'Перенесён', CANCELLED: 'Отменён' }, leagueMap: COMP_LABELS, matchLabels: { team: 'Команды', league: 'Лига', date: 'Дата', status: 'Статус' } } }; function offsetISO(dayOffset, hour) { const d = new Date(); d.setDate(d.getDate() + dayOffset); d.setHours(hour, 0, 0, 0); return d.toISOString(); } function fakeStandings(names) { const seed = names.join('').length; const rows = names.map((name, idx) => { const playedGames = 24 + ((seed + idx * 3 + name.length) % 9); const won = Math.max(6, Math.min(playedGames, Math.round(playedGames * (0.70 - idx * 0.055)) + ((name.length + idx) % 3) - 1)); const draw = Math.max(2, Math.min(playedGames - won, 3 + ((name.length + seed + idx) % 5))); const lost = Math.max(0, playedGames - won - draw); const points = won * 3 + draw; return { position: 0, team: { name }, playedGames, won, draw, lost, points }; }); return rows .sort((a, b) => b.points - a.points || b.won - a.won) .map((row, idx) => ({ ...row, position: idx + 1 })); } function getLang() { return document.documentElement.lang === 'ru' ? 'ru' : 'az'; } function getUi() { return UI[getLang()]; } function localDateTime(iso) { const lang = getLang() === 'ru' ? 'ru-RU' : 'az-AZ'; return new Intl.DateTimeFormat(lang, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }).format(new Date(iso)); } function localTime(iso) { const lang = getLang() === 'ru' ? 'ru-RU' : 'az-AZ'; return new Intl.DateTimeFormat(lang, { hour: '2-digit', minute: '2-digit' }).format(new Date(iso)); } function localDate(iso) { const lang = getLang() === 'ru' ? 'ru-RU' : 'az-AZ'; return new Intl.DateTimeFormat(lang, { day: '2-digit', month: '2-digit' }).format(new Date(iso)); } function getCache(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed.timestamp || (Date.now() - parsed.timestamp > CACHE_TTL)) return null; return parsed.payload; } catch (e) { return null; } } function setCache(key, payload) { try { localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), payload })); } catch (e) {} } async function apiFetch(endpoint) { const sep = endpoint.includes('?') ? '&' : '?'; const url = `${API_BASE}${endpoint}${sep}_=${Date.now()}`; const res = await fetch(url, { method: 'GET', mode: 'cors', headers: { 'X-Auth-Token': API_TOKEN }, cache: 'no-store' }); if (!res.ok) { let body = ''; try { body = await res.text(); } catch (e) {} throw new Error(`API error ${res.status}: ${body || endpoint}`); } return res.json(); } function groupByCompetition(matches) { return matches.reduce((acc, match) => { const code = match.competitionCode || match.competition?.code || 'GEN'; const name = match.competition || match.competition?.name || COMP_LABELS[code] || 'Competition'; if (!acc[code]) acc[code] = { code, name, items: [] }; acc[code].items.push(match); return acc; }, {}); } function mapApiMatch(item) { const fullTime = item.score?.fullTime || {}; let score = '-'; if (typeof fullTime.home !== 'undefined' && fullTime.home !== null && typeof fullTime.away !== 'undefined' && fullTime.away !== null) { score = `${fullTime.home} : ${fullTime.away}`; } return { competition: item.competition?.name || COMP_LABELS[item.competition?.code] || 'Competition', competitionCode: item.competition?.code || 'GEN', home: item.homeTeam?.shortName || item.homeTeam?.name || 'Home', away: item.awayTeam?.shortName || item.awayTeam?.name || 'Away', date: item.utcDate, status: item.status, score }; } async function loadMatchesRange(from, to, cacheKeySuffix, competitionCodes = COMPETITIONS) { const codes = Array.isArray(competitionCodes) ? competitionCodes : COMPETITIONS; const cacheKey = `soccerstand_matches_${CACHE_VERSION}_${cacheKeySuffix}_${from}_${to}_${codes.join('-')}`; const cached = getCache(cacheKey); if (cached && cached.length) return cached; const endpoints = codes.map(code => ({ code, endpoint: `/competitions/${code}/matches?dateFrom=${from}&dateTo=${to}` })); const results = await Promise.allSettled(endpoints.map(item => apiFetch(item.endpoint).then(data => ({ code: item.code, data })))); const mapped = []; const errors = []; results.forEach(result => { if (result.status === 'fulfilled') { const items = result.value.data.matches || []; items.forEach(match => mapped.push(mapApiMatch(match))); } else { errors.push(result.reason && result.reason.message ? result.reason.message : String(result.reason)); } }); if (mapped.length) { mapped.sort((a, b) => new Date(a.date) - new Date(b.date)); setCache(cacheKey, mapped); return mapped; } console.warn('Football-Data API did not return matches, using fallback data.', errors); return FALLBACK_MATCHES.filter(item => { const d = new Date(item.date); const start = new Date(`${from}T00:00:00Z`); const end = new Date(`${to}T23:59:59Z`); return d >= start && d <= end && codes.includes(item.competitionCode); }); } async function loadTodayMatches() { const today = new Date(); const from = formatDate(today); const to = formatDate(today); return loadMatchesRange(from, to, 'today'); } async function loadRecentResults() { const fromDate = new Date(); fromDate.setDate(fromDate.getDate() - 14); const toDate = new Date(); const matches = await loadMatchesRange(formatDate(fromDate), formatDate(toDate), 'recent14'); return matches.filter(m => ['FINISHED'].includes(m.status)).slice(0, 80); } async function loadUpcomingMatches() { const fromDate = new Date(); const toDate = new Date(); toDate.setDate(toDate.getDate() + 30); const matches = await loadMatchesRange(formatDate(fromDate), formatDate(toDate), 'upcoming30'); return matches.filter(m => ['SCHEDULED','TIMED'].includes(m.status)).slice(0, 120); } async function loadAllMatchesPage() { const fromDate = new Date(); fromDate.setDate(fromDate.getDate() - 14); const toDate = new Date(); toDate.setDate(toDate.getDate() + 30); const matches = await loadMatchesRange(formatDate(fromDate), formatDate(toDate), 'range14back30forward'); return matches.slice(0, 260); } async function loadStandings(code) { const cacheKey = `soccerstand_standings_${code}`; const cached = getCache(cacheKey); if (cached) return cached; try { const data = await apiFetch(`/competitions/${code}/standings`); let table = []; if (data.standings && data.standings.length) { const preferred = data.standings.find(s => s.type === 'TOTAL') || data.standings[0]; table = preferred.table || []; } setCache(cacheKey, table); return table; } catch (e) { return FALLBACK_STANDINGS[code] || []; } } function formatDate(date) { return date.toISOString().slice(0, 10); } function renderGroupedMatches(container, matches, options = {}) { const ui = getUi(); if (!container) return; if (!matches.length) { container.innerHTML = `
${escapeHtml(ui.noMatches)}
`; return; } const groups = Object.values(groupByCompetition(matches)); const order = COMPETITIONS; groups.sort((a, b) => { const ai = order.indexOf(a.code); const bi = order.indexOf(b.code); return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); }); const collapsible = Boolean(options.collapsible); const matchWord = getLang() === 'ru' ? 'матч.' : 'matç'; if (collapsible) { container.innerHTML = groups.map((group, index) => `${escapeHtml(ui.noStandings)}
`; return; } tableEl.innerHTML = `| # | ${getLang()==='ru' ? 'Команда' : 'Komanda'} | ${getLang()==='ru' ? 'И' : 'O'} | ${getLang()==='ru' ? 'В' : 'Q'} | ${getLang()==='ru' ? 'Н' : 'H'} | ${getLang()==='ru' ? 'П' : 'M'} | ${getLang()==='ru' ? 'О' : 'Xal'} |
|---|---|---|---|---|---|---|
| ${row.position} | ${escapeHtml(row.team.name)} | ${row.playedGames ?? row.played} | ${row.won} | ${row.draw} | ${row.lost} | ${row.points} |
${escapeHtml(getUi().noStandings)}
| # | ${getLang()==='ru' ? 'Команда' : 'Komanda'} | ${getLang()==='ru' ? 'И' : 'Oyun'} | ${getLang()==='ru' ? 'В' : 'Qələbə'} | ${getLang()==='ru' ? 'Н' : 'Heç-heçə'} | ${getLang()==='ru' ? 'П' : 'Məğlubiyyət'} | ${getLang()==='ru' ? 'Очки' : 'Xal'} |
|---|
${getLang() === 'ru' ? 'Таблицы загружаются.' : 'Cədvəllər yüklənir.'}
`; } for (const code of codes) { standingsMap[code] = await loadStandings(code); } function show(value) { const selected = value || 'all'; buttons.forEach(btn => { const btnValue = btn.dataset.standingsFilter || btn.dataset.fullStandingsTab || 'all'; normalizeActiveClass(btn, btnValue === selected); }); if (selected === 'all') { titleEl.textContent = getLang() === 'ru' ? 'Таблицы по лигам' : 'Liqalar üzrə cədvəllər'; renderStandingsList(container, standingsMap, codes); return; } titleEl.textContent = getUi().standingsTitles[selected] || COMP_LABELS[selected] || selected; renderStandingsList(container, standingsMap, [selected]); } buttons.forEach(btn => { btn.addEventListener('click', () => { show(btn.dataset.standingsFilter || btn.dataset.fullStandingsTab || 'all'); }); }); show('all'); const compactBoxes = document.querySelectorAll('[data-compact-standing]'); for (const box of compactBoxes) { const code = box.dataset.compactStanding; renderStandings(box, (standingsMap[code] || []).slice(0, 5)); } } function clearSoccerstandCache() { try { Object.keys(localStorage).forEach(key => { if (key.startsWith('soccerstand_matches_') || key.startsWith('soccerstand_standings_')) { localStorage.removeItem(key); } }); } catch (e) {} } function ensureCacheVersion() { try { const saved = localStorage.getItem('soccerstand_cache_version'); if (saved !== CACHE_VERSION) { clearSoccerstandCache(); localStorage.setItem('soccerstand_cache_version', CACHE_VERSION); } } catch (e) {} } function setupAutoRefresh() { setInterval(() => { clearSoccerstandCache(); location.reload(); }, REFRESH_INTERVAL); } function setupSeoYear() { document.querySelectorAll('[data-current-year]').forEach(el => el.textContent = new Date().getFullYear()); } async function init() { ensureCacheVersion(); setLastUpdated(); setupSeoYear(); setupAutoRefresh(); const page = document.body.dataset.page || 'home'; if (page === 'home') await initHome(); if (page === 'matches') await initMatchesPage(); if (page === 'standings') await initStandingsPage(); } document.addEventListener('DOMContentLoaded', init);