// 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; // 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: 'test-user', 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) { console.log('User data:', session.user); currentUser = session.user; displayUserInfo(session.user); } else { // Fallback if no user data currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' }; displayUserInfo({ email: 'admin@speedrun-arena.com' }); } showDashboard(); // Load times section checkLinkStatusAndLoadTimes(); } 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'; } }); // Initialize dashboard when page loads initDashboard(); // 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 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/user-player/${currentUser.id}`); 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); showMessage('rfidMessage', 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.', '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) { showMessage('rfidMessage', 'QR-Code enthält keine gültige RFID UID', 'error'); return; } // Format the UID to match database format (XX:XX:XX:XX) const formattedUid = formatRfidUid(rawUid); showMessage('rfidMessage', `QR-Code erkannt: ${rawUid} → ${formattedUid}`, 'info'); // Link the user using the formatted RFID UID await linkUserByRfidUid(formattedUid); } catch (error) { console.error('Error formatting RFID UID:', error); showMessage('rfidMessage', `Fehler beim Formatieren der RFID UID: ${error.message}`, 'error'); } } // Manual RFID linking async function linkManualRfid() { const rawUid = document.getElementById('manualRfidInput').value.trim(); if (!rawUid) { showMessage('rfidMessage', 'Bitte gib eine RFID UID ein', 'error'); return; } try { // Format the UID to match database format const formattedUid = formatRfidUid(rawUid); showMessage('rfidMessage', `Formatiert: ${rawUid} → ${formattedUid}`, 'info'); await linkUserByRfidUid(formattedUid); } catch (error) { console.error('Error formatting manual RFID UID:', error); showMessage('rfidMessage', `Fehler beim Formatieren: ${error.message}`, 'error'); } } // Link user by RFID UID (core function) async function linkUserByRfidUid(rfidUid) { if (!currentUser) { showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error'); return; } try { // First, find the player with this RFID UID const response = await fetch('/api/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) { showMessage('rfidMessage', `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}`, 'success'); setTimeout(() => { closeModal('rfidModal'); // Reload times section after successful linking checkLinkStatusAndLoadTimes(); }, 2000); } else { showMessage('rfidMessage', result.message || 'Fehler beim Verknüpfen', 'error'); } } catch (error) { console.error('Error linking RFID:', error); showMessage('rfidMessage', 'Fehler beim Verknüpfen der RFID', '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'; } // Show loading state function showTimesLoading() { document.getElementById('timesLoading').style.display = 'block'; document.getElementById('timesNotLinked').style.display = 'none'; document.getElementById('timesDisplay').style.display = 'none'; } // Load user times for the section async function loadUserTimesSection(playerData) { showTimesLoading(); try { const response = await fetch(`/api/user-times/${currentUser.id}`); const times = await response.json(); // Update stats updateTimesStats(times, playerData); // Display times displayUserTimes(times); // Show the times display document.getElementById('timesLoading').style.display = 'none'; document.getElementById('timesNotLinked').style.display = 'none'; document.getElementById('timesDisplay').style.display = 'block'; } 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) { timesGrid.innerHTML = `

Noch keine Zeiten aufgezeichnet

Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!

`; 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 = '🥇 Beste'; 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('de-DE')}
${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
${rankBadge}
`; }).join(''); return `
${locationName}
${formatTime(bestTime.recorded_time)}
${new Date(bestTime.created_at).toLocaleDateString('de-DE')} ${locationTimes.length} Läufe
Alle Läufe an diesem Standort:
${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) { // 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}
`; }