// 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) {
console.log('User data:', 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();
} 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();
// 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 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();
});
// 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}
${isGerman ? '❓ Hilfe anfordern' : '❓ Request Help'}
`;
// 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);
} 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 `
${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) {
// 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);
// Debug logging
if (achievement.name === 'Tageskönig') {
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt, completionCount });
}
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;
}
// 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();
}
// 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();
};
}
}
// Check for best time achievements and show notifications
async function checkBestTimeNotifications() {
try {
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) {
if (daily && daily.player_id === currentPlayerId) {
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, '👑');
}
if (weekly && weekly.player_id === currentPlayerId) {
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, '🏆');
}
if (monthly && monthly.player_id === currentPlayerId) {
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, '🥇');
}
}
}
} catch (error) {
console.error('Error checking best time notifications:', error);
}
}
// Check for new achievements and show notifications
async function checkAchievementNotifications() {
try {
if (!currentPlayerId) 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) {
newAchievements.forEach(achievement => {
const translatedAchievement = translateAchievement(achievement);
showWebNotification(
`🏆 ${translatedAchievement.name}`,
translatedAchievement.description,
achievement.icon || '🏆'
);
});
}
}
} 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();
}
});
}
});