+212
-10
@@ -48,33 +48,169 @@ function saveUserSimulations() {
|
||||
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() {
|
||||
// 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 {
|
||||
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);
|
||||
console.error('[WC2026] Failed to fetch local baseline scores:', res.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WC2026] Error fetching baseline scores:', e);
|
||||
console.error('[WC2026] Error fetching local baseline scores:', e);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
console.error('[WC2026] Failed to fetch local baseline scorers:', res.status);
|
||||
}
|
||||
} 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 ───────────────────────────────────────────────────────
|
||||
window.toggleMatch = function(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="CONCACAF">🌎 CONCACAF</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1216,9 +1409,14 @@ function initLiveTicking() {
|
||||
|
||||
// Sync with server once every 60 seconds (prevents rate limit blocking)
|
||||
setInterval(async () => {
|
||||
await fetchScores();
|
||||
updateMatchStates();
|
||||
render();
|
||||
try {
|
||||
await Promise.all([fetchScores(), fetchScorers()]);
|
||||
updateMatchStates();
|
||||
render();
|
||||
updateSyncTooltip();
|
||||
} catch (e) {
|
||||
console.error('[WC2026] Background sync failed:', e);
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
@@ -1231,6 +1429,10 @@ loadUserSimulations();
|
||||
Promise.all([fetchScores(), fetchScorers()]).then(() => {
|
||||
appGroups = JSON.parse(JSON.stringify(groups));
|
||||
initLiveTicking();
|
||||
|
||||
// Set initial sync time in tooltip
|
||||
updateSyncTooltip();
|
||||
|
||||
setupViewTabs();
|
||||
setupFilters();
|
||||
setupSearch();
|
||||
|
||||
+47
-4
@@ -808,10 +808,10 @@ body {
|
||||
.modal-group-id { font-size: 36px; }
|
||||
}
|
||||
|
||||
/* ── Reset Button in Header/Filter Bar ── */
|
||||
.reset-simulation-btn {
|
||||
/* ── Sync & Reset Buttons in Header/Filter Bar ── */
|
||||
.sync-scores-btn {
|
||||
background: var(--bg-element);
|
||||
border: 1px dashed var(--border);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
@@ -823,6 +823,40 @@ body {
|
||||
align-items: center;
|
||||
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 {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
@@ -830,6 +864,15 @@ body {
|
||||
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 ── */
|
||||
.team-rank {
|
||||
font-size: 11px;
|
||||
@@ -997,7 +1040,7 @@ body {
|
||||
padding: 10px 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.reset-simulation-btn {
|
||||
.sync-scores-btn, .reset-simulation-btn {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user