import { MatchFeed } from '../services/matchFeed.js'; import { CommentaryService } from '../services/commentary.js'; import { PlayerCardService } from '../services/playerCard.js'; import { NightOwlService } from '../services/nightOwl.js'; import { CrowdPulseService } from '../services/crowdPulse.js'; import { VenueMapService } from '../services/venueMap.js'; import { MatchProofService } from '../services/matchProof.js'; import { SustainabilityService } from '../services/sustainability.js'; import { StaffPanel } from '../components/StaffPanel.js'; import { SAMPLE_MATCH_EVENTS } from '../data/sampleMatch.js'; import { DEFAULT_MATCH_SPEED } from './constants.js'; import { Toast } from '../components/Toast.js'; import { SettingsPanel } from '../components/SettingsPanel.js'; import { setHTML } from '../utils/dom.js'; import { setTTSEnabled, speak as speakTTS } from '../utils/tts.js'; import { gsap } from 'gsap'; export const coreMethods = { _handleRoute() { const hash = window.location.hash.replace('#/', '') || 'home'; const validPages = ['home', 'live', 'highlights', 'predictions', 'standings', 'venue']; const page = validPages.includes(hash) ? hash : 'home'; this._showPage(page); }, _showPage(pageId) { document.querySelectorAll('.page').forEach(p => { p.classList.remove('page--active'); p.style.animation = 'none'; }); const page = document.getElementById(`page-${pageId}`); if (page) { page.classList.add('page--active'); page.style.animation = ''; void page.offsetHeight; page.style.animation = 'pageIn 0.6s var(--ease-out-expo) forwards'; } document.querySelectorAll('.site-nav__link').forEach(link => { const isActive = link.dataset.page === pageId; link.classList.toggle('site-nav__link--active', isActive); if (isActive) { link.setAttribute('aria-current', 'page'); } else { link.removeAttribute('aria-current'); } }); this.currentPage = pageId; window.scrollTo({ top: 0, behavior: 'smooth' }); document.getElementById('main-content')?.focus?.({ preventScroll: true }); this._initPageContent(pageId); }, _initPageContent(pageId) { switch (pageId) { case 'home': this._initHomePage(); break; case 'live': this._initLivePage(); break; case 'highlights': this._initHighlightsPage(); break; case 'predictions': this._initPredictionsPage(); break; case 'standings': this._initStandingsPage(); break; case 'venue': this._initVenuePage(); break; } setTimeout(() => this._initScrollAnimations(), 100); }, // ─── Navigation ───────────────────────────────────────── _bindNavigation() { document.querySelectorAll('[data-nav]').forEach(el => { el.addEventListener('click', () => { window.location.hash = '#/home'; }); }); const hamburger = document.getElementById('nav-hamburger'); const links = document.getElementById('nav-links'); if (hamburger && links) { const toggleMenu = () => { const isOpen = links.classList.toggle('open'); hamburger.setAttribute('aria-expanded', isOpen.toString()); }; hamburger.addEventListener('click', toggleMenu); links.querySelectorAll('.site-nav__link').forEach(link => { link.addEventListener('click', () => { links.classList.remove('open'); hamburger.setAttribute('aria-expanded', 'false'); }); }); } window.addEventListener('scroll', () => { const nav = document.getElementById('site-nav'); if (nav) { nav.classList.toggle('scrolled', window.scrollY > 50); } }); }, // ─── Theme Switcher ───────────────────────────────────── _bindThemeSwitcher() { const switcher = document.getElementById('theme-switcher'); if (!switcher) return; const savedTheme = this._normalizeTheme(typeof localStorage === 'undefined' ? 'neon' : localStorage.getItem('vantage_theme') || 'neon'); this._applyTheme(savedTheme); const dots = switcher.querySelectorAll('.theme-dot'); const applySelection = (dot, { focus = false } = {}) => { const theme = dot.dataset.theme; this._applyTheme(theme); dots.forEach(d => { const isActive = d === dot; d.classList.toggle('theme-dot--active', isActive); d.setAttribute('aria-checked', isActive.toString()); d.tabIndex = isActive ? 0 : -1; }); if (typeof localStorage !== 'undefined') { localStorage.setItem('vantage_theme', theme); } if (focus) dot.focus?.(); Toast.show({ message: `Theme: ${theme}`, type: 'info', duration: 2000 }); }; dots.forEach(dot => { const isCurrent = dot.dataset.theme === savedTheme; dot.classList.toggle('theme-dot--active', isCurrent); dot.setAttribute('aria-checked', isCurrent.toString()); dot.tabIndex = isCurrent ? 0 : -1; const activateTheme = () => { applySelection(dot); }; dot.addEventListener('click', activateTheme); dot.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activateTheme(); } else if (['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', 'Home', 'End'].includes(e.key)) { e.preventDefault(); const currentIndex = [...dots].indexOf(dot); let nextIndex = currentIndex; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') nextIndex = (currentIndex + 1) % dots.length; if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') nextIndex = (currentIndex - 1 + dots.length) % dots.length; if (e.key === 'Home') nextIndex = 0; if (e.key === 'End') nextIndex = dots.length - 1; applySelection(dots[nextIndex], { focus: true }); } }); }); }, _normalizeTheme(theme = 'neon') { const allowedThemes = ['neon', 'cyber', 'nightowl', 'rose', 'purple', 'light']; return allowedThemes.includes(theme) ? theme : 'neon'; }, _applyTheme(theme) { const safeTheme = this._normalizeTheme(theme); document.body.classList.forEach(cls => { if (cls.startsWith('theme-')) document.body.classList.remove(cls); }); if (safeTheme !== 'neon') { document.body.classList.add(`theme-${safeTheme}`); } this._syncThemeSwitcher(safeTheme); return safeTheme; }, _syncThemeSwitcher(theme = 'neon') { const safeTheme = this._normalizeTheme(theme); document.querySelectorAll('#theme-switcher .theme-dot').forEach(dot => { const isActive = dot.dataset.theme === safeTheme; dot.classList.toggle('theme-dot--active', isActive); dot.setAttribute('aria-checked', isActive.toString()); dot.tabIndex = isActive ? 0 : -1; }); }, // ─── Settings Panel ──────────────────────────────────── _initSettingsPanel() { const container = document.getElementById('settings-container'); if (!container) return; this.settingsPanel = new SettingsPanel(container); this.settingsPanel.render(); this.settingsPanel.onSettingsChange((next) => this._applySettings(next)); const btn = document.getElementById('btn-settings'); btn?.addEventListener('click', () => { const isOpen = !container.hidden; container.hidden = isOpen; btn.setAttribute('aria-expanded', (!isOpen).toString()); if (!isOpen) container.focus?.(); }); container.addEventListener?.('vantage:settings-close', () => { container.hidden = true; btn?.setAttribute('aria-expanded', 'false'); btn?.focus?.(); }); // Apply the persisted settings once on boot so toggles stay in sync. this._applySettings(this.settings); }, _applySettings(next = {}) { this.settings = { ...this.settings, ...next }; if (typeof localStorage !== 'undefined') { try { localStorage.setItem('vantage_settings', JSON.stringify(this.settings)); } catch (error) { console.warn('Settings could not be persisted.', error); } } const body = document.body; body.classList.toggle('reduced-motion', Boolean(this.settings.visionImpaired)); body.classList.toggle('high-contrast', Boolean(this.settings.visionImpaired)); body.classList.toggle('hearing-impaired', Boolean(this.settings.hearingImpaired)); if (this.settings.theme) { this.settings.theme = this._applyTheme(this.settings.theme); if (typeof localStorage !== 'undefined') { localStorage.setItem('vantage_theme', this.settings.theme); } } setTTSEnabled(Boolean(this.settings.visionImpaired)); // Reflect toggle state back into the panel checkboxes if they exist. const panel = this.settingsPanel?.containerEl; if (panel) { const hearing = panel.querySelector('#toggle-hearing'); const vision = panel.querySelector('#toggle-vision'); const staff = panel.querySelector('#toggle-staff'); hearing?.classList.toggle('toggle--active', Boolean(this.settings.hearingImpaired)); hearing?.setAttribute('aria-checked', Boolean(this.settings.hearingImpaired).toString()); vision?.classList.toggle('toggle--active', Boolean(this.settings.visionImpaired)); vision?.setAttribute('aria-checked', Boolean(this.settings.visionImpaired).toString()); staff?.classList.toggle('toggle--active', Boolean(this.settings.staffMode)); staff?.setAttribute('aria-checked', Boolean(this.settings.staffMode).toString()); const lang = panel.querySelector('#setting-lang'); if (lang && this.settings.language) lang.value = this.settings.language; const pace = panel.querySelector('#setting-pace'); if (pace && this.settings.pace) pace.value = this.settings.pace; const register = panel.querySelector('#setting-register'); if (register && this.settings.register) register.value = this.settings.register; const theme = panel.querySelector('#setting-theme'); if (theme && this.settings.theme) theme.value = this.settings.theme; } const staffContainer = document.getElementById('staff-command-container'); if (staffContainer) { staffContainer.hidden = !this.settings.staffMode; if (this.settings.staffMode) { if (!this.staffPanel) { this.staffPanel = new StaffPanel(staffContainer, this); } this.staffPanel.render(); } } }, _speak(text) { if (this.settings?.visionImpaired) speakTTS(text, { lang: this.settings?.language || 'en' }); }, // ─── Services ─────────────────────────────────────────── _initServices() { this.commentaryService = new CommentaryService(); this.playerCardService = new PlayerCardService(); this.venueMapService = new VenueMapService(); this.matchProofService = new MatchProofService(); this.sustainabilityService = new SustainabilityService(); const initialSpeed = parseInt(document.getElementById('speed-select')?.value || `${DEFAULT_MATCH_SPEED}`, 10); this.matchFeed = new MatchFeed(SAMPLE_MATCH_EVENTS, initialSpeed); this.nightOwlService = new NightOwlService(this.matchFeed); this.crowdPulseService = new CrowdPulseService(this.matchFeed); }, async _loadApiStatus() { try { const response = await fetch('/api/status'); this.apiStatus = response.ok ? await response.json() : null; } catch (error) { console.warn('API status unavailable; using offline service banner.', error); this.apiStatus = null; } }, _renderApiStatus() { const el = document.getElementById('api-status-banner'); if (!el) return; const isGeminiOnline = this.apiStatus?.services?.gemini?.configured; setHTML(el, `