File: /home/bigfreeodds/public_html/wp-content/public/scores/assets/js/app.js
/* =========================================================
Football Live Scores — vanilla JS app
Single-file logic for all pages. Designed to be easy
to swap to a real football API later (see DATA section).
========================================================= */
/* ============ TEAMS ============ */
const TEAMS = {
MNC: { name: "Manchester City", short: "MNC", cls: "t-mnc" },
ARS: { name: "Arsenal", short: "ARS", cls: "t-ars" },
RMA: { name: "Real Madrid", short: "RMA", cls: "t-rma" },
BAR: { name: "Barcelona", short: "BAR", cls: "t-bar" },
LBA: { name: "Las Palmas", short: "LBA", cls: "t-lba" },
ARG: { name: "Argentinos", short: "ARG", cls: "t-arg" },
USB: { name: "Union Berlin", short: "USB", cls: "t-usb" },
HBS: { name: "Hannover", short: "HBS", cls: "t-hbs" },
SMLH: { name: "St Mirren", short: "SMLH", cls: "t-smlh" },
VSC: { name: "Vasco", short: "VSC", cls: "t-vsc" },
};
/* ============ DATES ============
Day codes match the date strip on Home: MO TU WE(LIVE) TH FR SA SU.
This makes data easy to filter without a real calendar. Swap for ISO
date strings when wiring a real API. */
const DAY_CODES = ["MO","TU","WE","TH","FR","SA","SU"];
/* ============ MATCHES (canonical, with IDs) ============ */
const MATCHES = [
{ id:"m1", league:"Premier League", day:"WE", home:"MNC", away:"ARS", scoreH:2, scoreA:1, minute:"72'", live:true, status:"LIVE", redCards:true, venue:"Etihad", referee:"M. Oliver", weather:"Clear 14°" },
{ id:"m2", league:"Premier League", day:"MO", home:"MNC", away:"ARG", scoreH:2, scoreA:1, minute:"FT", live:false, status:"FT", redCards:false, venue:"Etihad", referee:"A. Taylor", weather:"Cloudy 11°" },
{ id:"m3", league:"Premier League", day:"TU", home:"RMA", away:"BAR", scoreH:3, scoreA:0, minute:"FT", live:false, status:"FT", redCards:false, venue:"Bernabeu", referee:"P. Tierney",weather:"Clear 18°" },
{ id:"m4", league:"La Liga", day:"WE", home:"LBA", away:"ARS", scoreH:0, scoreA:0, minute:"72'", live:true, status:"LIVE", redCards:false, venue:"Gran Canaria", referee:"J. Sanchez",weather:"Clear 22°" },
{ id:"m5", league:"La Liga", day:"TH", home:"RMA", away:"BAR", scoreH:0, scoreA:0, minute:"21:00",live:false, status:"UP", redCards:false, venue:"Bernabeu", referee:"D. Diaz", weather:"Clear 19°" },
{ id:"m6", league:"Bundesliga", day:"WE", home:"USB", away:"HBS", scoreH:1, scoreA:1, minute:"82'", live:true, status:"LIVE", redCards:false, venue:"Stadion an der Alten Försterei", referee:"F. Brych", weather:"Rain 9°" },
{ id:"m7", league:"Bundesliga", day:"FR", home:"HBS", away:"USB", scoreH:0, scoreA:0, minute:"19:30",live:false, status:"UP", redCards:false, venue:"HDI Arena", referee:"M. Aytekin",weather:"Clear 12°" },
{ id:"m8", league:"Champions League",day:"TU",home:"BAR", away:"MNC", scoreH:2, scoreA:2, minute:"FT", live:false, status:"FT", redCards:true, venue:"Camp Nou", referee:"S. Marciniak",weather:"Clear 17°" },
{ id:"m9", league:"Champions League",day:"SA",home:"ARS", away:"RMA", scoreH:0, scoreA:0, minute:"21:00",live:false, status:"UP", redCards:false, venue:"Emirates", referee:"C. Turpin", weather:"Cloudy 13°" },
{ id:"m10", league:"Premier League", day:"SU", home:"ARS", away:"MNC", scoreH:0, scoreA:0, minute:"17:30",live:false, status:"UP", redCards:false, venue:"Emirates", referee:"A. Madley", weather:"Clear 16°" },
{ id:"m11", league:"La Liga", day:"SA", home:"BAR", away:"LBA", scoreH:0, scoreA:0, minute:"20:00",live:false, status:"UP", redCards:false, venue:"Camp Nou", referee:"J. Munuera", weather:"Clear 21°" },
{ id:"m12", league:"Premier League", day:"WE", home:"SMLH",away:"VSC", scoreH:1, scoreA:0, minute:"58'", live:true, status:"LIVE", redCards:false, venue:"SMiSA Stadium", referee:"D. England", weather:"Rain 8°" },
];
const matchById = (id) => MATCHES.find(m => m.id === id);
const API_ROOT = "/wp-json/neon-kick/v1";
/* Group helpers */
const matchesByDay = (day) => MATCHES.filter(m => m.day === day);
const liveMatches = () => MATCHES.filter(m => m.live);
const leagueNames = () => ["Premier League","La Liga","Bundesliga","Serie A","Ligue 1","Champions League"];
function leagueSlug(name) {
return String(name || "").toLowerCase().replace(/\s+/g, "-");
}
async function apiFetch(path) {
try {
const res = await fetch(API_ROOT + path, {
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.warn("[demo] API fallback:", path, err.message);
return null;
}
}
/* ============ STANDINGS / SCORERS / LINEUPS / STATS ============ */
const STANDINGS = {
"Premier League": [
{ team:"MNC", p:18, w:15, d:0, l:1, gd:"+23", pts:73, top:true },
{ team:"ARS", p:18, w:13, d:3, l:2, gd:"+3", pts:59, top:true },
{ team:"USB", p:18, w:13, d:3, l:0, gd:"+0", pts:36, top:false },
{ team:"HBS", p:18, w:12, d:3, l:5, gd:"+6", pts:22, top:false },
{ team:"SMLH",p:18, w:11, d:3, l:4, gd:"-7", pts:19, top:false },
{ team:"VSC", p:18, w:9, d:2, l:6, gd:"-10", pts:19, top:false },
],
"La Liga": [
{ team:"RMA", p:18, w:16, d:1, l:1, gd:"+30", pts:49, top:true },
{ team:"BAR", p:18, w:14, d:2, l:2, gd:"+22", pts:44, top:true },
{ team:"LBA", p:18, w:9, d:4, l:5, gd:"+5", pts:31, top:false },
],
"Bundesliga": [
{ team:"USB", p:18, w:11, d:4, l:3, gd:"+12", pts:37, top:true },
{ team:"HBS", p:18, w:9, d:5, l:4, gd:"+4", pts:32, top:false },
],
};
const SCORERS = [
{ name:"Haaland", goals:15, club:"MNC" },
{ name:"Salah", goals:6, club:"ARS" },
{ name:"Maair", goals:3, club:"RMA" },
{ name:"Vinicius", goals:3, club:"RMA" },
];
/* Lineup positions (4-3-3 home, 4-2-3-1 away). Names per match get
personalised below. The pitch coordinates are relative percentages. */
const LINEUP_HOME = [
{ rating:8.4, x:50, y:92, gk:true }, { rating:7.8, x:18, y:72 },
{ rating:8.1, x:38, y:78 }, { rating:7.9, x:62, y:78 }, { rating:8.0, x:82, y:72 },
{ rating:8.6, x:50, y:58 }, { rating:8.4, x:28, y:44 }, { rating:7.7, x:72, y:44 },
{ rating:8.2, x:22, y:22 }, { rating:9.1, x:50, y:14 }, { rating:7.5, x:78, y:22 },
];
const LINEUP_AWAY = [
{ rating:7.9, x:50, y:92, gk:true }, { rating:7.4, x:18, y:72 },
{ rating:8.2, x:38, y:78 }, { rating:8.0, x:62, y:78 }, { rating:7.3, x:82, y:72 },
{ rating:8.1, x:36, y:56 }, { rating:7.8, x:64, y:56 },
{ rating:8.3, x:22, y:36 }, { rating:7.9, x:50, y:36 }, { rating:7.6, x:78, y:36 },
{ rating:7.4, x:50, y:14 },
];
/* Per-team realistic player names (subset for demo) */
const ROSTERS = {
MNC: ["Ederson","Walker","Dias","Akanji","Gvardiol","Rodri","De Bruyne","Bernardo","Foden","Haaland","Grealish","Doku","Stones","Kovacic","Alvarez"],
ARS: ["Raya","White","Saliba","Gabriel","Zinchenko","Rice","Partey","Saka","Odegaard","Martinelli","Jesus","Trossard","Havertz","Jorginho"],
RMA: ["Courtois","Carvajal","Rudiger","Alaba","Mendy","Kroos","Modric","Valverde","Bellingham","Vinicius","Rodrygo","Camavinga","Tchouameni"],
BAR: ["Ter Stegen","Kounde","Araujo","Christensen","Balde","De Jong","Pedri","Gavi","Yamal","Lewandowski","Raphinha","Joao Felix","Gundogan"],
LBA: ["Valles","Suarez","Mcburnie","Cardona","Alvaro","Loiodice","Munoz","Moleiro","Sandro","Sory Kaba","Marvin"],
ARG: ["Gimenez","Romero","Piedrahita","Castro","Lopez","Vera","Redondo","Cabrera","Ferreyra","Avalos","Ortega"],
USB: ["Ronnow","Trimmel","Knoche","Doekhi","Roussillon","Khedira","Tousart","Fofana","Haberer","Volland","Behrens"],
HBS: ["Zieler","Halstenberg","Borner","Dehm","Neumann","Leopold","Kunze","Voglsammer","Multhaup","Tresoldi","Nielsen"],
SMLH: ["Carson","Tanser","Strain","Dunne","McMonagle","Erhahon","Olusanya","Boyd","Reid","Mandron","Ayunga"],
VSC: ["Leo","Puma","Joao Victor","Robson","Lucas","Hugo Moura","Andrey","Coutinho","Praxedes","Vegetti","Adson"],
};
const STATS_BASE = [
{ name:"Possession", h:61, a:39, hLabel:"61%", aLabel:"39%" },
{ name:"Shots", h:12, a:5, hLabel:"12", aLabel:"5" },
{ name:"xG", h:1.84, a:0.92, hLabel:"1.84", aLabel:"0.92"},
{ name:"Fouls", h:7, a:11, hLabel:"7", aLabel:"11" },
{ name:"Corners", h:6, a:3, hLabel:"6", aLabel:"3" },
{ name:"Passes", h:482, a:311, hLabel:"482", aLabel:"311" },
];
/* Per-match stats derived from base + score so each match feels unique */
function statsFor(m) {
const swing = (m.scoreH - m.scoreA) * 4;
return STATS_BASE.map(s => {
const h = Math.max(1, s.h + swing);
const a = Math.max(1, s.a - swing);
return { ...s, h, a,
hLabel: s.name==="Possession" ? `${Math.min(75,Math.max(25,h))}%` : s.hLabel,
aLabel: s.name==="Possession" ? `${100-Math.min(75,Math.max(25,h))}%` : s.aLabel,
};
});
}
function eventsFor(m) {
const home = TEAMS[m.home].short, away = TEAMS[m.away].short;
const evs = [];
for (let i=0; i<m.scoreH; i++) evs.push({ time:`${10+i*22}'`, ic:"⚽", desc:`Goal — ${ROSTERS[m.home][9+i] || ROSTERS[m.home][0]} (${home})` });
for (let i=0; i<m.scoreA; i++) evs.push({ time:`${18+i*20}'`, ic:"⚽", desc:`Goal — ${ROSTERS[m.away][7+i] || ROSTERS[m.away][0]} (${away})` });
evs.push({ time:"27'", ic:"🟨", desc:`Yellow card — ${ROSTERS[m.away][5]} (${away})` });
if (m.redCards) evs.push({ time:"63'", ic:"🟥", desc:`Red card — ${ROSTERS[m.away][6]} (${away})` });
evs.push({ time:"68'", ic:"🔁", desc:`Substitution — ${ROSTERS[m.home][10]} ON (${home})` });
return evs.sort((a,b) => parseInt(a.time) - parseInt(b.time));
}
function h2hFor(m) {
const a = TEAMS[m.home]?.name || m.home;
const b = TEAMS[m.away]?.name || m.away;
const home_short = TEAMS[m.home]?.short || m.home;
const away_short = TEAMS[m.away]?.short || m.away;
return [
{ date:"12 Mar 2026", home:a, away:b, home_short, away_short, scoreH:1, scoreA:0, comp:m.league },
{ date:"08 Oct 2025", home:b, away:a, home_short:away_short, away_short:home_short, scoreH:0, scoreA:0, comp:m.league },
{ date:"26 Apr 2025", home:a, away:b, home_short, away_short, scoreH:4, scoreA:1, comp:"Cup" },
{ date:"14 Jan 2025", home:b, away:a, home_short:away_short, away_short:home_short, scoreH:2, scoreA:2, comp:m.league },
];
}
function setLeagueListSkeleton(container, rows = 6) {
if (!container) return;
container.innerHTML = Array.from({ length: rows }, () => `
<div class="menu-item skeleton-menu" aria-hidden="true">
<span class="skeleton skeleton-logo"></span>
<span style="display:flex;flex-direction:column;gap:5px;flex:1;min-width:0;">
<span class="skeleton skeleton-text skeleton-text--med"></span>
<span class="skeleton skeleton-text skeleton-text--tiny" style="width:140px;"></span>
</span>
<span class="chev skeleton skeleton-chevron"></span>
</div>`).join('');
}
function setLeagueDetailSkeleton(root) {
if (!root) return;
root.innerHTML = `
<section class="profile-hero" aria-hidden="true">
<span class="skeleton skeleton-logo skeleton-logo--xl"></span>
<h2><span class="skeleton skeleton-text skeleton-text--med" style="width:180px;display:inline-block;"></span></h2>
<p><span class="skeleton skeleton-text skeleton-text--tiny" style="width:90px;display:inline-block;"></span></p>
</section>
<div class="card"><div class="card-title">TOP TEAMS</div>
<div class="table-card">
${Array.from({ length: 5 }, () => `
<div class="table-row skeleton-row" aria-hidden="true">
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--med"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-score skeleton-score--sm"></span>
</div>`).join('')}
</div>
</div>`;
}
function setStandingsSkeleton(tableBody, scorersWrap) {
if (tableBody) {
tableBody.innerHTML = Array.from({ length: 5 }, () => `
<div class="table-row skeleton-row" aria-hidden="true">
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--med"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-text skeleton-text--tiny"></span>
<span class="skeleton skeleton-score skeleton-score--sm"></span>
</div>`).join('');
}
if (scorersWrap) {
scorersWrap.innerHTML = Array.from({ length: 4 }, () => `
<div class="scorer-row" aria-hidden="true">
<div class="scorer-photo skeleton"></div>
<div class="pname"><span class="skeleton skeleton-text skeleton-text--med" style="display:inline-block;width:120px;"></span><span class="pmeta"><span class="skeleton skeleton-text skeleton-text--tiny" style="display:inline-block;width:90px;"></span></span></div>
<span class="gnum"><span class="skeleton skeleton-score skeleton-score--sm"></span></span>
</div>`).join('');
}
}
function setMatchListSkeleton(container, rows = 3) {
if (!container) return;
container.innerHTML = `
<div class="match-list skeleton-list">
${Array.from({ length: rows }, () => `
<div class="match-row skeleton-row" aria-hidden="true">
<span class="skeleton skeleton-dot"></span>
<div class="home"><span class="skeleton skeleton-logo"></span><span class="skeleton skeleton-text skeleton-text--short"></span></div>
<div class="center"><span class="skeleton skeleton-pill"></span><span class="skeleton skeleton-text skeleton-text--tiny"></span></div>
<div class="away"><span class="skeleton skeleton-text skeleton-text--short"></span><span class="skeleton skeleton-logo"></span></div>
<span class="skeleton skeleton-star"></span>
</div>`).join('')}
</div>`;
}
const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve));
/* ============ AUTH (localStorage demo) ============ */
const AUTH_KEY = "lscores_user";
function getUser() { try { return JSON.parse(localStorage.getItem(AUTH_KEY) || "null"); } catch { return null; } }
function setUser(u) { localStorage.setItem(AUTH_KEY, JSON.stringify(u)); }
function clearUser() { localStorage.removeItem(AUTH_KEY); }
function requireLogin(returnUrl) {
const u = getUser();
if (!u) { window.location.href = `login.html?redirect=${encodeURIComponent(returnUrl || location.pathname.split("/").pop())}`; return false; }
return true;
}
/* ============ FAVOURITES ============ */
const FAV_KEY = "lscores_favs";
function getFavs() { try { return JSON.parse(localStorage.getItem(FAV_KEY) || "[]"); } catch { return []; } }
function setFavs(a){ localStorage.setItem(FAV_KEY, JSON.stringify(a)); }
function isFav(id) { return getFavs().includes(id); }
function toggleFav(id) {
const f = getFavs(); const i = f.indexOf(id);
if (i>=0) f.splice(i,1); else f.push(id);
setFavs(f); return i<0;
}
/* Sort matches so favourites bubble to the top. */
function sortByFav(arr) {
return arr.slice().sort((a, b) => (isFav(b.id)?1:0) - (isFav(a.id)?1:0));
}
/* Re-sort an existing .match-list container so favourites move to top instantly. */
function resortContainer(container) {
if (!container) return;
const rows = Array.from(container.querySelectorAll(".match-row[data-id]"));
rows.sort((a, b) => (isFav(b.dataset.id)?1:0) - (isFav(a.dataset.id)?1:0));
rows.forEach(r => container.appendChild(r));
}
function resortAllFavs() {
document.querySelectorAll(".match-list").forEach(resortContainer);
}
/* ============ Helpers ============ */
const CODE_TO_FULL_NAME = {
SBP: "Sporting CP B",
LUJ: "Club Luján",
DDC: "Deportivo D.C.",
EDS: "Elite Development Squad",
VS2: "Vasco da Gama 2",
NOB: "Newell's Old Boys",
MNC: "Manchester City",
ARS: "Arsenal",
RMA: "Real Madrid",
BAR: "Barcelona",
LBA: "Las Palmas",
ARG: "Argentinos Juniors",
USB: "Union Berlin",
HBS: "Hannover 96",
SMLH: "St Mirren",
STM: "St Mirren",
VSC: "Vasco da Gama"
};
const CODE_TO_EXPECTED_NAME = {
MNC: "Manchester City",
ARS: "Arsenal",
RMA: "Real Madrid",
BAR: "Barcelona",
LBA: "Las Palmas",
USB: "Union Berlin",
HBS: "Hannover 96",
STM: "St Mirren",
SMLH: "St Mirren",
VSC: "Vasco da Gama",
SBP: "Sporting CP B",
NOB: "Newell's Old Boys",
ARG: "Argentinos Juniors",
EDS: "Elite Development Squad",
VS2: "Vasco da Gama 2"
};
const CLUB_CRESTS = {
MNC: "https://upload.wikimedia.org/wikipedia/en/e/eb/Manchester_City_FC_badge.svg",
ARS: "https://upload.wikimedia.org/wikipedia/en/5/53/Arsenal_FC.svg",
RMA: "https://upload.wikimedia.org/wikipedia/en/5/56/Real_Madrid_CF.svg",
BAR: "https://upload.wikimedia.org/wikipedia/en/4/47/FC_Barcelona_%28crest%29.svg",
LBA: "https://upload.wikimedia.org/wikipedia/en/0/09/UD_Las_Palmas_logo.svg",
USB: "https://upload.wikimedia.org/wikipedia/commons/4/44/1._FC_Union_Berlin_Logo.svg",
HBS: "https://upload.wikimedia.org/wikipedia/commons/c/cd/Hannover_96_Logo.svg",
STM: "https://upload.wikimedia.org/wikipedia/en/b/b3/St_Mirren_F.C._crest.svg",
SMLH: "https://upload.wikimedia.org/wikipedia/en/b/b3/St_Mirren_F.C._crest.svg",
VSC: "https://upload.wikimedia.org/wikipedia/en/a/a2/CRVascoDaGama.svg",
SBP: "https://upload.wikimedia.org/wikipedia/en/3/3e/Sporting_CP.svg",
NOB: "https://upload.wikimedia.org/wikipedia/commons/e/e3/Newell%27s_Old_Boys_logo.svg",
ARG: "https://upload.wikimedia.org/wikipedia/commons/c/c7/Logo_Argentinos_Juniors.svg",
EDS: "https://upload.wikimedia.org/wikipedia/en/e/eb/Manchester_City_FC_badge.svg",
VS2: "https://upload.wikimedia.org/wikipedia/en/a/a2/CRVascoDaGama.svg"
};
const COUNTRY_FLAGS = {
ARGENTINA: 'ar',
BRAZIL: 'br',
BELARUS: 'by',
AUSTRALIA: 'au',
CAMEROON: 'cm',
CANADA: 'ca',
CHILE: 'cl',
CHINA: 'cn',
ESTONIA: 'ee',
ETHIOPIA: 'et',
"FAROE ISLANDS": 'fo',
FINLAND: 'fi',
ICELAND: 'is',
LATVIA: 'lv',
LEBANON: 'lb',
LITHUANIA: 'lt',
LUXEMBOURG: 'lu',
MALAWI: 'mw',
MOLDOVA: 'md',
MONGOLIA: 'mn',
MOROCCO: 'ma',
NORWAY: 'no',
PARAGUAY: 'py',
PERU: 'pe',
RUSSIA: 'ru',
"SOUTH KOREA": 'kr',
"KOREA REPUBLIC": 'kr',
SWEDEN: 'se',
SYRIA: 'sy',
TAJIKISTAN: 'tj',
URUGUAY: 'uy',
USA: 'us',
"UNITED STATES": 'us',
VENEZUELA: 've',
ZIMBABWE: 'zw',
ENGLAND: 'gb-eng',
FRANCE: 'fr',
GERMANY: 'de',
SPAIN: 'es',
ITALY: 'it',
PORTUGAL: 'pt',
NETHERLANDS: 'nl',
BELGIUM: 'be',
CROATIA: 'hr',
MEXICO: 'mx',
JAPAN: 'jp',
ALGERIA: 'dz',
SENEGAL: 'sn',
"SOUTH AFRICA": 'za',
AUSTRIA: 'at',
SWITZERLAND: 'ch',
TURKEY: 'tr',
SCOTLAND: 'gb-sct',
WALES: 'gb-wls',
"NORTHERN IRELAND": 'gb-nir',
DENMARK: 'dk',
GREECE: 'gr',
UKRAINE: 'ua',
POLAND: 'pl',
AZERBAIJAN: 'az',
SLOVAKIA: 'sk',
SLOVENIA: 'si',
ARMENIA: 'am',
BULGARIA: 'bg',
ROMANIA: 'ro',
BOSNIA: 'ba',
"BOSNIA AND HERZEGOVINA": 'ba',
ALBANIA: 'al',
SERBIA: 'rs',
COLOMBIA: 'co',
GUATEMALA: 'gt',
KAZAKHSTAN: 'kz',
KYRGYZSTAN: 'kg',
BHUTAN: 'bt'
};
const TEAM_TO_COUNTRY = {
"PREUSSEN MUNSTER 2": "Germany",
"PREUSSEN MUNSTER": "Germany",
"SABAH BAKU": "Azerbaijan",
"MEDZEV": "Slovakia",
"ALUMINIJ": "Slovenia",
"NOAH": "Armenia",
"MLADOST": "Serbia",
"ONHAYE": "Belgium",
"OC CHARLEROI": "Belgium",
"AMERICA DE CALI": "Colombia",
"NEW YORK COSMOS": "USA",
"SANTOS LAGUNA": "Mexico",
"GUATEMALA U20": "Guatemala",
"SCHWEINFURT": "Germany",
"BANIK PRIEVIDZA": "Slovakia",
"UFA": "Russia",
"FURTH II": "Germany",
"GORNIK ZABRZE": "Poland",
"DROCHTERSEN": "Germany",
"K. LIERSE S.K.": "Belgium",
"SARPSBORG 08": "Norway",
"KAISAR KYZYLORDA": "Kazakhstan",
"ORDABASY": "Kazakhstan",
"FC ASTANA": "Kazakhstan",
"ALTAI": "Kazakhstan",
"TALANT": "Kyrgyzstan",
"OZGON": "Kyrgyzstan",
"CSKA 1948 SOFIA": "Bulgaria",
"CSKA 1948": "Bulgaria",
"SOFIA": "Bulgaria",
"PARTIZAN": "Serbia",
"FC ARGES": "Romania",
"ARGES": "Romania",
"VLLAZNIA": "Albania",
"PARI NN": "Russia",
"TEKSTILSHTIK": "Russia",
"ODENSE": "Denmark",
"KOLDING IF": "Denmark",
"KOLDING": "Denmark",
"AKHMAT GROZNY": "Russia",
"AKHMAT": "Russia",
"BORAC BANJA LUKA": "Bosnia",
"BORAC": "Bosnia",
"BANJA LUKA": "Bosnia",
"UNIV. CRAIOVA": "Romania",
"CRAIOVA": "Romania",
"POLISSYA ZHYTOMYR": "Ukraine",
"POLISSYA": "Ukraine",
"ZHYTOMYR": "Ukraine",
"CRVENA ZVEZDA": "Serbia",
"SLOVAN BRATISLAVA": "Slovakia",
"SLOVAN": "Slovakia",
"BRATISLAVA": "Slovakia",
"RKDSV": "Netherlands",
"WILLEM II": "Netherlands",
"HANDW WELDERS": "Northern Ireland",
"LARNE": "Northern Ireland"
};
function normalizeTeamName(name) {
if (!name) return "";
let n = name.toLowerCase();
// Remove accents
n = n.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// Remove punctuation
n = n.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, " ");
// Remove common football suffixes/prefixes
const suffixes = [
"fc", "f c", "sc", "s c", "ec", "e c", "ac", "a c",
"club", "atletico", "deportivo", "cd", "c d", "sd", "s d", "ud", "u d", "ad", "a d",
"u20", "u23", "reserves", "reserve", "ii", "iii", "b team", "junior", "juniors", "youth",
"academy", "playoffs", "playoff", "sporting", "sportivo", "real", "union", "association",
"athletic", "clube", "club de futbol", "club de regatas", "social y deportivo",
"y deportivo", "social", "b", "2", "ii"
];
for (let s of suffixes) {
const regex = new RegExp(`\\b${s}\\b`, 'gi');
n = n.replace(regex, "");
}
// Collapse spaces
n = n.replace(/\s+/g, " ").trim();
return n;
}
function resolveTeamAlias(name) {
if (!name) return "";
let n = name.toUpperCase().trim();
// Standard expansions
n = n.replace(/\bDIN\.\b/g, "DINAMO");
n = n.replace(/\bATL\.\b/g, "ATLETICO");
n = n.replace(/\bDEP\.\b/g, "DEPORTIVO");
n = n.replace(/\bSP\.\b/g, "SPORTING");
const aliases = {
"DIN MINSK 2": "DINAMO MINSK 2",
"ATLETICO GO": "ATLETICO GOIANIENSE",
"SPORT RECIFE": "SPORT CLUB DO RECIFE",
"AVAI": "AVAI FC",
"CEARA": "CEARA SC",
"GOIAS": "GOIAS EC",
"PONTE PRETA": "AA PONTE PRETA",
"NAUTICO": "CLUBE NAUTICO CAPIBARIBE",
"BATE 2": "BATE BORISOV 2",
"NEMAN": "NEMAN GRODNO",
"DNEPR MOGILEV": "DNEPR MOGILEV"
};
if (aliases[n]) return aliases[n];
let clean = normalizeTeamName(n);
if (window.TEAM_LOGOS_MAPPING) {
const keys = Object.keys(window.TEAM_LOGOS_MAPPING);
for (let k of keys) {
if (normalizeTeamName(k) === clean) {
return k;
}
}
}
return name;
}
function isValidLogoImage(url, width, height) {
if (!url) return false;
if (url === "NO_IMAGE" || url.trim() === "") return false;
// Reject common non-logo patterns in Wikipedia files
const rejectedKeywords = /stadiu|stade|estadio|estacio|arena|pitch|field|crowd|fans|supporter|view|panoramic|match|versus|playing|action|jersey|shirt|photo|portrait|player|manager|coach|trainer|referee|lineup|group|building|facade|entrance|ground|aerial|skyline|complex|center|centre|complejo|sportpark|park|infobox_football|file:|wiki\/file/i;
if (rejectedKeywords.test(url)) return false;
// Reject landscape rectangular photos (logos are roughly square or portrait)
if (width && height) {
const ratio = width / height;
if (ratio > 1.25 || ratio < 0.5) return false;
}
return true;
}
// Logo cache
const wikiCache = {};
try {
const saved = localStorage.getItem("NK_WIKI_LOGOS");
if (saved) Object.assign(wikiCache, JSON.parse(saved));
} catch (e) {}
function saveWikiCache() {
try {
localStorage.setItem("NK_WIKI_LOGOS", JSON.stringify(wikiCache));
} catch (e) {}
}
const pendingFetches = {};
window.UNRESOLVED_TEAMS = window.UNRESOLVED_TEAMS || [];
function logUnresolvedTeam(code, teamName) {
const key = `${code} - ${teamName}`;
if (!window.UNRESOLVED_TEAMS.includes(key)) {
window.UNRESOLVED_TEAMS.push(key);
console.info("[NK Logo System] Unresolved logo:", key);
}
}
function fetchLogoFromWikipedia(code, teamName, matchContext = {}) {
const cacheKey = String(teamName || code || "").toUpperCase();
if (wikiCache[cacheKey] || pendingFetches[cacheKey]) return;
pendingFetches[cacheKey] = true;
const normalized = normalizeTeamName(teamName || code);
if (!normalized) {
wikiCache[cacheKey] = "NO_IMAGE";
saveWikiCache();
logUnresolvedTeam(code, teamName || code);
return;
}
const queryStr = encodeURIComponent(normalized + " football");
const url = `https://en.wikipedia.org/w/api.php?action=query&generator=search&gsrsearch=${queryStr}&gsrlimit=1&prop=pageimages&piprop=thumbnail&pithumbsize=120&format=json&origin=*`;
fetch(url)
.then(res => res.json())
.then(data => {
let logoUrl = "NO_IMAGE";
if (data && data.query && data.query.pages) {
const pages = data.query.pages;
const pageId = Object.keys(pages)[0];
if (pageId && pages[pageId].thumbnail) {
const thumb = pages[pageId].thumbnail;
if (isValidLogoImage(thumb.source, thumb.width, thumb.height)) {
logoUrl = thumb.source;
}
}
}
wikiCache[cacheKey] = logoUrl;
saveWikiCache();
delete pendingFetches[cacheKey];
if (logoUrl === "NO_IMAGE") {
logUnresolvedTeam(code, teamName || code);
}
updateImagesInDOM(code, teamName, logoUrl);
})
.catch(err => {
console.warn("Wikipedia logo fetch error for", teamName, err);
delete pendingFetches[cacheKey];
});
}
function updateImagesInDOM(code, teamName, logoUrl) {
const upperTeam = String(teamName || "").toUpperCase();
const upperCode = String(code || "").toUpperCase();
document.querySelectorAll(".logo-loading-placeholder").forEach(el => {
const elTeam = String(el.dataset.team || "").toUpperCase();
const elCode = String(el.dataset.code || "").toUpperCase();
if (elTeam === upperTeam || elCode === upperCode || resolveTeamAlias(elTeam) === upperTeam || resolveTeamAlias(elCode) === upperCode) {
const sizeClass = el.classList.contains("lg") ? "lg" : "";
const cls = el.className.replace("logo-loading-placeholder", "").trim();
let displayUrl = (logoUrl && logoUrl !== "NO_IMAGE") ? logoUrl : null;
let leagueText = "";
const leagueSec = el.closest(".league-section");
if (leagueSec) {
const nameEl = leagueSec.querySelector(".league-name");
if (nameEl) leagueText = nameEl.textContent.trim();
}
const dummyMatch = { league_name: leagueText };
const fallbackFlag = getFlagUrlForFallback(el.dataset.code, el.dataset.team, dummyMatch) || "";
if (!displayUrl) {
displayUrl = fallbackFlag;
}
if (displayUrl) {
const img = document.createElement("img");
img.className = `logo ${sizeClass} logo-img`;
img.src = displayUrl;
img.alt = el.dataset.team || el.dataset.code;
img.loading = "lazy";
img.setAttribute("data-fallback-flag", displayUrl === fallbackFlag ? "" : fallbackFlag);
img.setAttribute("data-initials", el.textContent);
img.onerror = function() {
handleLogoError(this);
};
el.replaceWith(img);
} else {
el.outerHTML = `<span class="logo ${sizeClass} ${cls}">${el.textContent}</span>`;
}
}
});
}
function initLogoDebugTool() {
if (!location.search.includes("debug=logos") && !location.search.includes("debug=1")) return;
const div = document.createElement("div");
div.id = "logo-debug-panel";
div.style.cssText = "position:fixed;bottom:10px;right:10px;z-index:9999;background:#0d1117;border:1px solid #ff4757;padding:12px;border-radius:8px;max-width:300px;max-height:200px;overflow-y:auto;font-family:monospace;font-size:11px;color:#ff4757;box-shadow:0 4px 12px rgba(0,0,0,0.5);";
div.innerHTML = `<strong>Unresolved Logos</strong><div id="logo-debug-list" style="margin-top:6px;">None yet</div>`;
document.body.appendChild(div);
setInterval(() => {
const list = window.UNRESOLVED_TEAMS || [];
const container = document.getElementById("logo-debug-list");
if (container) {
container.innerHTML = list.length ? list.map(item => `<div>• ${item}</div>`).join("") : "None yet";
}
}, 1000);
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initLogoDebugTool();
});
} else {
initLogoDebugTool();
}
}
// Load teamLogos.js dynamically on startup
(function() {
const script = document.createElement("script");
const isWP = typeof WP_PLUGIN_URL !== 'undefined';
script.src = isWP ? WP_PLUGIN_URL + "/assets/js/teamLogos.js" : "assets/js/teamLogos.js";
script.onload = () => {
if (window.TEAM_LOGOS_MAPPING) {
document.querySelectorAll(".logo-loading-placeholder").forEach(el => {
const elTeam = String(el.dataset.team || "").toUpperCase();
const elCode = String(el.dataset.code || "").toUpperCase();
const expandedTeam = resolveTeamAlias(elTeam).toUpperCase();
const expandedCode = resolveTeamAlias(elCode).toUpperCase();
const resolvedUrl = window.TEAM_LOGOS_MAPPING[expandedTeam] || window.TEAM_LOGOS_MAPPING[expandedCode];
if (resolvedUrl) {
const sizeClass = el.classList.contains("lg") ? "lg" : "";
const cls = el.className.replace("logo-loading-placeholder", "").trim();
let leagueText = "";
const leagueSec = el.closest(".league-section");
if (leagueSec) {
const nameEl = leagueSec.querySelector(".league-name");
if (nameEl) leagueText = nameEl.textContent.trim();
}
const dummyMatch = { league_name: leagueText };
const fallbackFlag = getFlagUrlForFallback(el.dataset.code, el.dataset.team, dummyMatch) || "";
const img = document.createElement("img");
img.className = `logo ${sizeClass} logo-img`;
img.src = resolvedUrl;
img.alt = el.dataset.team || el.dataset.code;
img.loading = "lazy";
img.setAttribute("data-fallback-flag", fallbackFlag);
img.setAttribute("data-initials", el.textContent);
img.onerror = function() {
handleLogoError(this);
};
el.replaceWith(img);
}
});
}
};
document.head.appendChild(script);
})();
function getCountryFlag(countryName) {
if (!countryName) return null;
const upper = String(countryName).trim().toUpperCase();
if (COUNTRY_FLAGS[upper]) {
return `https://flagcdn.com/w40/${COUNTRY_FLAGS[upper]}.png`;
}
const keys = Object.keys(COUNTRY_FLAGS);
for (let k of keys) {
if (upper.includes(k) || k.includes(upper)) {
return `https://flagcdn.com/w40/${COUNTRY_FLAGS[k]}.png`;
}
}
return null;
}
function getFlagUrlForFallback(code, teamName, matchContext = {}) {
const upperCode = String(code || "").toUpperCase();
const finalName = teamName || code;
const upperName = String(finalName || "").toUpperCase();
const teamCountryCode = COUNTRY_FLAGS[upperName] || COUNTRY_FLAGS[upperCode];
if (teamCountryCode) {
return `https://flagcdn.com/w40/${teamCountryCode.toLowerCase()}.png`;
}
const mappedCountry = TEAM_TO_COUNTRY[upperName] || TEAM_TO_COUNTRY[upperCode];
if (mappedCountry) {
const lCode = COUNTRY_FLAGS[mappedCountry.toUpperCase()];
if (lCode) {
return `https://flagcdn.com/w40/${lCode.toLowerCase()}.png`;
}
}
const countriesKeys = Object.keys(COUNTRY_FLAGS);
for (let countryKey of countriesKeys) {
if (upperName.includes(countryKey)) {
return `https://flagcdn.com/w40/${COUNTRY_FLAGS[countryKey].toLowerCase()}.png`;
}
}
const leagueName = String(matchContext.league_name || matchContext.league || "");
if (leagueName.includes(":")) {
const lCountry = leagueName.split(":")[0].trim().toUpperCase();
const lCode = COUNTRY_FLAGS[lCountry];
if (lCode) {
return `https://flagcdn.com/w40/${lCode.toLowerCase()}.png`;
}
}
return null;
}
window.handleLogoError = function(img) {
if (!img) return;
const fallbackFlag = img.getAttribute("data-fallback-flag");
if (fallbackFlag && img.src !== fallbackFlag) {
img.src = fallbackFlag;
img.setAttribute("data-fallback-flag", "");
} else {
const initials = img.getAttribute("data-initials") || "HOM";
const sizeClass = img.classList.contains("lg") ? "lg" : "";
const cls = img.className.replace("logo-img", "").replace("logo", "").replace("lg", "").trim();
img.outerHTML = `<span class="logo ${sizeClass} ${cls}">${initials}</span>`;
}
};
function resolveTeamVisual(code, teamName, matchContext = {}) {
const upperCode = String(code || "").toUpperCase();
const finalName = teamName || code;
const upperName = String(finalName || "").toUpperCase();
const expandedName = resolveTeamAlias(finalName);
const expandedCode = resolveTeamAlias(code);
if (window.TEAM_LOGOS_MAPPING) {
const kName = expandedName.toUpperCase();
const kCode = expandedCode.toUpperCase();
if (window.TEAM_LOGOS_MAPPING[kName]) return window.TEAM_LOGOS_MAPPING[kName];
if (window.TEAM_LOGOS_MAPPING[kCode]) return window.TEAM_LOGOS_MAPPING[kCode];
}
if (matchContext) {
const hLogo = matchContext.home_logo || matchContext.homeLogo ||
(matchContext.home_team && (typeof matchContext.home_team === 'object' ? (matchContext.home_team.logo || matchContext.home_team.crest) : null)) ||
(matchContext.homeTeam && (typeof matchContext.homeTeam === 'object' ? (matchContext.homeTeam.logo || matchContext.homeTeam.crest) : null)) ||
matchContext.home_crest || matchContext.homeCrest;
const aLogo = matchContext.away_logo || matchContext.awayLogo ||
(matchContext.away_team && (typeof matchContext.away_team === 'object' ? (matchContext.away_team.logo || matchContext.away_team.crest) : null)) ||
(matchContext.awayTeam && (typeof matchContext.awayTeam === 'object' ? (matchContext.awayTeam.logo || matchContext.awayTeam.crest) : null)) ||
matchContext.away_crest || matchContext.awayCrest;
const hCode = String(matchContext.home_short || matchContext.home || matchContext.home_code || "").toUpperCase();
const aCode = String(matchContext.away_short || matchContext.away || matchContext.away_code || "").toUpperCase();
if (upperCode === hCode && hLogo) return hLogo;
if (upperCode === aCode && aLogo) return aLogo;
const hName = String(typeof matchContext.home_team === 'string' ? matchContext.home_team : (matchContext.homeTeam || matchContext.homeName || "")).toUpperCase();
const aName = String(typeof matchContext.away_team === 'string' ? matchContext.away_team : (matchContext.awayTeam || matchContext.awayName || "")).toUpperCase();
if (upperName && upperName === hName && hLogo) return hLogo;
if (upperName && upperName === aName && aLogo) return aLogo;
}
if (matchContext.logo) return matchContext.logo;
if (matchContext.crest) return matchContext.crest;
if (matchContext.logoUrl) return matchContext.logoUrl;
const expected = CODE_TO_EXPECTED_NAME[upperCode];
if (expected) {
const normExpected = normalizeTeamName(expected);
const normActual = normalizeTeamName(finalName);
if (!normActual || normActual.includes(normExpected) || normExpected.includes(normActual)) {
return CLUB_CRESTS[upperCode];
}
}
const expectedByName = CODE_TO_EXPECTED_NAME[upperName];
if (expectedByName && CLUB_CRESTS[upperName]) return CLUB_CRESTS[upperName];
const cachedUrl = wikiCache[upperName] || wikiCache[upperCode];
if (cachedUrl && cachedUrl !== "NO_IMAGE") {
return cachedUrl;
}
const teamCountryCode = COUNTRY_FLAGS[upperName] || COUNTRY_FLAGS[upperCode];
if (teamCountryCode) {
return `https://flagcdn.com/w40/${teamCountryCode.toLowerCase()}.png`;
}
const mappedCountry = TEAM_TO_COUNTRY[upperName] || TEAM_TO_COUNTRY[upperCode];
if (mappedCountry) {
const lCode = COUNTRY_FLAGS[mappedCountry.toUpperCase()];
if (lCode) {
return `https://flagcdn.com/w40/${lCode.toLowerCase()}.png`;
}
}
const countriesKeys = Object.keys(COUNTRY_FLAGS);
for (let countryKey of countriesKeys) {
if (upperName.includes(countryKey)) {
return `https://flagcdn.com/w40/${COUNTRY_FLAGS[countryKey].toLowerCase()}.png`;
}
}
const leagueName = String(matchContext.league_name || matchContext.league || "").toUpperCase();
if (leagueName.includes(":")) {
const lCountry = leagueName.split(":")[0].trim();
const lCode = COUNTRY_FLAGS[lCountry];
if (lCode) {
return `https://flagcdn.com/w40/${lCode.toLowerCase()}.png`;
}
}
return null;
}
function getTeamDisplayAsset(code, teamName, matchContext = {}) {
return resolveTeamVisual(code, teamName, matchContext);
}
function getTeamDisplayName(team) {
if (!team) return "";
if (typeof team === 'string') {
const upperCode = team.toUpperCase();
if (typeof TEAMS !== 'undefined' && TEAMS[upperCode]) {
return getTeamDisplayName(TEAMS[upperCode]);
}
if (CODE_TO_FULL_NAME[upperCode]) {
return CODE_TO_FULL_NAME[upperCode];
}
return team;
}
const name = team.fullName || team.full_name || team['full name'] ||
team.name ||
team.displayName || team.display_name || team['display name'] ||
team.teamName || team.team_name || team['team name'] ||
team.shortName || team.short_name || team['short name'] || team.short ||
team.code ||
team.abbreviation || team.abbr;
if (name) {
if (typeof name === 'string') {
const upperName = name.toUpperCase();
if (CODE_TO_FULL_NAME[upperName]) {
return CODE_TO_FULL_NAME[upperName];
}
}
return name;
}
return "";
}
function el(html) { const t=document.createElement("template"); t.innerHTML=html.trim(); return t.content.firstChild; }
function logo(code, big=false, teamName="", matchContext={}) {
const upperCode = String(code || "").toUpperCase();
const t = (typeof TEAMS !== 'undefined' && TEAMS[upperCode]) || { short: code, name: teamName || code, cls: "" };
const initials = t.short || upperCode.slice(0, 3);
const sizeClass = big ? "lg" : "";
const finalName = t.name || teamName || code;
const imageUrl = resolveTeamVisual(code, finalName, matchContext);
const fallbackFlag = getFlagUrlForFallback(code, finalName, matchContext) || "";
if (imageUrl) {
return `<img class="logo ${sizeClass} logo-img" src="${imageUrl}" alt="${finalName}" loading="lazy" data-fallback-flag="${imageUrl === fallbackFlag ? "" : fallbackFlag}" data-initials="${initials}" onerror="handleLogoError(this)">`;
}
const cacheKey = String(finalName).toUpperCase();
if (wikiCache[cacheKey] === "NO_IMAGE") {
logUnresolvedTeam(code, finalName);
if (fallbackFlag) {
return `<img class="logo ${sizeClass} logo-img" src="${fallbackFlag}" alt="${finalName}" loading="lazy" data-fallback-flag="" data-initials="${initials}" onerror="handleLogoError(this)">`;
}
return `<span class="logo ${sizeClass} ${t.cls}">${initials}</span>`;
}
if (wikiCache[cacheKey] === undefined) {
fetchLogoFromWikipedia(code, finalName, matchContext);
const escapedName = finalName.replace(/'/g, "\\'");
return `<span class="logo ${sizeClass} logo-loading-placeholder ${t.cls}" data-team="${escapedName}" data-code="${upperCode}">${initials}</span>`;
}
if (fallbackFlag) {
return `<img class="logo ${sizeClass} logo-img" src="${fallbackFlag}" alt="${finalName}" loading="lazy" data-fallback-flag="" data-initials="${initials}" onerror="handleLogoError(this)">`;
}
return `<span class="logo ${sizeClass} ${t.cls}">${initials}</span>`;
}
function qs(name) { return new URLSearchParams(location.search).get(name); }
/* ============ Match row ============ */
function renderMatchRow(m, opts={}) {
const fav = isFav(m.id);
const homeRaw = m.home_team || m.homeTeam || m.home || m.homeName || m.home_short || m.homeShort || 'HOM';
const awayRaw = m.away_team || m.awayTeam || m.away || m.awayName || m.away_short || m.awayShort || 'AWY';
const homeName = getTeamDisplayName(homeRaw);
const awayName = getTeamDisplayName(awayRaw);
const homeShort = m.home_short || m.homeShort || m.home || (typeof homeRaw === 'string' ? homeRaw.slice(0, 3) : 'HOM');
const awayShort = m.away_short || m.awayShort || m.away || (typeof awayRaw === 'string' ? awayRaw.slice(0, 3) : 'AWY');
const node = el(`
<a class="match-row" href="match-detail.html?id=${m.id}" data-id="${m.id}" data-live="${m.live}">
<span class="live-dot ${m.live?"":"off"}"></span>
<div class="home">${logo(homeShort, false, homeName, m)}<span class="name">${homeName}${m.redCards?'<span class="red-card"></span>':''}</span></div>
<div class="center">
<span class="score-pill">${m.live || m.status==="FT" ? `${m.scoreH ?? m.home_score ?? 0} - ${m.scoreA ?? m.away_score ?? 0}` : m.minute}</span>
${m.live ? `<span class="minute">${m.minute}</span>` : `<small class="meta-time">${m.status==="FT"?"FT":m.minute}</small>`}
</div>
<div class="away"><span class="name">${awayName}</span>${logo(awayShort, false, awayName, m)}</div>
<button class="star ${fav?"active":""}" aria-label="favourite">${fav?"★":"☆"}</button>
</a>`);
node.querySelector(".star").addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
if (!getUser()) {
// Gate favourites behind login
window.location.href = `login.html?redirect=${encodeURIComponent(location.pathname.split("/").pop()+location.search)}`;
return;
}
const active = toggleFav(m.id);
e.currentTarget.classList.toggle("active", active);
e.currentTarget.textContent = active ? "★" : "☆";
// Move favourite rows to the top across all match lists on the page.
resortAllFavs();
});
return node;
}
/* =========================================================
HOME PAGE
========================================================= */
function renderHome() {
/* Hot matches (live + interesting) — clickable, with carousel */
const hot = document.getElementById("hot-row");
if (hot) {
const hotList = liveMatches().concat(MATCHES.filter(m => !m.live && m.status==="FT")).slice(0, 6);
hotList.forEach(m => {
const homeRaw = m.home_team || m.homeTeam || m.home || m.homeName || m.home_short || m.homeShort || 'HOM';
const awayRaw = m.away_team || m.awayTeam || m.away || m.awayName || m.away_short || m.awayShort || 'AWY';
const homeName = getTeamDisplayName(homeRaw);
const awayName = getTeamDisplayName(awayRaw);
const homeShort = m.home_short || m.homeShort || m.home || (typeof homeRaw === 'string' ? homeRaw.slice(0, 3) : 'HOM');
const awayShort = m.away_short || m.awayShort || m.away || (typeof awayRaw === 'string' ? awayRaw.slice(0, 3) : 'AWY');
const card = el(`
<a class="hot-card" href="match-detail.html?id=${m.id}">
<div class="team">${logo(homeShort, false, homeName, m)}<div><div class="name">${homeName}</div></div></div>
<div class="score">${m.scoreH ?? m.home_score ?? 0} - ${m.scoreA ?? m.away_score ?? 0}<small>${m.live ? m.minute : m.status}</small></div>
<div class="team right"><div><div class="name" style="text-align:right;">${awayName}</div></div>${logo(awayShort, false, awayName, m)}</div>
</a>`);
hot.appendChild(card);
});
setupHotCarousel(hot);
}
/* Initial league sections render — relies on filterByDay() */
const STATE = { day: "WE", liveOnly: false };
window._homeState = STATE;
renderLeagueSections(STATE);
/* Date strip */
document.querySelectorAll(".date-chip").forEach(chip => {
chip.addEventListener("click", () => {
document.querySelectorAll(".date-chip").forEach(c => c.classList.remove("live"));
chip.classList.add("live");
STATE.day = chip.dataset.day || "WE";
STATE.liveOnly = chip.dataset.day === "LIVE";
renderLeagueSections(STATE);
});
});
/* ALL LIVE toggle (simple live filter) */
const allLive = document.getElementById("all-live");
if (allLive) {
let on = false;
allLive.classList.toggle("off", !on);
allLive.addEventListener("click", () => {
on = !on; allLive.classList.toggle("off", !on);
STATE.liveOnly = on; renderLeagueSections(STATE);
});
}
setupLiveRefresh();
}
function renderLeagueSections(state) {
const list = document.getElementById("leagues-list");
if (!list) return;
list.innerHTML = "";
let pool = state.liveOnly ? liveMatches() : matchesByDay(state.day);
if (!pool.length) {
list.appendChild(el(`
<div class="empty"><div class="ic">📅</div>
<p>No matches available for this day.<br><small>Try another day from the strip above.</small></p>
</div>`));
return;
}
/* Group by league, then sort favourites to the top within each league. */
const byLeague = {};
pool.forEach(m => { (byLeague[m.league] = byLeague[m.league] || []).push(m); });
Object.entries(byLeague).forEach(([league, matches]) => {
const sec = el(`
<div class="league-section">
<div class="section-title">
<div class="league-name"><span class="league-badge"></span>${league}</div>
<a class="meta" href="standings.html" style="color:var(--accent);">SEE TABLE ›</a>
</div>
<div class="match-list" data-league="${league}"></div>
</div>`);
const ml = sec.querySelector(".match-list");
sortByFav(matches).forEach(m => ml.appendChild(renderMatchRow(m)));
list.appendChild(sec);
});
}
/* ============ Hot carousel (arrow + dots, swipeable) ============ */
function setupHotCarousel(track) {
const arrow = document.getElementById("hot-next");
const dotsWrap = document.getElementById("hot-dots");
const cards = track.querySelectorAll(".hot-card");
if (dotsWrap) {
dotsWrap.innerHTML = "";
cards.forEach((_, i) => {
const d = el(`<span class="dot ${i===0?"active":""}"></span>`);
d.addEventListener("click", () => scrollToCard(i));
dotsWrap.appendChild(d);
});
}
function scrollToCard(i) {
const card = cards[i]; if (!card) return;
track.scrollTo({ left: card.offsetLeft - track.offsetLeft, behavior: "smooth" });
}
function activeIndex() {
const x = track.scrollLeft;
let best = 0, bestDist = Infinity;
cards.forEach((c, i) => {
const d = Math.abs(c.offsetLeft - track.offsetLeft - x);
if (d < bestDist) { best = i; bestDist = d; }
});
return best;
}
function syncDots() {
if (!dotsWrap) return;
const idx = activeIndex();
dotsWrap.querySelectorAll(".dot").forEach((d,i) => d.classList.toggle("active", i===idx));
}
track.addEventListener("scroll", syncDots, { passive:true });
if (arrow) arrow.addEventListener("click", () => {
const next = (activeIndex() + 1) % cards.length;
scrollToCard(next);
});
}
/* ============ Live refresh (dummy ticker) ============ */
const REFRESH_INTERVAL_MS = 15000;
let refreshTimer = null;
function tickLive() {
liveMatches().forEach(m => {
const cur = parseInt(m.minute, 10) || 0;
m.minute = Math.min(cur + 1 + Math.floor(Math.random()*2), 90) + "'";
if (Math.random() < 0.12) (Math.random()<0.5 ? m.scoreH++ : m.scoreA++);
});
}
function refreshHome() {
if (window._homeState) renderLeagueSections(window._homeState);
const status = document.getElementById("refresh-status");
if (status) status.textContent = "LIVE · " + new Date().toTimeString().slice(0,5);
}
function setupLiveRefresh() {
const btn = document.getElementById("refresh-btn");
const doRefresh = (manual=false) => {
tickLive(); refreshHome();
if (manual && btn) {
const svg = btn.querySelector("svg");
svg && svg.classList.add("spinning");
setTimeout(() => svg && svg.classList.remove("spinning"), 700);
}
};
if (btn) btn.addEventListener("click", () => doRefresh(true));
const start = () => { if (!refreshTimer) refreshTimer = setInterval(() => doRefresh(false), REFRESH_INTERVAL_MS); };
const stop = () => { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } };
start();
document.addEventListener("visibilitychange", () => document.hidden ? stop() : start());
refreshHome();
}
/* =========================================================
FAVOURITES PAGE
========================================================= */
function renderFavourites() {
const wrap = document.getElementById("fav-list");
if (!wrap) return;
if (!getUser()) {
wrap.appendChild(el(`<div class="empty"><div class="ic">🔒</div><p>Please log in to see favourites.<br><a href="login.html" class="btn btn-primary" style="display:inline-block;margin-top:12px;">Log in</a></p></div>`));
return;
}
const favs = getFavs();
const matches = MATCHES.filter(m => favs.includes(m.id));
if (!matches.length) {
wrap.appendChild(el(`<div class="empty"><div class="ic">★</div><p>No favourites yet. Tap a star on any match to add it.</p></div>`));
return;
}
const list = el(`<div class="match-list"></div>`);
sortByFav(matches).forEach(m => list.appendChild(renderMatchRow(m)));
wrap.appendChild(list);
}
/* =========================================================
LEAGUES INDEX
========================================================= */
function getAllLeaguesFromMatches(matches) {
const leaguesMap = {};
matches.forEach(m => {
const leagueName = m.league || m.league_name || "Unknown League";
let country = m.country || "Unknown Country";
let displayName = leagueName;
if (leagueName.includes(":")) {
const parts = leagueName.split(":");
country = parts[0].trim();
displayName = parts.slice(1).join(":").trim();
}
const leagueId = m.league_id || m.leagueId || `${country}_${displayName}`;
if (!leaguesMap[leagueId]) {
leaguesMap[leagueId] = {
id: leagueId,
name: displayName,
fullName: leagueName,
country: country,
match_count: 0,
matches: [],
hasLiveMatches: false,
logo: m.league_logo || m.leagueLogo || null,
flag: m.league_flag || m.leagueFlag || null
};
}
leaguesMap[leagueId].match_count++;
leaguesMap[leagueId].matches.push(m);
if (m.live || m.status === "LIVE") {
leaguesMap[leagueId].hasLiveMatches = true;
}
});
return Object.values(leaguesMap).sort((a, b) => {
if (a.hasLiveMatches && !b.hasLiveMatches) return -1;
if (!a.hasLiveMatches && b.hasLiveMatches) return 1;
const countryCompare = a.country.localeCompare(b.country);
if (countryCompare !== 0) return countryCompare;
return a.name.localeCompare(b.name);
});
}
function groupLeaguesByCountry(leagues) {
const grouped = {};
leagues.forEach(l => {
const c = l.country || "Unknown Country";
if (!grouped[c]) grouped[c] = [];
grouped[c].push(l);
});
return grouped;
}
function filterLeaguesBySearch(leagues, searchText) {
if (!searchText) return leagues;
const query = searchText.toLowerCase().trim();
return leagues.filter(l =>
l.name.toLowerCase().includes(query) ||
l.country.toLowerCase().includes(query) ||
l.fullName.toLowerCase().includes(query)
);
}
function getMatchesByLeague(matches, selectedLeague) {
if (!selectedLeague) return matches;
return matches.filter(m => {
const leagueName = m.league || m.league_name || "Unknown League";
let country = m.country || "Unknown Country";
let displayName = leagueName;
if (leagueName.includes(":")) {
const parts = leagueName.split(":");
country = parts[0].trim();
displayName = parts.slice(1).join(":").trim();
}
const leagueId = m.league_id || m.leagueId || `${country}_${displayName}`;
return leagueId === selectedLeague.id;
});
}
async function renderLeaguesIndex() {
const wrap = document.getElementById("leagues-index");
if (!wrap) return;
// 1. Loading state (Skeleton layout)
wrap.innerHTML = `
<div class="leagues-page-layout">
<div class="leagues-sidebar-col">
<div class="leagues-search-box">
<div class="skeleton" style="height:36px; border-radius:18px;"></div>
</div>
<div class="leagues-scroll-area">
<div class="skeleton" style="height:48px; border-radius:8px; margin-bottom:12px;"></div>
<div class="skeleton" style="height:20px; width:80px; margin-bottom:12px;"></div>
<div class="skeleton" style="height:40px; border-radius:8px; margin-bottom:8px;"></div>
<div class="skeleton" style="height:40px; border-radius:8px; margin-bottom:8px;"></div>
</div>
</div>
<div class="leagues-main-col">
<div class="skeleton" style="height:88px; border-radius:18px; margin-bottom:16px;"></div>
<div class="skeleton" style="height:72px; border-radius:12px; margin-bottom:8px;"></div>
<div class="skeleton" style="height:72px; border-radius:12px; margin-bottom:8px;"></div>
</div>
</div>`;
await nextFrame();
// 2. Fetch matches
let allMatches = [];
try {
const matchesData = await apiFetch("/matches");
allMatches = matchesData?.matches || MATCHES;
} catch (err) {
console.error("Failed to load matches:", err);
allMatches = MATCHES;
}
// Local state for day filtering and searching
const filterState = {
day: "WE",
liveOnly: false,
searchText: "",
selectedLeague: null
};
function updateView() {
// A. Filter match pool by day / live status
let pool = filterState.liveOnly
? allMatches.filter(m => m.live || m.status === "LIVE")
: allMatches.filter(m => m.day === filterState.day);
// B. Extract leagues from the filtered match pool
const rawLeagues = getAllLeaguesFromMatches(pool);
const filteredLeagues = filterLeaguesBySearch(rawLeagues, filterState.searchText);
const grouped = groupLeaguesByCountry(filteredLeagues);
// Make sure selected league is still in the pool, otherwise fallback to null (All Leagues)
if (filterState.selectedLeague) {
const exists = rawLeagues.find(l => l.id === filterState.selectedLeague.id);
if (!exists) filterState.selectedLeague = null;
}
// C. Render full layout container
wrap.innerHTML = `
<div class="leagues-page-layout">
<!-- Sidebar Column (Desktop/Tablet) -->
<div class="leagues-sidebar-col">
<div class="leagues-search-box">
<input type="text" id="leagues-search-input" placeholder="Search leagues or countries..." value="${filterState.searchText}" />
</div>
<div class="leagues-scroll-area">
<button class="league-row-btn ${!filterState.selectedLeague ? "active" : ""}" id="all-leagues-btn">
<span class="league-badge" style="font-size: 14px;">🌍</span>
<span class="league-label-wrap">
<strong>All Leagues</strong>
<small>${pool.length} matches available</small>
</span>
</button>
<div id="leagues-list-container"></div>
</div>
</div>
<!-- Mobile Filter Selector -->
<div class="leagues-mobile-filter-bar">
<select id="leagues-mobile-dropdown" class="leagues-mobile-select">
<option value="all" ${!filterState.selectedLeague ? "selected" : ""}>🌍 All Leagues (${pool.length})</option>
${rawLeagues.map(l => `
<option value="${l.id}" ${filterState.selectedLeague && filterState.selectedLeague.id === l.id ? "selected" : ""}>
${l.country}: ${l.name} (${l.match_count})
</option>`).join("")}
</select>
</div>
<!-- Main Content Area -->
<div class="leagues-main-col">
<!-- Date filter chips -->
<div class="leagues-date-strip">
${["WE", "MO", "TU", "TH", "FR", "SA", "SU"].map(d => {
const label = d === "WE" ? "Today" : d;
return `<button class="date-chip ${filterState.day === d && !filterState.liveOnly ? "active" : ""}" data-day="${d}">${label}</button>`;
}).join("")}
<button class="date-chip ${filterState.liveOnly ? "active" : ""}" data-day="LIVE">🔴 Live Now</button>
</div>
<!-- Selected League Card -->
<div class="selected-league-header-card">
<div class="selected-league-title" id="current-league-title">
${filterState.selectedLeague ? `${filterState.selectedLeague.country}: ${filterState.selectedLeague.name}` : "ALL LEAGUES"}
</div>
<div class="selected-league-meta" id="current-league-meta">
${filterState.selectedLeague ? `${filterState.selectedLeague.match_count} matches available` : `Showing all ${pool.length} matches`}
</div>
</div>
<!-- Matches List -->
<div class="match-list" id="leagues-matches-container"></div>
</div>
</div>`;
// D. Render Grouped Leagues into Sidebar
const listContainer = document.getElementById("leagues-list-container");
if (listContainer) {
if (Object.keys(grouped).length === 0) {
listContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: var(--profile-muted, #a3a3a3); font-size: 13px;">No leagues found.</div>`;
} else {
Object.entries(grouped).forEach(([country, leagues]) => {
// Add country header
const head = document.createElement("div");
head.className = "leagues-country-header";
head.textContent = country;
listContainer.appendChild(head);
// Add league rows
leagues.forEach(l => {
const btn = document.createElement("button");
btn.className = `league-row-btn ${filterState.selectedLeague && filterState.selectedLeague.id === l.id ? "active" : ""}`;
// Get proper flag image
const flagUrl = getCountryFlag(l.country) || getCountryFlag(country);
let badgeHTML = `<span class="league-badge" style="font-size: 12px;">🏆</span>`;
if (flagUrl) {
badgeHTML = `<img class="league-badge" src="${flagUrl}" alt="${l.country}" />`;
}
btn.innerHTML = `
${badgeHTML}
<span class="league-label-wrap">
<strong>${l.name}</strong>
<small>${l.match_count} match${l.match_count > 1 ? "es" : ""}</small>
</span>`;
btn.addEventListener("click", () => {
filterState.selectedLeague = l;
updateView();
});
listContainer.appendChild(btn);
});
});
}
}
// E. Render Selected Matches
const matchesContainer = document.getElementById("leagues-matches-container");
if (matchesContainer) {
const activeMatches = getMatchesByLeague(pool, filterState.selectedLeague);
if (activeMatches.length === 0) {
matchesContainer.innerHTML = `
<div class="empty" style="padding: 40px;">
<div class="ic">🏆</div>
<p>No matches available for this selection.</p>
</div>`;
} else {
sortByFav(activeMatches).forEach(m => {
matchesContainer.appendChild(renderMatchRow(m));
});
}
}
// F. Setup Event Listeners
// Search input listener
const searchInput = document.getElementById("leagues-search-input");
if (searchInput) {
searchInput.addEventListener("input", e => {
filterState.searchText = e.target.value;
const rawL = getAllLeaguesFromMatches(pool);
const filteredL = filterLeaguesBySearch(rawL, filterState.searchText);
const newGrouped = groupLeaguesByCountry(filteredL);
if (listContainer) {
listContainer.innerHTML = "";
if (Object.keys(newGrouped).length === 0) {
listContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: var(--profile-muted, #a3a3a3); font-size: 13px;">No leagues found.</div>`;
} else {
Object.entries(newGrouped).forEach(([country, leagues]) => {
const head = document.createElement("div");
head.className = "leagues-country-header";
head.textContent = country;
listContainer.appendChild(head);
leagues.forEach(l => {
const btn = document.createElement("button");
btn.className = `league-row-btn ${filterState.selectedLeague && filterState.selectedLeague.id === l.id ? "active" : ""}`;
const flagUrl = getCountryFlag(l.country) || getCountryFlag(country);
let badgeHTML = `<span class="league-badge" style="font-size: 12px;">🏆</span>`;
if (flagUrl) {
badgeHTML = `<img class="league-badge" src="${flagUrl}" alt="${l.country}" />`;
}
btn.innerHTML = `
${badgeHTML}
<span class="league-label-wrap">
<strong>${l.name}</strong>
<small>${l.match_count} match${l.match_count > 1 ? "es" : ""}</small>
</span>`;
btn.addEventListener("click", () => {
filterState.selectedLeague = l;
updateView();
});
listContainer.appendChild(btn);
});
});
}
}
});
}
// "All Leagues" button listener
const allLeaguesBtn = document.getElementById("all-leagues-btn");
if (allLeaguesBtn) {
allLeaguesBtn.addEventListener("click", () => {
filterState.selectedLeague = null;
updateView();
});
}
// Mobile dropdown selection change listener
const mobileDropdown = document.getElementById("leagues-mobile-dropdown");
if (mobileDropdown) {
mobileDropdown.addEventListener("change", e => {
const val = e.target.value;
if (val === "all") {
filterState.selectedLeague = null;
} else {
const found = rawLeagues.find(l => l.id === val);
if (found) filterState.selectedLeague = found;
}
updateView();
});
}
// Date/live chips click listeners
document.querySelectorAll(".leagues-date-strip .date-chip").forEach(btn => {
btn.addEventListener("click", () => {
const day = btn.getAttribute("data-day");
if (day === "LIVE") {
filterState.liveOnly = true;
} else {
filterState.liveOnly = false;
filterState.day = day;
}
updateView();
});
});
}
// Initial draw
updateView();
}
/* =========================================================
MATCH DETAIL — fully dynamic from ?id=
========================================================= */
function renderMatchDetail() {
const id = qs("id") || "m1";
const m = matchById(id) || MATCHES[0];
const homeName = getTeamDisplayName(TEAMS[m.home]?.name || m.home);
const awayName = getTeamDisplayName(TEAMS[m.away]?.name || m.away);
/* Hero */
const hero = document.getElementById("match-hero");
if (hero) {
hero.innerHTML = `
<div class="team-block">${logo(m.home, true, homeName, m)}<span class="name">${homeName.toUpperCase()}</span></div>
<div class="big-score">${m.scoreH}-${m.scoreA}<div style="font-size:11px;color:var(--accent);font-weight:700;letter-spacing:1.5px;margin-top:4px;">${m.live?m.minute:m.status}</div></div>
<div class="team-block">${logo(m.away, true, awayName, m)}<span class="name">${awayName.toUpperCase()}</span></div>`;
}
const title = document.getElementById("match-title");
if (title) title.textContent = `${m.league.toUpperCase()} | ${homeName} vs ${awayName}`;
document.title = `${homeName} vs ${awayName} — Live Scores`;
/* Stats */
const stats = document.getElementById("stats-list");
if (stats) {
statsFor(m).forEach(s => {
const total = s.h + s.a || 1;
stats.appendChild(el(`
<div class="stat-row">
<div class="label"><span>${s.hLabel}</span><span class="name">${s.name}</span><span>${s.aLabel}</span></div>
<div class="bar"><div class="fill-h" style="width:${s.h/total*100}%"></div><div class="fill-a" style="width:${s.a/total*100}%"></div></div>
</div>`));
});
}
/* H2H */
const h2hList = document.getElementById("h2h-list");
if (h2hList) {
h2hFor(m).forEach(g => {
const homeShort = g.home_short || g.homeShort || g.home || 'HOM';
const awayShort = g.away_short || g.awayShort || g.away || 'AWY';
const hName = getTeamDisplayName(g.home_team || g.homeTeam || g.home || g.home_short);
const aName = getTeamDisplayName(g.away_team || g.awayTeam || g.away || g.away_short);
const dummyMatch = {
...g,
league_name: g.comp || g.league_name || g.league || m?.league || ''
};
h2hList.appendChild(el(`
<div class="match-row" style="cursor:default;">
<span class="live-dot off"></span>
<div class="home">${logo(homeShort, false, hName, dummyMatch)}<span class="name">${hName}</span></div>
<div class="center"><span class="score-pill">${g.scoreH ?? g.home_score ?? 0} - ${g.scoreA ?? g.away_score ?? 0}</span><small class="meta-time">${g.date}</small></div>
<div class="away"><span class="name">${aName}</span>${logo(awayShort, false, aName, dummyMatch)}</div><span></span>
</div>`));
});
}
/* Events */
const evWrap = document.getElementById("events-list");
if (evWrap) eventsFor(m).forEach(ev => evWrap.appendChild(el(`
<div class="event-row"><span class="time">${ev.time}</span><span class="ic">${ev.ic}</span><span class="desc">${ev.desc}</span></div>`)));
/* Lineups inside detail tab */
const linePanel = document.getElementById("panel-lineups");
if (linePanel) {
linePanel.innerHTML = `
<div class="card"><div class="card-title">FORMATIONS — 4-3-3 vs 4-2-3-1</div>
<div class="formations">
<div><div class="formation-label">${homeName}</div><div class="mini-pitch" id="pitch-home-d"></div></div>
<div><div class="formation-label">${awayName}</div><div class="mini-pitch" id="pitch-away-d"></div></div>
</div>
<div style="text-align:center;margin-top:14px;"><a class="btn btn-secondary" href="lineups.html">FULL LINEUPS PAGE ›</a></div>
</div>`;
paintLineup(document.getElementById("pitch-home-d"), LINEUP_HOME, ROSTERS[m.home], false);
paintLineup(document.getElementById("pitch-away-d"), LINEUP_AWAY, ROSTERS[m.away], true);
}
/* Match info: venue/referee/weather */
const info = document.getElementById("match-info");
if (info) info.innerHTML = `
<div class="col"><small>VENUE</small><strong>${m.venue}</strong></div>
<div class="col"><small>REFEREE</small><strong>${m.referee}</strong></div>
<div class="col"><small>WEATHER</small><strong>${m.weather}</strong></div>`;
/* Predictions panel content (no betting) */
const pred = document.getElementById("panel-predictions");
if (pred) pred.innerHTML = `
<div class="card"><div class="card-title">PREDICTION (ANALYTICAL)</div>
<div class="analysis-block"><h4>Form</h4><p>${homeName} unbeaten in last 5; ${awayName} mixed form away.</p></div>
<div class="analysis-block"><h4>Tactical edge</h4><p>Home press creates xG advantage in middle third.</p></div>
<div class="analysis-block"><h4>Likely outcome</h4><p>Tight contest with narrow home win projection. <em>Informational, not betting.</em></p></div>
</div>`;
/* Odds panel — placeholder only, no real odds, no gambling */
const odds = document.getElementById("panel-odds");
if (odds) odds.innerHTML = `
<div class="card"><div class="card-title">MARKET INSIGHT (PLACEHOLDER)</div>
<p style="color:var(--muted);font-size:13px;margin-bottom:10px;">Indicative model probabilities — no betting markets shown.</p>
<div class="odds-info">
<div class="col"><small>HOME WIN</small><strong>48%</strong></div>
<div class="col"><small>DRAW</small><strong>27%</strong></div>
<div class="col"><small>AWAY WIN</small><strong>25%</strong></div>
</div>
</div>`;
/* Standings tab — league table for this match's competition.
Easy to swap for `GET /standings?league=...` later. */
const standPanel = document.getElementById("panel-standings");
if (standPanel) {
const rows = STANDINGS[m.league] || STANDINGS["Premier League"] || [];
standPanel.innerHTML = `
<div class="card"><div class="card-title">${m.league.toUpperCase()} — TABLE</div>
<div class="table-card">
<div class="table-head">
<span>POS</span><span class="team">TEAM</span><span>P</span><span>W</span><span>D</span><span>L</span><span>GD</span><span>PTS</span>
</div>
${rows.map((row, i) => {
const rowTeamName = getTeamDisplayName(TEAMS[row.team]?.name || row.team);
const isActive = row.team === m.home || row.team === m.away || rowTeamName === homeName || rowTeamName === awayName;
return `
<div class="table-row ${row.top?"top":""} ${isActive?"active":""}">
<span class="num">${i+1}</span>
<span class="team">${logo(row.team, false, rowTeamName, m)}<span>${rowTeamName}</span></span>
<span class="num">${row.p}</span><span class="num">${row.w}</span>
<span class="num">${row.d}</span><span class="num">${row.l}</span>
<span class="num">${row.gd}</span><span class="pts">${row.pts}</span>
</div>`;
}).join("")}
</div>
<div style="text-align:center;margin-top:14px;"><a class="btn btn-secondary" href="standings.html">FULL STANDINGS ›</a></div>
</div>`;
}
/* Fixtures tab — upcoming matches in this league.
Easy to swap for `GET /fixtures?league=...&status=UP`. */
const fxPanel = document.getElementById("panel-fixtures");
if (fxPanel) {
const upcoming = MATCHES.filter(x => x.league === m.league && x.status === "UP");
fxPanel.innerHTML = `<div class="card"><div class="card-title">${m.league.toUpperCase()} — UPCOMING</div><div class="match-list" id="mfx-list" style="padding:0;"></div></div>`;
const c = fxPanel.querySelector("#mfx-list");
if (upcoming.length) sortByFav(upcoming).forEach(x => c.appendChild(renderMatchRow(x)));
else c.innerHTML = `<div class="empty" style="padding:24px;"><p>No upcoming fixtures in ${m.league}.</p></div>`;
}
/* Results tab — finished matches in this league.
Easy to swap for `GET /fixtures?league=...&status=FT`. */
const rsPanel = document.getElementById("panel-results");
if (rsPanel) {
const done = MATCHES.filter(x => x.league === m.league && x.status === "FT");
rsPanel.innerHTML = `<div class="card"><div class="card-title">${m.league.toUpperCase()} — RESULTS</div><div class="match-list" id="mrs-list" style="padding:0;"></div></div>`;
const c = rsPanel.querySelector("#mrs-list");
if (done.length) sortByFav(done).forEach(x => c.appendChild(renderMatchRow(x)));
else c.innerHTML = `<div class="empty" style="padding:24px;"><p>No recent results in ${m.league}.</p></div>`;
}
/* Tabs */
document.querySelectorAll(".tab").forEach(t => {
t.addEventListener("click", () => {
const target = t.dataset.tab;
document.querySelectorAll(".tab").forEach(x => x.classList.toggle("active", x===t));
document.querySelectorAll(".tab-panel").forEach(p => p.classList.toggle("active", p.id === `panel-${target}`));
});
});
}
function paintLineup(pitch, lineup, roster, away) {
if (!pitch) return;
lineup.forEach((p, i) => {
pitch.appendChild(el(`
<div class="player ${away?'away':''} ${p.gk?'gk':''}" style="left:${p.x}%; top:${away ? 100 - p.y : p.y}%;">
<div class="shirt"></div><div class="pname">${roster[i] || "Player"}</div><div class="prating">${p.rating}</div>
</div>`));
});
}
/* =========================================================
LINEUPS standalone page
========================================================= */
function renderLineups() {
const id = qs("id") || "m1";
const m = matchById(id) || MATCHES[0];
const home = document.getElementById("pitch-home");
const away = document.getElementById("pitch-away");
paintLineup(home, LINEUP_HOME, ROSTERS[m.home], false);
paintLineup(away, LINEUP_AWAY, ROSTERS[m.away], true);
const subsH = document.getElementById("subs-home");
const subsA = document.getElementById("subs-away");
if (subsH) ROSTERS[m.home].slice(11).forEach(n => subsH.appendChild(el(`<div class="sub-row">${logo(m.home, false, TEAMS[m.home]?.name || m.home, m)}<span class="pname">${n}</span></div>`)));
if (subsA) ROSTERS[m.away].slice(11).forEach(n => subsA.appendChild(el(`<div class="sub-row">${logo(m.away, false, TEAMS[m.away]?.name || m.away, m)}<span class="pname">${n}</span></div>`)));
const openBtn = document.getElementById("open-predictions");
const modal = document.getElementById("predictions-modal");
const closeBtn = document.getElementById("close-predictions");
if (openBtn && modal) {
openBtn.addEventListener("click", () => modal.classList.add("open"));
closeBtn && closeBtn.addEventListener("click", () => modal.classList.remove("open"));
modal.addEventListener("click", e => { if (e.target===modal) modal.classList.remove("open"); });
}
}
/* =========================================================
STANDINGS
========================================================= */
async function renderStandings() {
const dropdown = document.getElementById("comp-dropdown");
const dropdownList = document.getElementById("comp-list");
const tableBody = document.getElementById("standings-body");
const scorersWrap = document.getElementById("scorers-list");
setStandingsSkeleton(tableBody, scorersWrap);
const data = await apiFetch("/standings?league=all");
const standings = data?.standings || STANDINGS;
const keys = Object.keys(standings);
let current = keys[0] || "Premier League";
function paint() {
document.getElementById("comp-current").textContent = current.toUpperCase();
tableBody.innerHTML = "";
(standings[current] || []).forEach((row, i) => {
tableBody.appendChild(el(`
<div class="table-row ${row.top?"top":""}">
<span class="num">${i+1}</span>
<span class="team">${logo(row.team)}<span>${row.team_name || row.team}</span></span>
<span class="num">${row.p}</span><span class="num">${row.w}</span>
<span class="num">${row.d}</span><span class="num">${row.l}</span>
<span class="num">${row.gd}</span><span class="pts">${row.pts}</span>
</div>`));
});
}
if (dropdown && dropdownList) {
dropdownList.innerHTML = "";
keys.forEach(name => {
const b = el(`<button>${name}</button>`);
b.addEventListener("click", () => { current = name; paint(); dropdown.classList.remove("open"); });
dropdownList.appendChild(b);
});
dropdown.addEventListener("click", () => dropdown.classList.toggle("open"));
}
if (tableBody) paint();
if (scorersWrap) {
scorersWrap.innerHTML = "";
SCORERS.forEach(s => scorersWrap.appendChild(el(`
<div class="scorer-row"><div class="scorer-photo" style="background:transparent;border:none;">${logo(s.club, false, getTeamDisplayName(s.club))}</div>
<div class="pname">${s.name}<span class="pmeta">${TEAMS[s.club]?.name||""} · goals</span></div>
<span class="gnum">${s.goals}</span></div>`)));
}
}
/* =========================================================
DESKTOP SHELL — sidebar + right rail
========================================================= */
const NAV_ITEMS = [
{ href:"index.html", ic:"⌂", label:"Home", page:"home" },
{ href:"live.html", ic:"▶", label:"Live", page:"live" },
{ href:"favourites.html", ic:"★", label:"Favourites", page:"favourites" },
{ href:"leagues.html", ic:"🏆", label:"Leagues", page:"leagues" },
{ href:"search.html", ic:"🔎", label:"Search", page:"search" },
{ href:"notifications.html", ic:"🔔", label:"Notifications", page:"notifications" },
{ href:"profile.html", ic:"◉", label:"Profile", page:"profile" },
];
const SECONDARY_NAV = [
{ href:"dashboard.html", ic:"▦", label:"Dashboard", page:"dashboard" },
{ href:"settings.html", ic:"⚙", label:"Settings", page:"settings" },
{ href:"about.html", ic:"🛈", label:"About", page:"about" },
{ href:"contact.html", ic:"✉", label:"Contact", page:"contact" },
];
function isDesktop() { return window.matchMedia("(min-width: 700px)").matches; }
function buildSidebar(activePage) {
const u = getUser();
const items = NAV_ITEMS.map(n => `<a class="nav-item ${n.page===activePage?"active":""}" href="${n.href}"><span class="ic">${n.ic}</span><span>${n.label}</span></a>`).join("");
const sec = SECONDARY_NAV.map(n => `<a class="nav-item ${n.page===activePage?"active":""}" href="${n.href}"><span class="ic">${n.ic}</span><span>${n.label}</span></a>`).join("");
const authBlock = u
? `<a class="nav-item" href="dashboard.html"><span class="ic">👤</span><span>${u.name}</span></a>
<button class="nav-item" id="side-logout" type="button"><span class="ic">⎋</span><span>Logout</span></button>`
: `<a class="nav-item" href="login.html"><span class="ic">→</span><span>Log in</span></a>
<a class="nav-item" href="register.html"><span class="ic">+</span><span>Sign up</span></a>`;
return `<aside class="side-nav">
<div class="brand"><span class="logo-mark">⚽</span><span>LIVE SCORES</span></div>
${items}
<div class="nav-section">More</div>
${sec}
<div class="nav-section">Account</div>
${authBlock}
<button class="theme-toggle" id="theme-toggle"><span>🌓</span><span>Toggle theme</span></button>
</aside>`;
}
function buildRightRail() {
const trending = liveMatches().slice(0,4).map(m => {
const homeShort = m.home_short || m.homeShort || m.home || 'HOM';
const awayShort = m.away_short || m.awayShort || m.away || 'AWY';
const homeName = getTeamDisplayName(m.home_team || m.homeTeam || m.home || m.home_short);
const awayName = getTeamDisplayName(m.away_team || m.awayTeam || m.away || m.away_short);
return `
<a class="rail-item" href="match-detail.html?id=${m.id}">${logo(homeShort, false, homeName, m)}
<div class="meta">${homeName} vs ${awayName}<small>${m.minute} · LIVE</small></div>
<span class="val">${m.scoreH ?? m.home_score ?? 0}-${m.scoreA ?? m.away_score ?? 0}</span></a>`;
}).join("");
const upcoming = MATCHES.filter(m => m.status==="UP").slice(0,4).map(m => {
const homeShort = m.home_short || m.homeShort || m.home || 'HOM';
const awayShort = m.away_short || m.awayShort || m.away || 'AWY';
const homeName = getTeamDisplayName(m.home_team || m.homeTeam || m.home || m.home_short);
const awayName = getTeamDisplayName(m.away_team || m.awayTeam || m.away || m.away_short);
return `
<a class="rail-item" href="match-detail.html?id=${m.id}">${logo(homeShort, false, homeName, m)}
<div class="meta">${homeName} vs ${awayName}<small>${m.league_name || m.league}</small></div>
<span class="val" style="color:var(--text);font-size:11px;">${m.minute}</span></a>`;
}).join("");
const scorers = SCORERS.map(s => {
const clubName = getTeamDisplayName(s.club);
return `
<a class="rail-item" href="player-detail.html"><div class="scorer-photo" style="background:transparent;border:none;">${logo(s.club, false, getTeamDisplayName(s.club))}</div>
<div class="meta">${s.name}<small>${clubName}</small></div>
<span class="val">${s.goals}</span></a>`;
}).join("");
return `<aside class="side-rail">
<div class="rail-card"><h3>Trending Live <a class="more" href="live.html">SEE ALL</a></h3>
<div class="rail-list">${trending || '<div class="empty" style="padding:14px;"><p>No live matches</p></div>'}</div></div>
<div class="rail-card"><h3>Upcoming <a class="more" href="fixtures.html">SEE ALL</a></h3><div class="rail-list">${upcoming}</div></div>
<div class="rail-card"><h3>Top Scorers <a class="more" href="standings.html">SEE ALL</a></h3><div class="rail-list">${scorers}</div></div>
</aside>`;
}
function buildHeader() {
const u = getUser();
const profileHref = u ? "dashboard.html" : "login.html";
const profileLabel = u ? (u.name || "Account") : "Log in";
const initials = u ? (u.name||"U").split(/\s+/).map(s=>s[0]).join("").slice(0,2).toUpperCase() : "?";
return `<header class="site-header" role="banner">
<a class="site-brand" href="index.html" aria-label="Live Scores home">
<span class="logo-mark">⚽</span><span class="brand-text">LIVE SCORES</span>
</a>
<form class="site-search" role="search" id="header-search-form" action="search.html" method="get">
<span class="search-ic" aria-hidden="true">🔎</span>
<input id="header-search-input" name="q" type="search" autocomplete="off"
placeholder="Search teams, leagues, matches…" aria-label="Search" />
</form>
<a class="site-account" href="${profileHref}" id="header-account" aria-label="${profileLabel}">
<span class="avatar-sm">${initials}</span>
<span class="account-label">${profileLabel}</span>
</a>
</header>`;
}
function buildLayout() {
const page = document.body.dataset.page;
const app = document.querySelector(".app"); if (!app) return;
const railPages = ["home","live","favourites","fixtures","results","leagues","notifications","dashboard"];
// Desktop header (logo / search / account) — only injected on desktop layout.
const headerHTML = buildHeader();
const layout = document.createElement("div");
layout.className = "layout";
layout.innerHTML = buildSidebar(page);
app.parentNode.insertBefore(layout, app);
// Insert the header above the layout so it spans full width.
layout.insertAdjacentHTML("beforebegin", `<div class="site-header-wrap">${headerHTML}</div>`);
layout.appendChild(app);
if (railPages.includes(page)) layout.insertAdjacentHTML("beforeend", buildRightRail());
const tog = document.getElementById("theme-toggle");
if (tog) tog.addEventListener("click", toggleTheme);
const lo = document.getElementById("side-logout");
if (lo) lo.addEventListener("click", () => { clearUser(); window.location.href = "index.html"; });
// Header search: submit redirects to search.html?q=...; renderSearch will pick it up.
const sf = document.getElementById("header-search-form");
const si = document.getElementById("header-search-input");
if (sf && si) {
// Prefill if already on search page with a query.
const q = qs("q"); if (q) si.value = q;
sf.addEventListener("submit", (e) => {
const term = (si.value || "").trim();
if (!term) { e.preventDefault(); si.focus(); }
});
}
}
/* ============ Theme ============ */
function applyTheme() {
const saved = localStorage.getItem("lscores_theme") || "dark";
document.body.classList.toggle("light", saved === "light");
}
function toggleTheme() {
const cur = localStorage.getItem("lscores_theme") || "dark";
localStorage.setItem("lscores_theme", cur==="dark"?"light":"dark");
applyTheme();
}
/* =========================================================
LIVE / FIXTURES / RESULTS pages
========================================================= */
function renderLive() {
const wrap = document.getElementById("live-list"); if (!wrap) return;
const live = liveMatches();
if (!live.length) { wrap.appendChild(el(`<div class="empty"><div class="ic">⚽</div><p>No live matches right now.</p></div>`)); return; }
const list = el(`<div class="match-list"></div>`);
sortByFav(live).forEach(m => list.appendChild(renderMatchRow(m)));
wrap.appendChild(list);
}
async function renderFixtures() {
const wrap = document.getElementById("fixtures-list"); if (!wrap) return;
setMatchListSkeleton(wrap, 4);
const data = await apiFetch("/fixtures");
const upcoming = data?.matches?.length ? data.matches : MATCHES.filter(m => m.status==="UP");
if (!upcoming.length) { wrap.innerHTML = ""; wrap.appendChild(el(`<div class="empty"><div class="ic">📅</div><p>No upcoming fixtures.</p></div>`)); return; }
wrap.innerHTML = "";
const groups = {};
upcoming.forEach(m => { (groups[m.league_name || m.league]=groups[m.league_name || m.league]||[]).push(m); });
Object.entries(groups).forEach(([league, arr]) => {
const sec = el(`<div class="league-section"><div class="section-title"><div class="league-name"><span class="league-badge"></span>${league}</div></div><div class="match-list"></div></div>`);
const ml = sec.querySelector(".match-list");
sortByFav(arr).forEach(m => ml.appendChild(renderMatchRow(m)));
wrap.appendChild(sec);
});
}
async function renderResults() {
const wrap = document.getElementById("results-list"); if (!wrap) return;
setMatchListSkeleton(wrap, 4);
const data = await apiFetch("/results");
const finished = data?.matches?.length ? data.matches : MATCHES.filter(m => m.status==="FT");
if (!finished.length) { wrap.innerHTML = ""; wrap.appendChild(el(`<div class="empty"><div class="ic">📊</div><p>No results to show.</p></div>`)); return; }
wrap.innerHTML = "";
const groups = {};
finished.forEach(m => { (groups[m.league_name || m.league]=groups[m.league_name || m.league]||[]).push(m); });
Object.entries(groups).forEach(([league, arr]) => {
const sec = el(`<div class="league-section"><div class="section-title"><div class="league-name"><span class="league-badge"></span>${league}</div></div><div class="match-list"></div></div>`);
const ml = sec.querySelector(".match-list");
sortByFav(arr).forEach(m => ml.appendChild(renderMatchRow(m)));
wrap.appendChild(sec);
});
}
/* =========================================================
SEARCH
========================================================= */
async function renderSearch() {
const input = document.getElementById("search-input");
const out = document.getElementById("search-results");
if (!input || !out) return;
const teamItems = Object.entries(TEAMS).map(([code,t]) => ({ type:"Team", label:t.name, href:`team-detail.html?team=${code}` }));
const leagueItems = leagueNames().map(n => ({ type:"League", label:n, href:`league-detail.html?name=${encodeURIComponent(n)}` }));
const matchItems = MATCHES.map(m => ({ type:"Match", label:`${TEAMS[m.home].name} vs ${TEAMS[m.away].name}`, href:`match-detail.html?id=${m.id}` }));
const all = [...teamItems, ...leagueItems, ...matchItems];
async function paint(q) {
const term = q.trim().toLowerCase();
out.innerHTML = "";
if (!term) {
setLeagueListSkeleton(out, 4);
} else {
setLeagueListSkeleton(out, 4);
}
const data = await apiFetch("/search?q=" + encodeURIComponent(term));
const matches = data?.results?.length ? data.results : (term ? all.filter(i => i.label.toLowerCase().includes(term)) : all.slice(0, 12));
out.innerHTML = "";
if (!matches.length) { out.appendChild(el(`<div class="empty"><div class="ic">🔎</div><p>No results for "${q}"</p></div>`)); return; }
matches.slice(0, 40).forEach(i => {
const kind = String(i.type || "").toLowerCase();
const href = i.href || (kind === "match" ? `match-detail.html?id=${i.id}` : kind === "league" ? `league-detail.html?name=${encodeURIComponent(i.label || i.name || i.id)}` : `team-detail.html?team=${encodeURIComponent(i.id || i.label || "")}`);
out.appendChild(el(`
<a class="menu-item" href="${href}"><span class="ic">${kind==="team"?"👥":kind==="league"?"🏆":"⚽"}</span>
<span>${i.label || i.name || i.title}<small style="display:block;color:var(--muted);font-size:11px;">${i.meta || i.type || kind}</small></span>
<span class="chev">›</span></a>`));
});
}
input.addEventListener("input", e => paint(e.target.value));
const initial = qs("q") || "";
if (initial) input.value = initial;
await paint(initial);
}
/* =========================================================
NOTIFICATIONS
========================================================= */
async function renderNotifications() {
const wrap = document.getElementById("notif-list"); if (!wrap) return;
const shell = document.getElementById("notif-content");
const skeleton = document.getElementById("notif-skeleton");
if (shell || skeleton) await nextFrame();
const items = [
{ ic:"⚽", title:"Goal! Haaland scores for Manchester City", time:"2 min ago" },
{ ic:"🟥", title:"Red card for Partey (Arsenal)", time:"12 min ago" },
{ ic:"🏆", title:"La Liga standings updated", time:"1 hr ago" },
{ ic:"📅", title:"Real Madrid vs Barcelona starts in 1 hour", time:"1 hr ago" },
{ ic:"★", title:"Match you favourited is now live", time:"3 hr ago" },
];
items.forEach(n => wrap.appendChild(el(`
<div class="menu-item"><span class="ic">${n.ic}</span>
<span>${n.title}<small style="display:block;color:var(--muted);font-size:11px;margin-top:2px;">${n.time}</small></span></div>`)));
if (shell) shell.hidden = false;
skeleton?.remove();
}
/* =========================================================
TEAM / PLAYER / LEAGUE-DETAIL
========================================================= */
function renderTeamDetail() {
const code = qs("team") || "MNC"; const t = TEAMS[code] || TEAMS.MNC;
const root = document.getElementById("team-root"); if (!root) return;
const recent = MATCHES.filter(m => m.home===code || m.away===code).slice(0,6);
root.innerHTML = `
<section class="profile-hero"><span class="logo lg ${t.cls}">${t.short}</span><h2>${t.name}</h2><p>Club profile · Squad · Fixtures</p></section>
<div class="profile-stats">
<div class="stat"><strong>18</strong><span>PLAYED</span></div>
<div class="stat"><strong>15</strong><span>WINS</span></div>
<div class="stat"><strong>73</strong><span>POINTS</span></div>
</div>
<div class="card"><div class="card-title">RECENT FORM</div>
<div style="display:flex;gap:8px;">${["W","W","D","W","W"].map(r => `<span class="badge ${r==="W"?"live":r==="D"?"ht":"ft"}" style="padding:6px 10px;">${r}</span>`).join("")}</div>
</div>
<div class="card"><div class="card-title">RECENT MATCHES</div><div id="team-matches" class="match-list" style="padding:0;"></div></div>`;
const ml = document.getElementById("team-matches");
sortByFav(recent).forEach(m => ml.appendChild(renderMatchRow(m)));
}
function renderPlayerDetail() {
const root = document.getElementById("player-root"); if (!root) return;
root.innerHTML = `
<section class="profile-hero"><div class="avatar">H</div><h2>Haaland</h2><p>Forward · Manchester City · #9</p></section>
<div class="profile-stats">
<div class="stat"><strong>15</strong><span>GOALS</span></div>
<div class="stat"><strong>4</strong><span>ASSISTS</span></div>
<div class="stat"><strong>9.1</strong><span>RATING</span></div>
</div>`;
}
async function renderLeagueDetail() {
const root = document.getElementById("league-root"); if (!root) return;
const leagueName = qs("name") || qs("id") || "Premier League";
setLeagueDetailSkeleton(root);
const leaguesData = await apiFetch("/leagues");
const leagues = leaguesData?.leagues?.length ? leaguesData.leagues : leagueNames().map((n, i) => ({
id: leagueSlug(n),
name: n,
country: ["England","Spain","Germany","Italy","France","Europe"][i],
color: ["6f42c1","ff8c00","d8232a","2bd86b","1f4f8b","ffd400"][i],
}));
const selected = leagues.find(l => {
const key = String(leagueName).toLowerCase();
const countryName = String((l.country || "") + ":" + (l.name || "")).toLowerCase();
return String(l.lookup_key || "").toLowerCase() === key
|| String(l.id || "").toLowerCase() === key
|| String(l.sub_id || "").toLowerCase() === key
|| String(l.name || "").toLowerCase() === key
|| String(l.league_name || "").toLowerCase() === key
|| countryName === key;
}) || leagues.find(l => String(l.name || "").toLowerCase() === String(leagueName).toLowerCase());
const displayName = selected?.league_name || selected?.name || leagueName;
const displayLabel = selected ? (selected.league_name || selected.name || displayName) : displayName;
const standData = await apiFetch(`/standings?league=${encodeURIComponent(displayName)}`);
const standings = standData?.standings || STANDINGS;
const rows = standings[displayName] || standings["Premier League"] || STANDINGS[displayName] || STANDINGS["Premier League"] || [];
const matchesData = await apiFetch(`/league/${encodeURIComponent(selected?.lookup_key || selected?.id || selected?.sub_id || selected?.name || displayName)}`);
const matches = selected?.matches?.length
? selected.matches
: matchesData?.matches?.length
? matchesData.matches
: MATCHES.filter(m => String(m.league).toLowerCase() === String(displayName).toLowerCase());
if (!selected && !rows.length && !matches.length) {
root.innerHTML = `<div class="empty"><div class="ic">🏆</div><p>No league data available.</p></div>`;
return;
}
root.innerHTML = `
<section class="profile-hero">
<span class="league-badge" style="width:64px;height:64px;border-radius:50%;background:linear-gradient(135deg,#6f42c1,#2bd86b);"></span>
<h2>${displayLabel}</h2><p>${selected?.country || "Top division"}</p>
</section>
<div class="card"><div class="card-title">TOP TEAMS</div>
${rows.slice(0,5).length ? rows.slice(0,5).map((row, i) => `
<div class="table-row ${row.top?"top":""}">
<span class="num">${i+1}</span><span class="team">${logo(row.team, false, row.team_name || TEAMS[row.team]?.name || row.team)}<span>${row.team_name || TEAMS[row.team]?.name || row.team}</span></span>
<span class="num">${row.p}</span><span class="num">${row.w}</span><span class="num">${row.d}</span>
<span class="num">${row.l}</span><span class="num">${row.gd}</span><span class="pts">${row.pts}</span>
</div>`).join("") : `<div class="empty" style="padding:24px;"><p>No standings available for this league.</p></div>`}
</div>
<div class="card"><div class="card-title">RECENT MATCHES</div><div class="match-list" id="league-detail-matches" style="padding:0;"></div></div>`;
const list = document.getElementById("league-detail-matches");
if (list) {
if (!matches.length) list.innerHTML = `<div class="empty" style="padding:24px;"><p>No matches available for this league.</p></div>`;
else sortByFav(matches.slice(0, 6)).forEach(m => list.appendChild(renderMatchRow(m)));
}
}
/* =========================================================
AUTH PAGES (Login / Register / Forgot / Dashboard)
========================================================= */
async function renderLogin() {
const form = document.getElementById("login-form"); if (!form) return;
const shell = document.getElementById("login-content");
const skeleton = document.getElementById("login-skeleton");
if (shell || skeleton) await nextFrame();
if (shell) shell.hidden = false;
skeleton?.remove();
form.addEventListener("submit", e => {
e.preventDefault();
const fd = new FormData(form);
const email = (fd.get("email")||"").toString().trim();
if (!email) return;
const name = email.split("@")[0].replace(/\W/g," ").replace(/\b\w/g, c => c.toUpperCase()) || "User";
setUser({ email, name, joined: new Date().toISOString() });
const redirect = qs("redirect") || "dashboard.html";
window.location.href = redirect;
});
}
async function renderRegister() {
const form = document.getElementById("register-form"); if (!form) return;
const shell = document.getElementById("register-content");
const skeleton = document.getElementById("register-skeleton");
if (shell || skeleton) await nextFrame();
if (shell) shell.hidden = false;
skeleton?.remove();
form.addEventListener("submit", e => {
e.preventDefault();
const fd = new FormData(form);
const name = (fd.get("name")||"").toString().trim();
const email = (fd.get("email")||"").toString().trim();
const pass = fd.get("password"); const confirm = fd.get("confirm");
const err = document.getElementById("reg-error");
if (!name || !email || !pass) { err.textContent = "Please complete all fields."; return; }
if (pass !== confirm) { err.textContent = "Passwords do not match."; return; }
setUser({ name, email, joined: new Date().toISOString() });
window.location.href = "dashboard.html";
});
}
async function renderForgot() {
const form = document.getElementById("forgot-form"); if (!form) return;
const shell = document.getElementById("forgot-content");
const skeleton = document.getElementById("forgot-skeleton");
if (shell || skeleton) await nextFrame();
if (shell) shell.hidden = false;
skeleton?.remove();
form.addEventListener("submit", e => {
e.preventDefault();
document.getElementById("forgot-msg").textContent = "If an account exists, we've sent reset instructions.";
});
}
async function renderDashboard() {
if (!requireLogin("dashboard.html")) return;
const u = getUser();
const root = document.getElementById("dash-root"); if (!root) return;
const shell = document.getElementById("dashboard-content");
const skeleton = document.getElementById("dashboard-skeleton");
if (shell || skeleton) await nextFrame();
const initials = (u.name || "U").split(/\s+/).map(s=>s[0]).join("").slice(0,2).toUpperCase();
const favs = getFavs();
const favMatches = MATCHES.filter(m => favs.includes(m.id)).slice(0,4);
root.innerHTML = `
<section class="profile-hero">
<div class="avatar">${initials}</div>
<h2>${u.name}</h2><p>${u.email} · Joined ${new Date(u.joined).toLocaleDateString()}</p>
</section>
<div class="profile-stats">
<div class="stat"><strong>${favs.length}</strong><span>FAVOURITES</span></div>
<div class="stat"><strong>3</strong><span>LEAGUES</span></div>
<div class="stat"><strong>${liveMatches().length}</strong><span>LIVE NOW</span></div>
</div>
<div class="card"><div class="card-title">YOUR FAVOURITE MATCHES</div>
<div id="dash-favs" class="match-list" style="padding:0;"></div>
</div>
<div class="menu-list">
<a class="menu-item" href="edit-profile.html"><span class="ic">✎</span><span>Edit profile</span><span class="chev">›</span></a>
<a class="menu-item" href="favourites.html"><span class="ic">★</span><span>My favourites</span><span class="chev">›</span></a>
<a class="menu-item" href="followed-leagues.html"><span class="ic">🏆</span><span>Followed leagues</span><span class="chev">›</span></a>
<a class="menu-item" href="notification-settings.html"><span class="ic">🔔</span><span>Notification settings</span><span class="chev">›</span></a>
<a class="menu-item" href="account-settings.html"><span class="ic">⚙</span><span>Account settings</span><span class="chev">›</span></a>
<button class="menu-item" id="logout-btn" type="button" style="width:100%;text-align:left;"><span class="ic">⎋</span><span>Logout</span><span class="chev">›</span></button>
</div>`;
const dashFavs = document.getElementById("dash-favs");
if (favMatches.length) favMatches.forEach(m => dashFavs.appendChild(renderMatchRow(m)));
else dashFavs.innerHTML = `<div class="empty" style="padding:24px;"><p>No favourites yet. Star matches from <a href="index.html" style="color:var(--accent);">Home</a>.</p></div>`;
document.getElementById("logout-btn").addEventListener("click", () => { clearUser(); window.location.href = "index.html"; });
if (shell) shell.hidden = false;
skeleton?.remove();
}
async function renderEditProfile() {
if (!requireLogin("edit-profile.html")) return;
const shell = document.getElementById("edit-profile-content");
const skeleton = document.getElementById("edit-profile-skeleton");
if (shell || skeleton) await nextFrame();
if (shell) shell.hidden = false;
skeleton?.remove();
const u = getUser();
const form = document.getElementById("edit-form"); if (!form) return;
form.querySelector('[name="name"]').value = u.name;
form.querySelector('[name="email"]').value = u.email;
form.addEventListener("submit", e => {
e.preventDefault();
const fd = new FormData(form);
setUser({ ...u, name: fd.get("name").toString(), email: fd.get("email").toString() });
document.getElementById("edit-msg").textContent = "Profile saved.";
});
}
async function renderFollowedLeagues() {
if (!requireLogin("followed-leagues.html")) return;
const wrap = document.getElementById("followed-list"); if (!wrap) return;
const shell = document.getElementById("followed-content");
const skeleton = document.getElementById("followed-skeleton");
if (shell || skeleton) await nextFrame();
const KEY = "lscores_followed_leagues";
const followed = JSON.parse(localStorage.getItem(KEY) || "[]");
["Premier League","La Liga","Bundesliga","Serie A","Ligue 1","Champions League"].forEach(name => {
const isOn = followed.includes(name);
const row = el(`
<div class="menu-item">
<span class="league-badge"></span><span>${name}</span>
<button class="btn ${isOn?"btn-secondary":"btn-primary"}" style="margin-left:auto;padding:6px 12px;font-size:12px;">${isOn?"FOLLOWING":"FOLLOW"}</button>
</div>`);
row.querySelector("button").addEventListener("click", e => {
const list = JSON.parse(localStorage.getItem(KEY) || "[]");
const i = list.indexOf(name);
if (i>=0) list.splice(i,1); else list.push(name);
localStorage.setItem(KEY, JSON.stringify(list));
const on = i<0;
e.currentTarget.textContent = on ? "FOLLOWING" : "FOLLOW";
e.currentTarget.className = `btn ${on?"btn-secondary":"btn-primary"}`;
e.currentTarget.style.cssText = "margin-left:auto;padding:6px 12px;font-size:12px;";
});
wrap.appendChild(row);
});
if (shell) shell.hidden = false;
skeleton?.remove();
}
async function renderNotificationSettings() {
if (!requireLogin("notification-settings.html")) return;
const wrap = document.getElementById("notif-settings"); if (!wrap) return;
const shell = document.getElementById("notif-settings-content");
const skeleton = document.getElementById("notif-settings-skeleton");
if (shell || skeleton) await nextFrame();
const KEY = "lscores_notif_prefs";
const prefs = JSON.parse(localStorage.getItem(KEY) || '{"goals":true,"redCards":true,"kickoff":true,"results":false}');
Object.entries({ goals:"Goals", redCards:"Red cards", kickoff:"Kick-off", results:"Final results" }).forEach(([k,label]) => {
const row = el(`
<label class="menu-item" style="cursor:pointer;">
<span class="ic">🔔</span><span>${label}</span>
<input type="checkbox" ${prefs[k]?"checked":""} style="margin-left:auto;width:18px;height:18px;accent-color:var(--accent);">
</label>`);
row.querySelector("input").addEventListener("change", e => {
prefs[k] = e.target.checked; localStorage.setItem(KEY, JSON.stringify(prefs));
});
wrap.appendChild(row);
});
if (shell) shell.hidden = false;
skeleton?.remove();
}
async function renderAccountSettings() {
if (!requireLogin("account-settings.html")) return;
const wrap = document.getElementById("account-settings"); if (!wrap) return;
const shell = document.getElementById("account-settings-content");
const skeleton = document.getElementById("account-settings-skeleton");
if (shell || skeleton) await nextFrame();
const u = getUser();
wrap.innerHTML = `
<div class="card"><div class="card-title">EMAIL</div><p style="color:var(--text);">${u.email}</p></div>
<div class="card"><div class="card-title">PASSWORD</div>
<p style="color:var(--muted);font-size:13px;">Change is disabled in demo mode.</p>
<button class="btn btn-secondary" style="margin-top:10px;" disabled>Change password</button></div>
<div class="card"><div class="card-title" style="color:#ff6b6b;">DANGER ZONE</div>
<button class="btn btn-secondary" id="delete-acct" style="border-color:rgba(255,107,107,0.4);color:#ff6b6b;">Delete account</button></div>`;
document.getElementById("delete-acct").addEventListener("click", () => {
if (confirm("Delete your account? This clears local data only.")) {
clearUser(); localStorage.removeItem(FAV_KEY); window.location.href = "index.html";
}
});
if (shell) shell.hidden = false;
skeleton?.remove();
}
async function renderProfile() {
/* Public profile page — show user OR sign-in prompt */
const u = getUser();
const shell = document.getElementById("profile-content");
const skeleton = document.getElementById("profile-skeleton");
if (shell || skeleton) await nextFrame();
const heroAvatar = document.getElementById("profile-avatar");
const heroName = document.getElementById("profile-name");
const heroSub = document.getElementById("profile-sub");
const cta = document.getElementById("profile-cta");
if (heroAvatar) heroAvatar.textContent = u ? u.name.split(" ").map(s=>s[0]).join("").slice(0,2).toUpperCase() : "?";
if (heroName) heroName.textContent = u ? u.name : "Guest";
if (heroSub) heroSub.textContent = u ? `${u.email}` : "Sign in to save favourites and follow leagues";
if (cta) cta.innerHTML = u
? `<a class="btn btn-primary" href="dashboard.html">Open Dashboard</a> <button class="btn btn-secondary" id="profile-logout">Logout</button>`
: `<a class="btn btn-primary" href="login.html">Log in</a> <a class="btn btn-secondary" href="register.html">Sign up</a>`;
const lo = document.getElementById("profile-logout");
if (lo) lo.addEventListener("click", () => { clearUser(); window.location.reload(); });
if (shell) shell.hidden = false;
skeleton?.remove();
}
async function renderSettings() {
const shell = document.getElementById("settings-content");
const skeleton = document.getElementById("settings-skeleton");
if (!shell) return;
if (shell || skeleton) await nextFrame();
shell.hidden = false;
skeleton?.remove();
const sel = document.getElementById("theme-select");
if (sel) sel.value = localStorage.getItem("lscores_theme") || "dark";
}
/* =========================================================
BOOT
========================================================= */
document.addEventListener("DOMContentLoaded", async () => {
if (window.neonkickPage && !document.body.dataset.page) {
document.body.setAttribute('data-page', window.neonkickPage);
}
applyTheme();
if (isDesktop()) buildLayout();
const page = document.body.dataset.page;
const map = {
home: renderHome, match: renderMatchDetail, lineups: renderLineups,
standings: renderStandings, favourites: renderFavourites, leagues: renderLeaguesIndex,
live: renderLive, fixtures: renderFixtures, results: renderResults,
search: renderSearch, notifications: renderNotifications,
team: renderTeamDetail, player: renderPlayerDetail, "league-detail": renderLeagueDetail,
profile: renderProfile,
login: renderLogin, register: renderRegister, forgot: renderForgot,
dashboard: renderDashboard, settings: renderSettings, "edit-profile": renderEditProfile,
"followed-leagues": renderFollowedLeagues,
"notification-settings": renderNotificationSettings,
"account-settings": renderAccountSettings,
};
if (map[page]) await map[page]();
});