// Seasons Filter Utility // Parses and manages season/week data from the seasons constant file class SeasonsFilter { constructor() { this.seasons = []; this.loaded = false; } // Parse seasons text content into structured data parseSeasons(content) { const seasons = []; const lines = content.split('\n'); let currentSeason = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Check if it's a season header (e.g., "2025-V") const seasonMatch = line.match(/^(\d{4}-[IVX]+)$/); if (seasonMatch) { if (currentSeason) { seasons.push(currentSeason); } currentSeason = { name: seasonMatch[1], weeks: [] }; continue; } // Check if it's a week line // Format: "week 1 (01.09 – 07.09) : max BR 14.0" // or: "until eos (27.10 – 31.10) : max BR 5.0" const weekMatch = line.match(/(?:week\s+(\d+)|until eos)\s+\((\d{2})\.(\d{2})\s*[–—]\s*(\d{2})\.(\d{2})\)\s*/); if (weekMatch && currentSeason) { const weekNum = weekMatch[1] ? parseInt(weekMatch[1]) : null; const startDay = parseInt(weekMatch[2]); const startMonth = parseInt(weekMatch[3]); const endDay = parseInt(weekMatch[4]); const endMonth = parseInt(weekMatch[5]); const timestamp = parseInt(weekMatch[6]); // Extract max BR if present (preserve .0 formatting) const brMatch = line.match(/max BR ([\d.]+)/); const maxBR = brMatch ? brMatch[1] : null; // Keep as string to preserve ".0" // Use the Unix timestamp directly from the file (it's already correct!) // The timestamp is the START of the week const startDate = new Date(timestamp * 1000); // Convert to milliseconds // Calculate end date: 7 days from start (or until end of season for "until eos") // For "until eos" entries, we'll need to calculate based on the actual end day let endDate; // Determine year from season name (e.g., "2025-V" -> 2025) const yearMatch = currentSeason.name.match(/^(\d{4})/); const year = yearMatch ? parseInt(yearMatch[1]) : new Date().getFullYear(); // Build end date from the parsed end day/month // Need to handle year rollover (e.g., December -> January) let endYear = year; if (endMonth < startMonth) { endYear = year + 1; // Crossed into next year } endDate = new Date(endYear, endMonth - 1, endDay, 23, 59, 59, 999); // Build display name with max BR (preserve decimal formatting like 14.0) let displayName; if (weekNum) { displayName = `Week ${weekNum} (${weekMatch[2]}.${weekMatch[3]} – ${weekMatch[4]}.${weekMatch[5]})`; if (maxBR) displayName += ` - BR ${maxBR}`; } else { displayName = `Until EOS (${weekMatch[2]}.${weekMatch[3]} – ${weekMatch[4]}.${weekMatch[5]})`; if (maxBR) displayName += ` - BR ${maxBR}`; } currentSeason.weeks.push({ weekNumber: weekNum, name: weekNum ? `Week ${weekNum}` : 'Until EOS', startDate: startDate, endDate: endDate, startTimestamp: timestamp, // Store original Unix timestamp (seconds) maxBR: maxBR, displayName: displayName }); } } // Add the last season if (currentSeason) { seasons.push(currentSeason); } // Post-process: set each week's endDate to the next week's startDate - 1ms // This uses the exact Unix timestamps instead of the approximate text-parsed dates seasons.forEach(season => { season.weeks.forEach((week, i) => { if (i + 1 < season.weeks.length) { week.endDate = new Date(season.weeks[i + 1].startDate.getTime() - 1); } // Last week of each season keeps its text-parsed endDate }); }); return seasons; } // Load seasons data from the constant file async loadSeasons() { if (this.loaded) { return this.seasons; } try { const response = await fetch('/constants/seasons'); if (!response.ok) { throw new Error(`Failed to load seasons: ${response.status}`); } const content = await response.text(); this.seasons = this.parseSeasons(content); this.loaded = true; console.log('[Seasons Filter] Loaded seasons:', this.seasons); return this.seasons; } catch (error) { console.error('[Seasons Filter] Error loading seasons:', error); this.seasons = []; this.loaded = false; return []; } } // Get all seasons getSeasons() { return this.seasons; } // Get a specific season by name getSeason(seasonName) { return this.seasons.find(s => s.name === seasonName); } // Get current season (based on current date) getCurrentSeason() { const now = new Date(); for (const season of this.seasons) { for (const week of season.weeks) { if (now >= week.startDate && now <= week.endDate) { return season; } } } // If no current season found, return the most recent one return this.seasons.length > 0 ? this.seasons[this.seasons.length - 1] : null; } // Get current week (based on current date) getCurrentWeek() { const now = new Date(); for (const season of this.seasons) { for (const week of season.weeks) { if (now >= week.startDate && now <= week.endDate) { return { season, week }; } } } return null; } // Get date range for a specific season getSeasonDateRange(seasonName) { const season = this.getSeason(seasonName); if (!season || season.weeks.length === 0) { return null; } return { startDate: season.weeks[0].startDate, endDate: season.weeks[season.weeks.length - 1].endDate, season: season }; } // Get date range for a specific week in a season getWeekDateRange(seasonName, weekNumber) { const season = this.getSeason(seasonName); if (!season) { return null; } const week = season.weeks.find(w => w.weekNumber === weekNumber); if (!week) { return null; } return { startDate: week.startDate, endDate: week.endDate, week: week }; } // Get all unique BR values across all seasons (returns array of strings, preserving ".0") getAllBRValues() { const brSet = new Set(); this.seasons.forEach(season => { season.weeks.forEach(week => { if (week.maxBR) { brSet.add(week.maxBR); } }); }); // Sort descending (convert to float for comparison, but keep as strings) return Array.from(brSet).sort((a, b) => parseFloat(b) - parseFloat(a)); } // Get all weeks that match a specific BR getWeeksByBR(maxBR) { const matchingWeeks = []; this.seasons.forEach(season => { season.weeks.forEach(week => { if (week.maxBR === maxBR) { matchingWeeks.push({ season: season, week: week, displayName: `${season.name} - ${week.name}` }); } }); }); return matchingWeeks; } // Get combined date range for all weeks with a specific BR (for filtering multiple weeks) getDateRangeForBR(maxBR) { const weeks = this.getWeeksByBR(maxBR); if (weeks.length === 0) { return null; } // Return array of date ranges (one for each week with this BR) return weeks.map(({ season, week }) => ({ startDate: week.startDate, endDate: week.endDate, seasonName: season.name, weekName: week.name, displayName: `${season.name} - ${week.displayName}` })); } // Populate a select element with season options populateSeasonSelect(selectElement, includeAllOption = true) { selectElement.innerHTML = ''; if (includeAllOption) { const allOption = document.createElement('option'); allOption.value = 'all'; allOption.textContent = window.__t ? window.__t('leaderboard.allSeasons') : 'All Seasons'; selectElement.appendChild(allOption); } // Add seasons in reverse order (most recent first) for (let i = this.seasons.length - 1; i >= 0; i--) { const season = this.seasons[i]; const option = document.createElement('option'); option.value = season.name; option.textContent = season.name; selectElement.appendChild(option); } } // Populate a select element with BR options populateBRSelect(selectElement, includeAllOption = true) { selectElement.innerHTML = ''; if (includeAllOption) { const allOption = document.createElement('option'); allOption.value = ''; allOption.textContent = window.__t ? window.__t('leaderboard.allBR') : 'All BR'; selectElement.appendChild(allOption); } const brValues = this.getAllBRValues(); brValues.forEach(br => { const option = document.createElement('option'); option.value = br; option.textContent = `BR ${br}`; selectElement.appendChild(option); }); } // Populate a select element with week options for a given season populateWeekSelect(selectElement, seasonName, includeAllOption = true) { selectElement.innerHTML = ''; if (includeAllOption) { const allOption = document.createElement('option'); allOption.value = 'all'; allOption.textContent = window.__t ? window.__t('leaderboard.allWeeks') : 'All Weeks'; selectElement.appendChild(allOption); } const season = this.getSeason(seasonName); if (!season) { return; } season.weeks.forEach(week => { const option = document.createElement('option'); option.value = week.weekNumber !== null ? week.weekNumber.toString() : 'final'; option.textContent = week.displayName; selectElement.appendChild(option); }); } } // Global instance window.seasonsFilter = window.seasonsFilter || new SeasonsFilter();