// schedule.js — match schedule: list cards, filters (date, group, phase, // team, stadium), diacritic-insensitive search, date sort. // Knockout team names show as TBD until resolveBracketTeams() lands (step 7). import { getData, formatMatchTime, matchDateUTC, flagSrc } from './app.js'; import { t, translatePhase } from './i18n.js'; import { openMatchModal } from './modal.js'; import { resolveBracketTeams, getFavoriteMatches } from './bracket.js'; import { getFavorites } from './storage.js'; const KNOCKOUT_PHASES = ['Round of 32', 'Round of 16', 'Quarterfinals', 'Semifinals', 'Third Place', 'Final']; const state = { search: '', date: '', group: '', phase: '', team: '', stadium: '', sort: 'asc', favOnly: false }; export function initSchedule() { renderToolbar(); renderList(); document.addEventListener('langchange', () => { renderToolbar(); renderList(); }); // simulated picks change resolved knockout teams shown on cards document.addEventListener('simchange', renderList); document.addEventListener('favchange', renderList); document.addEventListener('timemodechange', renderList); // delegation on the panel root — survives every list re-render const root = document.getElementById('schedule-root'); root.addEventListener('click', (event) => { if (event.target.closest('.fav-btn')) return; // star toggles, never opens const card = event.target.closest('.match-card'); if (card) openMatchModal(Number(card.dataset.matchId)); }); root.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; if (event.target.closest('.fav-btn')) return; const card = event.target.closest('.match-card'); if (card) { event.preventDefault(); openMatchModal(Number(card.dataset.matchId)); } }); } // ------------------------------------------------------------- toolbar function renderToolbar() { const { teams, groups, stadiums } = getData(); const root = document.getElementById('schedule-root'); const teamOptions = [...teams] .sort((a, b) => a.name.localeCompare(b.name)) .map((team) => ``).join(''); const groupOptions = Object.keys(groups) .map((letter) => ``).join(''); const phaseOptions = KNOCKOUT_PHASES .map((phase) => ``).join(''); const stadiumOptions = stadiums .map((s) => ``).join(''); root.innerHTML = `

`; // restore current filter values after a rebuild (e.g. language change) byId('sched-search').value = state.search; byId('sched-date').value = state.date; byId('sched-group').value = state.group; byId('sched-phase').value = state.phase; byId('sched-team').value = state.team; byId('sched-stadium').value = state.stadium; syncSortLabel(); byId('sched-search').addEventListener('input', (e) => setFilter('search', e.target.value)); byId('sched-date').addEventListener('change', (e) => setFilter('date', e.target.value)); byId('sched-group').addEventListener('change', (e) => setFilter('group', e.target.value)); byId('sched-phase').addEventListener('change', (e) => setFilter('phase', e.target.value)); byId('sched-team').addEventListener('change', (e) => setFilter('team', e.target.value)); byId('sched-stadium').addEventListener('change', (e) => setFilter('stadium', e.target.value)); byId('sched-sort').addEventListener('click', () => { state.sort = state.sort === 'asc' ? 'desc' : 'asc'; syncSortLabel(); renderList(); }); byId('sched-fav').addEventListener('click', () => { state.favOnly = !state.favOnly; const btn = byId('sched-fav'); btn.classList.toggle('active', state.favOnly); btn.setAttribute('aria-pressed', String(state.favOnly)); renderList(); }); byId('sched-clear').addEventListener('click', () => { Object.assign(state, { search: '', date: '', group: '', phase: '', team: '', stadium: '', sort: 'asc', favOnly: false }); renderToolbar(); renderList(); }); } function byId(id) { return document.getElementById(id); } // external entry point (stadiums page) — show only matches at one stadium export function setStadiumFilter(stadiumName) { Object.assign(state, { search: '', date: '', group: '', phase: '', team: '', stadium: stadiumName, favOnly: false }); renderToolbar(); renderList(); } function setFilter(key, value) { state[key] = value; renderList(); } function syncSortLabel() { byId('sched-sort').textContent = t(state.sort === 'asc' ? 'schedule.sortAsc' : 'schedule.sortDesc'); } // ------------------------------------------------------------ filtering function normalize(text) { return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); } function matchesFilters(match) { if (state.date && match.date !== state.date) return false; if (state.group && match.phase !== `Group ${state.group}`) return false; if (state.phase === 'groups' && !match.phase.startsWith('Group')) return false; if (state.phase && state.phase !== 'groups' && match.phase !== state.phase) return false; if (state.stadium && match.stadium !== state.stadium) return false; if (state.team || state.search) { // resolved teams, so knockout matches are searchable once known const slots = resolveBracketTeams(match); if (state.team && slots.home.team?.id !== state.team && slots.away.team?.id !== state.team) return false; if (state.search) { const haystack = normalize(`${slots.home.label} ${slots.away.label} ${match.city} ${match.stadium}`); if (!haystack.includes(normalize(state.search))) return false; } } return true; } // -------------------------------------------------------------- render function renderList() { const { matches } = getData(); const direction = state.sort === 'asc' ? 1 : -1; const favIds = state.favOnly ? new Set(getFavoriteMatches(matches, getFavorites()).map((m) => m.id)) : null; const filtered = matches .filter((m) => (!favIds || favIds.has(m.id)) && matchesFilters(m)) .sort((a, b) => direction * (matchDateUTC(a) - matchDateUTC(b) || a.id - b.id)); byId('sched-count').textContent = `${filtered.length} ${filtered.length === 1 ? t('schedule.match') : t('schedule.matches')}`; byId('sched-list').innerHTML = filtered.length ? filtered.map(matchCardHTML).join('') : `

${t('schedule.noResults')}

`; } function teamColumnHTML(slot) { if (!slot.team) return `
${slot.label}
`; const fav = getFavorites().includes(slot.team.id); return `
${slot.team.name}
`; } function matchCardHTML(match) { const { resultByMatchId, stadiumByName } = getData(); const result = resultByMatchId.get(match.id); const status = result?.status ?? 'scheduled'; const stadium = stadiumByName.get(match.stadium); let statusChip = ''; if (status === 'live') statusChip = `● ${t('hero.live')}`; else if (status === 'finished') statusChip = `${t('status.finished')}`; let center; if (status === 'finished' || status === 'live') { const pens = result.penalties ? `(${result.penalties.home}–${result.penalties.away} ${t('status.pens')})` : ''; center = `
${result.homeScore}${result.awayScore}${pens}
`; } else { center = `${t('hero.vs')}`; } const slots = resolveBracketTeams(match); const favorites = getFavorites(); const hasFav = favorites.includes(slots.home.team?.id) || favorites.includes(slots.away.team?.id); return `
${translatePhase(match.phase)} ${statusChip}
${teamColumnHTML(slots.home)} ${center} ${teamColumnHTML(slots.away)}
`; }