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;
|
background: #0a0a0f;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
||||||
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
||||||
@@ -24,6 +24,43 @@ body {
|
|||||||
min-height: 100vh;
|
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 {
|
.header-section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
@@ -151,8 +188,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@@ -893,8 +935,10 @@ body {
|
|||||||
/* Touch-friendly improvements for mobile */
|
/* Touch-friendly improvements for mobile */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 44px; /* Apple's recommended minimum touch target */
|
min-height: 44px;
|
||||||
touch-action: manipulation; /* Prevents double-tap zoom */
|
/* Apple's recommended minimum touch target */
|
||||||
|
touch-action: manipulation;
|
||||||
|
/* Prevents double-tap zoom */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -930,7 +974,8 @@ body {
|
|||||||
/* Better spacing for mobile */
|
/* Better spacing for mobile */
|
||||||
.header-section {
|
.header-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
margin-top: 5rem; /* Account for fixed nav */
|
margin-top: 5rem;
|
||||||
|
/* Account for fixed nav */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improve modal usability on mobile */
|
/* Improve modal usability on mobile */
|
||||||
@@ -1019,7 +1064,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1292,6 +1337,7 @@ body {
|
|||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -1340,33 +1386,33 @@ body {
|
|||||||
.achievements-header h2 {
|
.achievements-header h2 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-stats {
|
.achievement-stats {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-stat {
|
.achievement-stat {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-tabs {
|
.category-tabs {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-tab {
|
.category-tab {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievements-grid {
|
.achievements-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-notification {
|
.achievement-notification {
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@@ -1452,11 +1498,11 @@ body {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-slider {
|
input:checked+.toggle-slider {
|
||||||
background-color: #00d4ff;
|
background-color: #00d4ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-slider:before {
|
input:checked+.toggle-slider:before {
|
||||||
transform: translateX(26px);
|
transform: translateX(26px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1482,17 +1528,17 @@ input:checked + .toggle-slider:before {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-control {
|
.setting-control {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-actions {
|
.settings-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-actions .btn {
|
.settings-actions .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ body {
|
|||||||
background: #0a0a0f;
|
background: #0a0a0f;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
|
||||||
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
|
||||||
@@ -43,6 +43,11 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
backdrop-filter: blur(10px);
|
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 {
|
.language-selector select:hover {
|
||||||
@@ -177,13 +182,18 @@ body {
|
|||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
animation: loading-sweep 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading-sweep {
|
@keyframes loading-sweep {
|
||||||
0% { left: -100%; }
|
0% {
|
||||||
100% { left: 100%; }
|
left: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Horizontal Time Tabs */
|
/* Horizontal Time Tabs */
|
||||||
@@ -524,9 +534,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
50% { opacity: 0.7; }
|
opacity: 1;
|
||||||
100% { opacity: 1; }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notification Bubble Styles */
|
/* Notification Bubble Styles */
|
||||||
@@ -587,12 +605,19 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 20%, 50%, 80%, 100% {
|
|
||||||
|
0%,
|
||||||
|
20%,
|
||||||
|
50%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
40% {
|
40% {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
60% {
|
60% {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
@@ -603,6 +628,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-50%) translateY(-20px);
|
transform: translateX(-50%) translateY(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
@@ -614,6 +640,7 @@ body {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-50%) translateY(-20px);
|
transform: translateX(-50%) translateY(-20px);
|
||||||
@@ -649,7 +676,8 @@ body {
|
|||||||
|
|
||||||
.header-section {
|
.header-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
margin-top: 5rem; /* Account for fixed buttons */
|
margin-top: 5rem;
|
||||||
|
/* Account for fixed buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
@@ -789,7 +817,9 @@ body {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-btn, .dashboard-btn, .logout-btn {
|
.admin-login-btn,
|
||||||
|
.dashboard-btn,
|
||||||
|
.logout-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@@ -1088,10 +1118,10 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,83 +70,91 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="main-container">
|
<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="nav-buttons">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-avatar" id="userAvatar">U</div>
|
<div class="user-avatar" id="userAvatar">U</div>
|
||||||
<span id="userEmail">user@example.com</span>
|
<span id="userEmail">user@example.com</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="/" class="btn btn-primary">Back to Times</a>
|
<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()">Logout</button>
|
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h1 class="main-title">DEIN DASHBOARD</h1>
|
<h1 class="main-title" data-de="DEIN DASHBOARD" data-en="YOUR DASHBOARD">DEIN DASHBOARD</h1>
|
||||||
<p class="tagline">Verwalte deine Läufe in der NINJACROSS ARENA</p>
|
<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>
|
||||||
<div id="loading" class="loading">
|
<div id="loading" class="loading">
|
||||||
<div class="spinner"></div>
|
<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>
|
||||||
|
|
||||||
<div id="dashboardContent" style="display: none;">
|
<div id="dashboardContent" style="display: none;">
|
||||||
<div class="welcome-card">
|
<div class="welcome-card">
|
||||||
<h2>Dein Dashboard 🥷</h2>
|
<h2 data-de="Dein Dashboard 🥷" data-en="Your Dashboard 🥷">Dein Dashboard 🥷</h2>
|
||||||
<p>Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
|
<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>
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>📊 Analytics</h3>
|
<h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h3>
|
||||||
<p>Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren.</p>
|
<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>
|
||||||
|
|
||||||
<div class="card" onclick="showSettings()" style="cursor: pointer;">
|
<div class="card" onclick="showSettings()" style="cursor: pointer;">
|
||||||
<h3>⚙️ Settings</h3>
|
<h3 data-de="⚙️ Settings" data-en="⚙️ Settings">⚙️ Settings</h3>
|
||||||
<p>Verwalte deine Privatsphäre-Einstellungen und andere Optionen.</p>
|
<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();">Einstellungen öffnen</button>
|
<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>
|
||||||
|
|
||||||
<div class="card" onclick="showRFIDSettings()" style="cursor: pointer;">
|
<div class="card" onclick="showRFIDSettings()" style="cursor: pointer;">
|
||||||
<h3>🏷️ RFID Verknüpfung</h3>
|
<h3 data-de="🏷️ RFID Verknüpfung" data-en="🏷️ RFID Linking">🏷️ RFID Verknüpfung</h3>
|
||||||
<p>Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken.</p>
|
<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();">RFID verknüpfen</button>
|
<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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>📊 Statistiken</h3>
|
<h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h3>
|
||||||
<p>Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Times Section -->
|
<!-- User Times Section -->
|
||||||
<div class="times-section">
|
<div class="times-section">
|
||||||
<div class="times-header">
|
<div class="times-header">
|
||||||
<h2>🏃♂️ Meine Zeiten</h2>
|
<h2 data-de="🏃♂️ Meine Zeiten" data-en="🏃♂️ My Times">🏃♂️ Meine Zeiten</h2>
|
||||||
<p>Deine persönlichen Bestzeiten an allen Standorten</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div id="timesLoading" class="times-loading" style="display: none;">
|
<div id="timesLoading" class="times-loading" style="display: none;">
|
||||||
<div class="spinner"></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Not Linked State -->
|
<!-- Not Linked State -->
|
||||||
<div id="timesNotLinked" class="times-not-linked">
|
<div id="timesNotLinked" class="times-not-linked">
|
||||||
<div class="not-linked-content">
|
<div class="not-linked-content">
|
||||||
<div class="not-linked-icon">🔗</div>
|
<div class="not-linked-icon">🔗</div>
|
||||||
<h3>RFID noch nicht verknüpft</h3>
|
<h3 data-de="RFID noch nicht verknüpft" data-en="RFID not linked yet">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>
|
<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()">
|
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
|
||||||
🏷️ RFID jetzt verknüpfen
|
🏷️ RFID jetzt verknüpfen
|
||||||
</button>
|
</button>
|
||||||
<div class="link-info">
|
<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>
|
<ol>
|
||||||
<li>Klicke auf "RFID jetzt verknüpfen"</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>Scanne den QR-Code auf deiner RFID-Karte</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>Fertig! Deine Zeiten werden automatisch hier angezeigt</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>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,19 +165,19 @@
|
|||||||
<div class="times-stats">
|
<div class="times-stats">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-number" id="totalRuns">0</div>
|
<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>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-number" id="bestTime">--:--</div>
|
<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>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-number" id="locationsCount">0</div>
|
<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>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-number" id="linkedPlayer">--</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,38 +192,38 @@
|
|||||||
<!-- Achievements Section -->
|
<!-- Achievements Section -->
|
||||||
<div class="achievements-section">
|
<div class="achievements-section">
|
||||||
<div class="achievements-header">
|
<div class="achievements-header">
|
||||||
<h2>🏆 Meine Achievements</h2>
|
<h2 data-de="🏆 Meine Achievements" data-en="🏆 My Achievements">🏆 Meine Achievements</h2>
|
||||||
<p>Sammele Punkte und erreiche neue Meilensteine!</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Achievement Stats -->
|
<!-- Achievement Stats -->
|
||||||
<div class="achievement-stats" id="achievementStats" style="display: none;">
|
<div class="achievement-stats" id="achievementStats" style="display: none;">
|
||||||
<div class="stat-card achievement-stat">
|
<div class="stat-card achievement-stat">
|
||||||
<div class="stat-number" id="totalPoints">0</div>
|
<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>
|
||||||
<div class="stat-card achievement-stat">
|
<div class="stat-card achievement-stat">
|
||||||
<div class="stat-number" id="completedAchievements">0</div>
|
<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>
|
||||||
<div class="stat-card achievement-stat">
|
<div class="stat-card achievement-stat">
|
||||||
<div class="stat-number" id="achievementsToday">0</div>
|
<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>
|
||||||
<div class="stat-card achievement-stat">
|
<div class="stat-card achievement-stat">
|
||||||
<div class="stat-number" id="completionPercentage">0%</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Achievement Categories -->
|
<!-- Achievement Categories -->
|
||||||
<div class="achievement-categories" id="achievementCategories" style="display: none;">
|
<div class="achievement-categories" id="achievementCategories" style="display: none;">
|
||||||
<div class="category-tabs">
|
<div class="category-tabs">
|
||||||
<button class="category-tab active" onclick="showAchievementCategory('all')" data-category="all">Alle</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">Konsistenz</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">Verbesserung</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">Saisonal</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">Monatlich</button>
|
<button class="category-tab" onclick="showAchievementCategory('monthly')" data-category="monthly" data-de="Monatlich" data-en="Monthly">Monatlich</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="achievements-grid" id="achievementsGrid">
|
<div class="achievements-grid" id="achievementsGrid">
|
||||||
@@ -226,16 +234,16 @@
|
|||||||
<!-- Achievement Loading State -->
|
<!-- Achievement Loading State -->
|
||||||
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
|
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
|
||||||
<div class="spinner"></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Achievement Not Available State -->
|
<!-- Achievement Not Available State -->
|
||||||
<div id="achievementsNotAvailable" class="achievements-not-available" style="display: none;">
|
<div id="achievementsNotAvailable" class="achievements-not-available" style="display: none;">
|
||||||
<div class="not-available-content">
|
<div class="not-available-content">
|
||||||
<div class="not-available-icon">🏆</div>
|
<div class="not-available-icon">🏆</div>
|
||||||
<h3>Achievements noch nicht verfügbar</h3>
|
<h3 data-de="Achievements noch nicht verfügbar" data-en="Achievements not available yet">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>
|
<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()">
|
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
|
||||||
🏷️ RFID jetzt verknüpfen
|
🏷️ RFID jetzt verknüpfen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -248,14 +256,14 @@
|
|||||||
<div id="rfidModal" class="modal">
|
<div id="rfidModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<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>
|
<span class="close" onclick="closeModal('rfidModal')">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="rfidMessage"></div>
|
<div id="rfidMessage"></div>
|
||||||
|
|
||||||
<!-- QR Scanner Step -->
|
<!-- QR Scanner Step -->
|
||||||
<div id="qrScannerStep">
|
<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.
|
Scanne den QR-Code auf deiner RFID-Karte, um sie mit deinem Account zu verknüpfen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -267,23 +275,23 @@
|
|||||||
|
|
||||||
<!-- Scanner Controls -->
|
<!-- Scanner Controls -->
|
||||||
<div style="text-align: center; margin: 1.5rem 0;">
|
<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
|
📷 Kamera starten
|
||||||
</button>
|
</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
|
🛑 Scanner stoppen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Input Fallback -->
|
<!-- Manual Input Fallback -->
|
||||||
<div style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
|
<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:
|
Kamera funktioniert nicht? RFID UID manuell eingeben:
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<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>
|
</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
|
Manuell verknüpfen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +299,7 @@
|
|||||||
<!-- Scanning Status -->
|
<!-- Scanning Status -->
|
||||||
<div id="scanningStatus" style="display: none; text-align: center; color: #00d4ff; margin-top: 1rem;">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,15 +309,15 @@
|
|||||||
<div id="settingsModal" class="modal">
|
<div id="settingsModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<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>
|
<span class="close" onclick="closeModal('settingsModal')">×</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<h3>🏆 Leaderboard Sichtbarkeit</h3>
|
<h3 data-de="🏆 Leaderboard Sichtbarkeit" data-en="🏆 Leaderboard Visibility">🏆 Leaderboard Sichtbarkeit</h3>
|
||||||
<p>Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen.</p>
|
<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>
|
||||||
<div class="setting-control">
|
<div class="setting-control">
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
@@ -320,14 +328,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-description">
|
<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.
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<button class="btn btn-primary" onclick="saveSettings()">Einstellungen speichern</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')">Abbrechen</button>
|
<button class="btn btn-secondary" onclick="closeModal('settingsModal')" data-de="Abbrechen" data-en="Cancel">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,12 +345,12 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/impressum.html" class="footer-link">Impressum</a>
|
<a href="/impressum.html" class="footer-link" data-de="Impressum" data-en="Imprint">Impressum</a>
|
||||||
<a href="/datenschutz.html" class="footer-link">Datenschutz</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">Cookie-Einstellungen</button>
|
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn" data-de="Cookie-Einstellungen" data-en="Cookie Settings">Cookie-Einstellungen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-text">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
<!-- Language Selector -->
|
<!-- Language Selector -->
|
||||||
<div class="language-selector">
|
<div class="language-selector">
|
||||||
<select id="languageSelect" onchange="changeLanguage()">
|
<select id="languageSelect" onchange="changeLanguage()">
|
||||||
<option value="de">🇩🇪 Deutsch</option>
|
<option value="de" data-flag="🇩🇪">Deutsch</option>
|
||||||
<option value="en">🇺🇸 English</option>
|
<option value="en" data-flag="🇺🇸">English</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
|
let currentLanguage = 'en'; // Default to English
|
||||||
|
|
||||||
// Check authentication and load dashboard
|
// Check authentication and load dashboard
|
||||||
async function initDashboard() {
|
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
|
// Initialize dashboard when page loads
|
||||||
initDashboard();
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
loadLanguagePreference();
|
||||||
|
changeLanguage(); // Apply saved language
|
||||||
|
initDashboard();
|
||||||
|
});
|
||||||
|
|
||||||
// Modal functions
|
// Modal functions
|
||||||
function openModal(modalId) {
|
function openModal(modalId) {
|
||||||
@@ -185,7 +301,10 @@ async function startQRScanner() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error accessing camera:', 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();
|
const rawUid = qrData.trim();
|
||||||
|
|
||||||
if (!rawUid) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the UID to match database format (XX:XX:XX:XX)
|
// Format the UID to match database format (XX:XX:XX:XX)
|
||||||
const formattedUid = formatRfidUid(rawUid);
|
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
|
// Link the user using the formatted RFID UID
|
||||||
await linkUserByRfidUid(formattedUid);
|
await linkUserByRfidUid(formattedUid);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error formatting RFID UID:', 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();
|
const rawUid = document.getElementById('manualRfidInput').value.trim();
|
||||||
|
|
||||||
if (!rawUid) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,20 +427,29 @@ async function linkManualRfid() {
|
|||||||
// Format the UID to match database format
|
// Format the UID to match database format
|
||||||
const formattedUid = formatRfidUid(rawUid);
|
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);
|
await linkUserByRfidUid(formattedUid);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error formatting manual RFID UID:', 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)
|
// Link user by RFID UID (core function)
|
||||||
async function linkUserByRfidUid(rfidUid) {
|
async function linkUserByRfidUid(rfidUid) {
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error');
|
const authErrorMsg = currentLanguage === 'de' ?
|
||||||
|
'Benutzer nicht authentifiziert' :
|
||||||
|
'User not authenticated';
|
||||||
|
showMessage('rfidMessage', authErrorMsg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,18 +469,27 @@ async function linkUserByRfidUid(rfidUid) {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
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(() => {
|
setTimeout(() => {
|
||||||
closeModal('rfidModal');
|
closeModal('rfidModal');
|
||||||
// Reload times section after successful linking
|
// Reload times section after successful linking
|
||||||
checkLinkStatusAndLoadTimes();
|
checkLinkStatusAndLoadTimes();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error linking RFID:', 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('timesLoading').style.display = 'none';
|
||||||
document.getElementById('timesNotLinked').style.display = 'block';
|
document.getElementById('timesNotLinked').style.display = 'block';
|
||||||
document.getElementById('timesDisplay').style.display = 'none';
|
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
|
// Show loading state
|
||||||
@@ -356,6 +549,14 @@ function showTimesLoading() {
|
|||||||
document.getElementById('timesLoading').style.display = 'block';
|
document.getElementById('timesLoading').style.display = 'block';
|
||||||
document.getElementById('timesNotLinked').style.display = 'none';
|
document.getElementById('timesNotLinked').style.display = 'none';
|
||||||
document.getElementById('timesDisplay').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
|
// Load user times for the section
|
||||||
@@ -422,10 +623,15 @@ function displayUserTimes(times) {
|
|||||||
const timesGrid = document.getElementById('userTimesGrid');
|
const timesGrid = document.getElementById('userTimesGrid');
|
||||||
|
|
||||||
if (times.length === 0) {
|
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 = `
|
timesGrid.innerHTML = `
|
||||||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
|
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
|
||||||
<h3>Noch keine Zeiten aufgezeichnet</h3>
|
<h3>${noTimesTitle}</h3>
|
||||||
<p>Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!</p>
|
<p>${noTimesDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -456,7 +662,7 @@ function displayUserTimes(times) {
|
|||||||
let rankClass = '';
|
let rankClass = '';
|
||||||
|
|
||||||
if (runIndex === 0) {
|
if (runIndex === 0) {
|
||||||
rankBadge = '🥇 Beste';
|
rankBadge = currentLanguage === 'de' ? '🥇 Beste' : '🥇 Best';
|
||||||
rankClass = 'best';
|
rankClass = 'best';
|
||||||
} else if (runIndex === 1) {
|
} else if (runIndex === 1) {
|
||||||
rankBadge = '🥈 2.';
|
rankBadge = '🥈 2.';
|
||||||
@@ -475,8 +681,8 @@ function displayUserTimes(times) {
|
|||||||
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="run-details">
|
<div class="run-details">
|
||||||
<div>${new Date(run.created_at).toLocaleDateString('de-DE')}</div>
|
<div>${new Date(run.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</div>
|
||||||
<div>${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</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>
|
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,13 +699,13 @@ function displayUserTimes(times) {
|
|||||||
<div class="card-main-content">
|
<div class="card-main-content">
|
||||||
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
|
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
|
||||||
<div class="time-date-info">
|
<div class="time-date-info">
|
||||||
<span>${new Date(bestTime.created_at).toLocaleDateString('de-DE')}</span>
|
<span>${new Date(bestTime.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</span>
|
||||||
<span class="time-rank">${locationTimes.length} Läufe</span>
|
<span class="time-rank">${locationTimes.length} ${currentLanguage === 'de' ? 'Läufe' : 'Runs'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="expanded-content">
|
<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}
|
${allRunsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -609,6 +815,14 @@ async function loadPlayerAchievements() {
|
|||||||
document.getElementById('achievementCategories').style.display = 'none';
|
document.getElementById('achievementCategories').style.display = 'none';
|
||||||
document.getElementById('achievementsNotAvailable').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)
|
// Load player achievements (includes all achievements with player status)
|
||||||
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -668,11 +882,16 @@ function displayAchievements() {
|
|||||||
const achievementsGrid = document.getElementById('achievementsGrid');
|
const achievementsGrid = document.getElementById('achievementsGrid');
|
||||||
|
|
||||||
if (!window.allAchievements || window.allAchievements.length === 0) {
|
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 = `
|
achievementsGrid.innerHTML = `
|
||||||
<div class="no-achievements">
|
<div class="no-achievements">
|
||||||
<div class="no-achievements-icon">🏆</div>
|
<div class="no-achievements-icon">🏆</div>
|
||||||
<h3>Noch keine Achievements</h3>
|
<h3>${noAchievementsTitle}</h3>
|
||||||
<p>Starte deine ersten Läufe, um Achievements zu sammeln!</p>
|
<p>${noAchievementsDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -692,6 +911,9 @@ function displayAchievements() {
|
|||||||
const progress = achievement.progress || 0;
|
const progress = achievement.progress || 0;
|
||||||
const earnedAt = achievement.earned_at;
|
const earnedAt = achievement.earned_at;
|
||||||
|
|
||||||
|
// Translate achievement
|
||||||
|
const translatedAchievement = translateAchievement(achievement);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (achievement.name === 'Tageskönig') {
|
if (achievement.name === 'Tageskönig') {
|
||||||
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
|
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
|
||||||
@@ -699,9 +921,11 @@ function displayAchievements() {
|
|||||||
|
|
||||||
let progressText = '';
|
let progressText = '';
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
|
const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on';
|
||||||
|
const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed';
|
||||||
progressText = earnedAt ?
|
progressText = earnedAt ?
|
||||||
`Erreicht am ${new Date(earnedAt).toLocaleDateString('de-DE')}` :
|
`${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` :
|
||||||
'Abgeschlossen';
|
completedText;
|
||||||
} else if (progress > 0) {
|
} else if (progress > 0) {
|
||||||
// Show progress for incomplete achievements
|
// Show progress for incomplete achievements
|
||||||
const conditionValue = getAchievementConditionValue(achievement.name);
|
const conditionValue = getAchievementConditionValue(achievement.name);
|
||||||
@@ -710,15 +934,17 @@ function displayAchievements() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
||||||
onclick="showAchievementDetails('${achievement.id}')">
|
onclick="showAchievementDetails('${achievement.id}')">
|
||||||
<div class="achievement-icon">${achievement.icon}</div>
|
<div class="achievement-icon">${achievement.icon}</div>
|
||||||
<div class="achievement-content">
|
<div class="achievement-content">
|
||||||
<h4 class="achievement-name">${achievement.name}</h4>
|
<h4 class="achievement-name">${translatedAchievement.name}</h4>
|
||||||
<p class="achievement-description">${achievement.description}</p>
|
<p class="achievement-description">${translatedAchievement.description}</p>
|
||||||
<div class="achievement-meta">
|
<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>` : ''}
|
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -780,6 +1006,51 @@ function showAchievementsNotAvailable() {
|
|||||||
document.getElementById('achievementStats').style.display = 'none';
|
document.getElementById('achievementStats').style.display = 'none';
|
||||||
document.getElementById('achievementCategories').style.display = 'none';
|
document.getElementById('achievementCategories').style.display = 'none';
|
||||||
document.getElementById('achievementsNotAvailable').style.display = 'block';
|
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
|
// Check achievements for current player
|
||||||
@@ -810,12 +1081,18 @@ function showAchievementNotification(newAchievements) {
|
|||||||
// Create notification element
|
// Create notification element
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = 'achievement-notification';
|
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 = `
|
notification.innerHTML = `
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
<div class="notification-icon">🏆</div>
|
<div class="notification-icon">🏆</div>
|
||||||
<div class="notification-text">
|
<div class="notification-text">
|
||||||
<h4>Neue Achievements erreicht!</h4>
|
<h4>${titleText}</h4>
|
||||||
<p>Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!</p>
|
<p>${descriptionText}</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -874,27 +1151,27 @@ async function checkBestTimeNotifications() {
|
|||||||
// Check if current player has best times
|
// Check if current player has best times
|
||||||
if (currentPlayerId) {
|
if (currentPlayerId) {
|
||||||
if (daily && daily.player_id === currentPlayerId) {
|
if (daily && daily.player_id === currentPlayerId) {
|
||||||
showWebNotification(
|
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
|
||||||
'🏆 Tageskönig!',
|
const message = currentLanguage === 'de' ?
|
||||||
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`,
|
`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) {
|
if (weekly && weekly.player_id === currentPlayerId) {
|
||||||
showWebNotification(
|
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
|
||||||
'🏆 Wochenchampion!',
|
const message = currentLanguage === 'de' ?
|
||||||
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!`,
|
`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) {
|
if (monthly && monthly.player_id === currentPlayerId) {
|
||||||
showWebNotification(
|
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
|
||||||
'🏆 Monatsmeister!',
|
const message = currentLanguage === 'de' ?
|
||||||
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!`,
|
`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) {
|
if (newAchievements.length > 0) {
|
||||||
newAchievements.forEach(achievement => {
|
newAchievements.forEach(achievement => {
|
||||||
|
const translatedAchievement = translateAchievement(achievement);
|
||||||
showWebNotification(
|
showWebNotification(
|
||||||
`🏆 ${achievement.name}`,
|
`🏆 ${translatedAchievement.name}`,
|
||||||
achievement.description,
|
translatedAchievement.description,
|
||||||
achievement.icon || '🏆'
|
achievement.icon || '🏆'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -956,11 +1234,11 @@ async function loadSettings() {
|
|||||||
console.error('No user ID available');
|
console.error('No user ID available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load current player settings using user ID
|
// Load current player settings using user ID
|
||||||
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
|
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const showInLeaderboard = result.data.show_in_leaderboard || false;
|
const showInLeaderboard = result.data.show_in_leaderboard || false;
|
||||||
document.getElementById('showInLeaderboard').checked = showInLeaderboard;
|
document.getElementById('showInLeaderboard').checked = showInLeaderboard;
|
||||||
@@ -1006,14 +1284,23 @@ async function saveSettings() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showNotification('Einstellungen erfolgreich gespeichert!', 'success');
|
const successMsg = currentLanguage === 'de' ?
|
||||||
|
'Einstellungen erfolgreich gespeichert!' :
|
||||||
|
'Settings saved successfully!';
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
closeModal('settingsModal');
|
closeModal('settingsModal');
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error saving settings:', 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
|
// Save language preference
|
||||||
localStorage.setItem('ninjacross_language', currentLanguage);
|
localStorage.setItem('ninjacross_language', currentLanguage);
|
||||||
|
|
||||||
|
// Update flag in select
|
||||||
|
updateLanguageFlag();
|
||||||
|
|
||||||
// Translate all elements with data attributes
|
// Translate all elements with data attributes
|
||||||
const elementsToTranslate = document.querySelectorAll('[data-de][data-en]');
|
const elementsToTranslate = document.querySelectorAll('[data-de][data-en]');
|
||||||
elementsToTranslate.forEach(element => {
|
elementsToTranslate.forEach(element => {
|
||||||
@@ -655,6 +658,20 @@ function changeLanguage() {
|
|||||||
console.log(`🌐 Language changed to: ${currentLanguage}`);
|
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
|
// Update dynamic content that's not in HTML
|
||||||
function updateDynamicContent() {
|
function updateDynamicContent() {
|
||||||
// Update location select placeholder
|
// Update location select placeholder
|
||||||
@@ -710,6 +727,8 @@ function loadLanguagePreference() {
|
|||||||
const languageSelect = document.getElementById('languageSelect');
|
const languageSelect = document.getElementById('languageSelect');
|
||||||
if (languageSelect) {
|
if (languageSelect) {
|
||||||
languageSelect.value = currentLanguage;
|
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) => {
|
router.get('/achievements', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(`
|
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
|
FROM achievements
|
||||||
WHERE is_active = true
|
WHERE is_active = true
|
||||||
ORDER BY category, points DESC
|
ORDER BY category, points DESC
|
||||||
@@ -2701,7 +2701,9 @@ router.get('/achievements/player/:playerId', async (req, res) => {
|
|||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.name,
|
a.name,
|
||||||
|
a.name_en,
|
||||||
a.description,
|
a.description,
|
||||||
|
a.description_en,
|
||||||
a.category,
|
a.category,
|
||||||
a.icon,
|
a.icon,
|
||||||
a.points,
|
a.points,
|
||||||
|
|||||||
Reference in New Issue
Block a user