Files
Ninjaserver/public/dashboard.html
2025-09-23 14:13:24 +02:00

903 lines
50 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPEEDRUN ARENA - Admin Dashboard</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="manifest" href="/manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Ninja Cross">
<link rel="apple-touch-icon" href="/pictures/favicon.ico">
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<!-- QR Code Scanner Library -->
<script src="https://unpkg.com/jsqr@1.4.0/dist/jsQR.js"></script>
<link rel="stylesheet" href="/css/dashboard.css">
<!-- Notification Permission Script -->
<script>
// Register Service Worker for iPhone Notifications
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('✅ Service Worker registered:', registration);
})
.catch(function(error) {
console.log('❌ Service Worker registration failed:', error);
});
}
// Don't automatically request notification permission
// User must click the button to enable push notifications
// 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;
}
// Convert ArrayBuffer to Base64 string
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// Push notification state
let pushSubscription = null;
let pushEnabled = false;
// Subscribe to push notifications
async function subscribeToPush() {
try {
console.log('🔔 Starting push subscription...');
const registration = await navigator.serviceWorker.ready;
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
});
pushSubscription = subscription;
// Player ID wird automatisch vom Server aus der Session geholt
console.log(`📱 Subscribing to push notifications...`);
// Convert ArrayBuffer keys to Base64 strings
const p256dhKey = subscription.getKey('p256dh');
const authKey = subscription.getKey('auth');
// Convert ArrayBuffer to Base64 URL-safe string
const p256dhString = arrayBufferToBase64(p256dhKey);
const authString = arrayBufferToBase64(authKey);
console.log('📱 Converted keys to Base64 strings');
console.log('📱 p256dh length:', p256dhString.length);
console.log('📱 auth length:', authString.length);
// Get current Supabase user ID
const { data: { session } } = await supabase.auth.getSession();
const supabaseUserId = session?.user?.id || null;
// Send subscription to server with player ID
const response = await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies for session
body: JSON.stringify({
endpoint: subscription.endpoint,
keys: {
p256dh: p256dhString,
auth: authString
},
supabaseUserId: supabaseUserId
})
});
const result = await response.json();
if (result.success) {
pushEnabled = true;
updatePushButton();
console.log('✅ Push subscription successful');
// Store player ID for notifications
if (result.playerId) {
localStorage.setItem('pushPlayerId', result.playerId);
}
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('❌ Push subscription failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Unsubscribe from push notifications
async function unsubscribeFromPush() {
try {
console.log('🔕 Unsubscribing from push notifications...');
// Get player ID from localStorage
const playerId = localStorage.getItem('pushPlayerId');
if (pushSubscription) {
// Store endpoint before unsubscribing
const endpoint = pushSubscription.endpoint;
await pushSubscription.unsubscribe();
pushSubscription = null;
console.log('✅ Local push subscription removed');
// Notify server to remove specific subscription from database
if (playerId && endpoint) {
try {
const response = await fetch('/api/v1/public/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies for session
body: JSON.stringify({
playerId: playerId,
endpoint: endpoint
})
});
const result = await response.json();
if (result.success) {
console.log('✅ Server notified - specific subscription removed from database');
} else {
console.warn('⚠️ Server notification failed:', result.message);
}
} catch (error) {
console.warn('⚠️ Failed to notify server:', error);
}
}
}
// Clear stored player ID
localStorage.removeItem('pushPlayerId');
pushEnabled = false;
updatePushButton();
console.log('🔕 Push notifications disabled');
} catch (error) {
console.error('❌ Push unsubscribe failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Check if user is on iOS
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// Check if PWA is installed
function isPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
// Show iOS PWA installation hint
function showIOSPWAHint() {
if (isIOS() && !isPWAInstalled()) {
const hint = document.createElement('div');
hint.id = 'ios-pwa-hint';
hint.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
z-index: 10000;
max-width: 90%;
text-align: center;
font-size: 14px;
line-height: 1.4;
`;
hint.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 20px;">📱</span>
<div>
<strong>Push-Benachrichtigungen für iOS</strong><br>
<small>Für Push-Benachrichtigungen auf iOS: Tippe auf <span style="background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 4px;">📤 Teilen</span> → <span style="background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 4px;">Zum Home-Bildschirm hinzufügen</span></small>
</div>
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0; margin-left: 10px;">✕</button>
</div>
`;
document.body.appendChild(hint);
// Auto-remove after 10 seconds
setTimeout(() => {
if (hint.parentNode) {
hint.remove();
}
}, 10000);
}
}
// Toggle push notifications
async function togglePushNotifications() {
if (pushEnabled) {
await unsubscribeFromPush();
} else {
// Check if iOS and not PWA
if (isIOS() && !isPWAInstalled()) {
showIOSPWAHint();
return;
}
// Check notification permission first
if (Notification.permission === 'denied') {
alert('Push-Benachrichtigungen sind blockiert. Bitte erlaube sie in den Browser-Einstellungen.');
return;
}
if (Notification.permission === 'default') {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
alert('Push-Benachrichtigungen wurden nicht erlaubt.');
return;
}
}
await subscribeToPush();
}
}
// Update push button appearance
function updatePushButton() {
const button = document.getElementById('pushButton');
if (!button) {
console.log('❌ Push button not found in DOM');
return;
}
console.log(`🔔 Updating push button - Status: ${pushEnabled ? 'ENABLED' : 'DISABLED'}`);
if (pushEnabled) {
button.classList.add('active');
button.setAttribute('data-de', '🔕 Push deaktivieren');
button.setAttribute('data-en', '🔕 Disable Push');
button.textContent = '🔕 Push deaktivieren';
console.log('✅ Button updated to: Push deaktivieren (RED)');
} else {
button.classList.remove('active');
button.setAttribute('data-de', '🔔 Push aktivieren');
button.setAttribute('data-en', '🔔 Enable Push');
button.textContent = '🔔 Push aktivieren';
console.log('✅ Button updated to: Push aktivieren (GREEN)');
}
}
// Check existing push subscription on page load
async function checkPushStatus() {
try {
console.log('🔍 Checking push status...');
if (!('serviceWorker' in navigator)) {
console.log('❌ Service Worker not supported');
pushEnabled = false;
updatePushButton();
return;
}
if (!('PushManager' in window)) {
console.log('❌ Push Manager not supported');
pushEnabled = false;
updatePushButton();
return;
}
const registration = await navigator.serviceWorker.ready;
console.log('✅ Service Worker ready');
const subscription = await registration.pushManager.getSubscription();
console.log('📱 Current subscription:', subscription ? 'EXISTS' : 'NONE');
if (subscription) {
pushSubscription = subscription;
pushEnabled = true;
updatePushButton();
console.log('✅ Existing push subscription found and activated');
// Also check if we have a stored player ID
const storedPlayerId = localStorage.getItem('pushPlayerId');
if (storedPlayerId) {
console.log(`📱 Push subscription linked to player: ${storedPlayerId}`);
}
} else {
pushEnabled = false;
updatePushButton();
console.log(' No existing push subscription found');
}
} catch (error) {
console.error('❌ Error checking push status:', error);
pushEnabled = false;
updatePushButton();
}
}
</script>
</head>
<body>
<div class="main-container">
<!-- Sticky Header Container -->
<div class="sticky-header">
<!-- Language Selector -->
<div class="language-selector">
<select id="languageSelect" onchange="changeLanguage()">
<option value="de" data-flag="🇩🇪">Deutsch</option>
<option value="en" data-flag="🇺🇸">English</option>
</select>
</div>
<div class="nav-buttons">
<div class="user-info">
<div class="user-avatar" id="userAvatar">U</div>
<span id="userEmail">user@example.com</span>
</div>
<button class="btn btn-push" id="pushButton" onclick="togglePushNotifications()" data-de="🔔 Push aktivieren" data-en="🔔 Enable Push">🔔 Push aktivieren</button>
<button class="btn btn-pwa" id="pwaButton" onclick="installPWA()" style="display: none;" data-de="📱 App installieren" data-en="📱 Install App">📱 App installieren</button>
<a href="/" class="btn btn-primary" data-de="Zurück zu Zeiten" data-en="Back to Times">Back to Times</a>
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
</div>
</div>
<div class="header-section">
<h1 class="main-title" data-de="DEIN DASHBOARD" data-en="YOUR DASHBOARD">DEIN DASHBOARD</h1>
<p class="tagline" data-de="Verwalte deine Läufe in der NINJACROSS ARENA" data-en="Manage your runs in the NINJACROSS ARENA">Verwalte deine Läufe in der NINJACROSS ARENA</p>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p data-de="Lade dein Dashboard..." data-en="Loading your dashboard...">Lade dein Dashboard...</p>
</div>
<div id="dashboardContent" style="display: none;">
<div class="welcome-card">
<h2 data-de="Dein Dashboard 🥷" data-en="Your Dashboard 🥷">Dein Dashboard 🥷</h2>
<p data-de="Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe." data-en="Welcome to your Dashboard panel! Your clear overview of all your runs.">Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
</div>
<div class="dashboard-grid">
<div class="card" id="analyticsCard" style="cursor: pointer;">
<h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 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">Durchschnitt diese Woche</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
<div class="mini-stat-label">Verbesserung</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="runsThisWeek">0</div>
<div class="mini-stat-label">Läufe diese Woche</div>
</div>
</div>
</div>
<p data-de="Verfolge deine Leistung und überwache wichtige Metriken." data-en="Track your performance and monitor important metrics.">Verfolge deine Leistung und überwache wichtige Metriken.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">Analytics öffnen</button>
</div>
<div class="card" onclick="showSettings()" style="cursor: pointer;">
<h3 data-de="⚙️ Settings" data-en="⚙️ Settings">⚙️ Settings</h3>
<p data-de="Verwalte deine Privatsphäre-Einstellungen und andere Optionen." data-en="Manage your privacy settings and other options.">Verwalte deine Privatsphäre-Einstellungen und andere Optionen.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showSettings();" data-de="Einstellungen öffnen" data-en="Open Settings">Einstellungen öffnen</button>
</div>
<div class="card" onclick="showRFIDSettings()" style="cursor: pointer;">
<h3 data-de="🏷️ RFID Verknüpfung" data-en="🏷️ RFID Linking">🏷️ RFID Verknüpfung</h3>
<p data-de="Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken." data-en="Link your RFID card with your account to automatically track your times.">Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken.</p>
<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>
</div>
<div class="card" id="statisticsCard" style="cursor: pointer;">
<h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 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">Persönliche Bestzeit</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="totalRunsCount">0</div>
<div class="mini-stat-label">Gesamte Läufe</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="rankPosition">-</div>
<div class="mini-stat-label">Ranglisten-Position</div>
</div>
</div>
</div>
<p data-de="Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche." data-en="Detailed statistics about your runs - best times, improvements and comparisons.">Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">Statistiken öffnen</button>
</div>
</div>
<!-- User Times Section -->
<div class="times-section">
<div class="times-header">
<h2 data-de="🏃‍♂️ Meine Zeiten" data-en="🏃‍♂️ My Times">🏃‍♂️ Meine Zeiten</h2>
<p data-de="Deine persönlichen Bestzeiten an allen Standorten" data-en="Your personal best times at all locations">Deine persönlichen Bestzeiten an allen Standorten</p>
</div>
<!-- Loading State -->
<div id="timesLoading" class="times-loading" style="display: none;">
<div class="spinner"></div>
<p data-de="Lade deine Zeiten..." data-en="Loading your times...">Lade deine Zeiten...</p>
</div>
<!-- Not Linked State -->
<div id="timesNotLinked" class="times-not-linked">
<div class="not-linked-content">
<div class="not-linked-icon">🔗</div>
<h3 data-de="RFID noch nicht verknüpft" data-en="RFID not linked yet">RFID noch nicht verknüpft</h3>
<p data-de="Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen." data-en="To see your personal times, you must first link your RFID card with your account.">Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen.</p>
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
🏷️ RFID jetzt verknüpfen
</button>
<div class="link-info">
<h4 data-de="So funktioniert's:" data-en="How it works:">So funktioniert's:</h4>
<ol>
<li data-de="Klicke auf \"RFID jetzt verknüpfen\"" data-en="Click on \"Link RFID now\"">Klicke auf "RFID jetzt verknüpfen"</li>
<li data-de="Scanne den QR-Code auf deiner RFID-Karte" data-en="Scan the QR code on your RFID card">Scanne den QR-Code auf deiner RFID-Karte</li>
<li data-de="Fertig! Deine Zeiten werden automatisch hier angezeigt" data-en="Done! Your times will be displayed here automatically">Fertig! Deine Zeiten werden automatisch hier angezeigt</li>
</ol>
</div>
</div>
</div>
<!-- Times Display -->
<div id="timesDisplay" style="display: none;">
<div class="times-stats">
<div class="stat-card">
<div class="stat-number" id="totalRuns">0</div>
<div class="stat-label" data-de="Gesamte Läufe" data-en="Total Runs">Gesamte Läufe</div>
</div>
<div class="stat-card">
<div class="stat-number" id="bestTime">--:--</div>
<div class="stat-label" data-de="Beste Zeit" data-en="Best Time">Beste Zeit</div>
</div>
<div class="stat-card">
<div class="stat-number" id="locationsCount">0</div>
<div class="stat-label" data-de="Standorte" data-en="Locations">Standorte</div>
</div>
<div class="stat-card">
<div class="stat-number" id="linkedPlayer">--</div>
<div class="stat-label" data-de="Verknüpfter Spieler" data-en="Linked Player">Verknüpfter Spieler</div>
</div>
</div>
<div class="times-content">
<div class="times-grid" id="userTimesGrid">
<!-- Times will be populated here -->
</div>
</div>
</div>
</div>
<!-- Analytics Section -->
<div id="analyticsSection" class="analytics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h2>
<p data-de="Detaillierte Analyse deiner Performance und Trends" data-en="Detailed analysis of your performance and trends">Detaillierte Analyse deiner Performance und Trends</p>
</div>
<!-- Performance Overview -->
<div class="analytics-grid">
<div class="analytics-card">
<h3>📈 Performance-Trends</h3>
<div class="trend-stats">
<div class="trend-item">
<span class="trend-label">Diese Woche:</span>
<span class="trend-value" id="avgTimeThisWeekDetail">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Letzte Woche:</span>
<span class="trend-value" id="avgTimeLastWeek">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Verbesserung:</span>
<span class="trend-value" id="improvementDetail">+0.0%</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏃‍♂️ Aktivitäts-Heatmap</h3>
<div class="activity-stats">
<div class="activity-item">
<span class="activity-label">Heute:</span>
<span class="activity-value" id="runsToday">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Diese Woche:</span>
<span class="activity-value" id="runsThisWeekDetail">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Durchschnitt/Tag:</span>
<span class="activity-value" id="avgRunsPerDay">0.0</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏆 Standort-Performance</h3>
<div class="location-stats" id="locationPerformance">
<p data-de="Lade Standort-Daten..." data-en="Loading location data...">Lade Standort-Daten...</p>
</div>
</div>
<div class="analytics-card">
<h3>📅 Monatlicher Fortschritt</h3>
<div class="monthly-stats">
<div class="monthly-item">
<span class="monthly-label">Dieser Monat:</span>
<span class="monthly-value" id="runsThisMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Letzter Monat:</span>
<span class="monthly-value" id="runsLastMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Beste Zeit:</span>
<span class="monthly-value" id="bestTimeThisMonth">--:--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Section -->
<div id="statisticsSection" class="statistics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h2>
<p data-de="Detaillierte Statistiken zu deinen Läufen und Vergleiche" data-en="Detailed statistics about your runs and comparisons">Detaillierte Statistiken zu deinen Läufen und Vergleiche</p>
</div>
<!-- Personal Records -->
<div class="statistics-grid">
<div class="statistics-card">
<h3>🏆 Persönliche Bestzeiten</h3>
<div class="personal-records" id="personalRecords">
<p data-de="Lade Bestzeiten..." data-en="Loading best times...">Lade Bestzeiten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📊 Konsistenz-Metriken</h3>
<div class="consistency-stats">
<div class="consistency-item">
<span class="consistency-label">Durchschnittszeit:</span>
<span class="consistency-value" id="averageTime">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Standardabweichung:</span>
<span class="consistency-value" id="timeDeviation">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Konsistenz-Score:</span>
<span class="consistency-value" id="consistencyScore">0%</span>
</div>
</div>
</div>
<div class="statistics-card">
<h3>🏅 Ranglisten-Positionen</h3>
<div class="ranking-stats" id="rankingStats">
<p data-de="Lade Ranglisten..." data-en="Loading rankings...">Lade Ranglisten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📈 Fortschritt-Übersicht</h3>
<div class="progress-stats">
<div class="progress-item">
<span class="progress-label">Gesamte Läufe:</span>
<span class="progress-value" id="totalRunsStats">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Aktive Tage:</span>
<span class="progress-value" id="activeDays">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Standorte besucht:</span>
<span class="progress-value" id="locationsVisited">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Achievements Section -->
<div class="achievements-section">
<div class="achievements-header">
<h2 data-de="🏆 Meine Achievements" data-en="🏆 My Achievements">🏆 Meine Achievements</h2>
<p data-de="Sammele Punkte und erreiche neue Meilensteine!" data-en="Collect points and reach new milestones!">Sammele Punkte und erreiche neue Meilensteine!</p>
</div>
<!-- Achievement Stats -->
<div class="achievement-stats" id="achievementStats" style="display: none;">
<div class="stat-card achievement-stat">
<div class="stat-number" id="totalPoints">0</div>
<div class="stat-label" data-de="Gesamtpunkte" data-en="Total Points">Gesamtpunkte</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="completedAchievements">0</div>
<div class="stat-label" data-de="Abgeschlossen" data-en="Completed">Abgeschlossen</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="achievementsToday">0</div>
<div class="stat-label" data-de="Heute erreicht" data-en="Achieved Today">Heute erreicht</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="completionPercentage">0%</div>
<div class="stat-label" data-de="Fortschritt" data-en="Progress">Fortschritt</div>
</div>
</div>
<!-- Achievement Categories -->
<div class="achievement-categories" id="achievementCategories" style="display: none;">
<div class="category-tabs">
<button class="category-tab active" onclick="showAchievementCategory('all')" data-category="all" data-de="Alle" data-en="All">Alle</button>
<button class="category-tab" onclick="showAchievementCategory('consistency')" data-category="consistency" data-de="Konsistenz" data-en="Consistency">Konsistenz</button>
<button class="category-tab" onclick="showAchievementCategory('improvement')" data-category="improvement" data-de="Verbesserung" data-en="Improvement">Verbesserung</button>
<button class="category-tab" onclick="showAchievementCategory('seasonal')" data-category="seasonal" data-de="Saisonal" data-en="Seasonal">Saisonal</button>
<button class="category-tab" onclick="showAchievementCategory('monthly')" data-category="monthly" data-de="Monatlich" data-en="Monthly">Monatlich</button>
</div>
<div class="achievements-grid" id="achievementsGrid">
<!-- Achievements will be populated here -->
</div>
</div>
<!-- Achievement Loading State -->
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
<div class="spinner"></div>
<p data-de="Lade deine Achievements..." data-en="Loading your achievements...">Lade deine Achievements...</p>
</div>
<!-- Achievement Not Available State -->
<div id="achievementsNotAvailable" class="achievements-not-available" style="display: none;">
<div class="not-available-content">
<div class="not-available-icon">🏆</div>
<h3 data-de="Achievements noch nicht verfügbar" data-en="Achievements not available yet">Achievements noch nicht verfügbar</h3>
<p data-de="Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren." data-en="To collect achievements, you must first link your RFID card with your account and complete some runs.">Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren.</p>
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
🏷️ RFID jetzt verknüpfen
</button>
</div>
</div>
</div>
</div>
</div>
<!-- RFID Settings Modal -->
<div id="rfidModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" data-de="📱 RFID QR-Code Scanner" data-en="📱 RFID QR Code Scanner">📱 RFID QR-Code Scanner</h2>
<span class="close" onclick="closeModal('rfidModal')">&times;</span>
</div>
<div id="rfidMessage"></div>
<!-- QR Scanner Step -->
<div id="qrScannerStep">
<p style="color: #8892b0; margin-bottom: 1.5rem; text-align: center;" data-de="Scanne den QR-Code auf deiner RFID-Karte, um sie mit deinem Account zu verknüpfen." data-en="Scan the QR code on your RFID card to link it with your account.">
Scanne den QR-Code auf deiner RFID-Karte, um sie mit deinem Account zu verknüpfen.
</p>
<!-- Camera Preview -->
<div id="cameraContainer" style="display: none;">
<video id="qrVideo" style="width: 100%; max-width: 400px; border-radius: 0.75rem; margin: 0 auto; display: block;"></video>
<canvas id="qrCanvas" style="display: none;"></canvas>
</div>
<!-- Scanner Controls -->
<div style="text-align: center; margin: 1.5rem 0;">
<button class="btn btn-primary" onclick="startQRScanner()" id="startScanBtn" data-de="📷 Kamera starten" data-en="📷 Start Camera">
📷 Kamera starten
</button>
<button class="btn btn-secondary" onclick="stopQRScanner()" id="stopScanBtn" style="display: none;" data-de="🛑 Scanner stoppen" data-en="🛑 Stop Scanner">
🛑 Scanner stoppen
</button>
</div>
<!-- Manual Input Fallback -->
<div style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
<p style="color: #8892b0; text-align: center; margin-bottom: 1rem; font-size: 0.9rem;" data-de="Kamera funktioniert nicht? RFID UID manuell eingeben:" data-en="Camera not working? Enter RFID UID manually:">
Kamera funktioniert nicht? RFID UID manuell eingeben:
</p>
<div class="form-group">
<input type="text" id="manualRfidInput" class="form-input" placeholder="z.B. aaaaaa, FFFFFF oder FF:FF:FF:FF" style="text-align: center; font-family: monospace;" data-de="z.B. aaaaaa, FFFFFF oder FF:FF:FF:FF" data-en="e.g. aaaaaa, FFFFFF or FF:FF:FF:FF">
</div>
<button class="btn btn-secondary" onclick="linkManualRfid()" style="width: 100%;" data-de="Manuell verknüpfen" data-en="Link Manually">
Manuell verknüpfen
</button>
</div>
<!-- Create New Player Section -->
<div id="createPlayerSection" style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
<p style="color: #8892b0; text-align: center; margin-bottom: 1rem; font-size: 0.9rem;" data-de="Neuen Spieler mit RFID erstellen:" data-en="Create new player with RFID:">
Neuen Spieler mit RFID erstellen:
</p>
<div class="form-group">
<label for="playerFirstname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Vorname:" data-en="First Name:">Vorname:</label>
<input type="text" id="playerFirstname" class="form-input" placeholder="Max" style="text-align: center;">
</div>
<div class="form-group">
<label for="playerLastname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Nachname:" data-en="Last Name:">Nachname:</label>
<input type="text" id="playerLastname" class="form-input" placeholder="Mustermann" style="text-align: center;">
</div>
<div class="form-group">
<label for="playerBirthdate" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Geburtsdatum:" data-en="Birth Date:">Geburtsdatum:</label>
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
</div>
<!-- AGB Section -->
<div class="agb-section" style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; margin: 15px 0;">
<div class="agb-checkbox" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<input type="checkbox" id="agbAccepted" name="agbAccepted" required style="width: auto; margin: 0; margin-top: 3px;">
<label for="agbAccepted" style="color: #e2e8f0; font-size: 0.85rem; line-height: 1.4; margin: 0; font-weight: normal;">
Ich habe die <a href="/agb.html" target="_blank" style="color: #00d4ff; text-decoration: none; font-weight: bold;">Allgemeinen Geschäftsbedingungen</a>
gelesen und stimme zu, dass mein Name und meine Laufzeiten im öffentlichen Leaderboard angezeigt werden.
</label>
</div>
<div class="agb-warning" style="color: #fbbf24; font-size: 0.8rem; margin-top: 10px;">
⚠️ <strong>Wichtig:</strong> Ohne Zustimmung zu den AGB können Sie das System nutzen,
aber Ihre Zeiten werden nicht im öffentlichen Leaderboard angezeigt.
</div>
</div>
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
Spieler erstellen
</button>
</div>
<!-- Scanning Status -->
<div id="scanningStatus" style="display: none; text-align: center; color: #00d4ff; margin-top: 1rem;">
<div class="spinner" style="width: 20px; height: 20px; margin: 0 auto 0.5rem;"></div>
<span data-de="Suche nach QR-Code..." data-en="Searching for QR code...">Suche nach QR-Code...</span>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" data-de="⚙️ Einstellungen" data-en="⚙️ Settings">⚙️ Einstellungen</h2>
<span class="close" onclick="closeModal('settingsModal')">&times;</span>
</div>
<div class="settings-content">
<div class="setting-item">
<div class="setting-info">
<h3 data-de="🏆 Leaderboard Sichtbarkeit" data-en="🏆 Leaderboard Visibility">🏆 Leaderboard Sichtbarkeit</h3>
<p data-de="Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen." data-en="Determine whether your times should be displayed in the global leaderboard.">Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen.</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="showInLeaderboard" onchange="updateLeaderboardSetting()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-description">
<p style="color: #8892b0; font-size: 0.9rem; margin-top: 1rem; padding: 1rem; background: #1e293b; border-radius: 0.5rem;" data-de="<strong>Hinweis:</strong> Wenn diese Option deaktiviert ist, werden deine Zeiten nur in deinem persönlichen Dashboard angezeigt, aber nicht im öffentlichen Leaderboard. Du kannst diese Einstellung jederzeit ändern." data-en="<strong>Note:</strong> If this option is disabled, your times will only be displayed in your personal dashboard, but not in the public leaderboard. You can change this setting at any time.">
<strong>Hinweis:</strong> Wenn diese Option deaktiviert ist, werden deine Zeiten nur in deinem persönlichen Dashboard angezeigt, aber nicht im öffentlichen Leaderboard. Du kannst diese Einstellung jederzeit ändern.
</p>
</div>
<div class="settings-actions">
<button class="btn btn-primary" onclick="saveSettings()" data-de="Einstellungen speichern" data-en="Save Settings">Einstellungen speichern</button>
<button class="btn btn-secondary" onclick="closeModal('settingsModal')" data-de="Abbrechen" data-en="Cancel">Abbrechen</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link" data-de="Impressum" data-en="Imprint">Impressum</a>
<a href="/datenschutz.html" class="footer-link" data-de="Datenschutz" data-en="Privacy">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn" data-de="Cookie-Einstellungen" data-en="Cookie Settings">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p data-de="&copy; 2024 NinjaCross. Alle Rechte vorbehalten." data-en="&copy; 2024 NinjaCross. All rights reserved.">&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/dashboard.js?v=1.6"></script>
<script>
// PWA Installation
let deferredPrompt;
// Listen for PWA install prompt
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'inline-block';
}
});
// Install PWA
async function installPWA() {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`PWA install outcome: ${outcome}`);
deferredPrompt = null;
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'none';
}
} else if (isIOS()) {
// Show iOS installation instructions
showIOSPWAHint();
}
}
// Check if PWA is already installed
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'none';
}
});
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', function () {
// Show PWA hint for iOS users
if (isIOS() && !isPWAInstalled()) {
setTimeout(showIOSPWAHint, 2000); // Show after 2 seconds
}
});
</script>
</body>
</html>