// 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); // Global variables let currentUser = null; let currentLanguage = 'en'; // Default to English // Check authentication and load dashboard async function initDashboard() { try { // Get current session const { data: { session }, error } = await supabase.auth.getSession(); if (error) { console.error('Error checking authentication:', error); // Temporarily show dashboard for testing currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' }; displayUserInfo({ email: 'admin@speedrun-arena.com' }); showDashboard(); // Check times section checkLinkStatusAndLoadTimes(); return; } if (!session) { // No session, redirect to login window.location.href = '/login'; return; } // User is authenticated, show dashboard if (session.user) { currentUser = session.user; displayUserInfo(session.user); } else { // Fallback if no user data currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' }; displayUserInfo({ email: 'admin@speedrun-arena.com' }); } showDashboard(); // Load times section checkLinkStatusAndLoadTimes(); // Update analytics and statistics cards updateAnalyticsAndStatisticsCards(); } catch (error) { console.error('An unexpected error occurred:', error); // window.location.href = '/login'; } } // Display user information function displayUserInfo(user) { const userEmail = document.getElementById('userEmail'); const userAvatar = document.getElementById('userAvatar'); userEmail.textContent = user.email; userAvatar.textContent = user.email.charAt(0).toUpperCase(); } // Show dashboard content function showDashboard() { document.getElementById('loading').style.display = 'none'; document.getElementById('dashboardContent').style.display = 'block'; } // Logout function async function logout() { try { const { error } = await supabase.auth.signOut(); if (error) { console.error('Error logging out:', error); } else { window.location.href = '/'; } } catch (error) { console.error('Error during logout:', error); } } // Listen for auth state changes supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_OUT' || !session) { window.location.href = '/login'; } }); // Language Management 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 achievement notifications updateAchievementNotifications(); // Update time display formats updateTimeDisplayFormats(); // Update achievement progress text updateAchievementProgressText(); // Update analytics and statistics cards updateAnalyticsAndStatisticsCards(); // Reload achievements if they're loaded if (window.allAchievements && window.allAchievements.length > 0) { displayAchievements(); } // Reload times if they're loaded if (document.getElementById('timesDisplay').style.display !== 'none') { // Times are displayed, reload them checkLinkStatusAndLoadTimes(); } } // 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(); } } } // Update analytics and statistics cards based on link status function updateAnalyticsAndStatisticsCards() { const analyticsCard = document.getElementById('analyticsCard'); const statisticsCard = document.getElementById('statisticsCard'); if (!currentPlayerId) { // User not linked - show appropriate message if (analyticsCard) { const isGerman = currentLanguage === 'de'; const message = isGerman ? 'RFID verknüpfen erforderlich' : 'RFID linking required'; const description = isGerman ? 'Verknüpfe deine RFID-Karte, um Analytics zu sehen.' : 'Link your RFID card to view analytics.'; analyticsCard.innerHTML = `

📊 Analytics

⚠️ ${message}
${description}
`; } if (statisticsCard) { const isGerman = currentLanguage === 'de'; const message = isGerman ? 'RFID verknüpfen erforderlich' : 'RFID linking required'; const description = isGerman ? 'Verknüpfe deine RFID-Karte, um Statistiken zu sehen.' : 'Link your RFID card to view statistics.'; statisticsCard.innerHTML = `

📊 Statistiken

⚠️ ${message}
${description}
`; } } else { // User is linked - restore original cards if (analyticsCard) { const isGerman = currentLanguage === 'de'; analyticsCard.innerHTML = `

📊 Analytics

${isGerman ? 'Verfolge deine Leistung und überwache wichtige Metriken.' : 'Track your performance and monitor important metrics.'}

`; } if (statisticsCard) { const isGerman = currentLanguage === 'de'; statisticsCard.innerHTML = `

📊 Statistiken

${isGerman ? 'Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.' : 'Detailed statistics about your runs - best times, improvements and comparisons.'}

`; } } } // Update achievement notifications function updateAchievementNotifications() { // This will be called when achievements are displayed } // Update time display formats function updateTimeDisplayFormats() { // This will be called when times are displayed } // Update achievement progress text function updateAchievementProgressText() { // This will be called when achievements are displayed } // Achievement translations are now handled by the database // Translate achievement data using database translations function translateAchievement(achievement) { if (currentLanguage === 'en' && achievement.name_en) { return { ...achievement, name: achievement.name_en, description: achievement.description_en || achievement.description }; } return achievement; } // Initialize dashboard when page loads document.addEventListener('DOMContentLoaded', function () { loadLanguagePreference(); changeLanguage(); // Apply saved language initDashboard(); // Push notifications are now handled manually via the button // No automatic subscription on page load }); // Modal functions function openModal(modalId) { document.getElementById(modalId).style.display = 'block'; } function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; // Reset modal state if (modalId === 'rfidModal') { stopQRScanner(); document.getElementById('manualRfidInput').value = ''; } } // Close modal when clicking outside window.onclick = function (event) { if (event.target.classList.contains('modal')) { closeModal(event.target.id); } } // QR Scanner variables let qrStream = null; let qrScanning = false; // Show RFID Settings async function showRFIDSettings() { openModal('rfidModal'); // Reset scanner state stopQRScanner(); // Check if user is already linked and hide/show create player section await updateCreatePlayerSectionVisibility(); } // Update visibility of create player section based on link status async function updateCreatePlayerSectionVisibility() { const createPlayerSection = document.getElementById('createPlayerSection'); if (!currentUser) { // No user logged in - hide create player section if (createPlayerSection) { createPlayerSection.style.display = 'none'; } return; } try { // Check if user has a linked player const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`); if (response.ok) { const result = await response.json(); if (result.success && result.data && result.data.id) { // User is already linked - hide create player section if (createPlayerSection) { createPlayerSection.style.display = 'none'; } } else { // User is not linked - show create player section if (createPlayerSection) { createPlayerSection.style.display = 'block'; } } } else { // Error checking link status - show create player section as fallback if (createPlayerSection) { createPlayerSection.style.display = 'block'; } } } catch (error) { console.error('Error checking link status:', error); // Error occurred - show create player section as fallback if (createPlayerSection) { createPlayerSection.style.display = 'block'; } } } // Check link status and load times async function checkLinkStatusAndLoadTimes() { if (!currentUser) { showTimesNotLinked(); return; } try { // Check if user has a linked player const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`); if (response.ok) { const result = await response.json(); // User is linked, load times await loadUserTimesSection(result.data); } else { // User is not linked showTimesNotLinked(); } } catch (error) { console.error('Error checking link status:', error); showTimesNotLinked(); } } // Start QR Scanner async function startQRScanner() { try { // Request camera access qrStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', // Use back camera if available width: { ideal: 1280 }, height: { ideal: 720 } } }); const video = document.getElementById('qrVideo'); const canvas = document.getElementById('qrCanvas'); const context = canvas.getContext('2d'); video.srcObject = qrStream; video.play(); // Show camera container and update buttons document.getElementById('cameraContainer').style.display = 'block'; document.getElementById('startScanBtn').style.display = 'none'; document.getElementById('stopScanBtn').style.display = 'inline-block'; document.getElementById('scanningStatus').style.display = 'block'; qrScanning = true; // Start scanning loop video.addEventListener('loadedmetadata', () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; scanQRCode(); }); } catch (error) { console.error('Error accessing camera:', error); const cameraErrorMsg = currentLanguage === 'de' ? 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.' : 'Camera access failed. Please use manual input.'; showMessage('rfidMessage', cameraErrorMsg, 'error'); } } // Stop QR Scanner function stopQRScanner() { qrScanning = false; if (qrStream) { qrStream.getTracks().forEach(track => track.stop()); qrStream = null; } // Reset UI document.getElementById('cameraContainer').style.display = 'none'; document.getElementById('startScanBtn').style.display = 'inline-block'; document.getElementById('stopScanBtn').style.display = 'none'; document.getElementById('scanningStatus').style.display = 'none'; } // Scan QR Code from video stream function scanQRCode() { if (!qrScanning) return; const video = document.getElementById('qrVideo'); const canvas = document.getElementById('qrCanvas'); const context = canvas.getContext('2d'); if (video.readyState === video.HAVE_ENOUGH_DATA) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { console.log('QR Code detected:', code.data); handleQRCodeDetected(code.data); return; } } // Continue scanning if (qrScanning) { requestAnimationFrame(scanQRCode); } } // Format RFID UID to match database format function formatRfidUid(rawUid) { // Remove any existing formatting (spaces, colons, etc.) let cleanUid = rawUid.replace(/[^a-fA-F0-9]/g, '').toUpperCase(); // Handle different UID lengths if (cleanUid.length === 6) { // Pad 6-digit UID to 8 digits by adding leading zeros cleanUid = '00' + cleanUid; } else if (cleanUid.length === 8) { // Already correct length } else if (cleanUid.length < 6) { // Pad shorter UIDs to 8 digits cleanUid = cleanUid.padStart(8, '0'); } else { throw new Error(`Ungültige RFID UID Länge: ${cleanUid.length} Zeichen (unterstützt: 6-8)`); } // Format as XX:XX:XX:XX return cleanUid.match(/.{2}/g).join(':'); } // Handle detected QR code async function handleQRCodeDetected(qrData) { stopQRScanner(); try { // Extract and format RFID UID from QR code const rawUid = qrData.trim(); if (!rawUid) { const qrErrorMsg = currentLanguage === 'de' ? 'QR-Code enthält keine gültige RFID UID' : 'QR code contains no valid RFID UID'; showMessage('rfidMessage', qrErrorMsg, 'error'); return; } // Format the UID to match database format (XX:XX:XX:XX) const formattedUid = formatRfidUid(rawUid); const qrDetectedMsg = currentLanguage === 'de' ? `QR-Code erkannt: ${rawUid} → ${formattedUid}` : `QR code detected: ${rawUid} → ${formattedUid}`; showMessage('rfidMessage', qrDetectedMsg, 'info'); // Link the user using the formatted RFID UID await linkUserByRfidUid(formattedUid); } catch (error) { console.error('Error formatting RFID UID:', error); const formatErrorMsg = currentLanguage === 'de' ? `Fehler beim Formatieren der RFID UID: ${error.message}` : `Error formatting RFID UID: ${error.message}`; showMessage('rfidMessage', formatErrorMsg, 'error'); } } // Manual RFID linking async function linkManualRfid() { const rawUid = document.getElementById('manualRfidInput').value.trim(); if (!rawUid) { const inputErrorMsg = currentLanguage === 'de' ? 'Bitte gib eine RFID UID ein' : 'Please enter a RFID UID'; showMessage('rfidMessage', inputErrorMsg, 'error'); return; } try { // Format the UID to match database format const formattedUid = formatRfidUid(rawUid); const formattedMsg = currentLanguage === 'de' ? `Formatiert: ${rawUid} → ${formattedUid}` : `Formatted: ${rawUid} → ${formattedUid}`; showMessage('rfidMessage', formattedMsg, 'info'); await linkUserByRfidUid(formattedUid); } catch (error) { console.error('Error formatting manual RFID UID:', error); const formatErrorMsg = currentLanguage === 'de' ? `Fehler beim Formatieren: ${error.message}` : `Error formatting: ${error.message}`; showMessage('rfidMessage', formatErrorMsg, 'error'); } } // Create new RFID player record async function createRfidPlayerRecord() { const rawUid = document.getElementById('manualRfidInput').value.trim(); const firstname = document.getElementById('playerFirstname').value.trim(); const lastname = document.getElementById('playerLastname').value.trim(); const birthdate = document.getElementById('playerBirthdate').value; // Validation if (!rawUid) { const inputErrorMsg = currentLanguage === 'de' ? 'Bitte gib eine RFID UID ein' : 'Please enter a RFID UID'; showMessage('rfidMessage', inputErrorMsg, 'error'); return; } if (!firstname) { const inputErrorMsg = currentLanguage === 'de' ? 'Bitte gib einen Vornamen ein' : 'Please enter a first name'; showMessage('rfidMessage', inputErrorMsg, 'error'); return; } if (!lastname) { const inputErrorMsg = currentLanguage === 'de' ? 'Bitte gib einen Nachnamen ein' : 'Please enter a last name'; showMessage('rfidMessage', inputErrorMsg, 'error'); return; } if (!birthdate) { const inputErrorMsg = currentLanguage === 'de' ? 'Bitte gib ein Geburtsdatum ein' : 'Please enter a birth date'; showMessage('rfidMessage', inputErrorMsg, 'error'); return; } try { // Format the UID to match database format const formattedUid = formatRfidUid(rawUid); const formattedMsg = currentLanguage === 'de' ? `Erstelle Spieler: ${firstname} ${lastname} (${formattedUid})` : `Creating player: ${firstname} ${lastname} (${formattedUid})`; showMessage('rfidMessage', formattedMsg, 'info'); // Create player record const response = await fetch('/api/v1/public/players/create-with-rfid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rfiduid: formattedUid, firstname: firstname, lastname: lastname, birthdate: birthdate, supabase_user_id: currentUser?.id || null }) }); const result = await response.json(); if (result.success) { const successMsg = currentLanguage === 'de' ? `Spieler erfolgreich erstellt: ${result.data.firstname} ${result.data.lastname}` : `Player successfully created: ${result.data.firstname} ${result.data.lastname}`; showMessage('rfidMessage', successMsg, 'success'); // Clear form document.getElementById('manualRfidInput').value = ''; document.getElementById('playerFirstname').value = ''; document.getElementById('playerLastname').value = ''; document.getElementById('playerBirthdate').value = ''; // Hide create player section since user is now linked const createPlayerSection = document.getElementById('createPlayerSection'); if (createPlayerSection) { createPlayerSection.style.display = 'none'; } // Refresh times if user is linked if (currentUser) { await checkLinkStatusAndLoadTimes(); } } else { let errorMsg = result.message; // Erweitere Fehlermeldung um Levenshtein-Details falls vorhanden if (result.details) { if (result.details.matchType === 'similar') { const similarityPercent = Math.round((1 - result.details.similarity) * 100); errorMsg += `\n\nÄhnlichkeit: ${similarityPercent}% (Distanz: ${result.details.levenshteinDistance})`; } } showMessage('rfidMessage', errorMsg, 'error'); } } catch (error) { console.error('Error creating RFID player record:', error); const errorMsg = currentLanguage === 'de' ? `Fehler beim Erstellen: ${error.message}` : `Error creating: ${error.message}`; showMessage('rfidMessage', errorMsg, 'error'); } } // Link user by RFID UID (core function) async function linkUserByRfidUid(rfidUid) { if (!currentUser) { const authErrorMsg = currentLanguage === 'de' ? 'Benutzer nicht authentifiziert' : 'User not authenticated'; showMessage('rfidMessage', authErrorMsg, 'error'); return; } try { // First, find the player with this RFID UID const response = await fetch('/api/v1/public/link-by-rfid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rfiduid: rfidUid, supabase_user_id: currentUser.id }) }); const result = await response.json(); if (response.ok) { const successMsg = currentLanguage === 'de' ? `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}` : `✅ RFID successfully linked!\nPlayer: ${result.data.firstname} ${result.data.lastname}`; showMessage('rfidMessage', successMsg, 'success'); setTimeout(() => { closeModal('rfidModal'); // Reload times section after successful linking checkLinkStatusAndLoadTimes(); }, 2000); } else { const errorMsg = currentLanguage === 'de' ? result.message || 'Fehler beim Verknüpfen' : result.message || 'Error linking'; showMessage('rfidMessage', errorMsg, 'error'); } } catch (error) { console.error('Error linking RFID:', error); const linkErrorMsg = currentLanguage === 'de' ? 'Fehler beim Verknüpfen der RFID' : 'Error linking RFID'; showMessage('rfidMessage', linkErrorMsg, 'error'); } } // Show not linked state function showTimesNotLinked() { document.getElementById('timesLoading').style.display = 'none'; document.getElementById('timesNotLinked').style.display = 'block'; document.getElementById('timesDisplay').style.display = 'none'; // Update the text content for the not linked state const notLinkedTitle = document.querySelector('#timesNotLinked h3'); const notLinkedDescription = document.querySelector('#timesNotLinked p'); const notLinkedButton = document.querySelector('#timesNotLinked button'); const notLinkedSteps = document.querySelectorAll('#timesNotLinked li'); if (notLinkedTitle) { notLinkedTitle.textContent = currentLanguage === 'de' ? 'RFID noch nicht verknüpft' : 'RFID not linked yet'; } if (notLinkedDescription) { notLinkedDescription.textContent = currentLanguage === 'de' ? 'Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen.' : 'To see your personal times, you must first link your RFID card with your account.'; } if (notLinkedButton) { notLinkedButton.textContent = currentLanguage === 'de' ? '🏷️ RFID jetzt verknüpfen' : '🏷️ Link RFID now'; } if (notLinkedSteps.length >= 3) { notLinkedSteps[0].textContent = currentLanguage === 'de' ? 'Klicke auf "RFID jetzt verknüpfen"' : 'Click on "Link RFID now"'; notLinkedSteps[1].textContent = currentLanguage === 'de' ? 'Scanne den QR-Code auf deiner RFID-Karte' : 'Scan the QR code on your RFID card'; notLinkedSteps[2].textContent = currentLanguage === 'de' ? 'Fertig! Deine Zeiten werden automatisch hier angezeigt' : 'Done! Your times will be displayed here automatically'; } // Update the "So funktioniert's" title const howItWorksTitle = document.querySelector('#timesNotLinked h4'); if (howItWorksTitle) { howItWorksTitle.textContent = currentLanguage === 'de' ? 'So funktioniert\'s:' : 'How it works:'; } } // Show RFID linked info with help button function showRFIDLinkedInfo(playerData) { // Find the RFID card and update it const rfidCard = document.querySelector('.card[onclick="showRFIDSettings()"]'); if (rfidCard) { const isGerman = currentLanguage === 'de'; rfidCard.innerHTML = `

${isGerman ? '🏷️ RFID Verknüpft' : '🏷️ RFID Linked'}

${isGerman ? 'Erfolgreich verknüpft' : 'Successfully linked'}
${isGerman ? 'Spieler:' : 'Player:'} ${playerData.firstname} ${playerData.lastname}
RFID: ${playerData.rfiduid}
`; // Remove the onclick from the card since we don't want it to open the modal rfidCard.removeAttribute('onclick'); rfidCard.style.cursor = 'default'; } } // Request RFID help function requestRFIDHelp() { const isGerman = currentLanguage === 'de'; const message = isGerman ? 'Hilfe-Anfrage gesendet! Ein Administrator wird sich bei dir melden, um bei der RFID-Änderung zu helfen.' : 'Help request sent! An administrator will contact you to help with the RFID change.'; alert(message); // Here you could send a notification to admins or log the help request console.log('RFID help requested by user:', currentUser?.email); } // Show loading state function showTimesLoading() { document.getElementById('timesLoading').style.display = 'block'; document.getElementById('timesNotLinked').style.display = 'none'; document.getElementById('timesDisplay').style.display = 'none'; // Update the loading text const loadingText = document.querySelector('#timesLoading p'); if (loadingText) { loadingText.textContent = currentLanguage === 'de' ? 'Lade deine Zeiten...' : 'Loading your times...'; } } // Load user times for the section async function loadUserTimesSection(playerData) { showTimesLoading(); try { const response = await fetch(`/api/v1/public/user-times/${currentUser.id}?t=${Date.now()}`); const result = await response.json(); if (!response.ok) { throw new Error(result.message || 'Failed to load times'); } const times = result.data || result; // Update stats updateTimesStats(times, playerData); // Display times displayUserTimes(times); // Show RFID info and help button showRFIDLinkedInfo(playerData); // Show the times display document.getElementById('timesLoading').style.display = 'none'; document.getElementById('timesNotLinked').style.display = 'none'; document.getElementById('timesDisplay').style.display = 'block'; // Initialize achievements for this player initializeAchievements(playerData.id); // Update analytics and statistics cards (user is now linked) updateAnalyticsAndStatisticsCards(); } catch (error) { console.error('Error loading user times:', error); showTimesNotLinked(); } } // Update stats cards function updateTimesStats(times, playerData) { // Total runs document.getElementById('totalRuns').textContent = times.length; // Best time if (times.length > 0) { const bestTimeValue = times.reduce((best, current) => { const currentSeconds = convertTimeToSeconds(current.recorded_time); const bestSeconds = convertTimeToSeconds(best.recorded_time); return currentSeconds < bestSeconds ? current : best; }); document.getElementById('bestTime').textContent = formatTime(bestTimeValue.recorded_time); } else { document.getElementById('bestTime').textContent = '--:--'; } // Unique locations count const uniqueLocations = [...new Set(times.map(time => time.location_name))]; document.getElementById('locationsCount').textContent = uniqueLocations.length; // Linked player name document.getElementById('linkedPlayer').textContent = `${playerData.firstname} ${playerData.lastname}`; } // Display user times in grid function displayUserTimes(times) { const timesGrid = document.getElementById('userTimesGrid'); if (times.length === 0) { const noTimesTitle = currentLanguage === 'de' ? 'Noch keine Zeiten aufgezeichnet' : 'No times recorded yet'; const noTimesDescription = currentLanguage === 'de' ? 'Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!' : 'Your first runs will be displayed here as soon as you complete them!'; timesGrid.innerHTML = `

${noTimesTitle}

${noTimesDescription}

`; return; } // Group times by location const timesByLocation = times.reduce((acc, time) => { if (!acc[time.location_name]) { acc[time.location_name] = []; } acc[time.location_name].push(time); return acc; }, {}); // Generate cards for each location const cards = Object.entries(timesByLocation).map(([locationName, locationTimes], index) => { // Sort times by performance (best first) const sortedTimes = locationTimes.sort((a, b) => { return convertTimeToSeconds(a.recorded_time) - convertTimeToSeconds(b.recorded_time); }); // Get best time for this location const bestTime = sortedTimes[0]; // Generate all runs for expanded view const allRunsHtml = sortedTimes.map((run, runIndex) => { let rankBadge = ''; let rankClass = ''; if (runIndex === 0) { rankBadge = currentLanguage === 'de' ? '🥇 Beste' : '🥇 Best'; rankClass = 'best'; } else if (runIndex === 1) { rankBadge = '🥈 2.'; rankClass = 'second'; } else if (runIndex === 2) { rankBadge = '🥉 3.'; rankClass = 'third'; } else { rankBadge = `${runIndex + 1}.`; rankClass = ''; } return `
${formatTime(run.recorded_time)}
${new Date(run.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}
${new Date(run.created_at).toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit' })}
${rankBadge}
`; }).join(''); return `
${locationName}
${formatTime(bestTime.recorded_time)}
${new Date(bestTime.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')} ${locationTimes.length} ${currentLanguage === 'de' ? 'Läufe' : 'Runs'}
${currentLanguage === 'de' ? 'Alle Läufe an diesem Standort:' : 'All runs at this location:'}
${allRunsHtml}
`; }).join(''); timesGrid.innerHTML = cards; } // Toggle time card expansion function toggleTimeCard(cardElement) { const isExpanded = cardElement.classList.contains('expanded'); // Close all other cards first document.querySelectorAll('.user-time-card.expanded').forEach(card => { if (card !== cardElement) { card.classList.remove('expanded'); } }); // Toggle current card if (isExpanded) { cardElement.classList.remove('expanded'); } else { cardElement.classList.add('expanded'); } } // Helper function to convert time to seconds for comparison function convertTimeToSeconds(timeValue) { if (typeof timeValue === 'string') { // Handle HH:MM:SS format const parts = timeValue.split(':'); if (parts.length === 3) { return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]); } // Handle MM:SS format if (parts.length === 2) { return parseInt(parts[0]) * 60 + parseFloat(parts[1]); } } return parseFloat(timeValue) || 0; } // Format time interval to readable format function formatTime(interval) { // Handle numeric values (seconds) if (typeof interval === 'number') { return formatSeconds(interval); } // Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45} if (typeof interval === 'object') { const { hours = 0, minutes = 0, seconds = 0 } = interval; const totalSeconds = hours * 3600 + minutes * 60 + seconds; return formatSeconds(totalSeconds); } // Fallback for string format if (typeof interval === 'string') { // Parse format like "00:01:23.45" const parts = interval.split(':'); if (parts.length === 3) { const hours = parseInt(parts[0]); const minutes = parseInt(parts[1]); const seconds = parseFloat(parts[2]); const totalSeconds = hours * 3600 + minutes * 60 + seconds; return formatSeconds(totalSeconds); } } return interval; } function formatSeconds(totalSeconds) { const minutes = Math.floor(totalSeconds / 60); const seconds = (totalSeconds % 60).toFixed(2); if (minutes > 0) { return `${minutes}:${seconds.padStart(5, '0')}`; } else { return `${seconds}s`; } } // Show message in modal function showMessage(containerId, message, type) { const container = document.getElementById(containerId); container.innerHTML = `
${message}
`; } // Initialize when DOM is loaded // ==================== ACHIEVEMENT FUNCTIONS ==================== // Global variables for achievements let currentPlayerId = null; let allAchievements = []; let playerAchievements = []; let currentAchievementCategory = 'all'; // Load achievements for the current player async function loadPlayerAchievements() { if (!currentPlayerId) { showAchievementsNotAvailable(); return; } try { // Show loading state document.getElementById('achievementsLoading').style.display = 'block'; document.getElementById('achievementStats').style.display = 'none'; document.getElementById('achievementCategories').style.display = 'none'; document.getElementById('achievementsNotAvailable').style.display = 'none'; // Update loading text const loadingText = document.querySelector('#achievementsLoading p'); if (loadingText) { loadingText.textContent = currentLanguage === 'de' ? 'Lade deine Achievements...' : 'Loading your achievements...'; } // Load player achievements (includes all achievements with player status) const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`); if (!response.ok) { throw new Error('Failed to load player achievements'); } const result = await response.json(); window.allAchievements = result.data; playerAchievements = result.data.filter(achievement => achievement.is_completed); // Load achievement statistics await loadAchievementStats(); // Show achievements displayAchievementStats(); displayAchievements(); // Hide loading state document.getElementById('achievementsLoading').style.display = 'none'; document.getElementById('achievementStats').style.display = 'flex'; document.getElementById('achievementCategories').style.display = 'block'; } catch (error) { console.error('Error loading achievements:', error); document.getElementById('achievementsLoading').style.display = 'none'; showAchievementsNotAvailable(); } } // Load achievement statistics async function loadAchievementStats() { try { const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats?t=${Date.now()}`); if (response.ok) { const result = await response.json(); window.achievementStats = result.data; } } catch (error) { console.error('Error loading achievement stats:', error); } } // Display achievement statistics function displayAchievementStats() { if (!window.achievementStats) return; const stats = window.achievementStats; document.getElementById('totalPoints').textContent = stats.total_points; document.getElementById('completedAchievements').textContent = `${stats.completed_achievements}/${stats.total_achievements}`; document.getElementById('achievementsToday').textContent = stats.achievements_today; document.getElementById('completionPercentage').textContent = `${stats.completion_percentage}%`; } // Display achievements in grid function displayAchievements() { const achievementsGrid = document.getElementById('achievementsGrid'); if (!window.allAchievements || window.allAchievements.length === 0) { const noAchievementsTitle = currentLanguage === 'de' ? 'Noch keine Achievements' : 'No Achievements Yet'; const noAchievementsDescription = currentLanguage === 'de' ? 'Starte deine ersten Läufe, um Achievements zu sammeln!' : 'Start your first runs to collect achievements!'; achievementsGrid.innerHTML = `
🏆

${noAchievementsTitle}

${noAchievementsDescription}

`; return; } // Filter achievements by category let filteredAchievements = window.allAchievements; if (currentAchievementCategory !== 'all') { filteredAchievements = window.allAchievements.filter(achievement => achievement.category === currentAchievementCategory ); } // Generate achievement cards const achievementCards = filteredAchievements.map(achievement => { const isCompleted = achievement.is_completed; const progress = achievement.progress || 0; const earnedAt = achievement.earned_at; const completionCount = achievement.completion_count || 0; // Translate achievement const translatedAchievement = translateAchievement(achievement); let progressText = ''; if (isCompleted) { const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on'; const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed'; const timesText = currentLanguage === 'de' ? 'x geschafft' : 'x completed'; if (completionCount > 1) { progressText = `${completionCount}${timesText}`; } else { progressText = earnedAt ? `${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` : completedText; } } else if (progress > 0) { // Show progress for incomplete achievements const conditionValue = getAchievementConditionValue(achievement.name); if (conditionValue) { progressText = `${progress}/${conditionValue}`; } } const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points'; const totalPoints = completionCount > 0 ? achievement.points * completionCount : achievement.points; return `
${achievement.icon}

${translatedAchievement.name}

${translatedAchievement.description}

+${totalPoints} ${pointsText} ${progressText ? `${progressText}` : ''}
${isCompleted ? '✅' : '⏳'}
`; }).join(''); achievementsGrid.innerHTML = achievementCards; } // Initialize Analytics and Statistics event listeners document.addEventListener('DOMContentLoaded', function() { const analyticsCard = document.getElementById('analyticsCard'); const statisticsCard = document.getElementById('statisticsCard'); if (analyticsCard) { analyticsCard.addEventListener('click', showAnalytics); console.log('Analytics card event listener added'); } if (statisticsCard) { statisticsCard.addEventListener('click', showStatistics); console.log('Statistics card event listener added'); } }); // Analytics Functions function showAnalytics() { console.log('showAnalytics called'); // Hide other sections const timesDisplay = document.getElementById('timesDisplay'); const achievementsDisplay = document.getElementById('achievementsDisplay'); const statisticsSection = document.getElementById('statisticsSection'); if (timesDisplay) timesDisplay.style.display = 'none'; if (achievementsDisplay) achievementsDisplay.style.display = 'none'; if (statisticsSection) statisticsSection.style.display = 'none'; // Show analytics section const analyticsSection = document.getElementById('analyticsSection'); if (analyticsSection) { analyticsSection.style.display = 'block'; console.log('Analytics section shown'); } else { console.error('Analytics section not found'); return; } // Load analytics data (will show fallback if not linked) loadAnalyticsData(); } function showStatistics() { console.log('showStatistics called'); // Hide other sections const timesDisplay = document.getElementById('timesDisplay'); const achievementsDisplay = document.getElementById('achievementsDisplay'); const analyticsSection = document.getElementById('analyticsSection'); if (timesDisplay) timesDisplay.style.display = 'none'; if (achievementsDisplay) achievementsDisplay.style.display = 'none'; if (analyticsSection) analyticsSection.style.display = 'none'; // Show statistics section const statisticsSection = document.getElementById('statisticsSection'); if (statisticsSection) { statisticsSection.style.display = 'block'; console.log('Statistics section shown'); } else { console.error('Statistics section not found'); return; } // Load statistics data (will show fallback if not linked) loadStatisticsData(); } async function loadAnalyticsData() { try { if (!currentPlayerId) { console.error('No player ID available - user not linked'); // Show fallback data when user is not linked displayAnalyticsFallback(); return; } // Load analytics data from API const response = await fetch(`/api/analytics/player/${currentPlayerId}`); if (!response.ok) { throw new Error('Failed to load analytics data'); } const analyticsData = await response.json(); displayAnalyticsData(analyticsData.data); // Update preview in main card updateAnalyticsPreview(analyticsData.data); } catch (error) { console.error('Error loading analytics data:', error); // Show fallback data displayAnalyticsFallback(); } } async function loadStatisticsData() { try { if (!currentPlayerId) { console.error('No player ID available - user not linked'); // Show fallback data when user is not linked displayStatisticsFallback(); return; } // Load statistics data from API const response = await fetch(`/api/statistics/player/${currentPlayerId}`); if (!response.ok) { throw new Error('Failed to load statistics data'); } const statisticsData = await response.json(); displayStatisticsData(statisticsData.data); // Update preview in main card updateStatisticsPreview(statisticsData.data); } catch (error) { console.error('Error loading statistics data:', error); // Show fallback data displayStatisticsFallback(); } } function displayAnalyticsData(data) { // Performance Trends document.getElementById('avgTimeThisWeekDetail').textContent = formatTime(data.performance.avgTimeThisWeek); document.getElementById('avgTimeLastWeek').textContent = formatTime(data.performance.avgTimeLastWeek); document.getElementById('improvementDetail').textContent = data.performance.improvement.toFixed(2) + '%'; // Activity Stats document.getElementById('runsToday').textContent = data.activity.runsToday + ' Läufe'; document.getElementById('runsThisWeekDetail').textContent = data.activity.runsThisWeek + ' Läufe'; document.getElementById('avgRunsPerDay').textContent = data.activity.avgRunsPerDay.toFixed(2); // Location Performance displayLocationPerformance(data.locationPerformance); // Monthly Stats document.getElementById('runsThisMonth').textContent = data.monthly.runsThisMonth + ' Läufe'; document.getElementById('runsLastMonth').textContent = data.monthly.runsLastMonth + ' Läufe'; document.getElementById('bestTimeThisMonth').textContent = formatTime(data.monthly.bestTimeThisMonth); } function displayStatisticsData(data) { // Personal Records displayPersonalRecords(data.personalRecords); // Consistency Metrics document.getElementById('averageTime').textContent = formatTime(data.consistency.averageTime); document.getElementById('timeDeviation').textContent = formatTime(data.consistency.timeDeviation); document.getElementById('consistencyScore').textContent = data.consistency.consistencyScore.toFixed(2) + '%'; // Ranking Stats displayRankingStats(data.rankings); // Progress Stats document.getElementById('totalRunsStats').textContent = data.progress.totalRuns; document.getElementById('activeDays').textContent = data.progress.activeDays; document.getElementById('locationsVisited').textContent = data.progress.locationsVisited; } function displayLocationPerformance(locations) { const container = document.getElementById('locationPerformance'); if (!locations || locations.length === 0) { container.innerHTML = '

Keine Standort-Daten verfügbar

'; return; } const locationHTML = locations.map(location => `
${location.name}
${formatTime(location.bestTime)} (${location.runs} Läufe)
`).join(''); container.innerHTML = locationHTML; } function displayPersonalRecords(records) { const container = document.getElementById('personalRecords'); if (!records || records.length === 0) { container.innerHTML = '

Keine Bestzeiten verfügbar

'; return; } const recordsHTML = records.map((record, index) => `
#${index + 1} ${formatTime(record.time)}
${record.location}
`).join(''); container.innerHTML = recordsHTML; } function displayRankingStats(rankings) { const container = document.getElementById('rankingStats'); if (!rankings || rankings.length === 0) { container.innerHTML = '

Keine Ranglisten-Daten verfügbar

'; return; } const rankingsHTML = rankings.map(ranking => `
${ranking.category}
#${ranking.position} von ${ranking.total}
`).join(''); container.innerHTML = rankingsHTML; } function updateAnalyticsPreview(data) { if (data && data.performance && data.activity) { document.getElementById('avgTimeThisWeek').textContent = formatTime(data.performance.avgTimeThisWeek); document.getElementById('improvementThisWeek').textContent = data.performance.improvement.toFixed(2) + '%'; document.getElementById('runsThisWeek').textContent = data.activity.runsThisWeek; document.getElementById('analyticsPreview').style.display = 'block'; } else { // Hide preview if no data document.getElementById('analyticsPreview').style.display = 'none'; } } function updateStatisticsPreview(data) { if (data && data.personalRecords && data.progress) { document.getElementById('personalBest').textContent = formatTime(data.personalRecords[0]?.time || 0); document.getElementById('totalRunsCount').textContent = data.progress.totalRuns; document.getElementById('rankPosition').textContent = data.rankings[0]?.position || '-'; document.getElementById('statisticsPreview').style.display = 'block'; } else { // Hide preview if no data document.getElementById('statisticsPreview').style.display = 'none'; } } function displayAnalyticsFallback() { // Show fallback data when API fails or user not linked const notLinkedMessage = currentLanguage === 'de' ? 'RFID nicht verknüpft - Analytics nicht verfügbar' : 'RFID not linked - Analytics not available'; document.getElementById('avgTimeThisWeekDetail').textContent = '--:--'; document.getElementById('avgTimeLastWeek').textContent = '--:--'; document.getElementById('improvementDetail').textContent = '+0.0%'; document.getElementById('runsToday').textContent = '0 Läufe'; document.getElementById('runsThisWeekDetail').textContent = '0 Läufe'; document.getElementById('avgRunsPerDay').textContent = '0.0'; document.getElementById('locationPerformance').innerHTML = `

${notLinkedMessage}

`; document.getElementById('runsThisMonth').textContent = '0 Läufe'; document.getElementById('runsLastMonth').textContent = '0 Läufe'; document.getElementById('bestTimeThisMonth').textContent = '--:--'; } function displayStatisticsFallback() { // Show fallback data when API fails or user not linked const notLinkedMessage = currentLanguage === 'de' ? 'RFID nicht verknüpft - Statistiken nicht verfügbar' : 'RFID not linked - Statistics not available'; document.getElementById('personalRecords').innerHTML = `

${notLinkedMessage}

`; document.getElementById('averageTime').textContent = '--:--'; document.getElementById('timeDeviation').textContent = '--:--'; document.getElementById('consistencyScore').textContent = '0%'; document.getElementById('rankingStats').innerHTML = `

${notLinkedMessage}

`; document.getElementById('totalRunsStats').textContent = '0'; document.getElementById('activeDays').textContent = '0'; document.getElementById('locationsVisited').textContent = '0'; } // Get achievement condition value for progress display function getAchievementConditionValue(achievementName) { const conditionMap = { 'Erste Schritte': 1, 'Durchhalter': 3, 'Fleißig': 5, 'Besessen': 10, 'Regelmäßig': 5, 'Stammgast': 10, 'Treue': 20, 'Veteran': 50, 'Fortschritt': 5, 'Durchbruch': 10, 'Transformation': 15, 'Perfektionist': 20 }; return conditionMap[achievementName] || null; } // Show achievement category function showAchievementCategory(category) { currentAchievementCategory = category; // Update active tab document.querySelectorAll('.category-tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelector(`[data-category="${category}"]`).classList.add('active'); // Display filtered achievements displayAchievements(); } // Show achievement details (placeholder for future modal) function showAchievementDetails(achievementId) { const achievement = playerAchievements.find(a => a.id === achievementId); if (achievement) { console.log('Achievement details:', achievement); // TODO: Implement achievement details modal } } // Show achievements not available state function showAchievementsNotAvailable() { document.getElementById('achievementsLoading').style.display = 'none'; document.getElementById('achievementStats').style.display = 'none'; document.getElementById('achievementCategories').style.display = 'none'; document.getElementById('achievementsNotAvailable').style.display = 'block'; // Update the text content for the not available state const notAvailableTitle = document.querySelector('#achievementsNotAvailable h3'); const notAvailableDescription = document.querySelector('#achievementsNotAvailable p'); const notAvailableButton = document.querySelector('#achievementsNotAvailable button'); if (notAvailableTitle) { notAvailableTitle.textContent = currentLanguage === 'de' ? 'Achievements noch nicht verfügbar' : 'Achievements not available yet'; } if (notAvailableDescription) { notAvailableDescription.textContent = currentLanguage === 'de' ? 'Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren.' : 'To collect achievements, you must first link your RFID card with your account and complete some runs.'; } if (notAvailableButton) { notAvailableButton.textContent = currentLanguage === 'de' ? '🏷️ RFID jetzt verknüpfen' : '🏷️ Link RFID now'; } // Update the "So funktioniert's" title const howItWorksTitle = document.querySelector('#achievementsNotAvailable h4'); if (howItWorksTitle) { howItWorksTitle.textContent = currentLanguage === 'de' ? 'So funktioniert\'s:' : 'How it works:'; } // Update the steps const notAvailableSteps = document.querySelectorAll('#achievementsNotAvailable li'); if (notAvailableSteps.length >= 3) { notAvailableSteps[0].textContent = currentLanguage === 'de' ? 'Klicke auf "RFID jetzt verknüpfen"' : 'Click on "Link RFID now"'; notAvailableSteps[1].textContent = currentLanguage === 'de' ? 'Scanne den QR-Code auf deiner RFID-Karte' : 'Scan the QR code on your RFID card'; notAvailableSteps[2].textContent = currentLanguage === 'de' ? 'Fertig! Deine Zeiten werden automatisch hier angezeigt' : 'Done! Your times will be displayed here automatically'; } } // Check achievements for current player async function checkPlayerAchievements() { if (!currentPlayerId) return; try { const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, { method: 'POST' }); if (response.ok) { const result = await response.json(); if (result.data.count > 0) { // Show notification for new achievements showAchievementNotification(result.data.new_achievements); // Reload achievements await loadPlayerAchievements(); } } } catch (error) { console.error('Error checking achievements:', error); } } // Show achievement notification function showAchievementNotification(newAchievements) { // Create notification element const notification = document.createElement('div'); notification.className = 'achievement-notification'; const titleText = currentLanguage === 'de' ? 'Neue Achievements erreicht!' : 'New Achievements Unlocked!'; const descriptionText = currentLanguage === 'de' ? `Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!` : `You have received ${newAchievements.length} new achievement${newAchievements.length > 1 ? 's' : ''}!`; notification.innerHTML = `
🏆

${titleText}

${descriptionText}

`; // Add to page document.body.appendChild(notification); // Auto-remove after 5 seconds setTimeout(() => { if (notification.parentElement) { notification.remove(); } }, 5000); } // Initialize achievements when player is loaded function initializeAchievements(playerId) { currentPlayerId = playerId; loadPlayerAchievements(); } // Convert VAPID key from base64url to Uint8Array function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // Push notifications are now handled in dashboard.html // This function has been moved to the HTML file for better integration // Web Notification Functions function showWebNotification(title, message, icon = '🏆') { if ('Notification' in window && Notification.permission === 'granted') { const notification = new Notification(title, { body: message, icon: '/pictures/icon-192.png', badge: '/pictures/icon-192.png', tag: 'ninjacross-achievement', requireInteraction: true }); // Auto-close after 10 seconds setTimeout(() => { notification.close(); }, 10000); // Handle click notification.onclick = function () { window.focus(); notification.close(); }; } } // Track which notifications have been sent today let notificationsSentToday = { daily: false, weekly: false, monthly: false }; // Check for best time achievements and show notifications async function checkBestTimeNotifications() { try { // Check if push notifications are enabled const pushPlayerId = localStorage.getItem('pushPlayerId'); if (!pushPlayerId) { console.log('🔕 Push notifications disabled, skipping best time check'); return; } const response = await fetch('/api/v1/public/best-times'); const result = await response.json(); if (result.success && result.data) { const { daily, weekly, monthly } = result.data; // Check if current player has best times if (currentPlayerId) { const now = new Date(); const isEvening = now.getHours() >= 19; // Check daily best time (only in the evening, only once per day) if (daily && daily.player_id === currentPlayerId && isEvening) { // Check if notification was already sent today const dailyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/daily_best`); const dailyResult = await dailyCheck.json(); if (!dailyResult.wasSent) { const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!'; const message = currentLanguage === 'de' ? `Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` : `Congratulations! You achieved the best time of the day with ${daily.best_time}!`; showWebNotification(title, message, '👑'); // Mark as sent in database await fetch('/api/v1/public/notification-sent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: currentPlayerId, notificationType: 'daily_best' }) }); console.log('🏆 Daily best notification sent'); } } // Check weekly best time (only on Sunday evening, only once per week) if (weekly && weekly.player_id === currentPlayerId) { const isSunday = now.getDay() === 0; // 0 = Sunday if (isSunday && isEvening) { // Check if notification was already sent this week const weeklyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/weekly_best`); const weeklyResult = await weeklyCheck.json(); if (!weeklyResult.wasSent) { const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!'; const message = currentLanguage === 'de' ? `Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` : `Fantastic! You are the weekly champion with ${weekly.best_time}!`; showWebNotification(title, message, '🏆'); // Mark as sent in database await fetch('/api/v1/public/notification-sent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: currentPlayerId, notificationType: 'weekly_best' }) }); console.log('🏆 Weekly best notification sent'); } } } // Check monthly best time (only on last evening of month at 19:00, only once per month) if (monthly && monthly.player_id === currentPlayerId) { const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); const isLastDayOfMonth = now.getDate() === lastDayOfMonth.getDate(); // Only show monthly notification on last day of month at 19:00 or later if (isLastDayOfMonth && isEvening) { // Check if notification was already sent this month const monthlyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/monthly_best`); const monthlyResult = await monthlyCheck.json(); if (!monthlyResult.wasSent) { const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!'; const message = currentLanguage === 'de' ? `Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` : `Incredible! You are the monthly master with ${monthly.best_time}!`; showWebNotification(title, message, '🥇'); // Mark as sent in database await fetch('/api/v1/public/notification-sent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: currentPlayerId, notificationType: 'monthly_best' }) }); console.log('🏆 Monthly best notification sent'); } } } } } } catch (error) { console.error('Error checking best time notifications:', error); } } // Check for new achievements and show notifications async function checkAchievementNotifications() { try { if (!currentPlayerId) return; // Check if push notifications are enabled const pushPlayerId = localStorage.getItem('pushPlayerId'); if (!pushPlayerId) { console.log('🔕 Push notifications disabled, skipping achievement check'); return; } const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`); const result = await response.json(); if (result.success && result.data) { const newAchievements = result.data.filter(achievement => { // Check if achievement was earned in the last 5 minutes const earnedAt = new Date(achievement.earned_at); const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); return earnedAt > fiveMinutesAgo; }); if (newAchievements.length > 0) { for (const achievement of newAchievements) { // Check if notification was already sent for this achievement const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`); const achievementResult = await achievementCheck.json(); if (!achievementResult.wasSent) { const translatedAchievement = translateAchievement(achievement); showWebNotification( `🏆 ${translatedAchievement.name}`, translatedAchievement.description, achievement.icon || '🏆' ); // Mark as sent in database await fetch('/api/v1/public/notification-sent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: currentPlayerId, notificationType: 'achievement', achievementId: achievement.achievement_id, locationId: achievement.location_id || null }) }); console.log(`🏆 Achievement notification sent: ${achievement.name}`); } } } } } catch (error) { console.error('Error checking achievement notifications:', error); } } // Periodic check for notifications (every 30 seconds) setInterval(() => { checkBestTimeNotifications(); checkAchievementNotifications(); }, 30000); // Settings Functions function showSettings() { const modal = document.getElementById('settingsModal'); if (modal) { modal.style.display = 'block'; loadSettings(); } } async function loadSettings() { try { if (!currentUser || !currentUser.id) { console.error('No user ID available'); return; } // Load current player settings using user ID const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`); const result = await response.json(); if (result.success && result.data) { const showInLeaderboard = result.data.show_in_leaderboard || false; document.getElementById('showInLeaderboard').checked = showInLeaderboard; console.log('Loaded settings - showInLeaderboard:', showInLeaderboard); } else { console.error('Failed to load player settings:', result.message); // Set default to false if loading fails document.getElementById('showInLeaderboard').checked = false; } } catch (error) { console.error('Error loading settings:', error); // Set default to false if loading fails document.getElementById('showInLeaderboard').checked = false; } } function updateLeaderboardSetting() { const checkbox = document.getElementById('showInLeaderboard'); console.log('Leaderboard setting changed:', checkbox.checked); } async function saveSettings() { try { if (!currentPlayerId) { console.error('No player ID available'); return; } const showInLeaderboard = document.getElementById('showInLeaderboard').checked; // Update player settings using public endpoint (no API key needed) const response = await fetch(`/api/v1/public/update-player-settings`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: currentPlayerId, show_in_leaderboard: showInLeaderboard }) }); const result = await response.json(); if (result.success) { const successMsg = currentLanguage === 'de' ? 'Einstellungen erfolgreich gespeichert!' : 'Settings saved successfully!'; showNotification(successMsg, 'success'); closeModal('settingsModal'); } else { const errorMsg = currentLanguage === 'de' ? 'Fehler beim Speichern der Einstellungen: ' + result.message : 'Error saving settings: ' + result.message; showNotification(errorMsg, 'error'); } } catch (error) { console.error('Error saving settings:', error); const errorMsg = currentLanguage === 'de' ? 'Fehler beim Speichern der Einstellungen' : 'Error saving settings'; showNotification(errorMsg, 'error'); } } function showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'}; color: white; padding: 1rem 1.5rem; border-radius: 0.5rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); z-index: 10000; max-width: 300px; font-weight: 500; `; notification.textContent = message; document.body.appendChild(notification); // Remove notification after 3 seconds setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 3000); } document.addEventListener('DOMContentLoaded', function () { // Add cookie settings button functionality const cookieSettingsBtn = document.getElementById('cookie-settings-footer'); if (cookieSettingsBtn) { cookieSettingsBtn.addEventListener('click', function () { if (window.cookieConsent) { window.cookieConsent.resetConsent(); } }); } // Check push notification status on dashboard load if (typeof checkPushStatus === 'function') { checkPushStatus(); } });