/**
* 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();
}