(function(){ const DATA_URL_JSON = '/data/club.json'; const TZ = 'Europe/Prague'; let state = { data: null, compIndex: 0, matchIndex: 0, intervalId: null, upcomingTimerId: null, }; function parseCZDate(s){ try{ // format: 02.01.2006 15:04 const [d, t] = s.split(' '); const [day, month, year] = d.split('.').map(Number); const [hour, minute] = t.split(':').map(Number); // Interpret as local time (Europe/Prague). Using local constructor avoids UTC offset skew. const dt = new Date(year, month-1, day, hour, minute); return dt; }catch(e){ return null; } } // using original logo URLs; no cleaning/proxy function ensureFacrStyles(){ if(document.getElementById('facr-styles')) return; const css = ` /* logo background unchanged */ .facr-nav{ background:#ffffff22; border:1px solid #ffffff55; color:#fff; padding:6px 10px; border-radius:6px; cursor:pointer; backdrop-filter: blur(2px); } .facr-nav:hover{ background:#ffffff40; } .facr-nav:disabled{ opacity:.5; cursor:default; } .facr-tab{ padding:6px 10px; margin:4px; border-radius:16px; border:1px solid #c42221; color:#c42221; background:#ffffff; font-weight:600; } .facr-tab.active{ background:#c42221; color:#ffffff; } .facr-tab:hover{ background:#c42221cc; color:#ffffff; } .facr-inline-status{ margin-left:8px; font-weight:700; font-size:14px; white-space:nowrap; color:inherit; display:inline-block; vertical-align:middle; } /* Default (desktop): show middle only */ #facr-countdown{ display:none !important; } .facr-inline-status{ display:none !important; } .facr-mob-center-score{ display:none; font-weight:700; font-size:24px; line-height:1; } /* Mobile: keep only the bottom countdown by default */ @media (max-width: 767px){ #facr-mid{ display:none !important; } #facr-countdown{ display:block !important; } .facr-inline-status{ display:none !important; } .facr-mob-center-score{ display:inline-block !important; } /* If finished, hide the bottom countdown */ .facr-finished #facr-countdown{ display:none !important; } } @media (max-width: 480px){ #facr-mid{ font-size:28px !important; min-width:100px; } .facr-tab{ padding:4px 8px; font-size:14px; } .facr-inline-status{ font-size:12px; margin-left:6px; } .facr-nav{ padding:4px 8px; } } `; const style = document.createElement('style'); style.id = 'facr-styles'; style.type = 'text/css'; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } function fmtCountdown(ms){ if(ms <= 0) return '0m'; const totalMin = Math.floor(ms/60000); const d = Math.floor(totalMin/(60*24)); const h = Math.floor((totalMin - d*60*24)/60); const m = totalMin % 60; const parts = []; if(d) parts.push(`${d}d`); if(h || d) parts.push(`${h}h`); parts.push(`${m}m`); return parts.join(' '); } function fmtCountdownLong(ms){ if(ms < 0) ms = 0; const totalSec = Math.floor(ms/1000); const d = Math.floor(totalSec / (24*3600)); const h = Math.floor((totalSec % (24*3600)) / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; const dd = d > 0 ? `${d}d ` : ''; const hh = String(h).padStart(2,'0'); const mm = String(m).padStart(2,'0'); const ss = String(s).padStart(2,'0'); return `${dd}${hh}:${mm}:${ss}`.trim(); } function todayCZ(){ const now = new Date(); const dd = String(now.getDate()).padStart(2,'0'); const mm = String(now.getMonth()+1).padStart(2,'0'); const yyyy = now.getFullYear(); return `${dd}.${mm}.${yyyy}`; } async function fetchData(){ const res = await fetch(DATA_URL_JSON, { cache: 'no-cache' }); if(!res.ok) throw new Error('Failed to fetch data'); const data = await res.json(); state.data = data; return data; } function escapeHTML(s){ if(s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function truncate(s, max){ const str = s == null ? '' : String(s); if(max <= 0) return ''; return str.length > max ? str.slice(0, max - 1) + '…' : str; } function isWithinMatchWindow(){ if(!state.data) return false; const now = new Date(); for(const comp of state.data.club_detail.competitions || []){ for(const m of comp.matches || []){ const dt = parseCZDate(m.date_time); if(!dt) continue; const diffMs = Math.abs(now - dt); if(diffMs <= 2*60*60*1000) return true; } } return false; } function upcomingMatchesAll(){ const list = []; if(!state.data) return list; const now = new Date(); const windowStart = new Date(now.getTime() - 3*24*60*60*1000); for(const comp of state.data.club_detail.competitions || []){ const candidates = (comp.matches || []) .map(m=>({ comp, match: m, dt: parseCZDate(m.date_time) })) .filter(x=> x.dt && x.dt >= windowStart) .sort((a,b)=> a.dt - b.dt); if(candidates.length === 0) continue; // Prefer the latest recently finished match within the 3-day window; // if none, then choose the next upcoming; if none, fall back to the latest overall in window let pick = null; // find latest finished (dt < now) within window for(let i = candidates.length - 1; i >= 0; i--){ if(candidates[i].dt < now){ pick = candidates[i]; break; } } if(!pick){ pick = candidates.find(x=> x.dt >= now) || candidates[candidates.length - 1]; } list.push(pick); } // sort resulting per-competition picks by time ascending for navigation list.sort((a,b)=> a.dt - b.dt); return list; } function renderUpcoming(){ const root = document.getElementById('facr-upcoming'); if(!root) return; ensureFacrStyles(); // clear any previous per-second timer if(state.upcomingTimerId){ clearInterval(state.upcomingTimerId); state.upcomingTimerId = null; } const items = upcomingMatchesAll(); if(items.length === 0){ root.innerHTML = '
Žádné nadcházející zápasy
'; return; } // Selection policy: // 1) If there is any finished match within the last 3 days across competitions, // show the latest such finished match (keep result visible for 3 days) // 2) Otherwise, show the first future match // 3) If none, show the latest overall (shouldn't happen as items filtered by 3d window per-comp) const now = new Date(); const threeDms = 3*24*60*60*1000; let latestRecentIdx = -1; let latestRecentTime = -Infinity; items.forEach((it, i) => { const dtms = it.dt.getTime(); if(dtms <= now.getTime() && now.getTime() - dtms <= threeDms){ if(dtms > latestRecentTime){ latestRecentTime = dtms; latestRecentIdx = i; } } }); let preferredIdx = latestRecentIdx; if(preferredIdx === -1){ preferredIdx = items.findIndex(it => it.dt >= now); if(preferredIdx === -1) preferredIdx = items.length - 1; } const idx = Math.min(state.matchIndex || preferredIdx, items.length-1); const { comp, match:m } = items[idx]; const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 60); const homeLogo = m.home_logo_url || 'img/logo.png'; const awayLogo = m.away_logo_url || 'img/logo.png'; const facrLink = m.facr_link || comp.matches_link || state.data.club_detail.url || '#'; const UP_MAX = 24; const homeName = truncate(escapeHTML(m.home), UP_MAX); const awayName = truncate(escapeHTML(m.away), UP_MAX); const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 40); // Determine if match is today (CZ date) and precompute mid display text const matchDayCZ = (m.date_time || '').split(' ')[0]; const isToday = matchDayCZ === todayCZ(); const startDt = m.date_time ? parseCZDate(m.date_time) : null; const diffMsPre = startDt ? (startDt.getTime() - new Date().getTime()) : 0; const midText = (diffMsPre > 0) ? `Za ${fmtCountdownLong(diffMsPre)}` : (m.score || '-'); // Determine status flags and parse score parts if any const now2_forTpl = new Date(); const startMs_forTpl = m.date_time ? parseCZDate(m.date_time).getTime() : 0; const diff_forTpl = startMs_forTpl - now2_forTpl.getTime(); const twoH_forTpl = 2*60*60*1000; const threeD_forTpl = 3*24*60*60*1000; const isFuture = diff_forTpl > 0; const isLive = Math.abs(diff_forTpl) <= twoH_forTpl; const isRecentFinished = (!isFuture && !isLive && -diff_forTpl < threeD_forTpl); const scoreStr = m.score || ''; const s1 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[0]) : ''; const s2 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[1]) : ''; // Date/time formatting for display lines const dtForDisp = startDt || (m.date_time ? parseCZDate(m.date_time) : null); const dd = dtForDisp ? String(dtForDisp.getDate()).padStart(1,'') : ''; const mm = dtForDisp ? String(dtForDisp.getMonth()+1).padStart(1,'') : ''; const yyyy = dtForDisp ? dtForDisp.getFullYear() : ''; const HH = dtForDisp ? String(dtForDisp.getHours()).padStart(2,'0') : ''; const MM = dtForDisp ? String(dtForDisp.getMinutes()).padStart(2,'0') : ''; const dateOnly = dtForDisp ? `${dd}. ${mm}. ${yyyy}` : ''; const timeToken = (m.date_time || '').split(' ')[1] || ''; const timeOnly = dtForDisp ? `${HH}:${MM}` : ''; const timeDisplay = dtForDisp ? ((timeToken === '' || timeToken === '00:00') ? 'Bude upřesněno' : timeOnly) : ''; const venue = m.venue || ''; const wrapperExtraClass = (isRecentFinished && s1 && s2) ? ' facr-finished' : ''; const headerLabel = isLive ? 'Aktuální zápas' : (isFuture ? 'Nadcházející zápas' : (isRecentFinished ? 'Poslední zápas' : `Zápasy (${idx+1}/${items.length})`)); root.innerHTML = `
${compName}
${headerLabel}
${homeName} ${isRecentFinished && s1 ? `${s1}` : ''} ${midText} ${isRecentFinished && s1 && s2 ? `${s1}:${s2}` : ''} ${isRecentFinished && s2 ? `${s2}` : ''}${awayName}
${escapeHTML(dateOnly + (venue?`, ${venue}`:''))} ${timeDisplay ? `${escapeHTML(timeDisplay)}` : ''}
Detail na FACR Tabulka bodů Všechny zápasy
`; const prev = document.getElementById('facr-prev'); const next = document.getElementById('facr-next'); if(prev) prev.onclick = ()=>{ state.matchIndex = (idx - 1 + items.length) % items.length; renderUpcoming(); }; if(next) next.onclick = ()=>{ state.matchIndex = (idx + 1) % items.length; renderUpcoming(); }; // setup countdown / status text const cd = document.getElementById('facr-countdown'); const inlineStatus = document.getElementById('facr-inline-status'); if(cd || inlineStatus){ const now2 = new Date(); const startMs = m.date_time ? parseCZDate(m.date_time).getTime() : 0; const diff = startMs - now2.getTime(); const twoH = 2*60*60*1000; const threeD = 3*24*60*60*1000; let text = ''; if(diff > 0){ text = `Začátek za ${fmtCountdown(diff)}`; }else if(Math.abs(diff) <= twoH){ text = 'Právě probíhá'; }else if(-diff < threeD){ text = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); }else{ text = ''; } if(cd) cd.textContent = text; if(inlineStatus) inlineStatus.textContent = text; } // Live countdown in the middle area when future and not today const midEl = document.getElementById('facr-mid'); if(midEl){ const startTime = m.date_time ? parseCZDate(m.date_time).getTime() : 0; function tick(){ const now = Date.now(); const diff = startTime - now; if(diff > 0){ const longTxt = `Za ${fmtCountdownLong(diff)}`; midEl.textContent = longTxt; const inlineEl = document.getElementById('facr-inline-status'); if(inlineEl) inlineEl.textContent = `Začátek za ${fmtCountdown(diff)}`; const cdEl = document.getElementById('facr-countdown'); if(cdEl) cdEl.textContent = `Začátek za ${fmtCountdown(diff)}`; }else{ // switch to score at/after kickoff const scoreTxt = m.score || '-'; midEl.textContent = scoreTxt; const inlineEl = document.getElementById('facr-inline-status'); if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); const cdEl = document.getElementById('facr-countdown'); if(cdEl) cdEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); if(state.upcomingTimerId){ clearInterval(state.upcomingTimerId); state.upcomingTimerId = null; } } } // Run live countdown for any future match (including today) if(startTime > Date.now()){ tick(); state.upcomingTimerId = setInterval(tick, 1000); } else { // Ensure inline status reflects finished state on initial render const inlineEl = document.getElementById('facr-inline-status'); if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); } } } function renderCompetitionTabs(){ const tabs = document.getElementById('facr-comp-tabs'); if(!tabs || !state.data) return; ensureFacrStyles(); const comps = state.data.club_table.competitions || []; tabs.innerHTML = comps.map((c,i)=> `` ).join(''); tabs.querySelectorAll('button').forEach(btn=>{ btn.addEventListener('click', ()=>{ state.compIndex = Number(btn.dataset.idx)||0; renderCompetitionTabs(); renderTable(); }); }); } function renderTable(){ const tbody = document.getElementById('facr-table-body'); const badge = document.getElementById('facr-comp-badge'); if(!tbody || !state.data) return; const comps = state.data.club_table.competitions || []; if(comps.length === 0){ tbody.innerHTML = ''; return; } const comp = comps[Math.min(state.compIndex, comps.length-1)]; if(badge){ badge.textContent = comp.name || comp.code || 'Soutěž'; } const rows = comp.table && comp.table.overall ? comp.table.overall : []; tbody.innerHTML = rows.map(r=>` ${r.rank} ${r.team} ${r.played} ${r.wins} ${r.draws} ${r.losses} ${r.score} ${r.points} `).join(''); } function renderAllMatches(){ const container = document.getElementById('facr-all-matches'); if(!container || !state.data) return; const comps = state.data.club_detail.competitions || []; const sections = []; for(const comp of comps){ const matches = (comp.matches || []) .map(m=>({ m, dt: parseCZDate(m.date_time) })) .filter(x=>!!x.dt) .sort((a,b)=> b.dt - a.dt); if(matches.length === 0) continue; const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 40); const itemsHtml = matches.map(({m})=>{ const homeLogo = m.home_logo_url || '../img/logo.png'; const awayLogo = m.away_logo_url || '../img/logo.png'; const facrLink = m.report_url || comp.matches_link || state.data.club_detail.url || '#'; const score = m.score || '-'; const GRID_MAX = 22; const home = truncate(escapeHTML(m.home), GRID_MAX); const away = truncate(escapeHTML(m.away), GRID_MAX); const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 36); const s1 = escapeHTML((score.split(':')[0]||'-')); const s2 = escapeHTML((score.split(':')[1]||'-')); return `
${home} ${s1} ${s1} : ${s2} ${away} ${s2}
`; }).join(''); sections.push(`

${compName}

${itemsHtml}
`); } container.innerHTML = sections.join(''); } function schedule(){ if(state.intervalId) clearInterval(state.intervalId); const intervalMs = isWithinMatchWindow() ? 2*60*1000 : 30*60*1000; state.intervalId = setInterval(async ()=>{ try{ await fetchData(); renderUpcoming(); renderCompetitionTabs(); renderTable(); renderAllMatches(); schedule(); // reevaluate interval if window changed }catch(e){ console.warn('refresh failed', e); } }, intervalMs); } async function init(){ try{ await fetchData(); renderUpcoming(); renderCompetitionTabs(); renderTable(); renderAllMatches(); schedule(); }catch(e){ console.error('FACR init failed', e); } } document.addEventListener('DOMContentLoaded', init); })();