903 lines
50 KiB
HTML
903 lines
50 KiB
HTML
<!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')">×</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')">×</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="© 2024 NinjaCross. Alle Rechte vorbehalten." data-en="© 2024 NinjaCross. All rights reserved.">© 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>
|