diff --git a/data/index.css b/data/index.css index 0397b27..3a06eb4 100644 --- a/data/index.css +++ b/data/index.css @@ -53,6 +53,32 @@ body { 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 { position: fixed; top: 20px; @@ -82,7 +108,7 @@ body { .heartbeat-indicators { position: fixed; top: 20px; - right: 90px; + right: 160px; display: flex; gap: 15px; z-index: 1000; @@ -93,6 +119,56 @@ body { 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 { width: 20px; height: 20px; @@ -300,7 +376,7 @@ body { transition: transform 0.3s ease; display: flex; flex-direction: column; - justify-content: center; + justify-content: space-between; height: 100%; overflow: hidden; } @@ -344,7 +420,7 @@ body { } .time-display { - font-size: clamp(3rem, 9vw, 10rem); + font-size: clamp(3rem, 13vw, 13rem); font-weight: bold; margin: clamp(10px, 1vh, 15px) 0; font-family: "Courier New", monospace; @@ -353,7 +429,7 @@ body { } .status { - font-size: clamp(1.5rem, 3vw, 3rem); + font-size: clamp(1.5rem, 4vw, 5rem); margin: clamp(8px, 1vh, 12px) 0; padding: clamp(6px, 1vh, 10px) clamp(12px, 2vw, 18px); border-radius: 20px; @@ -428,20 +504,40 @@ body { border-radius: 15px; padding: clamp(10px, 1.5vh, 15px); margin: 1vh 0 0 0; - width: 50%; - max-width: 50%; + width: clamp(320px, 80vw, 960px); + max-width: 960px; text-align: center; border: 1px solid rgba(255, 255, 255, 0.2); flex-shrink: 0; 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 { font-size: clamp(0.9rem, 1.8vw, 1.1rem); - margin-bottom: clamp(5px, 0.5vh, 8px); + margin: 0 auto; font-weight: bold; text-transform: uppercase; font-family: "Segoe UI", Arial, sans-serif; + text-align: center; } .best-time-row { @@ -468,10 +564,13 @@ body { font-size: clamp(1.1rem, 2.2vw, 1.4rem); font-weight: 600; 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: 1px solid rgba(255, 255, 255, 0.3); transition: all 0.3s ease; + min-height: 50px; + width: 100%; + box-sizing: border-box; } .leaderboard-entry:hover { @@ -502,6 +601,60 @@ body { 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 { text-align: center; color: rgba(255, 255, 255, 0.7); diff --git a/data/index.html b/data/index.html index 1856b16..9af052a 100644 --- a/data/index.html +++ b/data/index.html @@ -24,6 +24,7 @@ + 🏆 ⚙️
@@ -73,33 +74,7 @@

🏆 Lokales Leaderboard

-
-
- 1. - Max Mustermann - 23.45 -
-
- 2. - Anna Schmidt - 24.67 -
-
- 3. - Tom Weber - 25.89 -
-
- 4. - Lisa MĂźller - 26.12 -
-
- 5. - Paul Fischer - 27.34 -
-
+
+ + diff --git a/src/databasebackend.h b/src/databasebackend.h index 420e047..340b3c1 100644 --- a/src/databasebackend.h +++ b/src/databasebackend.h @@ -353,7 +353,7 @@ void setupBackendRoutes(AsyncWebServer &server) { // 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) { // Sortiere nach Zeit (beste zuerst) std::sort(localTimes.begin(), localTimes.end(), @@ -364,10 +364,10 @@ void setupBackendRoutes(AsyncWebServer &server) { DynamicJsonDocument doc(2048); JsonArray leaderboard = doc.createNestedArray("leaderboard"); - // Nimm die besten 5 + // Nimm die besten 6 int count = 0; for (const auto &time : localTimes) { - if (count >= 5) + if (count >= 6) break; JsonObject entry = leaderboard.createNestedObject(); @@ -403,6 +403,58 @@ void setupBackendRoutes(AsyncWebServer &server) { 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 } diff --git a/src/gamemodes.h b/src/gamemodes.h index 92acf3d..bcd523f 100644 --- a/src/gamemodes.h +++ b/src/gamemodes.h @@ -72,7 +72,8 @@ void IndividualMode(const char *action, int press, int lane, sendTimeToOnlineAPI(1, getStart1UID(), currentTime / 1000.0); } else { // 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); } else { // Kein User gefunden - speichere Zeit ohne UID und Namen - addLocalTime("", "Anonym", currentTime); + addLocalTime("", "Spieler " + String((localTimes.size() + 1)), + currentTime); } } } diff --git a/src/master.cpp b/src/master.cpp index 6ee5fbf..4f8ff5f 100644 --- a/src/master.cpp +++ b/src/master.cpp @@ -76,8 +76,6 @@ void loop() { loopRFID(); } - // loopBattery(); // Batterie-Loop aufrufen - // Kurze Pause um anderen Tasks Zeit zu geben delay(1); } diff --git a/src/webserverrouter.h b/src/webserverrouter.h index c3450b2..aaeb3e4 100644 --- a/src/webserverrouter.h +++ b/src/webserverrouter.h @@ -33,6 +33,10 @@ void setupRoutes() { 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) { if (SPIFFS.exists("/firmware.bin")) { request->send(SPIFFS, "/firmware.bin", "application/octet-stream");