diff --git a/public/api/scorers.json b/public/api/scorers.json
new file mode 100644
index 0000000..0aae68e
--- /dev/null
+++ b/public/api/scorers.json
@@ -0,0 +1,196 @@
+{
+ "scorers": [
+ {
+ "groupId": "A",
+ "matchday": 1,
+ "home": "Mexico",
+ "away": "South Africa",
+ "scorers": {
+ "home": [
+ { "name": "Julián Quiñones", "min": 9 },
+ { "name": "Raúl Jiménez", "min": 67 }
+ ],
+ "away": []
+ }
+ },
+ {
+ "groupId": "A",
+ "matchday": 1,
+ "home": "South Korea",
+ "away": "Czech Republic",
+ "scorers": {
+ "home": [
+ { "name": "Hwang In-beom", "min": 67 },
+ { "name": "Oh Hyeon-gyu", "min": 80 }
+ ],
+ "away": [
+ { "name": "Ladislav Krejčí", "min": 59 }
+ ]
+ }
+ },
+ {
+ "groupId": "B",
+ "matchday": 1,
+ "home": "Canada",
+ "away": "Bosnia & Herzegovina",
+ "scorers": {
+ "home": [
+ { "name": "Cyle Larin", "min": 78 }
+ ],
+ "away": [
+ { "name": "Jovo Lukić", "min": 21 }
+ ]
+ }
+ },
+ {
+ "groupId": "B",
+ "matchday": 1,
+ "home": "Qatar",
+ "away": "Switzerland",
+ "scorers": {
+ "home": [
+ { "name": "Boualem Khoukhi", "min": 94 }
+ ],
+ "away": [
+ { "name": "Breel Embolo", "min": 17, "type": "pen" }
+ ]
+ }
+ },
+ {
+ "groupId": "C",
+ "matchday": 1,
+ "home": "Brazil",
+ "away": "Morocco",
+ "scorers": {
+ "home": [
+ { "name": "Vinícius Júnior", "min": 32 }
+ ],
+ "away": [
+ { "name": "Ismael Saibari", "min": 21 }
+ ]
+ }
+ },
+ {
+ "groupId": "C",
+ "matchday": 1,
+ "home": "Haiti",
+ "away": "Scotland",
+ "scorers": {
+ "home": [],
+ "away": [
+ { "name": "John McGinn", "min": 28 }
+ ]
+ }
+ },
+ {
+ "groupId": "D",
+ "matchday": 1,
+ "home": "United States",
+ "away": "Paraguay",
+ "scorers": {
+ "home": [
+ { "name": "Damián Bobadilla (OG)", "min": 7 },
+ { "name": "Folarin Balogun", "min": 31 },
+ { "name": "Folarin Balogun", "min": 45 },
+ { "name": "Gio Reyna", "min": 98 }
+ ],
+ "away": [
+ { "name": "Maurício", "min": 73 }
+ ]
+ }
+ },
+ {
+ "groupId": "D",
+ "matchday": 1,
+ "home": "Australia",
+ "away": "Türkiye",
+ "scorers": {
+ "home": [
+ { "name": "Nestory Irankunda", "min": 27 },
+ { "name": "Connor Metcalfe", "min": 75 }
+ ],
+ "away": []
+ }
+ },
+ {
+ "groupId": "E",
+ "matchday": 1,
+ "home": "Germany",
+ "away": "Curaçao",
+ "scorers": {
+ "home": [
+ { "name": "Felix Nmecha", "min": 6 },
+ { "name": "Nico Schlotterbeck", "min": 38 },
+ { "name": "Kai Havertz", "min": 45, "type": "pen" },
+ { "name": "Jamal Musiala", "min": 47 },
+ { "name": "Nathaniel Brown", "min": 68 },
+ { "name": "Deniz Undav", "min": 78 },
+ { "name": "Kai Havertz", "min": 88 }
+ ],
+ "away": [
+ { "name": "Livano Comenencia", "min": 21 }
+ ]
+ }
+ },
+ {
+ "groupId": "E",
+ "matchday": 1,
+ "home": "Côte d'Ivoire",
+ "away": "Ecuador",
+ "scorers": {
+ "home": [
+ { "name": "Amad Diallo", "min": 90 }
+ ],
+ "away": []
+ }
+ },
+ {
+ "groupId": "F",
+ "matchday": 1,
+ "home": "Netherlands",
+ "away": "Japan",
+ "scorers": {
+ "home": [
+ { "name": "Virgil van Dijk", "min": 51 },
+ { "name": "Crysencio Summerville", "min": 64 }
+ ],
+ "away": [
+ { "name": "Keito Nakamura", "min": 57 },
+ { "name": "Daichi Kamada", "min": 88 }
+ ]
+ }
+ },
+ {
+ "groupId": "F",
+ "matchday": 1,
+ "home": "Sweden",
+ "away": "Tunisia",
+ "scorers": {
+ "home": [
+ { "name": "Yasin Ayari", "min": 7 },
+ { "name": "Alexander Isak", "min": 30 },
+ { "name": "Viktor Gyökeres", "min": 59 },
+ { "name": "Mattias Svanberg", "min": 84 },
+ { "name": "Yasin Ayari", "min": 96 }
+ ],
+ "away": [
+ { "name": "Omar Rekik", "min": 43 }
+ ]
+ }
+ },
+ {
+ "groupId": "H",
+ "matchday": 1,
+ "home": "Spain",
+ "away": "Cape Verde",
+ "scorers": {
+ "home": [
+ { "name": "Álvaro Morata", "min": 15 },
+ { "name": "Dani Olmo", "min": 55 },
+ { "name": "Nico Williams", "min": 82 }
+ ],
+ "away": []
+ }
+ }
+ ]
+}
diff --git a/public/api/scores.json b/public/api/scores.json
new file mode 100644
index 0000000..a3b0e3b
--- /dev/null
+++ b/public/api/scores.json
@@ -0,0 +1,95 @@
+{
+ "matches": [
+ {
+ "groupId": "A",
+ "matchday": 1,
+ "home": "Mexico",
+ "away": "South Africa",
+ "score": { "home": 2, "away": 0 }
+ },
+ {
+ "groupId": "A",
+ "matchday": 1,
+ "home": "South Korea",
+ "away": "Czech Republic",
+ "score": { "home": 2, "away": 1 }
+ },
+ {
+ "groupId": "B",
+ "matchday": 1,
+ "home": "Canada",
+ "away": "Bosnia & Herzegovina",
+ "score": { "home": 1, "away": 1 }
+ },
+ {
+ "groupId": "B",
+ "matchday": 1,
+ "home": "Qatar",
+ "away": "Switzerland",
+ "score": { "home": 1, "away": 1 }
+ },
+ {
+ "groupId": "C",
+ "matchday": 1,
+ "home": "Brazil",
+ "away": "Morocco",
+ "score": { "home": 1, "away": 1 }
+ },
+ {
+ "groupId": "C",
+ "matchday": 1,
+ "home": "Haiti",
+ "away": "Scotland",
+ "score": { "home": 0, "away": 1 }
+ },
+ {
+ "groupId": "D",
+ "matchday": 1,
+ "home": "United States",
+ "away": "Paraguay",
+ "score": { "home": 4, "away": 1 }
+ },
+ {
+ "groupId": "D",
+ "matchday": 1,
+ "home": "Australia",
+ "away": "Türkiye",
+ "score": { "home": 2, "away": 0 }
+ },
+ {
+ "groupId": "E",
+ "matchday": 1,
+ "home": "Germany",
+ "away": "Curaçao",
+ "score": { "home": 7, "away": 1 }
+ },
+ {
+ "groupId": "E",
+ "matchday": 1,
+ "home": "Côte d'Ivoire",
+ "away": "Ecuador",
+ "score": { "home": 1, "away": 0 }
+ },
+ {
+ "groupId": "F",
+ "matchday": 1,
+ "home": "Netherlands",
+ "away": "Japan",
+ "score": { "home": 2, "away": 2 }
+ },
+ {
+ "groupId": "F",
+ "matchday": 1,
+ "home": "Sweden",
+ "away": "Tunisia",
+ "score": { "home": 5, "away": 1 }
+ },
+ {
+ "groupId": "H",
+ "matchday": 1,
+ "home": "Spain",
+ "away": "Cape Verde",
+ "score": { "home": 3, "away": 0 }
+ }
+ ]
+}
diff --git a/src/data.js b/src/data.js
index 0b1171d..c080204 100644
--- a/src/data.js
+++ b/src/data.js
@@ -159,7 +159,7 @@ export const groups = [
],
matches: [
{ matchday: 1, home: 'Netherlands', away: 'Japan', date: 'Sun, Jun 14', time: '22:00', venue: 'Los Angeles' },
- { matchday: 1, home: 'Sweden', away: 'Tunisia', date: 'Mon, Jun 15', time: '16:00', venue: 'Boston' },
+ { matchday: 1, home: 'Sweden', away: 'Tunisia', date: 'Mon, Jun 15', time: '06:00', venue: 'Boston' },
{ matchday: 2, home: 'Netherlands', away: 'Sweden', date: 'Sat, Jun 20', time: '19:00', venue: 'New York' },
{ matchday: 2, home: 'Japan', away: 'Tunisia', date: 'Sat, Jun 20', time: '22:00', venue: 'Kansas City' },
{ matchday: 3, home: 'Netherlands', away: 'Tunisia', date: 'Fri, Jun 26', time: '18:00', venue: 'Dallas' },
@@ -194,7 +194,7 @@ export const groups = [
{ name: 'Uruguay', flag: '🇺🇾', confederation: 'CONMEBOL', isHost: false },
],
matches: [
- { matchday: 1, home: 'Spain', away: 'Cape Verde', date: 'Mon, Jun 15', time: '16:00', venue: 'Guadalajara' },
+ { matchday: 1, home: 'Spain', away: 'Cape Verde', date: 'Mon, Jun 15', time: '08:00', venue: 'Guadalajara' },
{ matchday: 1, home: 'Saudi Arabia', away: 'Uruguay', date: 'Tue, Jun 16', time: '16:00', venue: 'Mexico City' },
{ matchday: 2, home: 'Spain', away: 'Uruguay', date: 'Sat, Jun 21', time: '19:00', venue: 'Monterrey' },
{ matchday: 2, home: 'Cape Verde', away: 'Saudi Arabia', date: 'Sat, Jun 21', time: '22:00', venue: 'Guadalajara' },
diff --git a/src/main.js b/src/main.js
index 9975a1b..cd259f9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,28 +5,227 @@ import { getSquad } from './squads.js';
console.log('[WC2026] Loaded groups:', groups.length, '| First group matches:', groups[0]?.matches?.length);
// ── State ──────────────────────────────────────────────────────────────────
+let activeView = 'groups';
let activeFilter = 'all';
let searchQuery = '';
+let fetchedScores = [];
+let fetchedScorers = [];
+let userSimulations = {};
let appGroups = [];
-function loadState() {
- const saved = localStorage.getItem('wc2026_groups');
+function getMatchKey(groupId, matchday, home, away) {
+ return `${groupId}-${matchday}-${home}-${away}`;
+}
+
+function resetMatchToBaseState(groupId, matchday, home, away) {
+ const baseGroup = groups.find(g => g.id === groupId);
+ if (!baseGroup) return;
+ const baseMatch = baseGroup.matches.find(m => m.matchday === matchday && m.home === home && m.away === away);
+ if (!baseMatch) return;
+
+ const group = appGroups.find(g => g.id === groupId);
+ if (!group) return;
+ const matchIdx = group.matches.findIndex(m => m.matchday === matchday && m.home === home && m.away === away);
+ if (matchIdx !== -1) {
+ group.matches[matchIdx] = JSON.parse(JSON.stringify(baseMatch));
+ }
+}
+
+function loadUserSimulations() {
+ const saved = localStorage.getItem('wc2026_user_sims');
if (saved) {
try {
- appGroups = JSON.parse(saved);
+ userSimulations = JSON.parse(saved);
return;
} catch (e) {
- console.error('[WC2026] Failed to load saved state:', e);
+ console.error('[WC2026] Failed to load user simulations:', e);
}
}
- appGroups = JSON.parse(JSON.stringify(groups));
+ userSimulations = {};
}
-function saveState() {
- localStorage.setItem('wc2026_groups', JSON.stringify(appGroups));
+function saveUserSimulations() {
+ localStorage.setItem('wc2026_user_sims', JSON.stringify(userSimulations));
}
-loadState();
+async function fetchScores() {
+ try {
+ const res = await fetch('/api/scores.json');
+ if (res.ok) {
+ const data = await res.json();
+ fetchedScores = data.matches || [];
+ console.log('[WC2026] Fetched baseline scores:', fetchedScores.length);
+ } else {
+ console.error('[WC2026] Failed to fetch baseline scores:', res.status);
+ }
+ } catch (e) {
+ console.error('[WC2026] Error fetching baseline scores:', e);
+ }
+}
+
+async function fetchScorers() {
+ try {
+ const res = await fetch('/api/scorers.json');
+ if (res.ok) {
+ const data = await res.json();
+ fetchedScorers = data.scorers || [];
+ console.log('[WC2026] Fetched baseline scorers:', fetchedScorers.length);
+ } else {
+ console.error('[WC2026] Failed to fetch baseline scorers:', res.status);
+ }
+ } catch (e) {
+ console.error('[WC2026] Error fetching baseline scorers:', e);
+ }
+}
+
+function formatFetchedScorers(scorers) {
+ const formatted = { home: [], away: [] };
+ if (scorers.home) {
+ formatted.home = scorers.home.map(s => ({
+ name: s.name,
+ min: `${s.min}'`
+ }));
+ }
+ if (scorers.away) {
+ formatted.away = scorers.away.map(s => ({
+ name: s.name,
+ min: `${s.min}'`
+ }));
+ }
+ return formatted;
+}
+
+function updateMatchStates() {
+ const now = new Date();
+
+ appGroups.forEach(group => {
+ group.matches.forEach(match => {
+ const matchKey = getMatchKey(group.id, match.matchday, match.home, match.away);
+
+ // 1. Check if user has simulated this match
+ if (userSimulations[matchKey] !== undefined) {
+ const sim = userSimulations[matchKey];
+ match.status = 'FT';
+ match.score = { home: sim.home, away: sim.away };
+
+ // Preserve default scorers if user restored Mexico vs South Africa to 2-1
+ if (group.id === 'A' && match.matchday === 1 && match.home === 'Mexico' && sim.home === 2 && sim.away === 1) {
+ match.scorers = {
+ home: [
+ { name: 'Santiago Giménez', min: "24'" },
+ { name: 'Henry Martín', min: "78'" }
+ ],
+ away: [
+ { name: 'Percy Tau', min: "41' (pen)" }
+ ]
+ };
+ } else {
+ delete match.scorers;
+ }
+ return;
+ }
+
+ // 2. If the match is already finished in-memory, skip processing (caching completed games)
+ if (match.status === 'FT') {
+ return;
+ }
+
+ // 3. Blend fetched scores & dynamic live clock
+ const fetched = fetchedScores.find(f => f.groupId === group.id && f.matchday === match.matchday && f.home === match.home && f.away === match.away);
+ if (!fetched) {
+ delete match.status;
+ delete match.score;
+ delete match.scorers;
+ return;
+ }
+
+ // Look up scorers from the cached fetchedScorers array
+ const scorersInfo = fetchedScorers.find(s => s.groupId === group.id && s.matchday === match.matchday && s.home === match.home && s.away === match.away);
+ const matchScorers = scorersInfo ? scorersInfo.scorers : undefined;
+
+ const venue = venues[match.venue];
+ if (!venue || venue.utcOffset === undefined) {
+ match.status = 'FT';
+ match.score = fetched.score;
+ match.scorers = matchScorers ? formatFetchedScorers(matchScorers) : undefined;
+ return;
+ }
+
+ const matchResult = match.date.match(/([A-Za-z]+)\s+(\d+)/);
+ if (!matchResult) {
+ match.status = 'FT';
+ match.score = fetched.score;
+ match.scorers = matchScorers ? formatFetchedScorers(matchScorers) : undefined;
+ return;
+ }
+
+ const [, monthStr, dayStr] = matchResult;
+ const [hour, minute] = match.time.split(':');
+ const monthMap = { 'Jun': 5, 'Jul': 6 };
+
+ const utcHours = parseInt(hour, 10) - venue.utcOffset;
+ const kickOffUTC = new Date(Date.UTC(2026, monthMap[monthStr], parseInt(dayStr, 10), utcHours, parseInt(minute, 10)));
+
+ const diffMin = Math.floor((now - kickOffUTC) / 60000);
+
+ if (diffMin < 0) {
+ // Future match
+ delete match.status;
+ delete match.score;
+ delete match.scorers;
+ } else if (diffMin >= 110) {
+ // Completed match
+ match.status = 'FT';
+ match.score = fetched.score;
+ match.scorers = matchScorers ? formatFetchedScorers(matchScorers) : undefined;
+ } else {
+ // LIVE match in progress!
+ let currentMinute = diffMin;
+ let displayStatus = `${currentMinute}'`;
+
+ if (diffMin > 45 && diffMin <= 60) {
+ displayStatus = 'HT';
+ currentMinute = 45;
+ } else if (diffMin > 60 && diffMin <= 105) {
+ displayStatus = `${diffMin - 15}'`;
+ currentMinute = diffMin - 15;
+ } else if (diffMin > 105) {
+ displayStatus = `90+${diffMin - 105}'`;
+ currentMinute = 90;
+ }
+
+ match.status = displayStatus;
+ match.isLive = true;
+
+ const liveScorers = { home: [], away: [] };
+ let liveHomeScore = 0;
+ let liveAwayScore = 0;
+
+ if (matchScorers) {
+ if (matchScorers.home) {
+ matchScorers.home.forEach(g => {
+ if (g.min <= currentMinute) {
+ liveHomeScore++;
+ liveScorers.home.push({ name: g.name, min: `${g.min}'` });
+ }
+ });
+ }
+ if (matchScorers.away) {
+ matchScorers.away.forEach(g => {
+ if (g.min <= currentMinute) {
+ liveAwayScore++;
+ liveScorers.away.push({ name: g.name, min: `${g.min}'` });
+ }
+ });
+ }
+ }
+
+ match.score = { home: liveHomeScore, away: liveAwayScore };
+ match.scorers = liveScorers;
+ }
+ });
+ });
+}
// ── Helpers ────────────────────────────────────────────────────────────────
function confBadge(confederation) {
@@ -35,8 +234,10 @@ function confBadge(confederation) {
}
function calculateStandings(group) {
+ const fullGroup = appGroups.find(g => g.id === group.id) || group;
const standings = {};
- group.teams.forEach(team => {
+
+ fullGroup.teams.forEach(team => {
standings[team.name] = {
name: team.name,
flag: team.flag,
@@ -53,8 +254,8 @@ function calculateStandings(group) {
};
});
- group.matches.forEach(match => {
- if (match.status === 'FT' && match.score) {
+ fullGroup.matches.forEach(match => {
+ if (match.score && match.status && match.status !== 'Scheduled') {
const home = match.home;
const away = match.away;
const homeScore = parseInt(match.score.home, 10);
@@ -98,7 +299,7 @@ function calculateStandings(group) {
function teamCard(team, rank, pts, played) {
const hostBadge = team.isHost ? '🏟️ Host' : '';
- const ptsBadge = played > 0 ? `${pts} PTS` : '';
+ const ptsBadge = `${pts} PTS`;
const rankClass = rank <= 2 ? 'rank-adv' : rank === 3 ? 'rank-pot' : 'rank-el';
return `
@@ -113,13 +314,19 @@ function teamCard(team, rank, pts, played) {
}
function groupCard(group) {
- const standings = calculateStandings(group);
- const teamsHTML = standings.map((item, idx) => {
- return teamCard(item, idx + 1, item.pts, item.played);
+ const fullStandings = calculateStandings(group);
+ const displayedStandings = fullStandings.filter(item =>
+ group.teams.some(t => t.name === item.name)
+ );
+
+ const teamsHTML = displayedStandings.map((item) => {
+ const rank = fullStandings.findIndex(s => s.name === item.name) + 1;
+ return teamCard(item, rank, item.pts, item.played);
}).join('');
- const matchCount = group.matches ? group.matches.length : 0;
- const finishedCount = group.matches ? group.matches.filter(m => m.status === 'FT').length : 0;
+ const fullGroup = appGroups.find(g => g.id === group.id) || group;
+ const matchCount = fullGroup.matches ? fullGroup.matches.length : 0;
+ const finishedCount = fullGroup.matches ? fullGroup.matches.filter(m => m.status === 'FT').length : 0;
const matchCountStr = finishedCount > 0 ? `${finishedCount}/${matchCount} played` : `${matchCount} matches`;
return `
@@ -167,48 +374,57 @@ function getUserLocalTime(match, venue) {
function buildMatchdayHTML(group) {
const matchdayNames = { 1: 'Matchday 1', 2: 'Matchday 2', 3: 'Matchday 3' };
+ const fullGroup = appGroups.find(g => g.id === group.id) || group;
+
return [1, 2, 3].map(md => {
- const dayMatches = group.matches.filter(m => m.matchday === md);
+ const dayMatches = fullGroup.matches.filter(m => m.matchday === md);
const matchesHTML = dayMatches.map((match, idx) => {
const venue = venues[match.venue];
- const home = group.teams.find(t => t.name === match.home);
- const away = group.teams.find(t => t.name === match.away);
- const cardId = `match-${group.id}-${md}-${idx}`;
+ const home = fullGroup.teams.find(t => t.name === match.home);
+ const away = fullGroup.teams.find(t => t.name === match.away);
+ const cardId = `match-${fullGroup.id}-${md}-${idx}`;
const userLocalTimeStr = getUserLocalTime(match, venue);
const isFinished = match.status === 'FT';
+ const isLive = match.isLive;
let scoreDisplayHTML = `
VS`;
- if (isFinished && match.score) {
- const homeWinnerClass = match.score.home > match.score.away ? 'mc-team-winner' : (match.score.home < match.score.away ? 'mc-team-loser' : 'mc-team-draw');
- const awayWinnerClass = match.score.away > match.score.home ? 'mc-team-winner' : (match.score.away < match.score.home ? 'mc-team-loser' : 'mc-team-draw');
+ if (match.score && match.status && match.status !== 'Scheduled') {
+ const homeWinnerClass = isFinished ? (match.score.home > match.score.away ? 'mc-team-winner' : (match.score.home < match.score.away ? 'mc-team-loser' : 'mc-team-draw')) : '';
+ const awayWinnerClass = isFinished ? (match.score.away > match.score.home ? 'mc-team-winner' : (match.score.away < match.score.home ? 'mc-team-loser' : 'mc-team-draw')) : '';
scoreDisplayHTML = `
${match.score.home}
–
${match.score.away}
- FT
+ ${match.status}
`;
}
let scorersHTML = '';
- if (isFinished && match.scorers) {
+ if (match.score && match.scorers) {
const homeScorersList = match.scorers.home ? match.scorers.home.map(s => `${s.name} ${s.min}`).join(', ') : '';
const awayScorersList = match.scorers.away ? match.scorers.away.map(s => `${s.name} ${s.min}`).join(', ') : '';
- scorersHTML = `
-
-
⚽ ${homeScorersList || '—'}
-
-
⚽ ${awayScorersList || '—'}
-
- `;
+ if (homeScorersList || awayScorersList) {
+ scorersHTML = `
+
+
⚽ ${homeScorersList || '—'}
+
+
⚽ ${awayScorersList || '—'}
+
+ `;
+ }
}
+ const hasScoreInput = match.score !== undefined;
+ const inputHomeVal = hasScoreInput ? match.score.home : '';
+ const inputAwayVal = hasScoreInput ? match.score.away : '';
+
return `
-
+
@@ -356,9 +572,10 @@ function buildStandingsHTML(group) {
// ── Modal ──────────────────────────────────────────────────────────────────
function openModal(group) {
- if (!group || !group.matches || group.matches.length === 0) return;
+ const fullGroup = appGroups.find(g => g.id === group.id) || group;
+ if (!fullGroup || !fullGroup.matches || fullGroup.matches.length === 0) return;
- const teamsHTML = group.teams.map(t =>
+ const teamsHTML = fullGroup.teams.map(t =>
`
${t.flag}${t.name}
`
).join('');
@@ -366,10 +583,10 @@ function openModal(group) {
modal.innerHTML = `
- `;
@@ -442,25 +659,11 @@ window.saveMatchScore = function(groupId, matchday, matchIdx, cardId) {
const dayMatches = group.matches.filter(m => m.matchday === matchday);
const match = dayMatches[matchIdx];
if (match) {
- match.status = 'FT';
- match.score = { home: homeScore, away: awayScore };
-
- // Preserve or set scorers for Mexico vs South Africa if restored to 2 - 1
- if (groupId === 'A' && matchday === 1 && matchIdx === 0 && homeScore === 2 && awayScore === 1) {
- match.scorers = {
- home: [
- { name: 'Santiago Giménez', min: "24'" },
- { name: 'Henry Martín', min: "78'" }
- ],
- away: [
- { name: 'Percy Tau', min: "41' (pen)" }
- ]
- };
- } else {
- delete match.scorers;
- }
-
- saveState();
+ const matchKey = getMatchKey(groupId, matchday, match.home, match.away);
+ userSimulations[matchKey] = { home: homeScore, away: awayScore };
+ saveUserSimulations();
+ resetMatchToBaseState(groupId, matchday, match.home, match.away);
+ updateMatchStates();
render();
const updatedGroup = appGroups.find(g => g.id === groupId);
@@ -479,11 +682,11 @@ window.clearMatchScore = function(groupId, matchday, matchIdx, cardId) {
const dayMatches = group.matches.filter(m => m.matchday === matchday);
const match = dayMatches[matchIdx];
if (match) {
- delete match.status;
- delete match.score;
- delete match.scorers;
-
- saveState();
+ const matchKey = getMatchKey(groupId, matchday, match.home, match.away);
+ delete userSimulations[matchKey];
+ saveUserSimulations();
+ resetMatchToBaseState(groupId, matchday, match.home, match.away);
+ updateMatchStates();
render();
const updatedGroup = appGroups.find(g => g.id === groupId);
@@ -498,8 +701,10 @@ window.clearMatchScore = function(groupId, matchday, matchIdx, cardId) {
window.resetAllScores = function() {
if (confirm('Are you sure you want to reset all simulated scores back to default?')) {
- localStorage.removeItem('wc2026_groups');
- loadState();
+ localStorage.removeItem('wc2026_user_sims');
+ loadUserSimulations();
+ appGroups = JSON.parse(JSON.stringify(groups));
+ updateMatchStates();
render();
closeModal();
animateCounters();
@@ -522,11 +727,220 @@ window.toggleMatch = function(cardId) {
}
};
+// ── Rankings & Scorers Helpers ─────────────────────────────────────────────
+function calculateOverallRankings() {
+ let allStandings = [];
+ appGroups.forEach(group => {
+ const groupStandings = calculateStandings(group);
+ groupStandings.forEach((teamStandings, idx) => {
+ allStandings.push({
+ ...teamStandings,
+ groupId: group.id,
+ groupRank: idx + 1
+ });
+ });
+ });
+
+ return allStandings.sort((a, b) => {
+ if (b.pts !== a.pts) return b.pts - a.pts;
+ if (b.gd !== a.gd) return b.gd - a.gd;
+ if (b.gf !== a.gf) return b.gf - a.gf;
+ return a.name.localeCompare(b.name);
+ });
+}
+
+function buildOverallRankingsHTML() {
+ const sortedTeams = calculateOverallRankings();
+
+ let filtered = sortedTeams;
+ if (activeFilter !== 'all') {
+ filtered = filtered.filter(t => t.confederation === activeFilter);
+ }
+ if (searchQuery) {
+ filtered = filtered.filter(t =>
+ t.name.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }
+
+ if (filtered.length === 0) {
+ return `
`;
+ }
+
+ const rows = filtered.map((team, idx) => {
+ const overallRank = sortedTeams.findIndex(t => t.name === team.name) + 1;
+ const rankClass = overallRank <= 32 ? 'rank-adv' : 'rank-el';
+ return `
+
+ | ${overallRank} |
+
+ ${team.flag}
+ ${team.name}
+ Group ${team.groupId}
+ ${team.isHost ? '🏟️' : ''}
+ |
+ ${team.confederation} |
+ ${team.played} |
+ ${team.won} |
+ ${team.drawn} |
+ ${team.lost} |
+ ${team.gf} |
+ ${team.ga} |
+ ${team.gd > 0 ? '+' + team.gd : team.gd} |
+ ${team.pts} |
+
+ `;
+ }).join('');
+
+ return `
+
+
+
+
+
+
+ | # |
+ Team |
+ Conf. |
+ P |
+ W |
+ D |
+ L |
+ GF |
+ GA |
+ GD |
+ Pts |
+
+
+
+ ${rows}
+
+
+
+
+ `;
+}
+
+function getTopScorers() {
+ const scorersMap = {};
+
+ appGroups.forEach(group => {
+ group.matches.forEach(match => {
+ if (match.score && match.status && match.status !== 'Scheduled' && match.scorers) {
+ if (match.scorers.home) {
+ match.scorers.home.forEach(s => {
+ if (s.name.includes('(OG)') || s.name.includes('OG') || s.name.includes('Own Goal')) return;
+ const name = s.name.replace(/\s*\(pen\)/g, '').trim();
+ if (!scorersMap[name]) {
+ scorersMap[name] = { name, goals: 0, team: match.home, flag: '' };
+ const teamObj = group.teams.find(t => t.name === match.home);
+ if (teamObj) scorersMap[name].flag = teamObj.flag;
+ }
+ scorersMap[name].goals += 1;
+ });
+ }
+ if (match.scorers.away) {
+ match.scorers.away.forEach(s => {
+ if (s.name.includes('(OG)') || s.name.includes('OG') || s.name.includes('Own Goal')) return;
+ const name = s.name.replace(/\s*\(pen\)/g, '').trim();
+ if (!scorersMap[name]) {
+ scorersMap[name] = { name, goals: 0, team: match.away, flag: '' };
+ const teamObj = group.teams.find(t => t.name === match.away);
+ if (teamObj) scorersMap[name].flag = teamObj.flag;
+ }
+ scorersMap[name].goals += 1;
+ });
+ }
+ }
+ });
+ });
+
+ return Object.values(scorersMap).sort((a, b) => {
+ if (b.goals !== a.goals) return b.goals - a.goals;
+ return a.name.localeCompare(b.name);
+ });
+}
+
+function buildTopScorersHTML() {
+ const scorers = getTopScorers();
+
+ let filtered = scorers;
+ if (searchQuery) {
+ filtered = filtered.filter(s =>
+ s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ s.team.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }
+
+ if (filtered.length === 0) {
+ return `
`;
+ }
+
+ const rows = filtered.map((player, idx) => {
+ const rank = scorers.findIndex(s => s.name === player.name) + 1;
+ const rankClass = rank === 1 ? 'rank-gold' : rank === 2 ? 'rank-silver' : rank === 3 ? 'rank-bronze' : '';
+ const trophy = rank === 1 ? '🏆' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `#${rank}`;
+
+ return `
+
+
${trophy}
+
+ ${player.name}
+ ${player.flag} ${player.team}
+
+
+ ${player.goals}
+ ${player.goals === 1 ? 'goal' : 'goals'}
+
+
+ `;
+ }).join('');
+
+ return `
+
+ `;
+}
+
// ── Render ─────────────────────────────────────────────────────────────────
-function render() {
+let renderCache = {};
+let lastFilterState = '';
+
+function render(forceFull = false) {
const grid = document.getElementById('groups-grid');
if (!grid) return;
+ const currentFilterState = `${activeView}-${activeFilter}-${searchQuery}`;
+ if (currentFilterState !== lastFilterState) {
+ forceFull = true;
+ lastFilterState = currentFilterState;
+ }
+
+ if (activeView === 'rankings') {
+ grid.style.display = 'block';
+ grid.innerHTML = buildOverallRankingsHTML();
+ renderCache = {};
+ return;
+ }
+
+ if (activeView === 'scorers') {
+ grid.style.display = 'block';
+ grid.innerHTML = buildTopScorersHTML();
+ renderCache = {};
+ return;
+ }
+
+ grid.style.display = 'grid';
+
const filteredGroups = appGroups.map(group => {
let filteredTeams = group.teams;
if (activeFilter !== 'all') {
@@ -542,18 +956,49 @@ function render() {
if (filteredGroups.length === 0) {
grid.innerHTML = `
`;
+ renderCache = {};
return;
}
- grid.innerHTML = filteredGroups.map(groupCard).join('');
+ if (forceFull || Object.keys(renderCache).length === 0) {
+ renderCache = {};
+ grid.innerHTML = filteredGroups.map(group => {
+ const html = groupCard(group);
+ renderCache[group.id] = html;
+ return html;
+ }).join('');
- // Animate cards in
- requestAnimationFrame(() => {
- document.querySelectorAll('.group-card').forEach((card, i) => {
- card.style.animationDelay = `${i * 60}ms`;
- card.classList.add('animate-in');
+ // Animate cards in
+ requestAnimationFrame(() => {
+ document.querySelectorAll('.group-card').forEach((card, i) => {
+ card.style.animationDelay = `${i * 60}ms`;
+ card.classList.add('animate-in');
+ });
});
- });
+ } else {
+ // Targeted update: only re-render groups that actually changed
+ filteredGroups.forEach(group => {
+ const html = groupCard(group);
+ if (renderCache[group.id] !== html) {
+ const cardEl = document.querySelector(`.group-card[data-group="${group.id}"]`);
+ if (cardEl) {
+ cardEl.outerHTML = html;
+ // Re-trigger animate-in on updated card
+ const newCardEl = document.querySelector(`.group-card[data-group="${group.id}"]`);
+ if (newCardEl) {
+ newCardEl.classList.add('animate-in');
+ }
+ } else {
+ forceFull = true;
+ }
+ renderCache[group.id] = html;
+ }
+ });
+
+ if (forceFull) {
+ render(true);
+ }
+ }
}
// ── Global click handler (set ONCE on document.body) ──────────────────────
@@ -573,6 +1018,28 @@ function setupGlobalClick() {
});
}
+// ── View Switcher Tabs ─────────────────────────────────────────────────────
+function setupViewTabs() {
+ document.querySelectorAll('.view-tab-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('.view-tab-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ activeView = btn.dataset.view;
+
+ const filterNav = document.querySelector('.filter-nav');
+ if (filterNav) {
+ if (activeView === 'scorers') {
+ filterNav.style.display = 'none';
+ } else {
+ filterNav.style.display = 'flex';
+ }
+ }
+
+ render(true);
+ });
+ });
+}
+
// ── Filter buttons ─────────────────────────────────────────────────────────
function setupFilters() {
document.querySelectorAll('.filter-btn').forEach(btn => {
@@ -653,9 +1120,15 @@ function buildShell() {
+
+ 🗂️ Groups
+ 📈 Overall Ranking
+ ⚽ Top Scorers
+
+
🔍
-
+