2105 lines
81 KiB
JavaScript
2105 lines
81 KiB
JavaScript
// 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,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="5" fill="%23000000"/><rect y="5" width="20" height="5" fill="%23DD0000"/><rect y="10" width="20" height="5" fill="%23FFCE00"/></svg>')`;
|
||
} else {
|
||
// USA flag
|
||
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="15" fill="%23B22234"/><rect width="20" height="1.15" fill="%23FFFFFF"/><rect y="2.3" width="20" height="1.15" fill="%23FFFFFF"/><rect y="4.6" width="20" height="1.15" fill="%23FFFFFF"/><rect y="6.9" width="20" height="1.15" fill="%23FFFFFF"/><rect y="9.2" width="20" height="1.15" fill="%23FFFFFF"/><rect y="11.5" width="20" height="1.15" fill="%23FFFFFF"/><rect y="13.8" width="20" height="1.15" fill="%23FFFFFF"/><rect width="7.7" height="8.05" fill="%230033A0"/></svg>')`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 = `
|
||
<h3>📊 Analytics</h3>
|
||
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
|
||
<span style="color: #ef4444; font-weight: 600;">${message}</span>
|
||
</div>
|
||
<div style="font-size: 0.9rem; color: #8892b0;">
|
||
${description}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
|
||
`;
|
||
}
|
||
|
||
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 = `
|
||
<h3>📊 Statistiken</h3>
|
||
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
|
||
<span style="color: #ef4444; font-weight: 600;">${message}</span>
|
||
</div>
|
||
<div style="font-size: 0.9rem; color: #8892b0;">
|
||
${description}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
|
||
`;
|
||
}
|
||
} else {
|
||
// User is linked - restore original cards
|
||
if (analyticsCard) {
|
||
const isGerman = currentLanguage === 'de';
|
||
analyticsCard.innerHTML = `
|
||
<h3>📊 Analytics</h3>
|
||
<div id="analyticsPreview" style="display: none;">
|
||
<div class="analytics-stats">
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="avgTimeThisWeek">--:--</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Durchschnitt diese Woche' : 'Average this week'}</div>
|
||
</div>
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Verbesserung' : 'Improvement'}</div>
|
||
</div>
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="runsThisWeek">0</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Läufe diese Woche' : 'Runs this week'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p>${isGerman ? 'Verfolge deine Leistung und überwache wichtige Metriken.' : 'Track your performance and monitor important metrics.'}</p>
|
||
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">${isGerman ? 'Analytics öffnen' : 'Open Analytics'}</button>
|
||
`;
|
||
}
|
||
|
||
if (statisticsCard) {
|
||
const isGerman = currentLanguage === 'de';
|
||
statisticsCard.innerHTML = `
|
||
<h3>📊 Statistiken</h3>
|
||
<div id="statisticsPreview" style="display: none;">
|
||
<div class="statistics-stats">
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="personalBest">--:--</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Persönliche Bestzeit' : 'Personal Best'}</div>
|
||
</div>
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="totalRunsCount">0</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Gesamte Läufe' : 'Total Runs'}</div>
|
||
</div>
|
||
<div class="mini-stat">
|
||
<div class="mini-stat-number" id="rankPosition">-</div>
|
||
<div class="mini-stat-label">${isGerman ? 'Ranglisten-Position' : 'Ranking Position'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p>${isGerman ? 'Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.' : 'Detailed statistics about your runs - best times, improvements and comparisons.'}</p>
|
||
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">${isGerman ? 'Statistiken öffnen' : 'Open Statistics'}</button>
|
||
`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 = `
|
||
<h3>${isGerman ? '🏷️ RFID Verknüpft' : '🏷️ RFID Linked'}</h3>
|
||
<div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<span style="color: #10b981; font-weight: 600;">✅</span>
|
||
<span style="color: #10b981; font-weight: 600;">
|
||
${isGerman ? 'Erfolgreich verknüpft' : 'Successfully linked'}
|
||
</span>
|
||
</div>
|
||
<div style="font-size: 0.9rem; color: #8892b0;">
|
||
<div><strong>${isGerman ? 'Spieler:' : 'Player:'}</strong> ${playerData.firstname} ${playerData.lastname}</div>
|
||
<div><strong>RFID:</strong> <code style="background: rgba(255,255,255,0.1); padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace;">${playerData.rfiduid}</code></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-secondary" onclick="requestRFIDHelp()" style="margin-top: 1rem; font-size: 0.9rem; padding: 0.5rem 1rem;">
|
||
${isGerman ? '❓ Hilfe anfordern' : '❓ Request Help'}
|
||
</button>
|
||
`;
|
||
|
||
// 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 = `
|
||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
|
||
<h3>${noTimesTitle}</h3>
|
||
<p>${noTimesDescription}</p>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<div class="run-item">
|
||
<div>
|
||
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
||
</div>
|
||
<div class="run-details">
|
||
<div>${new Date(run.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</div>
|
||
<div>${new Date(run.created_at).toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit' })}</div>
|
||
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="user-time-card" onclick="toggleTimeCard(this)" data-location="${locationName}">
|
||
<div class="card-header">
|
||
<div class="time-location-name">${locationName}</div>
|
||
<div class="expand-indicator">▼</div>
|
||
</div>
|
||
|
||
<div class="card-main-content">
|
||
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
|
||
<div class="time-date-info">
|
||
<span>${new Date(bestTime.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</span>
|
||
<span class="time-rank">${locationTimes.length} ${currentLanguage === 'de' ? 'Läufe' : 'Runs'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="expanded-content">
|
||
<div class="all-runs-title">${currentLanguage === 'de' ? 'Alle Läufe an diesem Standort:' : 'All runs at this location:'}</div>
|
||
${allRunsHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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 = `<div class="message ${type}">${message}</div>`;
|
||
}
|
||
|
||
// 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 = `
|
||
<div class="no-achievements">
|
||
<div class="no-achievements-icon">🏆</div>
|
||
<h3>${noAchievementsTitle}</h3>
|
||
<p>${noAchievementsDescription}</p>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
||
onclick="showAchievementDetails('${achievement.id}')">
|
||
<div class="achievement-icon">${achievement.icon}</div>
|
||
<div class="achievement-content">
|
||
<h4 class="achievement-name">${translatedAchievement.name}</h4>
|
||
<p class="achievement-description">${translatedAchievement.description}</p>
|
||
<div class="achievement-meta">
|
||
<span class="achievement-points">+${totalPoints} ${pointsText}</span>
|
||
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="achievement-status">
|
||
${isCompleted ? '✅' : '⏳'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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 = '<p>Keine Standort-Daten verfügbar</p>';
|
||
return;
|
||
}
|
||
|
||
const locationHTML = locations.map(location => `
|
||
<div class="location-item">
|
||
<span class="location-name">${location.name}</span>
|
||
<div>
|
||
<span class="location-best">${formatTime(location.bestTime)}</span>
|
||
<span class="location-runs">(${location.runs} Läufe)</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
container.innerHTML = locationHTML;
|
||
}
|
||
|
||
function displayPersonalRecords(records) {
|
||
const container = document.getElementById('personalRecords');
|
||
|
||
if (!records || records.length === 0) {
|
||
container.innerHTML = '<p>Keine Bestzeiten verfügbar</p>';
|
||
return;
|
||
}
|
||
|
||
const recordsHTML = records.map((record, index) => `
|
||
<div class="record-item">
|
||
<div>
|
||
<span class="record-rank">#${index + 1}</span>
|
||
<span class="record-time">${formatTime(record.time)}</span>
|
||
</div>
|
||
<span class="record-location">${record.location}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
container.innerHTML = recordsHTML;
|
||
}
|
||
|
||
function displayRankingStats(rankings) {
|
||
const container = document.getElementById('rankingStats');
|
||
|
||
if (!rankings || rankings.length === 0) {
|
||
container.innerHTML = '<p>Keine Ranglisten-Daten verfügbar</p>';
|
||
return;
|
||
}
|
||
|
||
const rankingsHTML = rankings.map(ranking => `
|
||
<div class="ranking-item">
|
||
<span class="ranking-category">${ranking.category}</span>
|
||
<div>
|
||
<span class="ranking-position">#${ranking.position}</span>
|
||
<span class="ranking-total">von ${ranking.total}</span>
|
||
</div>
|
||
</div>
|
||
`).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 = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
|
||
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 = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
|
||
document.getElementById('averageTime').textContent = '--:--';
|
||
document.getElementById('timeDeviation').textContent = '--:--';
|
||
document.getElementById('consistencyScore').textContent = '0%';
|
||
document.getElementById('rankingStats').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
|
||
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 = `
|
||
<div class="notification-content">
|
||
<div class="notification-icon">🏆</div>
|
||
<div class="notification-text">
|
||
<h4>${titleText}</h4>
|
||
<p>${descriptionText}</p>
|
||
</div>
|
||
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
||
</div>
|
||
`;
|
||
|
||
// 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();
|
||
}
|
||
});
|