Add real country flags to language selector
- Replace emoji flags with SVG-based country flags - German flag: Black-Red-Gold (official colors) - USA flag: Red-White-Blue with stars - Dynamic flag switching on language change - Applied to both dashboard and leaderboard - Database achievements now support English translations - Extended achievements table with name_en and description_en columns - Updated API routes to return English translations - Simplified frontend translation system to use database translations
This commit is contained in:
@@ -11,7 +11,7 @@ body {
|
||||
background: #0a0a0f;
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
background-image:
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
||||
@@ -24,6 +24,43 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Language Selector */
|
||||
.language-selector {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
left: 2rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: #ffffff;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
padding-left: 2.5rem;
|
||||
background-image: 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>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0.5rem center;
|
||||
background-size: 20px 15px;
|
||||
}
|
||||
|
||||
.language-selector select:hover {
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.language-selector select:focus {
|
||||
outline: none;
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
@@ -151,8 +188,13 @@ body {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@@ -893,8 +935,10 @@ body {
|
||||
/* Touch-friendly improvements for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.btn {
|
||||
min-height: 44px; /* Apple's recommended minimum touch target */
|
||||
touch-action: manipulation; /* Prevents double-tap zoom */
|
||||
min-height: 44px;
|
||||
/* Apple's recommended minimum touch target */
|
||||
touch-action: manipulation;
|
||||
/* Prevents double-tap zoom */
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -930,7 +974,8 @@ body {
|
||||
/* Better spacing for mobile */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 5rem; /* Account for fixed nav */
|
||||
margin-top: 5rem;
|
||||
/* Account for fixed nav */
|
||||
}
|
||||
|
||||
/* Improve modal usability on mobile */
|
||||
@@ -1019,7 +1064,7 @@ body {
|
||||
text-align: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.footer-links {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@@ -1292,6 +1337,7 @@ body {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
@@ -1340,33 +1386,33 @@ body {
|
||||
.achievements-header h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.achievement-stats {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.achievement-stat {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
|
||||
.category-tabs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.category-tab {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.achievements-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.achievement-notification {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
@@ -1452,11 +1498,11 @@ body {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
input:checked+.toggle-slider {
|
||||
background-color: #00d4ff;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
input:checked+.toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
@@ -1482,17 +1528,17 @@ input:checked + .toggle-slider:before {
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.setting-control {
|
||||
margin-left: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
|
||||
.settings-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.settings-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ body {
|
||||
background: #0a0a0f;
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
background-image:
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
||||
@@ -43,6 +43,11 @@ body {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
padding-left: 2.5rem;
|
||||
background-image: 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>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0.5rem center;
|
||||
background-size: 20px 15px;
|
||||
}
|
||||
|
||||
.language-selector select:hover {
|
||||
@@ -177,13 +182,18 @@ body {
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
animation: loading-sweep 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading-sweep {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Horizontal Time Tabs */
|
||||
@@ -524,9 +534,17 @@ body {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Notification Bubble Styles */
|
||||
@@ -587,12 +605,19 @@ body {
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
|
||||
0%,
|
||||
20%,
|
||||
50%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
@@ -603,6 +628,7 @@ body {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
@@ -614,6 +640,7 @@ body {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-20px);
|
||||
@@ -649,7 +676,8 @@ body {
|
||||
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 5rem; /* Account for fixed buttons */
|
||||
margin-top: 5rem;
|
||||
/* Account for fixed buttons */
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
@@ -789,7 +817,9 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-login-btn, .dashboard-btn, .logout-btn {
|
||||
.admin-login-btn,
|
||||
.dashboard-btn,
|
||||
.logout-btn {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
@@ -1088,10 +1118,10 @@ body {
|
||||
text-align: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.footer-links {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,83 +70,91 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<!-- 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>
|
||||
<a href="/" class="btn btn-primary">Back to Times</a>
|
||||
<button class="btn btn-logout" onclick="logout()">Logout</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 class="header-section">
|
||||
<h1 class="main-title">DEIN DASHBOARD</h1>
|
||||
<p class="tagline">Verwalte deine Läufe in der NINJACROSS ARENA</p>
|
||||
<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>Lade dein Dashboard...</p>
|
||||
<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>Dein Dashboard 🥷</h2>
|
||||
<p>Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
|
||||
<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">
|
||||
<h3>📊 Analytics</h3>
|
||||
<p>Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren.</p>
|
||||
<h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h3>
|
||||
<p data-de="Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren." data-en="Track your performance and monitor important metrics. This section will show detailed analyses once we implement the feature.">Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren.</p>
|
||||
</div>
|
||||
|
||||
<div class="card" onclick="showSettings()" style="cursor: pointer;">
|
||||
<h3>⚙️ Settings</h3>
|
||||
<p>Verwalte deine Privatsphäre-Einstellungen und andere Optionen.</p>
|
||||
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showSettings();">Einstellungen öffnen</button>
|
||||
<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>🏷️ RFID Verknüpfung</h3>
|
||||
<p>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();">RFID verknüpfen</button>
|
||||
<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">
|
||||
<h3>📊 Statistiken</h3>
|
||||
<p>Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern.</p>
|
||||
<h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h3>
|
||||
<p data-de="Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern." data-en="Detailed statistics about your runs will be displayed here soon - best times, improvements and comparisons with other players.">Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Times Section -->
|
||||
<div class="times-section">
|
||||
<div class="times-header">
|
||||
<h2>🏃♂️ Meine Zeiten</h2>
|
||||
<p>Deine persönlichen Bestzeiten an allen Standorten</p>
|
||||
<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>Lade deine Zeiten...</p>
|
||||
<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>RFID noch nicht verknüpft</h3>
|
||||
<p>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()">
|
||||
<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>So funktioniert's:</h4>
|
||||
<h4 data-de="So funktioniert's:" data-en="How it works:">So funktioniert's:</h4>
|
||||
<ol>
|
||||
<li>Klicke auf "RFID jetzt verknüpfen"</li>
|
||||
<li>Scanne den QR-Code auf deiner RFID-Karte</li>
|
||||
<li>Fertig! Deine Zeiten werden automatisch hier angezeigt</li>
|
||||
<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>
|
||||
@@ -157,19 +165,19 @@
|
||||
<div class="times-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalRuns">0</div>
|
||||
<div class="stat-label">Gesamte Läufe</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">Beste Zeit</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">Standorte</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">Verknüpfter Spieler</div>
|
||||
<div class="stat-label" data-de="Verknüpfter Spieler" data-en="Linked Player">Verknüpfter Spieler</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,38 +192,38 @@
|
||||
<!-- Achievements Section -->
|
||||
<div class="achievements-section">
|
||||
<div class="achievements-header">
|
||||
<h2>🏆 Meine Achievements</h2>
|
||||
<p>Sammele Punkte und erreiche neue Meilensteine!</p>
|
||||
<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">Gesamtpunkte</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">Abgeschlossen</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">Heute erreicht</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">Fortschritt</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">Alle</button>
|
||||
<button class="category-tab" onclick="showAchievementCategory('consistency')" data-category="consistency">Konsistenz</button>
|
||||
<button class="category-tab" onclick="showAchievementCategory('improvement')" data-category="improvement">Verbesserung</button>
|
||||
<button class="category-tab" onclick="showAchievementCategory('seasonal')" data-category="seasonal">Saisonal</button>
|
||||
<button class="category-tab" onclick="showAchievementCategory('monthly')" data-category="monthly">Monatlich</button>
|
||||
<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">
|
||||
@@ -226,16 +234,16 @@
|
||||
<!-- Achievement Loading State -->
|
||||
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>Lade deine Achievements...</p>
|
||||
<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>Achievements noch nicht verfügbar</h3>
|
||||
<p>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()">
|
||||
<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>
|
||||
@@ -248,14 +256,14 @@
|
||||
<div id="rfidModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">📱 RFID QR-Code Scanner</h2>
|
||||
<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;">
|
||||
<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>
|
||||
|
||||
@@ -267,23 +275,23 @@
|
||||
|
||||
<!-- Scanner Controls -->
|
||||
<div style="text-align: center; margin: 1.5rem 0;">
|
||||
<button class="btn btn-primary" onclick="startQRScanner()" id="startScanBtn">
|
||||
<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;">
|
||||
<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;">
|
||||
<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;">
|
||||
<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%;">
|
||||
<button class="btn btn-secondary" onclick="linkManualRfid()" style="width: 100%;" data-de="Manuell verknüpfen" data-en="Link Manually">
|
||||
Manuell verknüpfen
|
||||
</button>
|
||||
</div>
|
||||
@@ -291,7 +299,7 @@
|
||||
<!-- 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>
|
||||
Suche nach QR-Code...
|
||||
<span data-de="Suche nach QR-Code..." data-en="Searching for QR code...">Suche nach QR-Code...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,15 +309,15 @@
|
||||
<div id="settingsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">⚙️ Einstellungen</h2>
|
||||
<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>🏆 Leaderboard Sichtbarkeit</h3>
|
||||
<p>Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen.</p>
|
||||
<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">
|
||||
@@ -320,14 +328,14 @@
|
||||
</div>
|
||||
|
||||
<div class="setting-description">
|
||||
<p style="color: #8892b0; font-size: 0.9rem; margin-top: 1rem; padding: 1rem; background: #1e293b; border-radius: 0.5rem;">
|
||||
<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()">Einstellungen speichern</button>
|
||||
<button class="btn btn-secondary" onclick="closeModal('settingsModal')">Abbrechen</button>
|
||||
<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>
|
||||
@@ -337,12 +345,12 @@
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-links">
|
||||
<a href="/impressum.html" class="footer-link">Impressum</a>
|
||||
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
|
||||
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
|
||||
<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>© 2024 NinjaCross. Alle Rechte vorbehalten.</p>
|
||||
<p data-de="© 2024 NinjaCross. Alle Rechte vorbehalten." data-en="© 2024 NinjaCross. All rights reserved.">© 2024 NinjaCross. Alle Rechte vorbehalten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<!-- Language Selector -->
|
||||
<div class="language-selector">
|
||||
<select id="languageSelect" onchange="changeLanguage()">
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
<option value="en">🇺🇸 English</option>
|
||||
<option value="de" data-flag="🇩🇪">Deutsch</option>
|
||||
<option value="en" data-flag="🇺🇸">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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() {
|
||||
@@ -88,8 +89,123 @@ supabase.auth.onAuthStateChange((event, session) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Language Management
|
||||
function translateElement(element, language) {
|
||||
if (element.dataset[language]) {
|
||||
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();
|
||||
|
||||
// Reload achievements if they're loaded
|
||||
if (window.allAchievements && window.allAchievements.length > 0) {
|
||||
displayAchievements();
|
||||
}
|
||||
|
||||
// Reload times if they're loaded
|
||||
if (document.getElementById('timesDisplay').style.display !== 'none') {
|
||||
// Times are displayed, reload them
|
||||
checkLinkStatusAndLoadTimes();
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved language preference
|
||||
function loadLanguagePreference() {
|
||||
const savedLanguage = localStorage.getItem('ninjacross_language');
|
||||
if (savedLanguage && (savedLanguage === 'de' || savedLanguage === 'en')) {
|
||||
currentLanguage = savedLanguage;
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
languageSelect.value = currentLanguage;
|
||||
// Update flag when loading
|
||||
updateLanguageFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update achievement notifications
|
||||
function updateAchievementNotifications() {
|
||||
// This will be called when achievements are displayed
|
||||
}
|
||||
|
||||
// Update time display formats
|
||||
function updateTimeDisplayFormats() {
|
||||
// This will be called when times are displayed
|
||||
}
|
||||
|
||||
// Update achievement progress text
|
||||
function updateAchievementProgressText() {
|
||||
// This will be called when achievements are displayed
|
||||
}
|
||||
|
||||
// Achievement translations are now handled by the database
|
||||
|
||||
// Translate achievement data using database translations
|
||||
function translateAchievement(achievement) {
|
||||
if (currentLanguage === 'en' && achievement.name_en) {
|
||||
return {
|
||||
...achievement,
|
||||
name: achievement.name_en,
|
||||
description: achievement.description_en || achievement.description
|
||||
};
|
||||
}
|
||||
return achievement;
|
||||
}
|
||||
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
initDashboard();
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadLanguagePreference();
|
||||
changeLanguage(); // Apply saved language
|
||||
initDashboard();
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
function openModal(modalId) {
|
||||
@@ -185,7 +301,10 @@ async function startQRScanner() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error accessing camera:', error);
|
||||
showMessage('rfidMessage', 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.', 'error');
|
||||
const cameraErrorMsg = currentLanguage === 'de' ?
|
||||
'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.' :
|
||||
'Camera access failed. Please use manual input.';
|
||||
showMessage('rfidMessage', cameraErrorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,21 +384,30 @@ async function handleQRCodeDetected(qrData) {
|
||||
const rawUid = qrData.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'QR-Code enthält keine gültige RFID UID', 'error');
|
||||
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);
|
||||
|
||||
showMessage('rfidMessage', `QR-Code erkannt: ${rawUid} → ${formattedUid}`, 'info');
|
||||
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);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren der RFID UID: ${error.message}`, 'error');
|
||||
const formatErrorMsg = currentLanguage === 'de' ?
|
||||
`Fehler beim Formatieren der RFID UID: ${error.message}` :
|
||||
`Error formatting RFID UID: ${error.message}`;
|
||||
showMessage('rfidMessage', formatErrorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +416,10 @@ async function linkManualRfid() {
|
||||
const rawUid = document.getElementById('manualRfidInput').value.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'Bitte gib eine RFID UID ein', 'error');
|
||||
const inputErrorMsg = currentLanguage === 'de' ?
|
||||
'Bitte gib eine RFID UID ein' :
|
||||
'Please enter a RFID UID';
|
||||
showMessage('rfidMessage', inputErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,20 +427,29 @@ async function linkManualRfid() {
|
||||
// Format the UID to match database format
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
showMessage('rfidMessage', `Formatiert: ${rawUid} → ${formattedUid}`, 'info');
|
||||
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);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren: ${error.message}`, 'error');
|
||||
const formatErrorMsg = currentLanguage === 'de' ?
|
||||
`Fehler beim Formatieren: ${error.message}` :
|
||||
`Error formatting: ${error.message}`;
|
||||
showMessage('rfidMessage', formatErrorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Link user by RFID UID (core function)
|
||||
async function linkUserByRfidUid(rfidUid) {
|
||||
if (!currentUser) {
|
||||
showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error');
|
||||
const authErrorMsg = currentLanguage === 'de' ?
|
||||
'Benutzer nicht authentifiziert' :
|
||||
'User not authenticated';
|
||||
showMessage('rfidMessage', authErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,18 +469,27 @@ async function linkUserByRfidUid(rfidUid) {
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showMessage('rfidMessage', `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}`, 'success');
|
||||
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 {
|
||||
showMessage('rfidMessage', result.message || 'Fehler beim Verknüpfen', 'error');
|
||||
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);
|
||||
showMessage('rfidMessage', 'Fehler beim Verknüpfen der RFID', 'error');
|
||||
const linkErrorMsg = currentLanguage === 'de' ?
|
||||
'Fehler beim Verknüpfen der RFID' :
|
||||
'Error linking RFID';
|
||||
showMessage('rfidMessage', linkErrorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +498,50 @@ 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 loading state
|
||||
@@ -356,6 +549,14 @@ 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
|
||||
@@ -422,10 +623,15 @@ 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>Noch keine Zeiten aufgezeichnet</h3>
|
||||
<p>Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!</p>
|
||||
<h3>${noTimesTitle}</h3>
|
||||
<p>${noTimesDescription}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -456,7 +662,7 @@ function displayUserTimes(times) {
|
||||
let rankClass = '';
|
||||
|
||||
if (runIndex === 0) {
|
||||
rankBadge = '🥇 Beste';
|
||||
rankBadge = currentLanguage === 'de' ? '🥇 Beste' : '🥇 Best';
|
||||
rankClass = 'best';
|
||||
} else if (runIndex === 1) {
|
||||
rankBadge = '🥈 2.';
|
||||
@@ -475,8 +681,8 @@ function displayUserTimes(times) {
|
||||
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
||||
</div>
|
||||
<div class="run-details">
|
||||
<div>${new Date(run.created_at).toLocaleDateString('de-DE')}</div>
|
||||
<div>${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
<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>
|
||||
@@ -493,13 +699,13 @@ function displayUserTimes(times) {
|
||||
<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('de-DE')}</span>
|
||||
<span class="time-rank">${locationTimes.length} Läufe</span>
|
||||
<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">Alle Läufe an diesem Standort:</div>
|
||||
<div class="all-runs-title">${currentLanguage === 'de' ? 'Alle Läufe an diesem Standort:' : 'All runs at this location:'}</div>
|
||||
${allRunsHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -609,6 +815,14 @@ async function loadPlayerAchievements() {
|
||||
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) {
|
||||
@@ -668,11 +882,16 @@ 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>Noch keine Achievements</h3>
|
||||
<p>Starte deine ersten Läufe, um Achievements zu sammeln!</p>
|
||||
<h3>${noAchievementsTitle}</h3>
|
||||
<p>${noAchievementsDescription}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -692,6 +911,9 @@ function displayAchievements() {
|
||||
const progress = achievement.progress || 0;
|
||||
const earnedAt = achievement.earned_at;
|
||||
|
||||
// Translate achievement
|
||||
const translatedAchievement = translateAchievement(achievement);
|
||||
|
||||
// Debug logging
|
||||
if (achievement.name === 'Tageskönig') {
|
||||
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
|
||||
@@ -699,9 +921,11 @@ function displayAchievements() {
|
||||
|
||||
let progressText = '';
|
||||
if (isCompleted) {
|
||||
const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on';
|
||||
const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed';
|
||||
progressText = earnedAt ?
|
||||
`Erreicht am ${new Date(earnedAt).toLocaleDateString('de-DE')}` :
|
||||
'Abgeschlossen';
|
||||
`${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);
|
||||
@@ -710,15 +934,17 @@ function displayAchievements() {
|
||||
}
|
||||
}
|
||||
|
||||
const pointsText = currentLanguage === 'de' ? 'Punkte' : '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">${achievement.name}</h4>
|
||||
<p class="achievement-description">${achievement.description}</p>
|
||||
<h4 class="achievement-name">${translatedAchievement.name}</h4>
|
||||
<p class="achievement-description">${translatedAchievement.description}</p>
|
||||
<div class="achievement-meta">
|
||||
<span class="achievement-points">+${achievement.points} Punkte</span>
|
||||
<span class="achievement-points">+${achievement.points} ${pointsText}</span>
|
||||
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -780,6 +1006,51 @@ function showAchievementsNotAvailable() {
|
||||
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
|
||||
@@ -810,12 +1081,18 @@ 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>Neue Achievements erreicht!</h4>
|
||||
<p>Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!</p>
|
||||
<h4>${titleText}</h4>
|
||||
<p>${descriptionText}</p>
|
||||
</div>
|
||||
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
</div>
|
||||
@@ -874,27 +1151,27 @@ async function checkBestTimeNotifications() {
|
||||
// Check if current player has best times
|
||||
if (currentPlayerId) {
|
||||
if (daily && daily.player_id === currentPlayerId) {
|
||||
showWebNotification(
|
||||
'🏆 Tageskönig!',
|
||||
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`,
|
||||
'👑'
|
||||
);
|
||||
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
|
||||
const message = currentLanguage === 'de' ?
|
||||
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
|
||||
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
|
||||
showWebNotification(title, message, '👑');
|
||||
}
|
||||
|
||||
if (weekly && weekly.player_id === currentPlayerId) {
|
||||
showWebNotification(
|
||||
'🏆 Wochenchampion!',
|
||||
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!`,
|
||||
'🏆'
|
||||
);
|
||||
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
|
||||
const message = currentLanguage === 'de' ?
|
||||
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
|
||||
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
|
||||
showWebNotification(title, message, '🏆');
|
||||
}
|
||||
|
||||
if (monthly && monthly.player_id === currentPlayerId) {
|
||||
showWebNotification(
|
||||
'🏆 Monatsmeister!',
|
||||
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!`,
|
||||
'🥇'
|
||||
);
|
||||
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, '🥇');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,9 +1198,10 @@ async function checkAchievementNotifications() {
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
newAchievements.forEach(achievement => {
|
||||
const translatedAchievement = translateAchievement(achievement);
|
||||
showWebNotification(
|
||||
`🏆 ${achievement.name}`,
|
||||
achievement.description,
|
||||
`🏆 ${translatedAchievement.name}`,
|
||||
translatedAchievement.description,
|
||||
achievement.icon || '🏆'
|
||||
);
|
||||
});
|
||||
@@ -956,11 +1234,11 @@ async function loadSettings() {
|
||||
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;
|
||||
@@ -1006,14 +1284,23 @@ async function saveSettings() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification('Einstellungen erfolgreich gespeichert!', 'success');
|
||||
const successMsg = currentLanguage === 'de' ?
|
||||
'Einstellungen erfolgreich gespeichert!' :
|
||||
'Settings saved successfully!';
|
||||
showNotification(successMsg, 'success');
|
||||
closeModal('settingsModal');
|
||||
} else {
|
||||
showNotification('Fehler beim Speichern der Einstellungen: ' + result.message, 'error');
|
||||
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);
|
||||
showNotification('Fehler beim Speichern der Einstellungen', 'error');
|
||||
const errorMsg = currentLanguage === 'de' ?
|
||||
'Fehler beim Speichern der Einstellungen' :
|
||||
'Error saving settings';
|
||||
showNotification(errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -643,6 +643,9 @@ function changeLanguage() {
|
||||
// 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 => {
|
||||
@@ -655,6 +658,20 @@ function changeLanguage() {
|
||||
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 location select placeholder
|
||||
@@ -710,6 +727,8 @@ function loadLanguagePreference() {
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
languageSelect.value = currentLanguage;
|
||||
// Update flag when loading
|
||||
updateLanguageFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2627,7 +2627,7 @@ router.get('/v1/public/push-status', async (req, res) => {
|
||||
router.get('/achievements', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT id, name, description, category, icon, points, is_active
|
||||
SELECT id, name, name_en, description, description_en, category, icon, points, is_active
|
||||
FROM achievements
|
||||
WHERE is_active = true
|
||||
ORDER BY category, points DESC
|
||||
@@ -2701,7 +2701,9 @@ router.get('/achievements/player/:playerId', async (req, res) => {
|
||||
SELECT
|
||||
a.id,
|
||||
a.name,
|
||||
a.name_en,
|
||||
a.description,
|
||||
a.description_en,
|
||||
a.category,
|
||||
a.icon,
|
||||
a.points,
|
||||
|
||||
Reference in New Issue
Block a user