update
dongho-repo/worldcup2026/pipeline/head This commit looks good

This commit is contained in:
Dongho Kim
2026-06-16 14:41:43 +02:00
parent 3938bfae5b
commit dcc6538632
2 changed files with 259 additions and 14 deletions
+212 -10
View File
@@ -48,33 +48,169 @@ function saveUserSimulations() {
localStorage.setItem('wc2026_user_sims', JSON.stringify(userSimulations)); localStorage.setItem('wc2026_user_sims', JSON.stringify(userSimulations));
} }
const apiTeamNameMap = {
'Bosnia and Herzegovina': 'Bosnia & Herzegovina',
'Ivory Coast': "Côte d'Ivoire",
'Turkey': 'Türkiye',
'Democratic Republic of the Congo': 'DR Congo'
};
function mapApiTeamName(name) {
return apiTeamNameMap[name] || name;
}
function parseApiScorers(scorerStr) {
if (!scorerStr || scorerStr === 'null') return [];
// Clean characters: { } " “ ”
let cleaned = scorerStr.replace(/[{}"“”]/g, '');
if (!cleaned.trim()) return [];
// Split by comma
const parts = cleaned.split(',');
return parts.map(p => {
p = p.trim();
// Match name followed by minute (e.g. "Santiago Giménez 24'")
const match = p.match(/(.+?)\s+(\d+)'?/);
if (match) {
return {
name: match[1].trim(),
min: parseInt(match[2], 10)
};
}
return { name: p, min: 90 }; // fallback
}).filter(s => s.name);
}
const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in ms
const CACHE_KEY_SCORES = 'wc2026_scores_cache';
const CACHE_KEY_SCORERS = 'wc2026_scorers_cache';
const CACHE_KEY_TIMESTAMP = 'wc2026_sync_timestamp';
function updateSyncTooltip() {
const btn = document.getElementById('sync-scores-btn');
if (btn) {
const cachedTime = localStorage.getItem(CACHE_KEY_TIMESTAMP);
const syncDate = cachedTime ? new Date(parseInt(cachedTime, 10)) : new Date();
const timeStr = syncDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
btn.title = `Last synced: ${timeStr}. Sync live scores with server.`;
}
}
async function fetchScores() { async function fetchScores() {
// Check if we have valid cached data
try {
const cachedTime = localStorage.getItem(CACHE_KEY_TIMESTAMP);
const cachedScores = localStorage.getItem(CACHE_KEY_SCORES);
const cachedScorers = localStorage.getItem(CACHE_KEY_SCORERS);
if (cachedTime && cachedScores && cachedScorers) {
const age = Date.now() - parseInt(cachedTime, 10);
if (age < CACHE_DURATION) {
fetchedScores = JSON.parse(cachedScores);
fetchedScorers = JSON.parse(cachedScorers);
return; // Success, skip fetch
}
}
} catch (err) {
console.error('[WC2026] Error reading sync cache:', err);
}
// Cache is missing or expired, fetch fresh data
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 25000);
const res = await fetch('https://worldcup26.ir/get/games', { signal: controller.signal });
clearTimeout(timeoutId);
if (res.ok) {
const data = await res.json();
if (data && Array.isArray(data.games)) {
const translatedScores = [];
const translatedScorers = [];
data.games.forEach(game => {
const groupId = game.group;
const matchday = parseInt(game.matchday, 10);
const home = mapApiTeamName(game.home_team_name_en);
const away = mapApiTeamName(game.away_team_name_en);
const homeScore = game.home_score !== null && game.home_score !== 'null' ? parseInt(game.home_score, 10) : null;
const awayScore = game.away_score !== null && game.away_score !== 'null' ? parseInt(game.away_score, 10) : null;
if (groupId && !isNaN(matchday) && home && away && homeScore !== null && awayScore !== null && !isNaN(homeScore) && !isNaN(awayScore)) {
translatedScores.push({
groupId,
matchday,
home,
away,
score: { home: homeScore, away: awayScore }
});
const homeScorersList = parseApiScorers(game.home_scorers);
const awayScorersList = parseApiScorers(game.away_scorers);
if (homeScorersList.length > 0 || awayScorersList.length > 0) {
translatedScorers.push({
groupId,
matchday,
home,
away,
scorers: {
home: homeScorersList,
away: awayScorersList
}
});
}
}
});
fetchedScores = translatedScores;
fetchedScorers = translatedScorers;
// Save to cache
try {
localStorage.setItem(CACHE_KEY_SCORES, JSON.stringify(fetchedScores));
localStorage.setItem(CACHE_KEY_SCORERS, JSON.stringify(fetchedScorers));
localStorage.setItem(CACHE_KEY_TIMESTAMP, Date.now().toString());
} catch (cacheErr) {
console.error('[WC2026] Error writing sync cache:', cacheErr);
}
return; // Success!
}
}
} catch (e) {
// Bypassed online API console warning
}
// Fallback to local scores
try { try {
const res = await fetch('/api/scores.json'); const res = await fetch('/api/scores.json');
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
fetchedScores = data.matches || []; fetchedScores = data.matches || [];
console.log('[WC2026] Fetched baseline scores:', fetchedScores.length);
} else { } else {
console.error('[WC2026] Failed to fetch baseline scores:', res.status); console.error('[WC2026] Failed to fetch local baseline scores:', res.status);
} }
} catch (e) { } catch (e) {
console.error('[WC2026] Error fetching baseline scores:', e); console.error('[WC2026] Error fetching local baseline scores:', e);
} }
} }
async function fetchScorers() { async function fetchScorers() {
// If we already loaded scorers from the online API or cache, skip local fetch
if (fetchedScorers.length > 0 && fetchedScores.length > 0) {
return;
}
try { try {
const res = await fetch('/api/scorers.json'); const res = await fetch('/api/scorers.json');
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
fetchedScorers = data.scorers || []; fetchedScorers = data.scorers || [];
console.log('[WC2026] Fetched baseline scorers:', fetchedScorers.length);
} else { } else {
console.error('[WC2026] Failed to fetch baseline scorers:', res.status); console.error('[WC2026] Failed to fetch local baseline scorers:', res.status);
} }
} catch (e) { } catch (e) {
console.error('[WC2026] Error fetching baseline scorers:', e); console.error('[WC2026] Error fetching local baseline scorers:', e);
} }
} }
@@ -711,6 +847,62 @@ window.resetAllScores = function() {
} }
}; };
window.triggerManualSync = async function() {
const btn = document.getElementById('sync-scores-btn');
if (!btn || btn.classList.contains('syncing')) return;
btn.classList.add('syncing');
btn.disabled = true;
const icon = btn.querySelector('.sync-icon');
const textNode = btn.querySelector('.sync-text');
if (icon) icon.classList.add('spin-anim');
if (textNode) textNode.textContent = 'Syncing...';
try {
// Fetch both scores and scorers
await Promise.all([fetchScores(), fetchScorers()]);
// Update local match states based on newly fetched baseline scores & time-based ticking
updateMatchStates();
render();
// Success feedback
btn.classList.remove('syncing');
btn.classList.add('success');
if (textNode) textNode.textContent = 'Synced!';
if (icon) {
icon.classList.remove('spin-anim');
icon.textContent = '✅';
}
// Update tooltip with exact sync time
updateSyncTooltip();
setTimeout(() => {
btn.classList.remove('success');
btn.disabled = false;
if (textNode) textNode.textContent = 'Sync';
if (icon) icon.textContent = '🔄';
}, 1500);
} catch (err) {
console.error('[WC2026] Manual sync failed:', err);
btn.classList.remove('syncing');
btn.classList.add('error');
if (textNode) textNode.textContent = 'Failed';
if (icon) {
icon.classList.remove('spin-anim');
icon.textContent = '❌';
}
setTimeout(() => {
btn.classList.remove('error');
btn.disabled = false;
if (textNode) textNode.textContent = 'Sync';
if (icon) icon.textContent = '🔄';
}, 2000);
}
};
// ── Accordion toggle ─────────────────────────────────────────────────────── // ── Accordion toggle ───────────────────────────────────────────────────────
window.toggleMatch = function(cardId) { window.toggleMatch = function(cardId) {
const card = document.getElementById(cardId); const card = document.getElementById(cardId);
@@ -1138,7 +1330,8 @@ function buildShell() {
<button class="filter-btn" data-filter="CAF">🌍 CAF</button> <button class="filter-btn" data-filter="CAF">🌍 CAF</button>
<button class="filter-btn" data-filter="CONCACAF">🌎 CONCACAF</button> <button class="filter-btn" data-filter="CONCACAF">🌎 CONCACAF</button>
<button class="filter-btn" data-filter="OFC">🌊 OFC</button> <button class="filter-btn" data-filter="OFC">🌊 OFC</button>
<button class="reset-simulation-btn" onclick="resetAllScores()" title="Reset all custom scores to defaults">🔄 Reset</button> <button class="sync-scores-btn" id="sync-scores-btn" onclick="triggerManualSync()" title="Sync live scores with server"><span class="sync-icon">🔄</span> <span class="sync-text">Sync</span></button>
<button class="reset-simulation-btn" onclick="resetAllScores()" title="Reset all custom scores to defaults">🧹 Reset</button>
</nav> </nav>
</div> </div>
</div> </div>
@@ -1216,9 +1409,14 @@ function initLiveTicking() {
// Sync with server once every 60 seconds (prevents rate limit blocking) // Sync with server once every 60 seconds (prevents rate limit blocking)
setInterval(async () => { setInterval(async () => {
await fetchScores(); try {
updateMatchStates(); await Promise.all([fetchScores(), fetchScorers()]);
render(); updateMatchStates();
render();
updateSyncTooltip();
} catch (e) {
console.error('[WC2026] Background sync failed:', e);
}
}, 60000); }, 60000);
} }
@@ -1231,6 +1429,10 @@ loadUserSimulations();
Promise.all([fetchScores(), fetchScorers()]).then(() => { Promise.all([fetchScores(), fetchScorers()]).then(() => {
appGroups = JSON.parse(JSON.stringify(groups)); appGroups = JSON.parse(JSON.stringify(groups));
initLiveTicking(); initLiveTicking();
// Set initial sync time in tooltip
updateSyncTooltip();
setupViewTabs(); setupViewTabs();
setupFilters(); setupFilters();
setupSearch(); setupSearch();
+47 -4
View File
@@ -808,10 +808,10 @@ body {
.modal-group-id { font-size: 36px; } .modal-group-id { font-size: 36px; }
} }
/* ── Reset Button in Header/Filter Bar ── */ /* ── Sync & Reset Buttons in Header/Filter Bar ── */
.reset-simulation-btn { .sync-scores-btn {
background: var(--bg-element); background: var(--bg-element);
border: 1px dashed var(--border); border: 1px solid var(--border);
border-radius: 20px; border-radius: 20px;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
@@ -823,6 +823,40 @@ body {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.sync-scores-btn:hover:not(:disabled) {
background: var(--bg-element-hover);
border-color: var(--gold);
color: var(--gold);
transform: translateY(-1px);
}
.sync-scores-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.sync-scores-btn.success {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.4);
color: #22c55e;
}
.sync-scores-btn.error {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.4);
color: #ef4444;
}
.reset-simulation-btn {
background: var(--bg-element);
border: 1px dashed var(--border);
border-radius: 20px;
color: var(--text-muted);
cursor: pointer;
font-family: inherit; font-size: 12px; font-weight: 600;
padding: 6px 14px;
transition: all var(--transition);
display: flex;
align-items: center;
gap: 6px;
}
.reset-simulation-btn:hover { .reset-simulation-btn:hover {
background: rgba(239, 68, 68, 0.1); background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.4); border-color: rgba(239, 68, 68, 0.4);
@@ -830,6 +864,15 @@ body {
transform: translateY(-1px); transform: translateY(-1px);
} }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin-anim {
animation: spin 1s linear infinite;
display: inline-block;
}
/* ── Group Card Ranks & Points ── */ /* ── Group Card Ranks & Points ── */
.team-rank { .team-rank {
font-size: 11px; font-size: 11px;
@@ -997,7 +1040,7 @@ body {
padding: 10px 6px; padding: 10px 6px;
font-size: 13px; font-size: 13px;
} }
.reset-simulation-btn { .sync-scores-btn, .reset-simulation-btn {
margin-left: 0; margin-left: 0;
margin-top: 10px; margin-top: 10px;
width: 100%; width: 100%;