Add Local leaderboard, CSS optimiztion

This commit is contained in:
Carsten Graf
2025-09-23 20:07:35 +02:00
parent 8fac847a75
commit 5ca67d8804
8 changed files with 849 additions and 45 deletions

View File

@@ -53,6 +53,32 @@ body {
border-radius: 10px; border-radius: 10px;
} }
.leaderboard-btn {
position: fixed;
top: 20px;
right: 90px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 15px;
border-radius: 50%;
text-decoration: none;
font-size: 1.5rem;
transition: all 0.3s ease;
z-index: 1000;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.leaderboard-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.1);
}
.settings-btn { .settings-btn {
position: fixed; position: fixed;
top: 20px; top: 20px;
@@ -82,7 +108,7 @@ body {
.heartbeat-indicators { .heartbeat-indicators {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 90px; right: 160px;
display: flex; display: flex;
gap: 15px; gap: 15px;
z-index: 1000; z-index: 1000;
@@ -93,6 +119,56 @@ body {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
@media (max-width: 768px) {
.logo {
width: 40px;
height: 40px;
top: 15px;
left: 15px;
padding: 3px;
}
.leaderboard-btn {
top: 15px;
right: 60px;
padding: 10px;
font-size: 1.2rem;
}
.settings-btn {
top: 15px;
right: 15px;
padding: 10px;
font-size: 1.2rem;
}
.heartbeat-indicators {
top: 15px;
right: 90px;
gap: 8px;
padding: 8px 12px;
font-size: 0.8rem;
}
.heartbeat-indicator {
width: 12px;
height: 12px;
}
.heartbeat-indicator::before {
font-size: 8px;
top: -20px;
}
.header h1 {
font-size: clamp(1.2rem, 3vw, 1.8rem);
}
.header p {
font-size: clamp(0.7rem, 1.5vw, 0.9rem);
}
}
.heartbeat-indicator { .heartbeat-indicator {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -300,7 +376,7 @@ body {
transition: transform 0.3s ease; transition: transform 0.3s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: space-between;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
@@ -344,7 +420,7 @@ body {
} }
.time-display { .time-display {
font-size: clamp(3rem, 9vw, 10rem); font-size: clamp(3rem, 13vw, 13rem);
font-weight: bold; font-weight: bold;
margin: clamp(10px, 1vh, 15px) 0; margin: clamp(10px, 1vh, 15px) 0;
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
@@ -353,7 +429,7 @@ body {
} }
.status { .status {
font-size: clamp(1.5rem, 3vw, 3rem); font-size: clamp(1.5rem, 4vw, 5rem);
margin: clamp(8px, 1vh, 12px) 0; margin: clamp(8px, 1vh, 12px) 0;
padding: clamp(6px, 1vh, 10px) clamp(12px, 2vw, 18px); padding: clamp(6px, 1vh, 10px) clamp(12px, 2vw, 18px);
border-radius: 20px; border-radius: 20px;
@@ -428,20 +504,40 @@ body {
border-radius: 15px; border-radius: 15px;
padding: clamp(10px, 1.5vh, 15px); padding: clamp(10px, 1.5vh, 15px);
margin: 1vh 0 0 0; margin: 1vh 0 0 0;
width: 50%; width: clamp(320px, 80vw, 960px);
max-width: 50%; max-width: 960px;
text-align: center; text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
flex-shrink: 0; flex-shrink: 0;
align-self: center; align-self: center;
display: flex;
flex-direction: column;
align-items: stretch;
gap: clamp(12px, 2vh, 20px);
box-sizing: border-box;
}
#leaderboard-container {
text-align: left;
display: grid;
grid-template-columns: 1fr;
gap: clamp(12px, 2vh, 20px);
width: 100%;
}
@media (min-width: 768px) {
#leaderboard-container {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
.best-times h3 { .best-times h3 {
font-size: clamp(0.9rem, 1.8vw, 1.1rem); font-size: clamp(0.9rem, 1.8vw, 1.1rem);
margin-bottom: clamp(5px, 0.5vh, 8px); margin: 0 auto;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
font-family: "Segoe UI", Arial, sans-serif; font-family: "Segoe UI", Arial, sans-serif;
text-align: center;
} }
.best-time-row { .best-time-row {
@@ -468,10 +564,13 @@ body {
font-size: clamp(1.1rem, 2.2vw, 1.4rem); font-size: clamp(1.1rem, 2.2vw, 1.4rem);
font-weight: 600; font-weight: 600;
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
padding: clamp(8px, 1.5vh, 12px) clamp(12px, 2vw, 16px); padding: clamp(12px, 2vh, 16px) clamp(16px, 3vw, 24px);
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease; transition: all 0.3s ease;
min-height: 50px;
width: 100%;
box-sizing: border-box;
} }
.leaderboard-entry:hover { .leaderboard-entry:hover {
@@ -502,6 +601,60 @@ body {
text-align: right; text-align: right;
} }
.leaderboard-entry.gold {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
border-color: #ffd700;
color: #b8860b;
font-weight: bold;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
}
.leaderboard-entry.gold .rank {
color: #7a4d00;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.leaderboard-entry.gold .time {
color: #0f5132;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
}
.leaderboard-entry.silver {
background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
border-color: #c0c0c0;
color: #696969;
font-weight: bold;
box-shadow: 0 4px 15px rgba(192, 192, 192, 0.3);
}
.leaderboard-entry.silver .rank {
color: #4b5563;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.leaderboard-entry.silver .time {
color: #0f5132;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
}
.leaderboard-entry.bronze {
background: linear-gradient(135deg, #cd7f32 0%, #e6a85c 100%);
border-color: #cd7f32;
color: #8b4513;
font-weight: bold;
box-shadow: 0 4px 15px rgba(205, 127, 50, 0.3);
}
.leaderboard-entry.bronze .rank {
color: #7a3410;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.leaderboard-entry.bronze .time {
color: #0f5132;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
}
.no-times { .no-times {
text-align: center; text-align: center;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);

View File

@@ -24,6 +24,7 @@
</div> </div>
<img src="/pictures/erlebniss.png" class="logo" alt="NinjaCross Logo" /> <img src="/pictures/erlebniss.png" class="logo" alt="NinjaCross Logo" />
<a href="/leaderboard.html" class="leaderboard-btn">🏆</a>
<a href="/settings" class="settings-btn">⚙️</a> <a href="/settings" class="settings-btn">⚙️</a>
<div class="heartbeat-indicators"> <div class="heartbeat-indicators">
@@ -73,33 +74,7 @@
<div class="best-times"> <div class="best-times">
<h3>🏆 Lokales Leaderboard</h3> <h3>🏆 Lokales Leaderboard</h3>
<div id="leaderboard-container"> <div id="leaderboard-container"></div>
<div class="leaderboard-entry">
<span class="rank">1.</span>
<span class="name">Max Mustermann</span>
<span class="time">23.45</span>
</div>
<div class="leaderboard-entry">
<span class="rank">2.</span>
<span class="name">Anna Schmidt</span>
<span class="time">24.67</span>
</div>
<div class="leaderboard-entry">
<span class="rank">3.</span>
<span class="name">Tom Weber</span>
<span class="time">25.89</span>
</div>
<div class="leaderboard-entry">
<span class="rank">4.</span>
<span class="name">Lisa Müller</span>
<span class="time">26.12</span>
</div>
<div class="leaderboard-entry">
<span class="rank">5.</span>
<span class="name">Paul Fischer</span>
<span class="time">27.34</span>
</div>
</div>
</div> </div>
<script> <script>
@@ -396,10 +371,25 @@
return; return;
} }
// Erstelle zwei Reihen für 2x3 Layout
const row1 = document.createElement("div");
row1.className = "leaderboard-row";
const row2 = document.createElement("div");
row2.className = "leaderboard-row";
leaderboardData.forEach((entry, index) => { leaderboardData.forEach((entry, index) => {
const entryDiv = document.createElement("div"); const entryDiv = document.createElement("div");
entryDiv.className = "leaderboard-entry"; entryDiv.className = "leaderboard-entry";
// Podium-Plätze hervorheben
if (index === 0) {
entryDiv.classList.add("gold");
} else if (index === 1) {
entryDiv.classList.add("silver");
} else if (index === 2) {
entryDiv.classList.add("bronze");
}
const rankSpan = document.createElement("span"); const rankSpan = document.createElement("span");
rankSpan.className = "rank"; rankSpan.className = "rank";
rankSpan.textContent = entry.rank + "."; rankSpan.textContent = entry.rank + ".";
@@ -415,8 +405,19 @@
entryDiv.appendChild(rankSpan); entryDiv.appendChild(rankSpan);
entryDiv.appendChild(nameSpan); entryDiv.appendChild(nameSpan);
entryDiv.appendChild(timeSpan); entryDiv.appendChild(timeSpan);
container.appendChild(entryDiv);
// Erste 3 Einträge in die erste Reihe, nächste 3 in die zweite Reihe
if (index < 3) {
row1.appendChild(entryDiv);
} else if (index < 6) {
row2.appendChild(entryDiv);
}
}); });
container.appendChild(row1);
if (leaderboardData.length > 3) {
container.appendChild(row2);
}
} }
function updateDisplay() { function updateDisplay() {
@@ -452,7 +453,7 @@
s1.textContent = "Bereit für den Start!"; s1.textContent = "Bereit für den Start!";
break; break;
case "running": case "running":
s1.textContent = "Läuft - Du schaffst das!"; s1.textContent = "Läuft - Gib alles!";
break; break;
case "finished": case "finished":
s1.textContent = "Geschafft!"; s1.textContent = "Geschafft!";
@@ -478,7 +479,7 @@
s2.textContent = "Bereit für den Start!"; s2.textContent = "Bereit für den Start!";
break; break;
case "running": case "running":
s2.textContent = "Läuft - Du schaffst das!"; s2.textContent = "Läuft - Gib alles!";
break; break;
case "finished": case "finished":
s2.textContent = "Geschafft!"; s2.textContent = "Geschafft!";

367
data/leaderboard.css Normal file
View File

@@ -0,0 +1,367 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Segoe UI", Arial, sans-serif;
background: linear-gradient(0deg, #0d1733 0%, #223c83 100%);
min-height: 100vh;
padding: 20px;
}
.back-btn {
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 15px;
border-radius: 50%;
text-decoration: none;
font-size: 1.5rem;
transition: all 0.3s ease;
z-index: 1000;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.1);
}
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: visible;
backdrop-filter: blur(10px);
}
.header {
background: linear-gradient(135deg, #49bae4 0%, #223c83 100%);
color: white;
padding: 30px;
text-align: center;
position: relative;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
position: relative;
z-index: 1;
font-weight: bold;
text-transform: uppercase;
font-family: "Segoe UI", Arial, sans-serif;
}
.content {
padding: 30px;
}
.leaderboard-container {
background: white;
border-radius: 12px;
padding: 20px;
border: 2px solid #e9ecef;
min-height: 150px;
max-height: none;
overflow: visible;
}
.leaderboard-row {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.leaderboard-row:last-child {
margin-bottom: 0;
}
@media (min-width: 768px) {
.leaderboard-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
grid-auto-rows: min-content;
}
.leaderboard-row {
margin-bottom: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
}
.leaderboard-entry {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px 0;
font-size: 1.1em;
font-weight: 600;
background: #f8f9fa;
padding: 15px 20px;
border-radius: 10px;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.leaderboard-entry:hover {
background: #e9ecef;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.leaderboard-entry.gold {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
border-color: #ffd700;
color: #b8860b;
font-weight: bold;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
}
.leaderboard-entry.silver {
background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
border-color: #c0c0c0;
color: #696969;
font-weight: bold;
box-shadow: 0 4px 15px rgba(192, 192, 192, 0.3);
}
.leaderboard-entry.bronze {
background: linear-gradient(135deg, #cd7f32 0%, #e6a85c 100%);
border-color: #cd7f32;
color: #8b4513;
font-weight: bold;
box-shadow: 0 4px 15px rgba(205, 127, 50, 0.3);
}
.leaderboard-entry .rank {
font-weight: bold;
min-width: 40px;
font-size: 1.2em;
text-align: center;
}
.leaderboard-entry .name {
flex: 1;
margin: 0 20px;
font-weight: 600;
}
.leaderboard-entry .time {
font-weight: bold;
font-family: 'Courier New', monospace;
min-width: 100px;
text-align: right;
font-size: 1.1em;
}
.no-entries {
text-align: center;
color: #6c757d;
font-style: italic;
font-size: 1.1em;
padding: 40px;
}
.loading {
text-align: center;
color: #49bae4;
font-size: 1.1em;
padding: 40px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* Modern Notification Toast */
.notification-toast {
position: fixed;
top: 24px;
right: 24px;
min-width: 320px;
max-width: 400px;
background: rgba(255, 255, 255, 0.98);
border-radius: 16px;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px);
z-index: 99999;
display: none;
align-items: flex-start;
gap: 12px;
padding: 16px;
transform: translateX(100%);
opacity: 0;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.notification-toast.show {
transform: translateX(0);
opacity: 1;
}
.notification-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #10b981, #059669);
}
.notification-body {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 14px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
line-height: 1.2;
}
.notification-message {
font-size: 13px;
color: #6b7280;
line-height: 1.4;
word-wrap: break-word;
}
.notification-close {
flex-shrink: 0;
width: 32px;
height: 32px;
border: none;
background: none;
color: #9ca3af;
cursor: pointer;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
margin-top: -4px;
margin-right: -4px;
}
.notification-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
.notification-close:active {
transform: scale(0.95);
}
/* Toast Types */
.notification-toast.success .notification-icon {
background: linear-gradient(135deg, #10b981, #059669);
}
.notification-toast.error .notification-icon {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.notification-toast.info .notification-icon {
background: linear-gradient(135deg, #3b82f6, #2563eb);
}
.notification-toast.warning .notification-icon {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 15px;
}
.content {
padding: 20px;
}
.leaderboard-entry {
flex-direction: column;
gap: 10px;
text-align: center;
}
.leaderboard-entry .name {
margin: 0;
order: 1;
}
.leaderboard-entry .rank {
order: 2;
}
.leaderboard-entry .time {
order: 3;
}
/* Mobile notification adjustments */
.notification-toast {
top: 10px;
right: 10px;
left: 10px;
max-width: none;
font-size: 14px;
padding: 12px 16px;
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 2em;
}
.leaderboard-entry {
padding: 12px 15px;
font-size: 1em;
}
.leaderboard-entry .rank {
font-size: 1.1em;
}
.leaderboard-entry .time {
font-size: 1em;
}
}

227
data/leaderboard.html Normal file
View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="de">
<head>
<!-- Meta Tags -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico" />
<!-- Stylesheets -->
<link rel="stylesheet" href="leaderboard.css" />
<title>Ninjacross Timer - Leaderboard</title>
</head>
<body>
<!-- Modern Notification Toast -->
<div
id="notificationBubble"
class="notification-toast"
style="display: none"
>
<div class="notification-icon">
<span id="notificationIcon"></span>
</div>
<div class="notification-body">
<div class="notification-title" id="notificationTitle">Erfolg</div>
<div class="notification-message" id="notificationText">Bereit</div>
</div>
<button class="notification-close" onclick="hideNotification()">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"
/>
</svg>
</button>
</div>
<!-- Zurück Button -->
<a href="/" class="back-btn">🏠</a>
<div class="container">
<!-- Header Section -->
<div class="header">
<h1>🏆 Leaderboard</h1>
</div>
<div class="content">
<!-- Leaderboard Section -->
<div id="leaderboard-container" class="leaderboard-container">
<div class="loading">Lade Leaderboard...</div>
</div>
</div>
</div>
<!-- JavaScript Code -->
<script>
let leaderboardData = [];
let lastUpdateTime = null;
// Seite laden
window.onload = function () {
loadLeaderboard();
// Leaderboard alle 5 Sekunden aktualisieren
setInterval(loadLeaderboard, 5000);
};
// Leaderboard laden
async function loadLeaderboard() {
try {
const response = await fetch("/api/leaderboard-full");
const data = await response.json();
leaderboardData = data.leaderboard || [];
lastUpdateTime = new Date();
updateLeaderboardDisplay();
} catch (error) {
console.error("Fehler beim Laden des Leaderboards:", error);
showMessage("Fehler beim Laden des Leaderboards", "error");
}
}
// Leaderboard anzeigen
function updateLeaderboardDisplay() {
const container = document.getElementById("leaderboard-container");
container.innerHTML = "";
if (leaderboardData.length === 0) {
container.innerHTML =
'<div class="no-entries">Noch keine Zeiten erfasst</div>';
return;
}
// Alle Einträge anzeigen
const displayData = leaderboardData;
// Erstelle zwei Reihen
const row1 = document.createElement("div");
row1.className = "leaderboard-row";
const row2 = document.createElement("div");
row2.className = "leaderboard-row";
displayData.forEach((entry, index) => {
const entryDiv = document.createElement("div");
entryDiv.className = "leaderboard-entry";
// Podium-Plätze hervorheben
if (index === 0) {
entryDiv.classList.add("gold");
} else if (index === 1) {
entryDiv.classList.add("silver");
} else if (index === 2) {
entryDiv.classList.add("bronze");
}
const rankSpan = document.createElement("span");
rankSpan.className = "rank";
rankSpan.textContent = entry.rank + ".";
const nameSpan = document.createElement("span");
nameSpan.className = "name";
nameSpan.textContent = entry.name;
const timeSpan = document.createElement("span");
timeSpan.className = "time";
timeSpan.textContent = entry.timeFormatted;
entryDiv.appendChild(rankSpan);
entryDiv.appendChild(nameSpan);
entryDiv.appendChild(timeSpan);
// Erste 5 Einträge in die erste Reihe, nächste 5 in die zweite Reihe
if (index < 5) {
row1.appendChild(entryDiv);
} else {
row2.appendChild(entryDiv);
}
});
container.appendChild(row1);
if (displayData.length > 5) {
container.appendChild(row2);
}
}
// Moderne Notification anzeigen
function showMessage(message, type = "info") {
console.log("showMessage called:", message, type);
const toast = document.getElementById("notificationBubble");
const icon = document.getElementById("notificationIcon");
const title = document.getElementById("notificationTitle");
const text = document.getElementById("notificationText");
if (!toast || !icon || !title || !text) {
console.error("Notification elements not found!");
return;
}
// Clear any existing timeout
if (window.notificationTimeout) {
clearTimeout(window.notificationTimeout);
}
// Set content
text.textContent = message;
// Set type-specific styling and content
toast.className = "notification-toast";
switch (type) {
case "success":
toast.classList.add("success");
icon.textContent = "✓";
title.textContent = "Erfolg";
break;
case "error":
toast.classList.add("error");
icon.textContent = "✕";
title.textContent = "Fehler";
break;
case "info":
toast.classList.add("info");
icon.textContent = "";
title.textContent = "Information";
break;
case "warning":
toast.classList.add("warning");
icon.textContent = "⚠";
title.textContent = "Warnung";
break;
default:
toast.classList.add("info");
icon.textContent = "";
title.textContent = "Information";
}
// Show toast with animation
toast.style.display = "flex";
// Force reflow
toast.offsetHeight;
// Add show class after a small delay to ensure display is set
setTimeout(() => {
toast.classList.add("show");
}, 10);
// Auto-hide after 5 seconds
window.notificationTimeout = setTimeout(() => {
hideNotification();
}, 5000);
}
// Notification verstecken mit Animation
function hideNotification() {
const toast = document.getElementById("notificationBubble");
if (!toast) return;
// Clear timeout if exists
if (window.notificationTimeout) {
clearTimeout(window.notificationTimeout);
}
// Remove show class for animation
toast.classList.remove("show");
// Hide after animation completes
setTimeout(() => {
toast.style.display = "none";
}, 400); // Match CSS transition duration
}
</script>
</body>
</html>

View File

@@ -353,7 +353,7 @@ void setupBackendRoutes(AsyncWebServer &server) {
// Andere Logik wie in getBestLocs // Andere Logik wie in getBestLocs
}); });
// Lokales Leaderboard API // Lokales Leaderboard API (für Hauptseite - 6 Einträge)
server.on("/api/leaderboard", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/leaderboard", HTTP_GET, [](AsyncWebServerRequest *request) {
// Sortiere nach Zeit (beste zuerst) // Sortiere nach Zeit (beste zuerst)
std::sort(localTimes.begin(), localTimes.end(), std::sort(localTimes.begin(), localTimes.end(),
@@ -364,10 +364,10 @@ void setupBackendRoutes(AsyncWebServer &server) {
DynamicJsonDocument doc(2048); DynamicJsonDocument doc(2048);
JsonArray leaderboard = doc.createNestedArray("leaderboard"); JsonArray leaderboard = doc.createNestedArray("leaderboard");
// Nimm die besten 5 // Nimm die besten 6
int count = 0; int count = 0;
for (const auto &time : localTimes) { for (const auto &time : localTimes) {
if (count >= 5) if (count >= 6)
break; break;
JsonObject entry = leaderboard.createNestedObject(); JsonObject entry = leaderboard.createNestedObject();
@@ -403,6 +403,58 @@ void setupBackendRoutes(AsyncWebServer &server) {
request->send(200, "application/json", result); request->send(200, "application/json", result);
}); });
// Erweiterte Leaderboard API (für Leaderboard-Seite - 10 Einträge)
server.on(
"/api/leaderboard-full", HTTP_GET, [](AsyncWebServerRequest *request) {
// Sortiere nach Zeit (beste zuerst)
std::sort(localTimes.begin(), localTimes.end(),
[](const LocalTime &a, const LocalTime &b) {
return a.timeMs < b.timeMs;
});
DynamicJsonDocument doc(2048);
JsonArray leaderboard = doc.createNestedArray("leaderboard");
// Nimm die besten 10
int count = 0;
for (const auto &time : localTimes) {
if (count >= 10)
break;
JsonObject entry = leaderboard.createNestedObject();
entry["rank"] = count + 1;
entry["name"] = time.name;
entry["uid"] = time.uid;
entry["time"] = time.timeMs / 1000.0;
// Format time inline
float seconds = time.timeMs / 1000.0;
int totalSeconds = (int)seconds;
int minutes = totalSeconds / 60;
int remainingSeconds = totalSeconds % 60;
int milliseconds = (int)((seconds - totalSeconds) * 100);
String timeFormatted;
if (minutes > 0) {
timeFormatted =
String(minutes) + ":" + (remainingSeconds < 10 ? "0" : "") +
String(remainingSeconds) + "." +
(milliseconds < 10 ? "0" : "") + String(milliseconds);
} else {
timeFormatted = String(remainingSeconds) + "." +
(milliseconds < 10 ? "0" : "") +
String(milliseconds);
}
entry["timeFormatted"] = timeFormatted;
count++;
}
String result;
serializeJson(doc, result);
request->send(200, "application/json", result);
});
// Add more routes as needed // Add more routes as needed
} }

View File

@@ -72,7 +72,8 @@ void IndividualMode(const char *action, int press, int lane,
sendTimeToOnlineAPI(1, getStart1UID(), currentTime / 1000.0); sendTimeToOnlineAPI(1, getStart1UID(), currentTime / 1000.0);
} else { } else {
// Kein User gefunden - speichere Zeit ohne UID und Namen // Kein User gefunden - speichere Zeit ohne UID und Namen
addLocalTime("", "Anonym", currentTime); addLocalTime("", "Spieler " + String((localTimes.size() + 1)),
currentTime);
} }
} }
} }
@@ -118,7 +119,8 @@ void IndividualMode(const char *action, int press, int lane,
sendTimeToOnlineAPI(2, getStart2UID(), currentTime / 1000.0); sendTimeToOnlineAPI(2, getStart2UID(), currentTime / 1000.0);
} else { } else {
// Kein User gefunden - speichere Zeit ohne UID und Namen // Kein User gefunden - speichere Zeit ohne UID und Namen
addLocalTime("", "Anonym", currentTime); addLocalTime("", "Spieler " + String((localTimes.size() + 1)),
currentTime);
} }
} }
} }

View File

@@ -76,8 +76,6 @@ void loop() {
loopRFID(); loopRFID();
} }
// loopBattery(); // Batterie-Loop aufrufen
// Kurze Pause um anderen Tasks Zeit zu geben // Kurze Pause um anderen Tasks Zeit zu geben
delay(1); delay(1);
} }

View File

@@ -33,6 +33,10 @@ void setupRoutes() {
request->send(SPIFFS, "/settings.html", "text/html"); request->send(SPIFFS, "/settings.html", "text/html");
}); });
server.on("/leaderboard", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(SPIFFS, "/leaderboard.html", "text/html");
});
server.on("/firmware.bin", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/firmware.bin", HTTP_GET, [](AsyncWebServerRequest *request) {
if (SPIFFS.exists("/firmware.bin")) { if (SPIFFS.exists("/firmware.bin")) {
request->send(SPIFFS, "/firmware.bin", "application/octet-stream"); request->send(SPIFFS, "/firmware.bin", "application/octet-stream");