/** * Lazy Loading Manager for Fixtures - Loads league fixtures on demand * Integrates with existing smart polling and fixtures update system */ class LazyFixturesManager { constructor(options = {}) { this.options = { fixturesContentSelector: '#fixtures-content', debug: options.debug || false, ...options }; this.loadedLeagues = new Set(); this.loading = new Set(); // Track currently loading leagues this.loadingTimestamps = new Map(); // Track when loading started this.initialized = false; // Start periodic check for stuck loading states this.startLoadingStateMonitor(); this.log('Lazy Fixtures Manager initialized'); } /** * Initialize lazy loading system */ async initialize() { if (this.initialized) return; try { // Load initial skeleton structure await this.loadSkeleton(); // Set up accordion event listeners this.setupAccordionListeners(); // Load top leagues immediately await this.loadTopLeagues(); this.initialized = true; this.log('Lazy loading system initialized'); } catch (error) { this.log('Error initializing lazy loading:', error); // Fallback to full loading await this.fallbackToFullLoad(); } } /** * Load the skeleton structure with league headers */ async loadSkeleton() { const currentDate = this.currentDate || this.getCurrentDate(); if (!currentDate) { throw new Error('Could not determine current date'); } this.log('Loading skeleton for date:', currentDate); try { const response = await fetch(`/public/ajax/league-skeleton.php?date=${currentDate}`, { headers: { 'X-AJAX-TOKEN': window.ajaxToken || '' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load skeleton'); } const fixturesContent = document.querySelector(this.options.fixturesContentSelector); if (fixturesContent) { fixturesContent.innerHTML = data.html; this.log(`Loaded skeleton with ${data.leagues?.length || 0} leagues`); // Re-initialize accordion controls after skeleton loads if (typeof window.initializeAccordionControls === 'function') { window.initializeAccordionControls(); } // Load location options using skeleton data if (typeof window.loadLocationOptionsFromData === 'function' && data.leagues) { window.loadLocationOptionsFromData(data.leagues); } // Load top leagues after skeleton is populated setTimeout(() => { this.loadTopLeagues(); }, 50); } } catch (error) { this.log('Error loading skeleton:', error); throw error; } } /** * Set up event listeners for accordion toggles */ setupAccordionListeners() { const fixturesContent = document.querySelector(this.options.fixturesContentSelector); if (!fixturesContent) return; // Use event delegation for league headers fixturesContent.addEventListener('click', async (e) => { const header = e.target.closest('.league-header'); if (!header) return; const leagueContainer = header.closest('.league-fixtures'); if (!leagueContainer) return; const leagueId = leagueContainer.dataset.leagueId; const isCurrentlyCollapsed = leagueContainer.classList.contains('collapsed'); // Toggle accordion state leagueContainer.classList.toggle('collapsed'); // Update toggle arrow const toggle = header.querySelector('.accordion-toggle'); if (toggle) { toggle.textContent = isCurrentlyCollapsed ? '▼' : '▲'; } // Load fixtures if expanding (was collapsed, now expanding) and not already loaded if (isCurrentlyCollapsed && !this.loadedLeagues.has(leagueId)) { this.log(`League ${leagueId} is being expanded, loading fixtures automatically`); await this.loadLeagueFixtures(leagueId, leagueContainer); } }); this.log('Accordion listeners set up'); } /** * Load fixtures for top leagues immediately */ async loadTopLeagues() { const topLeagues = document.querySelectorAll('.league-fixtures:not(.collapsed)'); this.log(`Found ${topLeagues.length} expanded leagues to load`); const loadPromises = []; for (const league of topLeagues) { const leagueId = league.dataset.leagueId; this.log(`Checking league ${leagueId}, loaded: ${this.loadedLeagues.has(leagueId)}`); if (leagueId && !this.loadedLeagues.has(leagueId)) { this.log(`Loading fixtures for top league ${leagueId}`); loadPromises.push(this.loadLeagueFixtures(leagueId)); } } if (loadPromises.length > 0) { await Promise.all(loadPromises); this.log(`Loaded ${loadPromises.length} top leagues`); } else { this.log('No top leagues to load'); } } /** * Load fixtures for a specific league with retry logic and timeout handling */ async loadLeagueFixtures(leagueId, leagueContainer = null, retryCount = 0) { if (this.loading.has(leagueId) || this.loadedLeagues.has(leagueId)) { return; } const maxRetries = 2; const timeoutMs = 15000; // 15 second timeout this.loading.add(leagueId); this.loadingTimestamps.set(leagueId, Date.now()); // Track when loading started try { const currentDate = this.currentDate || this.getCurrentDate(); if (!leagueContainer) { leagueContainer = document.querySelector(`[data-league-id="${leagueId}"]`); } const matchesList = leagueContainer?.querySelector('.matches-list'); if (!matchesList) { throw new Error(`League container not found for ID: ${leagueId}`); } // Show loading state with retry indicator const loadingText = retryCount > 0 ? `Loading matches... (Retry ${retryCount}/${maxRetries})` : 'Loading matches...'; matchesList.innerHTML = `
${loadingText}
`; // Create AbortController for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, timeoutMs); const response = await fetch(`/public/ajax/fixtures.php?date=${currentDate}&league_id=${leagueId}&timezone=user&lazy=1`, { headers: { 'X-AJAX-TOKEN': window.ajaxToken || '' }, signal: controller.signal }); // Clear timeout if request completed clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } let data; try { data = await response.json(); } catch (parseError) { throw new Error('Invalid JSON response from server'); } // Validate response structure if (!data || typeof data.success === 'undefined') { throw new Error('Invalid response structure'); } if (!data.success) { throw new Error(data.error || 'Server returned error'); } // For lazy loading, the API returns the matches HTML directly if (data.html && data.html.length > 0) { matchesList.innerHTML = data.html; } else if (data.html === '') { // Empty HTML means hide the entire league container leagueContainer.style.display = 'none'; } else { matchesList.innerHTML = '
No matches found
'; } // Mark as loaded and clear loading state this.loadedLeagues.add(leagueId); leagueContainer.dataset.loaded = 'true'; // Re-initialize event rotators if they exist if (typeof initializeEventRotators === 'function') { initializeEventRotators(matchesList); } // Dispatch event for filter system document.dispatchEvent(new CustomEvent('lazyFixturesLoaded', { detail: { leagueId: leagueId } })); this.log(`Successfully loaded fixtures for league ${leagueId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}`); } catch (error) { this.log(`Error loading league ${leagueId} (attempt ${retryCount + 1}):`, error); // Determine if we should retry const shouldRetry = retryCount < maxRetries && this.shouldRetryError(error); if (shouldRetry) { // Calculate exponential backoff delay const delay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Max 5 second delay this.log(`Retrying league ${leagueId} in ${delay}ms...`); // Show retry countdown const leagueContainer = document.querySelector(`[data-league-id="${leagueId}"]`); const matchesList = leagueContainer?.querySelector('.matches-list'); if (matchesList) { matchesList.innerHTML = `
Retrying in ${Math.ceil(delay/1000)}s...
`; } // Wait and retry setTimeout(() => { this.loading.delete(leagueId); // Clear loading state before retry this.loadLeagueFixtures(leagueId, leagueContainer, retryCount + 1); }, delay); return; // Don't clear loading state yet, retry will handle it } else { // Show error state with manual retry option const leagueContainer = document.querySelector(`[data-league-id="${leagueId}"]`); const matchesList = leagueContainer?.querySelector('.matches-list'); if (matchesList) { const errorMsg = this.getErrorMessage(error); matchesList.innerHTML = `
${errorMsg}
`; } // Log error for debugging console.error(`Failed to load league ${leagueId} after ${retryCount + 1} attempts:`, error); } } finally { // Always clear loading state and timestamp this.loading.delete(leagueId); this.loadingTimestamps.delete(leagueId); } } /** * Determine if an error should trigger a retry */ shouldRetryError(error) { // Retry on network errors, timeouts, and 5xx server errors if (error.name === 'AbortError') return true; // Timeout if (error.message.includes('fetch')) return true; // Network error if (error.message.includes('HTTP 5')) return true; // Server error if (error.message.includes('Invalid JSON')) return true; // Parse error // Don't retry on 4xx client errors if (error.message.includes('HTTP 4')) return false; // Default to retry for other errors return true; } /** * Get user-friendly error message */ getErrorMessage(error) { if (error.name === 'AbortError') { return 'Request timed out'; } if (error.message.includes('HTTP 5')) { return 'Server error'; } if (error.message.includes('HTTP 4')) { return 'Request failed'; } if (error.message.includes('Invalid JSON')) { return 'Invalid server response'; } if (error.message.includes('fetch')) { return 'Network error'; } return 'Failed to load matches'; } /** * Manual retry method for user-triggered retries */ retryLeague(leagueId) { // Remove from loaded leagues so it can be loaded again this.loadedLeagues.delete(leagueId); // Find the league container and reset its loaded state const leagueContainer = document.querySelector(`[data-league-id="${leagueId}"]`); if (leagueContainer) { leagueContainer.dataset.loaded = 'false'; } // Load the league again this.loadLeagueFixtures(leagueId, leagueContainer, 0); } /** * Get loaded leagues for smart polling integration */ getLoadedLeagues() { return Array.from(this.loadedLeagues); } /** * Check if a league is loaded */ isLeagueLoaded(leagueId) { return this.loadedLeagues.has(leagueId); } /** * Get the current date being used by the lazy fixtures system */ getActiveDate() { return this.currentDate || this.getCurrentDate(); } /** * Fallback to full fixture loading if lazy loading fails */ async fallbackToFullLoad() { try { const currentDate = this.currentDate || this.getCurrentDate(); const response = await fetch(`/public/ajax/fixtures.php?date=${currentDate}&timezone=user&lazy=1`, { headers: { 'X-AJAX-TOKEN': window.ajaxToken || '' } }); const data = await response.json(); if (data.success) { const fixturesContent = document.querySelector(this.options.fixturesContentSelector); if (fixturesContent) { fixturesContent.innerHTML = data.html; } } this.log('Fallback to full loading completed'); } catch (error) { this.log('Fallback loading also failed:', error); } } /** * Refresh a specific league's fixtures */ async refreshLeague(leagueId) { if (this.loadedLeagues.has(leagueId)) { this.loadedLeagues.delete(leagueId); await this.loadLeagueFixtures(leagueId); } } /** * Change the date and reload fixtures */ async changeDate(newDate) { this.log('Changing date to:', newDate); this.currentDate = newDate; // Clear loaded leagues cache so they reload for new date this.loadedLeagues.clear(); this.loading.clear(); // Clear all existing content and reinitialize const fixturesContent = document.getElementById('fixtures-content'); if (fixturesContent) { fixturesContent.innerHTML = '
Loading fixtures...
'; try { // Reinitialize the skeleton for the new date await this.loadSkeleton(); // Load top leagues after skeleton is loaded await this.loadTopLeagues(); this.log('Date change completed successfully'); } catch (error) { this.log('Error changing date:', error); fixturesContent.innerHTML = '
Error loading fixtures for this date
'; throw error; // Re-throw so caller can handle it } } } /** * Get current date in YYYY-MM-DD format (in user's local timezone) */ getCurrentDate() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Start monitoring for stuck loading states */ startLoadingStateMonitor() { // Check every 30 seconds for leagues that have been loading too long setInterval(() => { this.checkStuckLoadingStates(); }, 30000); } /** * Check for leagues that have been loading for too long */ checkStuckLoadingStates() { const maxLoadingTime = 60000; // 1 minute const now = Date.now(); for (const [leagueId, startTime] of this.loadingTimestamps.entries()) { if (now - startTime > maxLoadingTime) { this.log(`League ${leagueId} has been loading for over 1 minute, forcing error state`); // Force error state for stuck loading this.loading.delete(leagueId); this.loadingTimestamps.delete(leagueId); const leagueContainer = document.querySelector(`[data-league-id="${leagueId}"]`); const matchesList = leagueContainer?.querySelector('.matches-list'); if (matchesList) { matchesList.innerHTML = `
Loading timed out
`; } } } } /** * Debug logging */ log(...args) { if (this.options.debug) { console.log('[LazyFixtures]', ...args); } } } // Initialize lazy loading manager when DOM is ready or immediately if already ready function initializeLazyFixtures() { const fixturesModule = document.getElementById('fixtures-module'); if (fixturesModule && !window.lazyFixtures) { window.lazyFixtures = new LazyFixturesManager({ debug: true // Set to true for debugging }); // Also expose LazyFixturesManager for legacy code window.LazyFixturesManager = window.lazyFixtures; // Initialize after a short delay to ensure everything is ready setTimeout(() => { window.lazyFixtures.initialize(); }, 100); } } // Check if DOM is already loaded, otherwise wait for DOMContentLoaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeLazyFixtures); } else { // DOM is already loaded, initialize immediately initializeLazyFixtures(); }