From 3938bfae5b7a8399cf4a298931980db035259a73 Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Mon, 15 Jun 2026 16:51:24 +0200 Subject: [PATCH] added ranking and fetching sync --- public/api/scorers.json | 196 ++++++++++++ public/api/scores.json | 95 ++++++ src/data.js | 4 +- src/main.js | 691 +++++++++++++++++++++++++++++++++++----- src/style.css | 155 +++++++++ 5 files changed, 1052 insertions(+), 89 deletions(-) create mode 100644 public/api/scorers.json create mode 100644 public/api/scores.json 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 ` -
+
- ${isFinished ? `` : ''} + + ${hasScoreInput ? `` : ''}
@@ -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 => `` ).join(''); @@ -366,10 +583,10 @@ function openModal(group) { modal.innerHTML = `