Rerace | More racing, more action

Schedule

See when the next session starts for you

this.sessionUpdateInterval = null; } /** * Fetch complete 2025 F1 season schedule from API */ async fetch2025Schedule() { console.log('🏎️ Fetching live 2025 F1 season schedule...'); try { // Primary API: Ergast with 2025 data const response = await fetch(`${this.ergastApi}/${this.currentSeason}.json`); if (!response.ok) { throw new Error(`API failed: ${response.status}`); } const data = await response.json(); const races = data.MRData?.RaceTable?.Races; if (!races || races.length === 0) { throw new Error('No 2025 races found in API'); } console.log(`βœ… Loaded ${races.length} races for 2025 season`); // Enhance with session details from OpenF1 API const enhancedRaces = await this.enhanceWithSessionData(races); return enhancedRaces; } catch (error) { console.error('❌ Failed to fetch 2025 schedule:', error); // Fallback: Return current race weekend data if API fails return this.getCurrentRaceFallback(); } } /** * Enhance race data with live session information from OpenF1 */ async enhanceWithSessionData(races) { console.log('πŸ”§ Enhancing race data with live session information...'); const enhancedRaces = []; for (const race of races) { try { // Get session data from OpenF1 API const sessionsResponse = await fetch(`${this.openF1Api}/sessions?year=${this.currentSeason}&round=${race.round}`); let sessions = []; if (sessionsResponse.ok) { const sessionsData = await sessionsResponse.json(); sessions = sessionsData || []; } // Add session data to race const enhancedRace = { ...race, sessions: sessions, isSprintWeekend: this.detectSprintWeekend(race, sessions) }; enhancedRaces.push(enhancedRace); // Small delay to avoid overwhelming the API await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { console.warn(`⚠️ Could not enhance ${race.raceName}:`, error); // Add race without session enhancement enhancedRaces.push({ ...race, sessions: [], isSprintWeekend: this.detectSprintWeekendByName(race) }); } } console.log(`βœ… Enhanced ${enhancedRaces.length} races with session data`); return enhancedRaces; } /** * Detect sprint weekend from API sessions or race name */ detectSprintWeekend(race, sessions) { // Check if sessions include sprint-related sessions if (sessions && sessions.length > 0) { const hasSprintQualifying = sessions.some(s => s.session_name?.toLowerCase().includes('sprint qualifying') || s.session_type?.toLowerCase().includes('sprint_qualifying') ); const hasSprint = sessions.some(s => s.session_name?.toLowerCase().includes('sprint') && !s.session_name?.toLowerCase().includes('qualifying') ); if (hasSprintQualifying || hasSprint) { console.log(`πŸƒ Sprint weekend detected for ${race.raceName} via API`); return true; } } // Fallback to name-based detection return this.detectSprintWeekendByName(race); } /** * Detect sprint weekend by race name (2025 confirmed sprint races) */ detectSprintWeekendByName(race) { const sprintRaces2025 = [ 'chinese grand prix', 'miami grand prix', 'austrian grand prix', 'belgian grand prix', 'united states grand prix', 'brazilian grand prix', 'qatar grand prix' ]; const raceName = race.raceName.toLowerCase(); const isSprintRace = sprintRaces2025.some(sprint => raceName.includes(sprint.replace(' grand prix', '')) ); if (isSprintRace) { console.log(`πŸƒ Sprint weekend detected for ${race.raceName} via name`); } return isSprintRace; } /** * Find current or next race weekend based on today's date */ async getCurrentRace() { const races = await this.fetch2025Schedule(); if (!races || races.length === 0) return null; const now = new Date(); console.log(`πŸ” Finding current race from ${races.length} races (Current: ${now.toISOString()})`); // Find current race weekend or next upcoming race for (const race of races) { const raceDate = new Date(race.date + 'T' + (race.time || '14:00:00Z')); // Race weekend: Friday to Sunday const weekendStart = new Date(raceDate); weekendStart.setDate(weekendStart.getDate() - 2); // Friday weekendStart.setHours(0, 0, 0, 0); const weekendEnd = new Date(raceDate); weekendEnd.setHours(23, 59, 59, 999); // End of Sunday if (now >= weekendStart && now <= weekendEnd) { console.log(`βœ… Current race weekend: ${race.raceName}`); return race; } else if (raceDate > now) { console.log(`⏰ Next race: ${race.raceName}`); return race; } } // Return last race if season is over console.log(`πŸ“… Season ended, returning final race`); return races[races.length - 1]; } /** * Generate session schedule with live API times and timezone conversion */ async generateLiveSessionSchedule(race) { console.log(`πŸ• Generating live session schedule for ${race.raceName}...`); const sessions = []; // If we have live session data from OpenF1, use it if (race.sessions && race.sessions.length > 0) { console.log('πŸ“‘ Using live session data from OpenF1 API'); for (const apiSession of race.sessions) { if (apiSession.date_start && apiSession.date_end) { sessions.push({ name: this.formatSessionName(apiSession.session_name || apiSession.session_type), startTime: new Date(apiSession.date_start), endTime: new Date(apiSession.date_end), isLive: this.isSessionLive(apiSession.date_start, apiSession.date_end) }); } } // Sort sessions chronologically sessions.sort((a, b) => a.startTime - b.startTime); } // If no live data, generate estimated schedule if (sessions.length === 0) { console.log('⚠️ No live session data, generating estimated schedule'); sessions.push(...this.generateEstimatedSessions(race)); } console.log(`βœ… Generated ${sessions.length} sessions for ${race.raceName}`); return sessions; } /** * Format session name from API data */ formatSessionName(sessionName) { if (!sessionName) return 'Session'; const nameMap = { 'practice_1': 'Practice 1', 'practice_2': 'Practice 2', 'practice_3': 'Practice 3', 'qualifying': 'Qualifying', 'sprint_qualifying': 'Sprint Qualifying', 'sprint': 'Sprint', 'race': 'Race' }; const normalizedName = sessionName.toLowerCase().replace(/\s+/g, '_'); return nameMap[normalizedName] || sessionName; } /** * Check if session is currently live */ isSessionLive(startTime, endTime) { const now = new Date(); const start = new Date(startTime); const end = new Date(endTime); return now >= start && now <= end; } /** * Generate estimated session times when API data is unavailable */ generateEstimatedSessions(race) { const raceDate = new Date(race.date + 'T' + (race.time || '14:00:00Z')); const sessions = []; if (race.isSprintWeekend) { // Sprint weekend format const friday = new Date(raceDate); friday.setDate(friday.getDate() - 2); const saturday = new Date(raceDate); saturday.setDate(saturday.getDate() - 1); sessions.push( this.createSession('Practice 1', friday, '13:30', 60), this.createSession('Sprint Qualifying', friday, '16:30', 44), this.createSession('Sprint', saturday, '12:00', 60), this.createSession('Qualifying', saturday, '16:00', 63), this.createSession('Race', raceDate, race.time?.substr(0, 5) || '15:00', 120) ); } else { // Conventional weekend format const friday = new Date(raceDate); friday.setDate(friday.getDate() - 2); const saturday = new Date(raceDate); saturday.setDate(saturday.getDate() - 1); sessions.push( this.createSession('Practice 1', friday, '13:30', 60), this.createSession('Practice 2', friday, '17:00', 60), this.createSession('Practice 3', saturday, '12:30', 60), this.createSession('Qualifying', saturday, '16:00', 63), this.createSession('Race', raceDate, race.time?.substr(0, 5) || '15:00', 120) ); } return sessions; } /** * Create session object with timezone-aware timestamps */ createSession(name, date, timeString, durationMinutes) { const [hours, minutes] = timeString.split(':'); const startTime = new Date(date); startTime.setHours(parseInt(hours), parseInt(minutes), 0, 0); const endTime = new Date(startTime); endTime.setMinutes(endTime.getMinutes() + durationMinutes); return { name, startTime, endTime, isLive: this.isSessionLive(startTime, endTime) }; } /** * Update the schedule display with live data */ async updateScheduleDisplay() { console.log('πŸ”„ Updating schedule display with live data...'); try { const currentRace = await this.getCurrentRace(); if (!currentRace) { console.error('❌ No current race data available'); return; } console.log(`πŸ“ Current race: ${currentRace.raceName}`); this.currentRaceData = currentRace; const sessions = await this.generateLiveSessionSchedule(currentRace); // Update page content this.updatePageContent(currentRace); this.updateSessionData(sessions, currentRace); this.selectActiveSession(sessions); console.log('βœ… Schedule display updated successfully'); } catch (error) { console.error('❌ Failed to update schedule display:', error); this.showError('Unable to load F1 schedule data'); } } /** * Update page title and description */ updatePageContent(race) { // Update page description const description = document.querySelector('.mx-auto.mb-12.max-w-xl p'); if (description) { description.textContent = race.raceName; } // Update main title const mainTitle = document.querySelector('.mx-auto.mb-12.max-w-xl h1'); if (mainTitle) { mainTitle.textContent = 'Schedule'; } } /** * Update session data in DOM with proper timezone conversion */ updateSessionData(sessions, race) { console.log(`πŸ”§ Updating session data for ${race.raceName}`); // Map sessions to DOM elements (reverse order for display) const sessionMapping = this.getSessionMapping(sessions, race.isSprintWeekend); sessionMapping.forEach(({ session, domAttr }, index) => { if (session) { // Update session title const titleElement = document.querySelector(`[data-session="${domAttr}"]`); if (titleElement) { let title = session.name; if (session.isLive) { title += ' πŸ”΄ LIVE'; } titleElement.textContent = title; } // Update session details with timezone conversion const detailsElement = document.querySelector(`[data-session-details="${domAttr}"]`); if (detailsElement) { const startTimeLocal = this.formatTimeInUserTimezone(session.startTime); const endTimeLocal = this.formatTimeInUserTimezone(session.endTime); const dateLocal = this.formatDateInUserTimezone(session.startTime); detailsElement.innerHTML = ` ${dateLocal}
${startTimeLocal} - ${endTimeLocal} `; } } }); } /** * Map sessions to DOM attributes based on weekend format */ getSessionMapping(sessions, isSprintWeekend) { // DOM order: Race (top), Qualifying, Session3, Session2, Session1 (bottom) const domOrder = ['race', 'qualifying', 'practice3', 'practice2', 'practice1']; // Reverse sessions array to match DOM order (most important first) const reversedSessions = [...sessions].reverse(); return domOrder.map((domAttr, index) => ({ session: reversedSessions[index] || null, domAttr })); } /** * Format time in user's local timezone */ formatTimeInUserTimezone(utcDateTime) { return utcDateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); } /** * Format date in user's local timezone */ formatDateInUserTimezone(utcDateTime) { return utcDateTime.toLocaleDateString([], { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } /** * Auto-select active or next session */ selectActiveSession(sessions) { console.log('🎯 Auto-selecting active session...'); const now = new Date(); let targetSession = null; let targetIndex = 0; // Find live session first for (let i = 0; i < sessions.length; i++) { if (sessions[i].isLive) { targetSession = sessions[i]; targetIndex = i; console.log(`πŸ”΄ Live session found: ${targetSession.name}`); break; } } // If no live session, find next upcoming session if (!targetSession) { for (let i = 0; i < sessions.length; i++) { if (sessions[i].startTime > now) { targetSession = sessions[i]; targetIndex = i; console.log(`⏰ Next session: ${targetSession.name}`); break; } } } // If no upcoming session, select the race (last session) if (!targetSession) { targetSession = sessions[sessions.length - 1]; targetIndex = sessions.length - 1; console.log(`🏁 Selecting race session: ${targetSession?.name}`); } if (targetSession) { this.activateSessionInDOM(targetIndex); } // Set up automatic updates for live sessions this.setupLiveSessionUpdates(sessions); } /** * Activate session in DOM */ activateSessionInDOM(sessionIndex) { const domOrder = ['race', 'qualifying', 'practice3', 'practice2', 'practice1']; const reversedIndex = domOrder.length - 1 - sessionIndex; const domAttr = domOrder[reversedIndex]; // Find button and content const sessionButton = document.querySelector(`[data-session="${domAttr}"]`)?.closest('button'); const sessionContent = document.querySelector(`[data-session-details="${domAttr}"]`)?.closest('.nav-link-content'); if (sessionButton && sessionContent) { // Clear all active states document.querySelectorAll('.nav-link').forEach(btn => { btn.classList.remove('active'); btn.setAttribute('aria-selected', 'false'); }); document.querySelectorAll('.nav-link-content').forEach(content => { content.classList.add('hidden'); }); // Activate target session sessionButton.classList.add('active'); sessionButton.setAttribute('aria-selected', 'true'); sessionContent.classList.remove('hidden'); console.log(`βœ… Activated session: ${domAttr}`); } } /** * Setup live session updates */ setupLiveSessionUpdates(sessions) { // Clear existing interval if (this.sessionUpdateInterval) { clearInterval(this.sessionUpdateInterval); } // Update every 30 seconds during race weekends this.sessionUpdateInterval = setInterval(() => { console.log('πŸ”„ Checking for session updates...'); this.selectActiveSession(sessions); }, 30000); } /** * Show error message */ showError(message) { const description = document.querySelector('.mx-auto.mb-12.max-w-xl p'); if (description) { description.textContent = message; } } /** * Fallback race data if API fails */ getCurrentRaceFallback() { console.log('πŸ”„ Using fallback race data...'); // Return a basic race structure return [{ raceName: 'Formula 1 Grand Prix', round: '1', date: new Date().toISOString().split('T')[0], time: '15:00:00Z', Circuit: { circuitName: 'Circuit', Location: { locality: 'Location', country: 'Country' } }, isSprintWeekend: false, sessions: [] }]; } /** * Initialize the F1 Schedule Manager */ async init() { console.log('🏎️ Initializing F1 Schedule Manager for 2025 season...'); try { await this.updateScheduleDisplay(); // Set up automatic refresh every 15 minutes this.autoRefreshInterval = setInterval(() => { console.log('πŸ”„ Auto-refreshing F1 schedule...'); this.updateScheduleDisplay().catch(err => { console.error('Auto-refresh failed:', err); }); }, 15 * 60 * 1000); console.log('βœ… F1 Schedule Manager initialized successfully'); } catch (error) { console.error('❌ Failed to initialize F1 Schedule Manager:', error); this.showError('Unable to load F1 data. Please refresh the page.'); } } /** * Cleanup intervals */ cleanup() { if (this.autoRefreshInterval) { clearInterval(this.autoRefreshInterval); } if (this.sessionUpdateInterval) { clearInterval(this.sessionUpdateInterval); } } } try { console.log('Fetching schedule from:', `${this.baseUrl}/${this.currentSeason}.json`); let response = await fetch(`${this.baseUrl}/${this.currentSeason}.json`); if (!response.ok) { console.log('Primary API failed, trying fallback...'); response = await fetch(`${this.fallbackUrl}/${this.currentSeason}.json`); } if (!response.ok) { throw new Error(`Both APIs failed. Status: ${response.status}`); } const data = await response.json(); console.log('API Response:', data); if (!data.MRData || !data.MRData.RaceTable || !data.MRData.RaceTable.Races) { throw new Error('Invalid API response structure'); } return data.MRData.RaceTable.Races; } catch (error) { console.error('Error fetching F1 schedule:', error); return null; } } /** * Fetch 2025 schedule from OpenF1 API */ async fetchOpenF1Schedule() { try { const response = await fetch(`${this.openF1Url}/meetings?year=${this.currentSeason}`); if (!response.ok) throw new Error('OpenF1 API failed'); const meetings = await response.json(); console.log('OpenF1 meetings data:', meetings.slice(0, 3)); return meetings; } catch (error) { console.warn('OpenF1 schedule fetch failed:', error); return null; } } /** * Convert OpenF1 format to Ergast-compatible format */ convertOpenF1ToErgastFormat(openF1Meetings) { return openF1Meetings.map((meeting, index) => ({ season: meeting.year?.toString() || this.currentSeason.toString(), round: (index + 1).toString(), raceName: meeting.meeting_name || meeting.meeting_official_name || 'Unknown Grand Prix', Circuit: { circuitId: meeting.circuit_short_name?.toLowerCase().replace(/\s+/g, '_') || 'unknown', circuitName: meeting.circuit_short_name || meeting.location || 'Unknown Circuit', Location: { locality: meeting.location || 'Unknown', country: meeting.country_name || 'Unknown' } }, date: meeting.date_start ? meeting.date_start.split('T')[0] : new Date().toISOString().split('T')[0], time: meeting.date_start ? meeting.date_start.split('T')[1]?.substring(0, 8) : '14:00:00Z' })); } /** * Fetch detailed session information for a specific race */ async getRaceSessionDetails(season, round) { try { console.log(`Fetching session details for round ${round}`); let response = await fetch(`${this.baseUrl}/${season}/${round}.json`); if (!response.ok) { console.log('Primary API failed, trying fallback...'); response = await fetch(`${this.fallbackUrl}/${season}/${round}.json`); } if (!response.ok) { throw new Error(`Session details API failed. Status: ${response.status}`); } const data = await response.json(); console.log('Session details response:', data); if (data.MRData && data.MRData.RaceTable && data.MRData.RaceTable.Races && data.MRData.RaceTable.Races[0]) { return data.MRData.RaceTable.Races[0]; } return null; } catch (error) { console.error('Error fetching session details:', error); return null; } } /** * Find the next/current race based on current date */ async getCurrentRace() { const races = await this.getCurrentSeasonSchedule(); if (!races) return null; console.log('πŸ” Finding current race from', races.length, 'races'); console.log('Current date:', new Date().toISOString()); // Find the next race or current race weekend const now = new Date(); for (const race of races) { const raceDate = new Date(race.date + 'T' + (race.time || '14:00:00Z')); console.log(`πŸ“… Checking race: ${race.raceName} on ${raceDate.toISOString()}`); // Check if we're within the race weekend (Friday to Sunday) const weekendStart = new Date(raceDate); weekendStart.setDate(weekendStart.getDate() - 2); // Friday starts the weekend const weekendEnd = new Date(raceDate); weekendEnd.setHours(23, 59, 59, 999); // End of race day console.log(` Weekend: ${weekendStart.toISOString()} to ${weekendEnd.toISOString()}`); console.log(` Is current weekend: ${now >= weekendStart && now <= weekendEnd}`); console.log(` Is future race: ${raceDate > now}`); if (now >= weekendStart && now <= weekendEnd) { // We're in the current race weekend console.log(`βœ… Current race weekend: ${race.raceName}`); return race; } else if (raceDate > now) { // This is the next upcoming race console.log(`⏰ Next upcoming race: ${race.raceName}`); return race; } } // If no upcoming race, return the last race of the season console.log('πŸ“ Returning last race of season'); return races[races.length - 1]; } /** * Generate session schedule for a race weekend using API data * Creates proper UTC timestamps for accurate timezone conversion */ async generateSessionSchedule(race) { console.log('Generating session schedule for:', race.raceName); // Get detailed session information from API const raceDetails = await this.getRaceSessionDetails(this.currentSeason, race.round); // Extract race date and time from API const raceDate = race.date; const raceTime = race.time || '14:00:00Z'; console.log('Race date from API:', raceDate); console.log('Race time from API:', raceTime); // Create race datetime as UTC const raceDateTimeUTC = new Date(raceDate + 'T' + raceTime); // Calculate session dates const friday = new Date(raceDateTimeUTC); friday.setDate(friday.getDate() - 2); const saturday = new Date(raceDateTimeUTC); saturday.setDate(saturday.getDate() - 1); const sessions = []; const isSprintWeekend = this.isSprintWeekend(race); if (isSprintWeekend) { // Sprint weekend format - use common sprint times in UTC sessions.push({ name: 'Practice 1', dateTime: this.createSessionDateTime(friday, '11:30:00Z'), // Typical FP1 time utcDateTime: this.createSessionDateTime(friday, '11:30:00Z'), duration: 60 // minutes }); sessions.push({ name: 'Sprint Qualifying', dateTime: this.createSessionDateTime(friday, '15:30:00Z'), // Typical SQ time utcDateTime: this.createSessionDateTime(friday, '15:30:00Z'), duration: 44 // minutes }); sessions.push({ name: 'Sprint', dateTime: this.createSessionDateTime(saturday, '10:00:00Z'), // Typical Sprint time utcDateTime: this.createSessionDateTime(saturday, '10:00:00Z'), duration: 60 // minutes }); sessions.push({ name: 'Qualifying', dateTime: this.createSessionDateTime(saturday, '14:00:00Z'), // Typical Q time utcDateTime: this.createSessionDateTime(saturday, '14:00:00Z'), duration: 63 // minutes }); sessions.push({ name: 'Race', dateTime: raceDateTimeUTC, utcDateTime: raceDateTimeUTC, duration: 120 // minutes }); } else { // Conventional weekend format sessions.push({ name: 'Practice 1', dateTime: this.createSessionDateTime(friday, '11:30:00Z'), utcDateTime: this.createSessionDateTime(friday, '11:30:00Z'), duration: 60 }); sessions.push({ name: 'Practice 2', dateTime: this.createSessionDateTime(friday, '15:00:00Z'), utcDateTime: this.createSessionDateTime(friday, '15:00:00Z'), duration: 60 }); sessions.push({ name: 'Practice 3', dateTime: this.createSessionDateTime(saturday, '10:30:00Z'), utcDateTime: this.createSessionDateTime(saturday, '10:30:00Z'), duration: 60 }); sessions.push({ name: 'Qualifying', dateTime: this.createSessionDateTime(saturday, '14:00:00Z'), utcDateTime: this.createSessionDateTime(saturday, '14:00:00Z'), duration: 63 }); sessions.push({ name: 'Race', dateTime: raceDateTimeUTC, utcDateTime: raceDateTimeUTC, duration: 120 }); } // Calculate end times sessions.forEach(session => { session.endDateTime = new Date(session.dateTime.getTime() + (session.duration * 60000)); }); console.log('Generated sessions with UTC times:', sessions); return sessions; } /** * Create a session datetime by combining date and time in UTC */ createSessionDateTime(baseDate, timeString) { const date = baseDate.toISOString().split('T')[0]; // Get YYYY-MM-DD return new Date(date + 'T' + timeString); } /** * Get accurate session times based on race location and specific schedule patterns * Updated with 2025 F1 calendar official session times */ getSessionTimesForCircuit(race, isSprintWeekend) { const circuitName = race.Circuit.circuitName.toLowerCase(); const country = race.Circuit.Location.country.toLowerCase(); const raceName = race.raceName.toLowerCase(); // Official 2025 F1 session times per circuit (in local circuit time) const customTimes = { // Australia (Melbourne) - Traditional start of season 'australia': { conventional: { fp1: ['12:30', '13:30'], fp2: ['16:00', '17:00'], fp3: ['12:00', '13:00'], qualifying: ['15:00', '16:00'], race: ['15:00', '17:00'] } }, // Bahrain - Often night race 'bahrain': { conventional: { fp1: ['15:30', '16:30'], fp2: ['19:00', '20:00'], fp3: ['16:30', '17:30'], qualifying: ['20:00', '21:00'], race: ['18:00', '20:00'] } }, // Saudi Arabia - Night street circuit 'saudi arabia': { conventional: { fp1: ['17:30', '18:30'], fp2: ['21:00', '22:00'], fp3: ['17:30', '18:30'], qualifying: ['21:00', '22:00'], race: ['20:00', '22:00'] } }, // Japan (Suzuka) 'japan': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['13:30', '14:30'], qualifying: ['17:00', '18:00'], race: ['14:00', '16:00'] } }, // China (Shanghai) - Sprint weekend 'china': { sprint: { fp1: ['11:30', '12:30'], sq: ['15:30', '16:30'], sprint: ['11:00', '12:00'], qualifying: ['15:00', '16:00'], race: ['15:00', '17:00'] } }, // Miami - Sprint weekend, Eastern US time 'usa': { sprint: { fp1: ['12:30', '13:30'], sq: ['16:30', '17:30'], sprint: ['12:00', '13:00'], qualifying: ['16:00', '17:00'], race: ['15:30', '17:30'] } }, // Monaco - Special Sunday schedule 'monaco': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Canada (Montreal) 'canada': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['14:00', '16:00'] } }, // Spain (Barcelona) 'spain': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Austria - Sprint weekend 'austria': { sprint: { fp1: ['13:30', '14:30'], sq: ['17:30', '18:30'], sprint: ['12:00', '13:00'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Great Britain (Silverstone) 'uk': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, 'united kingdom': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Hungary 'hungary': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Belgium (Spa) - Sprint weekend (July 27, 2025) 'belgium': { sprint: { fp1: ['13:30', '14:30'], // Friday 13:30 local time sq: ['16:30', '17:14'], // Friday 16:30 local time sprint: ['12:00', '13:00'], // Saturday 12:00 local time qualifying: ['16:00', '17:00'], // Saturday 16:00 local time race: ['15:00', '17:00'] // Sunday 15:00 local time (July 27, 2025) } }, // Netherlands (Zandvoort) 'netherlands': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Italy (Monza) 'italy': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['15:00', '17:00'] } }, // Azerbaijan (Baku) 'azerbaijan': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['14:30', '15:30'], qualifying: ['18:00', '19:00'], race: ['15:00', '17:00'] } }, // Singapore - Night race 'singapore': { conventional: { fp1: ['17:30', '18:30'], fp2: ['21:00', '22:00'], fp3: ['17:30', '18:30'], qualifying: ['21:00', '22:00'], race: ['20:00', '22:00'] } }, // United States (Austin) - Sprint weekend 'united states': { sprint: { fp1: ['12:30', '13:30'], sq: ['16:30', '17:30'], sprint: ['12:00', '13:00'], qualifying: ['16:00', '17:00'], race: ['14:00', '16:00'] } }, // Mexico 'mexico': { conventional: { fp1: ['12:30', '13:30'], fp2: ['16:00', '17:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: ['14:00', '16:00'] } }, // Brazil (Interlagos) - Sprint weekend 'brazil': { sprint: { fp1: ['11:30', '12:30'], sq: ['15:30', '16:30'], sprint: ['11:00', '12:00'], qualifying: ['15:00', '16:00'], race: ['14:00', '16:00'] } }, // Qatar - Sprint weekend, night race 'qatar': { sprint: { fp1: ['14:30', '15:30'], sq: ['18:30', '19:30'], sprint: ['16:00', '17:00'], qualifying: ['19:00', '20:00'], race: ['18:00', '20:00'] } }, // UAE (Abu Dhabi) - Night/twilight race 'uae': { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['14:30', '15:30'], qualifying: ['18:00', '19:00'], race: ['17:00', '19:00'] } } }; // Default times for circuits not specifically configured const defaultTimes = { conventional: { fp1: ['13:30', '14:30'], fp2: ['17:00', '18:00'], fp3: ['12:30', '13:30'], qualifying: ['16:00', '17:00'], race: [race.time ? race.time.substr(0, 5) : '15:00', this.calculateRaceEndTime(race.time ? race.time.substr(0, 5) : '15:00')] }, sprint: { fp1: ['13:30', '14:30'], sq: ['17:30', '18:30'], sprint: ['12:00', '13:00'], qualifying: ['16:00', '17:00'], race: [race.time ? race.time.substr(0, 5) : '15:00', this.calculateRaceEndTime(race.time ? race.time.substr(0, 5) : '15:00')] } }; // Determine the format and get appropriate times const format = isSprintWeekend ? 'sprint' : 'conventional'; let times = defaultTimes[format]; // Check for custom times by country first, then by circuit name if (customTimes[country] && customTimes[country][format]) { times = customTimes[country][format]; } else if (circuitName && customTimes[circuitName] && customTimes[circuitName][format]) { times = customTimes[circuitName][format]; } console.log(`Using ${format} weekend times for ${country}:`, times); return times; } /** * Determine if a race weekend is a sprint weekend - Updated for 2025 F1 Calendar */ isSprintWeekend(race) { const sprintRaces2025 = [ 'chinese grand prix', 'china grand prix', 'miami grand prix', 'austrian grand prix', 'austria grand prix', 'belgian grand prix', 'belgium grand prix', 'united states grand prix', 'us grand prix', 'austin grand prix', 'sΓ£o paulo grand prix', 'sao paulo grand prix', 'brazilian grand prix', 'brazil grand prix', 'qatar grand prix' ]; const raceName = race.raceName.toLowerCase(); const country = race.Circuit.Location.country.toLowerCase(); const circuitName = race.Circuit.circuitName.toLowerCase(); // Check by race name first const isSprintByName = sprintRaces2025.some(sprintRace => { return raceName.includes(sprintRace.toLowerCase()) || raceName.includes(sprintRace.replace(' grand prix', '').toLowerCase()); }); // Check by country as backup const sprintCountries = ['china', 'usa', 'united states', 'austria', 'belgium', 'brazil', 'qatar']; const isSprintByCountry = sprintCountries.includes(country); // Special checks for circuits that might have different naming const sprintCircuits = ['shanghai', 'miami', 'red bull ring', 'spa', 'circuit of the americas', 'interlagos', 'losail']; const isSprintByCircuit = sprintCircuits.some(circuit => circuitName.includes(circuit)); const result = isSprintByName || isSprintByCountry || isSprintByCircuit; console.log(`Sprint weekend check for ${raceName}:`, { raceName, country, circuitName, isSprintByName, isSprintByCountry, isSprintByCircuit, result }); return result; } /** * Calculate estimated race end time (assuming 2 hour duration) */ calculateRaceEndTime(startTime) { const [hours, minutes] = startTime.split(':'); const endHour = parseInt(hours) + 2; return `${endHour.toString().padStart(2, '0')}:${minutes}`; } /** * Format date for display */ formatDate(date) { const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; return date.toLocaleDateString('en-US', options); } /** * Update the schedule display in the HTML */ async updateScheduleDisplay() { const currentRace = await this.getCurrentRace(); if (!currentRace) { console.error('Could not fetch current race information'); return; } console.log('Current/Next Race:', currentRace.raceName, 'at', currentRace.Circuit.circuitName); // Store race data for access in other methods this.currentRaceData = currentRace; const sessions = await this.generateSessionSchedule(currentRace); // Update session information await this.updateSessionData(sessions, currentRace); } /** * Update individual session data in the DOM with proper timezone conversion */ async updateSessionData(sessions, race) { console.log('Updating session data for:', race.raceName); console.log('Sessions:', sessions); console.log('Is sprint weekend:', this.isSprintWeekend(race)); // Check if this is a sprint weekend const isSprintWeekend = this.isSprintWeekend(race); // Reorder sessions chronologically and map to DOM elements // DOM order: Race (top), Qualifying, Session3, Session2, Session1 (bottom) let sessionMapping; if (isSprintWeekend) { // Sprint weekend: P1, SQ, Sprint, Qualifying, Race // Map to DOM: Race, Qualifying, Sprint, Sprint Qualifying, Practice 1 sessionMapping = [ { session: sessions[4], domAttr: 'race' }, // Race at top { session: sessions[3], domAttr: 'qualifying' }, // Qualifying { session: sessions[2], domAttr: 'practice3' }, // Sprint { session: sessions[1], domAttr: 'practice2' }, // Sprint Qualifying { session: sessions[0], domAttr: 'practice1' } // Practice 1 at bottom ]; } else { // Conventional weekend: P1, P2, P3, Qualifying, Race // Map to DOM: Race, Qualifying, Practice 3, Practice 2, Practice 1 sessionMapping = [ { session: sessions[4], domAttr: 'race' }, // Race at top { session: sessions[3], domAttr: 'qualifying' }, // Qualifying { session: sessions[2], domAttr: 'practice3' }, // Practice 3 { session: sessions[1], domAttr: 'practice2' }, // Practice 2 { session: sessions[0], domAttr: 'practice1' } // Practice 1 at bottom ]; } // Update each session in the DOM sessionMapping.forEach(({ session, domAttr }) => { if (session) { // Update session title const titleElement = document.querySelector(`[data-session="${domAttr}"]`); if (titleElement) { titleElement.textContent = session.name; console.log(`Updated title for ${domAttr}:`, session.name); } // Convert times to user's local timezone const startTimeLocal = this.formatTimeInUserTimezone(session.dateTime); const endTimeLocal = this.formatTimeInUserTimezone(session.endDateTime); // Update session details const detailsElement = document.querySelector(`[data-session-details="${domAttr}"]`); if (detailsElement) { const formattedDate = this.formatDateInUserTimezone(session.dateTime); detailsElement.innerHTML = ` ${formattedDate}
${startTimeLocal} - ${endTimeLocal} `; console.log(`Updated details for ${domAttr}:`, formattedDate, startTimeLocal, '-', endTimeLocal); } } }); // Update the page description with race location const description = document.querySelector('.mx-auto.mb-12.max-w-xl p'); if (description && race.Circuit) { description.textContent = `${race.raceName}`; } // Update the main title to include race information const mainTitle = document.querySelector('.mx-auto.mb-12.max-w-xl h1'); if (mainTitle) { mainTitle.textContent = `Schedule`; } // Auto-select active session this.selectActiveSession(sessions); } /** * Format UTC datetime in user's local timezone */ formatTimeInUserTimezone(utcDateTime) { return utcDateTime.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }); } /** * Format UTC date in user's local timezone */ formatDateInUserTimezone(utcDateTime) { return utcDateTime.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } /** * Get proper timezone identifier for circuit location * Uses IANA timezone database identifiers for accurate conversion including DST */ getCircuitTimezone(country, circuitName = '') { const circuitTimezones = { // Specific circuits with known locations 'albert park grand prix circuit': 'Australia/Melbourne', 'melbourne grand prix circuit': 'Australia/Melbourne', 'bahrain international circuit': 'Asia/Bahrain', 'jeddah corniche circuit': 'Asia/Riyadh', 'suzuka circuit': 'Asia/Tokyo', 'shanghai international circuit': 'Asia/Shanghai', 'miami international autodrome': 'America/New_York', 'circuit de monaco': 'Europe/Monaco', 'monte carlo': 'Europe/Monaco', 'circuit gilles villeneuve': 'America/Toronto', 'circuit de catalunya': 'Europe/Madrid', 'red bull ring': 'Europe/Vienna', 'silverstone circuit': 'Europe/London', 'hungaroring': 'Europe/Budapest', 'circuit de spa-francorchamps': 'Europe/Brussels', 'spa-francorchamps': 'Europe/Brussels', 'circuit zandvoort': 'Europe/Amsterdam', 'autodromo nazionale di monza': 'Europe/Rome', 'monza': 'Europe/Rome', 'baku city circuit': 'Asia/Baku', 'marina bay street circuit': 'Asia/Singapore', 'circuit of the americas': 'America/Chicago', 'autodromo hermanos rodriguez': 'America/Mexico_City', 'autodromo jose carlos pace': 'America/Sao_Paulo', 'interlagos': 'America/Sao_Paulo', 'losail international circuit': 'Asia/Qatar', 'yas marina circuit': 'Asia/Dubai' }; // First try to match by circuit name const circuitLower = circuitName.toLowerCase(); for (const [circuit, timezone] of Object.entries(circuitTimezones)) { if (circuitLower.includes(circuit) || circuit.includes(circuitLower)) { return timezone; } } // Fallback to country-based mapping const countryTimezones = { 'australia': 'Australia/Melbourne', 'bahrain': 'Asia/Bahrain', 'saudi arabia': 'Asia/Riyadh', 'japan': 'Asia/Tokyo', 'china': 'Asia/Shanghai', 'usa': 'America/New_York', // Miami uses Eastern 'united states': 'America/New_York', 'monaco': 'Europe/Monaco', 'canada': 'America/Toronto', 'spain': 'Europe/Madrid', 'austria': 'Europe/Vienna', 'uk': 'Europe/London', 'united kingdom': 'Europe/London', 'hungary': 'Europe/Budapest', 'belgium': 'Europe/Brussels', 'netherlands': 'Europe/Amsterdam', 'italy': 'Europe/Rome', 'azerbaijan': 'Asia/Baku', 'singapore': 'Asia/Singapore', 'mexico': 'America/Mexico_City', 'brazil': 'America/Sao_Paulo', 'qatar': 'Asia/Qatar', 'uae': 'Asia/Dubai' }; return countryTimezones[country.toLowerCase()] || 'UTC'; } /** * Convert session time from circuit timezone to user's local timezone * Uses proper timezone conversion with DST support */ convertSessionTimeToUserTimezone(sessionTime, sessionDate, circuitCountry, circuitName = '') { try { const [hours, minutes] = sessionTime.split(':'); // Get the circuit timezone const circuitTimezone = this.getCircuitTimezone(circuitCountry, circuitName); // Create a date in the circuit's timezone const year = sessionDate.getFullYear(); const month = sessionDate.getMonth(); const day = sessionDate.getDate(); // Format the datetime string for the circuit timezone const dateTimeString = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${hours}:${minutes}:00`; // Create a date assuming it's in the circuit timezone // We'll use a workaround since JavaScript doesn't directly support creating dates in specific timezones const tempDate = new Date(dateTimeString + 'Z'); // UTC const circuitOffset = this.getTimezoneOffsetForDate(tempDate, circuitTimezone); const circuitTimeInUTC = new Date(tempDate.getTime() - (circuitOffset * 60000)); // Format to user's local timezone return circuitTimeInUTC.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }); } catch (error) { console.error('Error converting timezone:', error); return sessionTime; // Fallback to original time } } /** * Get timezone offset in minutes for a specific date and timezone */ getTimezoneOffsetForDate(date, timezone) { try { // Create formatters for UTC and target timezone const utcFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const tzFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const utcTime = new Date(utcFormatter.format(date).replace(/(\d{4})-(\d{2})-(\d{2}), (\d{2}):(\d{2}):(\d{2})/, '$1-$2-$3T$4:$5:$6Z')); const tzTime = new Date(tzFormatter.format(date).replace(/(\d{4})-(\d{2})-(\d{2}), (\d{2}):(\d{2}):(\d{2})/, '$1-$2-$3T$4:$5:$6Z')); return (tzTime.getTime() - utcTime.getTime()) / 60000; } catch (error) { console.error('Error calculating timezone offset:', error); return 0; } } /** * Legacy method - timezone conversion now happens in updateSessionData * Kept for backward compatibility but times are already converted */ convertTimesToUserTimezone() { console.log('Times are already converted to user timezone in updateSessionData method'); // Times are now converted during the update process, so this method is mainly for logging const startTimeElements = document.querySelectorAll('.start-time'); console.log('Found', startTimeElements.length, 'time elements already in user timezone'); } /** * Determine which session is currently active or upcoming */ getActiveSession(sessions) { const now = new Date(); let activeSession = null; let nextSession = null; console.log('=== GETTING ACTIVE SESSION ==='); console.log('Current time UTC:', now.toISOString()); for (let i = 0; i < sessions.length; i++) { const session = sessions[i]; // Use the UTC datetime directly from the session const sessionStartUTC = new Date(session.utcDateTime); const sessionEndUTC = new Date(sessionStartUTC.getTime() + session.duration * 60 * 1000); console.log(`Session ${i}: ${session.name}`); console.log(` Start: ${sessionStartUTC.toISOString()}`); console.log(` End: ${sessionEndUTC.toISOString()}`); console.log(` Is Live: ${now >= sessionStartUTC && now <= sessionEndUTC}`); console.log(` Is Future: ${now < sessionStartUTC}`); // Check if session is currently happening if (now >= sessionStartUTC && now <= sessionEndUTC) { activeSession = { session, index: i, status: 'live' }; console.log(` βœ… LIVE SESSION FOUND: ${session.name}`); break; } // Check if this is the next upcoming session if (now < sessionStartUTC && !nextSession) { nextSession = { session, index: i, status: 'upcoming' }; console.log(` ⏰ NEXT SESSION FOUND: ${session.name}`); } } const result = activeSession || nextSession || { session: sessions[sessions.length - 1], index: sessions.length - 1, status: 'finished' }; console.log('Final result:', result.session.name, 'with status:', result.status); console.log('=== END GETTING ACTIVE SESSION ==='); // Return active session if live, otherwise next upcoming session return result; } /** * Map session index to DOM attribute for the reordered display */ getSessionDomAttribute(sessionIndex, isSprintWeekend) { if (isSprintWeekend) { // Sprint: [P1, SQ, Sprint, Qualifying, Race] -> [race, qualifying, practice3, practice2, practice1] const mapping = ['practice1', 'practice2', 'practice3', 'qualifying', 'race']; return mapping[sessionIndex]; } else { // Conventional: [P1, P2, P3, Qualifying, Race] -> [race, qualifying, practice3, practice2, practice1] const mapping = ['practice1', 'practice2', 'practice3', 'qualifying', 'race']; return mapping[sessionIndex]; } } /** * Auto-select the active session and expand its details */ selectActiveSession(sessions) { console.log('=== AUTO-SELECTING ACTIVE SESSION ==='); console.log('Current time:', new Date().toISOString()); console.log('Sessions:', sessions.map(s => ({ name: s.name, utcDateTime: s.utcDateTime?.toISOString(), endDateTime: s.endDateTime?.toISOString() }))); const activeSessionInfo = this.getActiveSession(sessions); if (!activeSessionInfo) { console.log('No active session found'); return; } const { session, index, status } = activeSessionInfo; console.log(`Found active session: ${session.name} (${status}) at index ${index}`); const race = this.currentRaceData; // Store race data for access const isSprintWeekend = race ? this.isSprintWeekend(race) : false; const domAttribute = this.getSessionDomAttribute(index, isSprintWeekend); console.log(`Mapping to DOM attribute: ${domAttribute} (Sprint weekend: ${isSprintWeekend})`); // Find the button and content elements const sessionButton = document.querySelector(`[data-session="${domAttribute}"]`)?.closest('button'); const sessionContent = document.querySelector(`[data-session-details="${domAttribute}"]`)?.closest('.nav-link-content'); console.log('Button found:', !!sessionButton); console.log('Content found:', !!sessionContent); if (sessionButton && sessionContent) { // Remove active class from all buttons document.querySelectorAll('.nav-link').forEach(btn => { btn.classList.remove('active'); btn.setAttribute('aria-selected', 'false'); }); // Hide all content sections document.querySelectorAll('.nav-link-content').forEach(content => { content.classList.add('hidden'); }); // Activate the current session sessionButton.classList.add('active'); sessionButton.setAttribute('aria-selected', 'true'); sessionContent.classList.remove('hidden'); // Add status indicator to the session title const titleElement = sessionButton.querySelector(`[data-session="${domAttribute}"]`); if (titleElement) { let statusIndicator = ''; if (status === 'live') { statusIndicator = ''; } else if (status === 'upcoming') { statusIndicator = ''; } // Remove existing status indicators titleElement.textContent = titleElement.textContent.replace(/ (πŸ”΄ LIVE|⏰ NEXT|βœ… FINISHED)/, ''); titleElement.textContent += statusIndicator; } console.log(`βœ… Auto-selected session: ${session.name} with status: ${status}`); } else { console.error('❌ Could not find DOM elements for session:', domAttribute); } // Set up automatic switching for live sessions this.setupAutoSwitching(sessions); } /** * Setup automatic session switching based on time */ setupAutoSwitching(sessions) { // Clear any existing interval if (this.autoSwitchInterval) { clearInterval(this.autoSwitchInterval); } // Check every minute for session changes this.autoSwitchInterval = setInterval(() => { console.log('Checking for session changes...'); this.selectActiveSession(sessions); }, 60000); // Check every minute console.log('Auto-switching enabled - checking every minute'); } /** * Cleanup method to clear intervals */ cleanup() { if (this.autoSwitchInterval) { clearInterval(this.autoSwitchInterval); this.autoSwitchInterval = null; console.log('Auto-switching disabled'); } } /** * Initialize the schedule manager */ async init() { console.log('F1 Schedule Manager initializing...'); try { // Show loading state const description = document.querySelector('.mx-auto.mb-12.max-w-xl p'); if (description) { description.textContent = 'Grand Prix'; } // Test API connectivity first console.log('Testing API connectivity...'); const testResponse = await fetch(`${this.baseUrl}/${this.currentSeason}.json`); if (!testResponse.ok) { throw new Error(`API responded with status: ${testResponse.status}`); } console.log('API test successful'); await this.updateScheduleDisplay(); console.log('F1 Schedule Manager initialized successfully'); // Set up automatic refresh every hour setInterval(async () => { console.log('Refreshing F1 schedule data...'); try { await this.updateScheduleDisplay(); console.log('Automatic refresh complete'); } catch (error) { console.error('Automatic refresh failed:', error); } }, 3600000); // 1 hour } catch (error) { console.error('Error initializing F1 Schedule Manager:', error); // Show error in description const description = document.querySelector('.mx-auto.mb-12.max-w-xl p'); if (description) { description.textContent = 'Unable to load live F1 data. Showing fallback schedule. Check console for details.'; } // Reset session titles to defaults const sessionElements = { 'race': 'Race', 'qualifying': 'Qualifying', 'practice3': 'Practice 3', 'practice2': 'Practice 2', 'practice1': 'Practice 1' }; Object.entries(sessionElements).forEach(([attr, title]) => { const element = document.querySelector(`[data-session="${attr}"]`); if (element) element.textContent = title; }); } } } // Initialize when DOM is loaded let scheduleManager; document.addEventListener('DOMContentLoaded', () => { scheduleManager = new F1ScheduleManager(); scheduleManager.init(); }); // Global function for manual testing window.refreshF1Schedule = () => { if (scheduleManager) { console.log('Manually refreshing F1 schedule...'); scheduleManager.updateScheduleDisplay().then(() => { console.log('Manual refresh complete'); }).catch(err => { console.error('Manual refresh failed:', err); }); } else { console.log('Schedule manager not initialized yet'); } }; // Global function to test API window.testF1API = async () => { if (scheduleManager) { console.log('Testing F1 API...'); const races = await scheduleManager.getCurrentSeasonSchedule(); console.log('API Test Result:', races ? 'SUCCESS' : 'FAILED'); if (races) { console.log('Found', races.length, 'races'); const belgianGP = races.find(r => r.raceName.toLowerCase().includes('belgian')); if (belgianGP) { console.log('Belgian GP found:', belgianGP); console.log('Is sprint weekend:', scheduleManager.isSprintWeekend(belgianGP)); } } return races; } }; // Function to show user timezone info window.showTimezoneInfo = () => { const now = new Date(); console.log('Current time:', now.toString()); console.log('User timezone:', Intl.DateTimeFormat().resolvedOptions().timeZone); console.log('Timezone offset (minutes):', now.getTimezoneOffset()); console.log('Local time string:', now.toLocaleTimeString()); // Test timezone conversion for current race if (scheduleManager) { scheduleManager.getCurrentRace().then(race => { if (race) { console.log('Current race:', race.raceName, 'in', race.Circuit.Location.country); const circuitTz = scheduleManager.getCircuitTimezone(race.Circuit.Location.country, race.Circuit.circuitName); console.log('Circuit timezone:', circuitTz); // Test conversion const testTime = '15:00'; const testDate = new Date(); const converted = scheduleManager.convertSessionTimeToUserTimezone(testTime, testDate, race.Circuit.Location.country, race.Circuit.circuitName); console.log(`Test conversion: ${testTime} circuit time = ${converted} your local time`); } }); } }; // Function to test timezone conversion with any time window.testTimezoneConversion = (time = '15:00', country = 'Belgium', circuitName = 'Spa-Francorchamps') => { if (scheduleManager) { const testDate = new Date(); const converted = scheduleManager.convertSessionTimeToUserTimezone(time, testDate, country, circuitName); console.log(`Converting ${time} from ${country} (${circuitName}) to your timezone: ${converted}`); return converted; } return 'Schedule manager not available'; }; // Function to test active session detection window.testActiveSession = () => { if (scheduleManager && scheduleManager.currentRaceData) { const sessions = scheduleManager.generateSessionSchedule(scheduleManager.currentRaceData); const activeInfo = scheduleManager.getActiveSession(sessions); console.log('Active session info:', activeInfo); if (activeInfo) { console.log(`${activeInfo.session.name} is ${activeInfo.status}`); console.log('Session times (circuit):', activeInfo.session.startTime, '-', activeInfo.session.endTime); // Show UTC conversion const startUTC = scheduleManager.convertCircuitTimeToUTC( activeInfo.session.startTime, activeInfo.session.date, scheduleManager.currentRaceData.Circuit.Location.country, scheduleManager.currentRaceData.Circuit.circuitName ); console.log('Start time in UTC:', startUTC.toISOString()); console.log('Current time UTC:', new Date().toISOString()); } return activeInfo; } return 'No race data available'; }; // Function to manually trigger session selection window.selectActiveSession = () => { if (scheduleManager && scheduleManager.currentRaceData) { const sessions = scheduleManager.generateSessionSchedule(scheduleManager.currentRaceData); scheduleManager.selectActiveSession(sessions); console.log('Active session selection triggered'); } else { console.log('No schedule manager or race data available'); } };