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:
2025-09-10 19:40:57 +02:00
parent 11d0647ab9
commit 340e22a815
7 changed files with 541 additions and 149 deletions

View File

@@ -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%;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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')">&times;</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')">&times;</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>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
<p data-de="&copy; 2024 NinjaCross. Alle Rechte vorbehalten." data-en="&copy; 2024 NinjaCross. All rights reserved.">&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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();
}
}
}

View File

@@ -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,