+212
-10
@@ -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
@@ -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%;
|
||||||
|
|||||||
Reference in New Issue
Block a user