// Supabase configuration const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co'; const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8'; // Initialize Supabase client const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // Initialize Socket.IO connection const socket = io(); // Global variable to store locations with coordinates let locationsData = []; let lastSelectedLocation = null; // Cookie Functions (inline implementation) function setCookie(name, value, days = 30) { const expires = new Date(); expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`; } function getCookie(name) { const nameEQ = name + "="; const ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); } return null; } function loadLastSelectedLocation() { try { const cookieValue = getCookie('ninjacross_last_location'); if (cookieValue) { const lastLocation = JSON.parse(cookieValue); lastSelectedLocation = lastLocation; console.log('📍 Last selected location loaded:', lastLocation.name); return lastLocation; } } catch (error) { console.error('Error loading last location:', error); } return null; } function saveLocationSelection(locationId, locationName) { try { // Remove emoji from location name for storage const cleanName = locationName.replace(/^📍\s*/, ''); const locationData = { id: locationId, name: cleanName, timestamp: new Date().toISOString() }; setCookie('ninjacross_last_location', JSON.stringify(locationData), 90); lastSelectedLocation = { id: locationId, name: cleanName }; console.log('💾 Location saved to cookie:', cleanName); } catch (error) { console.error('Error saving location:', error); } } // WebSocket Event Handlers socket.on('connect', () => { console.log('🔌 WebSocket connected'); }); socket.on('disconnect', () => { console.log('🔌 WebSocket disconnected'); }); socket.on('newTime', (data) => { console.log('🏁 New time received:', data); showNotification(data); // Reload data to show the new time loadData(); }); // Notification Functions function showNotification(timeData) { const notificationBubble = document.getElementById('notificationBubble'); const notificationTitle = document.getElementById('notificationTitle'); const notificationSubtitle = document.getElementById('notificationSubtitle'); // Format the time data const playerName = timeData.player_name || (currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player'); const locationName = timeData.location_name || (currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location'); const timeString = timeData.recorded_time || '--:--'; // Update notification content const newTimeText = currentLanguage === 'de' ? 'Neue Zeit von' : 'New time from'; notificationTitle.textContent = `🏁 ${newTimeText} ${playerName}!`; notificationSubtitle.textContent = `${timeString} • ${locationName}`; // Show notification notificationBubble.classList.remove('hide'); notificationBubble.classList.add('show'); // Auto-hide after 5 seconds setTimeout(() => { hideNotification(); }, 5000); } function hideNotification() { const notificationBubble = document.getElementById('notificationBubble'); notificationBubble.classList.remove('show'); notificationBubble.classList.add('hide'); // Remove hide class after animation setTimeout(() => { notificationBubble.classList.remove('hide'); }, 300); } // Check authentication status async function checkAuth() { try { const { data: { session } } = await supabase.auth.getSession(); if (session) { // User is logged in, show dashboard button document.getElementById('adminLoginBtn').style.display = 'none'; document.getElementById('dashboardBtn').style.display = 'inline-block'; document.getElementById('logoutBtn').style.display = 'inline-block'; } else { // User is not logged in, show admin login button document.getElementById('adminLoginBtn').style.display = 'inline-block'; document.getElementById('dashboardBtn').style.display = 'none'; document.getElementById('logoutBtn').style.display = 'none'; } } catch (error) { console.error('Error checking auth:', error); // Fallback: show login button if auth check fails document.getElementById('adminLoginBtn').style.display = 'inline-block'; document.getElementById('dashboardBtn').style.display = 'none'; document.getElementById('logoutBtn').style.display = 'none'; } } // Logout function async function logout() { try { const { error } = await supabase.auth.signOut(); if (error) { console.error('Error logging out:', error); } else { window.location.reload(); } } catch (error) { console.error('Error during logout:', error); window.location.reload(); } } // Load locations from database async function loadLocations() { try { const response = await fetch('/api/v1/public/locations'); if (!response.ok) { throw new Error('Failed to fetch locations'); } const responseData = await response.json(); const locations = responseData.data || responseData; // Handle both formats const locationSelect = document.getElementById('locationSelect'); // Store locations globally for distance calculations locationsData = locations; // Clear existing options and set default placeholder const placeholderText = currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location'; locationSelect.innerHTML = ``; // Add locations from database locations.forEach(location => { const option = document.createElement('option'); option.value = location.name; option.textContent = `📍 ${location.name}`; locationSelect.appendChild(option); }); // Load and set last selected location const lastLocation = loadLastSelectedLocation(); if (lastLocation) { // Find the option that matches the last location name const matchingOption = Array.from(locationSelect.options).find(option => option.textContent === `📍 ${lastLocation.name}` || option.value === lastLocation.name ); if (matchingOption) { locationSelect.value = matchingOption.value; console.log('📍 Last selected location restored:', lastLocation.name); // Update the current selection display updateCurrentSelection(); // Load data for the restored location loadData(); } } } catch (error) { console.error('Error loading locations:', error); } } // Calculate distance between two points using Haversine formula function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // Earth's radius in kilometers const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; // Distance in kilometers return distance; } function toRadians(degrees) { return degrees * (Math.PI / 180); } // Find nearest location based on user's current position async function findNearestLocation() { const btn = document.getElementById('findLocationBtn'); const locationSelect = document.getElementById('locationSelect'); // Check if geolocation is supported if (!navigator.geolocation) { const errorMsg = currentLanguage === 'de' ? 'Geolocation wird von diesem Browser nicht unterstützt.' : 'Geolocation is not supported by this browser.'; showLocationError(errorMsg); return; } // Update button state to loading btn.disabled = true; btn.classList.add('loading'); btn.textContent = currentLanguage === 'de' ? '🔍 Suche...' : '🔍 Searching...'; try { // Get user's current position const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( resolve, reject, { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 // 5 minutes } ); }); const userLat = position.coords.latitude; const userLon = position.coords.longitude; // Calculate distances to all locations const locationsWithDistance = locationsData.map(location => ({ ...location, distance: calculateDistance( userLat, userLon, parseFloat(location.latitude), parseFloat(location.longitude) ) })); // Find the nearest location const nearestLocation = locationsWithDistance.reduce((nearest, current) => { return current.distance < nearest.distance ? current : nearest; }); // Select the nearest location in the dropdown locationSelect.value = nearestLocation.name; // Trigger change event to update the leaderboard locationSelect.dispatchEvent(new Event('change')); // Show success notification showLocationSuccess(nearestLocation.name, nearestLocation.distance); } catch (error) { console.error('Error getting location:', error); let errorMessage = currentLanguage === 'de' ? 'Standort konnte nicht ermittelt werden.' : 'Location could not be determined.'; if (error.code) { switch (error.code) { case error.PERMISSION_DENIED: errorMessage = currentLanguage === 'de' ? 'Standortzugriff wurde verweigert. Bitte erlaube den Standortzugriff in den Browser-Einstellungen.' : 'Location access was denied. Please allow location access in browser settings.'; break; case error.POSITION_UNAVAILABLE: errorMessage = currentLanguage === 'de' ? 'Standortinformationen sind nicht verfügbar.' : 'Location information is not available.'; break; case error.TIMEOUT: errorMessage = currentLanguage === 'de' ? 'Zeitüberschreitung beim Abrufen des Standorts.' : 'Timeout while retrieving location.'; break; } } showLocationError(errorMessage); } finally { // Reset button state btn.disabled = false; btn.classList.remove('loading'); btn.textContent = currentLanguage === 'de' ? '📍 Mein Standort' : '📍 My Location'; } } // Show success notification for location finding function showLocationSuccess(locationName, distance) { const notificationBubble = document.getElementById('notificationBubble'); const notificationTitle = document.getElementById('notificationTitle'); const notificationSubtitle = document.getElementById('notificationSubtitle'); // Update notification content const locationFoundText = currentLanguage === 'de' ? 'Standort gefunden!' : 'Location found!'; const distanceText = currentLanguage === 'de' ? 'km entfernt' : 'km away'; notificationTitle.textContent = `📍 ${locationFoundText}`; notificationSubtitle.textContent = `${locationName} (${distance.toFixed(1)} ${distanceText})`; // Show notification notificationBubble.classList.remove('hide'); notificationBubble.classList.add('show'); // Auto-hide after 4 seconds setTimeout(() => { hideNotification(); }, 4000); } // Show error notification for location finding function showLocationError(message) { const notificationBubble = document.getElementById('notificationBubble'); const notificationTitle = document.getElementById('notificationTitle'); const notificationSubtitle = document.getElementById('notificationSubtitle'); // Change notification style to error notificationBubble.style.background = 'linear-gradient(135deg, #dc3545, #c82333)'; // Update notification content const errorText = currentLanguage === 'de' ? 'Fehler' : 'Error'; notificationTitle.textContent = `❌ ${errorText}`; notificationSubtitle.textContent = message; // Show notification notificationBubble.classList.remove('hide'); notificationBubble.classList.add('show'); // Auto-hide after 6 seconds setTimeout(() => { hideNotification(); // Reset notification style notificationBubble.style.background = 'linear-gradient(135deg, #00d4ff, #0891b2)'; }, 6000); } // Show prompt when no location is selected function showLocationSelectionPrompt() { const rankingList = document.getElementById('rankingList'); const emptyTitle = currentLanguage === 'de' ? 'Standort auswählen' : 'Select Location'; const emptyDescription = currentLanguage === 'de' ? 'Bitte wähle einen Standort aus dem Dropdown-Menü aus
oder nutze den "📍 Mein Standort" Button, um automatisch
den nächstgelegenen Standort zu finden.' : 'Please select a location from the dropdown menu
or use the "📍 My Location" button to automatically
find the nearest location.'; rankingList.innerHTML = `
📍
${emptyTitle}
${emptyDescription}
`; // Reset stats to show no data document.getElementById('totalPlayers').textContent = '0'; document.getElementById('bestTime').textContent = '--:--'; document.getElementById('totalRecords').textContent = '0'; // Update current selection display updateCurrentSelection(); } // Load data from local database via MCP async function loadData() { try { const location = document.getElementById('locationSelect').value; const period = document.querySelector('.time-tab.active').dataset.period; // Don't load data if no location is selected if (!location || location === '') { showLocationSelectionPrompt(); return; } // Build query parameters const params = new URLSearchParams(); if (location && location !== 'all') { params.append('location', location); } if (period && period !== 'all') { params.append('period', period); } // Fetch times with player and location data from local database const response = await fetch(`/api/v1/public/times-with-details?${params.toString()}`); if (!response.ok) { throw new Error('Failed to fetch times'); } const times = await response.json(); // Convert to the format expected by the leaderboard const leaderboardData = times.map(time => { const { minutes, seconds, milliseconds } = time.recorded_time; const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`; const playerName = time.player ? `${time.player.firstname} ${time.player.lastname}` : (currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player'); const locationName = time.location ? time.location.name : (currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location'); const date = new Date(time.created_at).toISOString().split('T')[0]; return { name: playerName, time: timeString, date: date, location: locationName }; }); // Sort by time (fastest first) leaderboardData.sort((a, b) => { const timeA = timeToSeconds(a.time); const timeB = timeToSeconds(b.time); return timeA - timeB; }); updateLeaderboard(leaderboardData); updateStats(leaderboardData); updateCurrentSelection(); } catch (error) { console.error('Error loading data:', error); // Fallback to sample data if API fails loadSampleData(); } } // Fallback sample data based on real database data function loadSampleData() { const sampleData = [ { name: "Carsten Graf", time: "01:28.945", date: "2025-08-30", location: "Ulm Donaubad" }, { name: "Carsten Graf", time: "01:30.945", date: "2025-08-30", location: "Ulm Donaubad" }, { name: "Max Mustermann", time: "01:50.945", date: "2025-08-30", location: "Ulm Donaubad" }, { name: "Carsten Graf", time: "02:50.945", date: "2025-08-31", location: "Test" }, { name: "Max Mustermann", time: "02:50.945", date: "2025-08-31", location: "Test" }, { name: "Carsten Graf", time: "01:10.945", date: "2025-09-02", location: "Test" }, { name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Test" }, { name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Ulm Donaubad" } ]; updateLeaderboard(sampleData); updateStats(sampleData); updateCurrentSelection(); } function timeToSeconds(timeStr) { const [minutes, seconds] = timeStr.split(':'); return parseFloat(minutes) * 60 + parseFloat(seconds); } function updateStats(data) { const totalPlayers = new Set(data.map(item => item.name)).size; const bestTime = data.length > 0 ? data[0].time : '--:--'; const totalRecords = data.length; document.getElementById('totalPlayers').textContent = totalPlayers; document.getElementById('bestTime').textContent = bestTime; document.getElementById('totalRecords').textContent = totalRecords; } function updateCurrentSelection() { const location = document.getElementById('locationSelect').value; const period = document.querySelector('.time-tab.active').dataset.period; // Get the display text from the selected option const locationSelect = document.getElementById('locationSelect'); const selectedLocationOption = locationSelect.options[locationSelect.selectedIndex]; const locationDisplay = selectedLocationOption ? selectedLocationOption.textContent : (currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location'); const periodIcons = currentLanguage === 'de' ? { 'today': '📅 Heute', 'week': '📊 Diese Woche', 'month': '📈 Dieser Monat', 'all': '♾️ Alle Zeiten' } : { 'today': '📅 Today', 'week': '📊 This Week', 'month': '📈 This Month', 'all': '♾️ All Times' }; document.getElementById('currentSelection').textContent = `${locationDisplay} • ${periodIcons[period]}`; const lastSyncText = currentLanguage === 'de' ? 'Letzter Sync' : 'Last Sync'; document.getElementById('lastUpdated').textContent = `${lastSyncText}: ${new Date().toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}`; } function updateLeaderboard(data) { const rankingList = document.getElementById('rankingList'); if (data.length === 0) { const emptyTitle = currentLanguage === 'de' ? 'Keine Rekorde gefunden' : 'No records found'; const emptyDescription = currentLanguage === 'de' ? 'Für diese Filtereinstellungen liegen noch keine Zeiten vor.
Versuche es mit einem anderen Zeitraum oder Standort.' : 'No times available for these filter settings.
Try a different time period or location.'; rankingList.innerHTML = `
🏁
${emptyTitle}
${emptyDescription}
`; return; } rankingList.innerHTML = data.map((player, index) => { const rank = index + 1; let positionClass = ''; let trophy = ''; if (rank === 1) { positionClass = 'gold'; trophy = '👑'; } else if (rank === 2) { positionClass = 'silver'; trophy = '🥈'; } else if (rank === 3) { positionClass = 'bronze'; trophy = '🥉'; } else if (rank <= 10) { trophy = '⭐'; } const formatDate = new Date(player.date).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US', { day: '2-digit', month: 'short' }); return `
#${rank}
${player.name}
${player.location} 🗓️ ${formatDate}
${player.time}
${trophy ? `
${trophy}
` : '
'}
`; }).join(''); } // Event Listeners Setup function setupEventListeners() { // Location select event listener document.getElementById('locationSelect').addEventListener('change', function () { // Save location selection to cookie const selectedOption = this.options[this.selectedIndex]; if (selectedOption.value) { saveLocationSelection(selectedOption.value, selectedOption.textContent); } // Load data loadData(); }); // Time tab event listeners document.querySelectorAll('.time-tab').forEach(tab => { tab.addEventListener('click', function () { // Remove active class from all tabs document.querySelectorAll('.time-tab').forEach(t => t.classList.remove('active')); // Add active class to clicked tab this.classList.add('active'); // Load data with new period loadData(); }); }); // Smooth scroll for better UX const rankingsList = document.querySelector('.rankings-list'); if (rankingsList) { rankingsList.style.scrollBehavior = 'smooth'; } } // Initialize page async function init() { await checkAuth(); await loadLocations(); showLocationSelectionPrompt(); // Show prompt instead of loading data initially setupEventListeners(); } // Auto-refresh function function startAutoRefresh() { setInterval(loadData, 45000); } // Language Management let currentLanguage = 'en'; // Default to English // Translation function function translateElement(element, language) { if (element.dataset[language]) { // Check if the content contains HTML tags if (element.dataset[language].includes('<')) { element.innerHTML = element.dataset[language]; } else { element.textContent = element.dataset[language]; } } } // Change language function function changeLanguage() { const languageSelect = document.getElementById('languageSelect'); currentLanguage = languageSelect.value; // Save language preference localStorage.setItem('ninjacross_language', currentLanguage); // Update flag in select updateLanguageFlag(); // Translate all elements with data attributes const elementsToTranslate = document.querySelectorAll('[data-de][data-en]'); elementsToTranslate.forEach(element => { translateElement(element, currentLanguage); }); // Update dynamic content updateDynamicContent(); console.log(`🌐 Language changed to: ${currentLanguage}`); } // Update flag in language selector function updateLanguageFlag() { const languageSelect = document.getElementById('languageSelect'); if (languageSelect) { if (currentLanguage === 'de') { // German flag (black-red-gold) languageSelect.style.backgroundImage = `url('data:image/svg+xml,')`; } else { // USA flag languageSelect.style.backgroundImage = `url('data:image/svg+xml,')`; } } } // Update dynamic content that's not in HTML function updateDynamicContent() { // Update location select placeholder const locationSelect = document.getElementById('locationSelect'); if (locationSelect && locationSelect.options[0]) { locationSelect.options[0].textContent = currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location'; } // Update find location button const findLocationBtn = document.getElementById('findLocationBtn'); if (findLocationBtn) { findLocationBtn.textContent = currentLanguage === 'de' ? '📍 Mein Standort' : '📍 My Location'; findLocationBtn.title = currentLanguage === 'de' ? 'Nächstgelegenen Standort finden' : 'Find nearest location'; } // Update refresh button const refreshBtn = document.querySelector('.refresh-btn'); if (refreshBtn) { refreshBtn.textContent = currentLanguage === 'de' ? '⚡ Live Update' : '⚡ Live Update'; } // Update notification elements const notificationTitle = document.getElementById('notificationTitle'); const notificationSubtitle = document.getElementById('notificationSubtitle'); if (notificationTitle) { notificationTitle.textContent = currentLanguage === 'de' ? 'Neue Zeit!' : 'New Time!'; } if (notificationSubtitle) { notificationSubtitle.textContent = currentLanguage === 'de' ? 'Ein neuer Rekord wurde erstellt' : 'A new record has been created'; } // Update current selection display updateCurrentSelection(); // Reload data to update any dynamic content if (document.getElementById('locationSelect').value) { loadData(); } else { showLocationSelectionPrompt(); } } // Load saved language preference function loadLanguagePreference() { const savedLanguage = localStorage.getItem('ninjacross_language'); if (savedLanguage && (savedLanguage === 'de' || savedLanguage === 'en')) { currentLanguage = savedLanguage; const languageSelect = document.getElementById('languageSelect'); if (languageSelect) { languageSelect.value = currentLanguage; // Update flag when loading updateLanguageFlag(); } } } // Start the application when DOM is loaded document.addEventListener('DOMContentLoaded', function () { loadLanguagePreference(); changeLanguage(); // Apply saved language init(); startAutoRefresh(); // Add cookie settings button functionality const cookieSettingsBtn = document.getElementById('cookie-settings-footer'); if (cookieSettingsBtn) { cookieSettingsBtn.addEventListener('click', function () { if (window.cookieConsent) { window.cookieConsent.resetConsent(); } }); } });