From 9f995e213ac8d7a611dd6abf751148bfebb460f1 Mon Sep 17 00:00:00 2001 From: Trompi001 Date: Wed, 22 Apr 2026 10:23:35 +0200 Subject: [PATCH] creating leaderboard --- assets/css/custom.css | 64 ++++++++++++++++ index.html | 1 + js/leaderboard.js | 164 +++++++++++++++++++++++++++++++++++++++++ js/navigation.js | 3 + pages/leaderboard.html | 16 +++- 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 js/leaderboard.js diff --git a/assets/css/custom.css b/assets/css/custom.css index a9b6183..9536f2c 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -213,3 +213,67 @@ p { } +/* Table styles leaderboard */ +.leaderboard-table { + border-collapse: collapse; + width: 100%; + background: #ffffff; + border-radius: 10px; + overflow: hidden; +} + +.leaderboard-table th, +.leaderboard-table td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +.leaderboard-table thead th { + background: #1b1b2f; + color: #ffffff; + font-weight: 600; + letter-spacing: 0.02em; +} + +.leaderboard-table tbody tr { + transition: background-color 0.2s ease; +} + +.leaderboard-table tbody tr:nth-child(even) { + background-color: #f7f9fc; +} + +.leaderboard-table tbody tr:hover { + background-color: #dbe6f7; +} + +.leaderboard-table tbody tr.leaderboard-row-current-user, +.leaderboard-table tbody tr.leaderboard-row-current-user:nth-child(even), +.leaderboard-table tbody tr.leaderboard-row-current-user:hover { + background-color: #dff3df; +} + +.leaderboard-table tbody tr.leaderboard-row-current-user td { + font-weight: 600; +} + +.leaderboard-table tbody tr.leaderboard-row-gap td { + border-bottom: none; + padding: 0; + height: 16px; + background: transparent; +} + +.leaderboard-table tbody tr.leaderboard-row-gap:hover, +.leaderboard-table tbody tr.leaderboard-row-gap:hover td { + background: transparent; +} + +.leaderboard-table tbody tr.leaderboard-row-current-user-extra td { + border-top: 2px solid #b8dfb8; +} + +.leaderboard-table tbody tr:last-child td { + border-bottom: none; +} \ No newline at end of file diff --git a/index.html b/index.html index d9b72dd..66319c0 100644 --- a/index.html +++ b/index.html @@ -58,6 +58,7 @@ + diff --git a/js/leaderboard.js b/js/leaderboard.js new file mode 100644 index 0000000..3595811 --- /dev/null +++ b/js/leaderboard.js @@ -0,0 +1,164 @@ +// Formatiert Sekunden als m:ss. Bei ungültigem Wert wird ein Platzhalter angezeigt. +function formatTime(seconds) { + if (typeof seconds !== "number" || Number.isNaN(seconds)) { + return "-"; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; +} + +// Liefert den aktuellen Login-Kontext, falls Auth global verfügbar ist. +function getLoggedInAuth() { + if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") { + return null; + } + + const auth = window.AppAuth.getAuth(); + if (!auth || !auth.username) { + return null; + } + + return auth; +} + +// Vereinheitlicht Benutzernamen für robuste Vergleiche (z. B. Groß-/Kleinschreibung). +function normalizeUsername(username) { + return String(username ?? "").trim().toLowerCase(); +} + +function getLoggedInUsername() { + const auth = getLoggedInAuth(); + if (!auth) { + return null; + } + + return normalizeUsername(auth.username); +} + +// Nutzt den vom Backend gelieferten Rang, fallback auf die aktuelle Listenposition. +function getDisplayedRank(entry, index) { + const place = Number(entry?.place); + if (!Number.isNaN(place) && place > 0) { + return place; + } + + return index + 1; +} + +// Bestes Ergebnis: höchste Punktzahl, bei Gleichstand die geringere Zeit. +function getBestScoreEntry(entries) { + return entries + .slice() + .sort((a, b) => { + const scoreA = Number(a.score ?? 0); + const scoreB = Number(b.score ?? 0); + if (scoreB !== scoreA) { + return scoreB - scoreA; + } + + const timeA = Number(a.time ?? Number.MAX_SAFE_INTEGER); + const timeB = Number(b.time ?? Number.MAX_SAFE_INTEGER); + return timeA - timeB; + })[0] ?? null; +} + +async function getCurrentUserLeaderboardEntry(username) { + if (!window.ScoreService || !username) { + return null; + } + + const scoreService = new window.ScoreService(window.config); + const result = await scoreService.getScoreByName(username); + + if (!result.ok || !Array.isArray(result.body) || result.body.length === 0) { + return null; + } + + return getBestScoreEntry(result.body); +} + +// Rendert die Top-Liste und markiert den eingeloggten Nutzer visuell. +function renderLeaderboard(entries, extraUserEntry = null) { + const tableBody = document.getElementById("leaderboard-body"); + if (!tableBody) { + return; + } + + const loggedInUsername = getLoggedInUsername(); + + tableBody.innerHTML = ""; + + entries.forEach((entry, index) => { + const row = document.createElement("tr"); + const rowUsername = normalizeUsername(entry.username); + + if (loggedInUsername && rowUsername === loggedInUsername) { + row.classList.add("leaderboard-row-current-user"); + } + + row.innerHTML = ` + ${getDisplayedRank(entry, index)} + ${entry.username ?? "-"} + ${formatTime(entry.time)} min + ${entry.score ?? "-"} + `; + + tableBody.appendChild(row); + }); + + if (extraUserEntry) { + // Trennt Top-10 und eigenen Eintrag optisch, wenn der Nutzer nicht in den Top-10 ist. + const spacerRow = document.createElement("tr"); + spacerRow.classList.add("leaderboard-row-gap"); + spacerRow.innerHTML = ''; + tableBody.appendChild(spacerRow); + + const userRow = document.createElement("tr"); + userRow.classList.add("leaderboard-row-current-user"); + userRow.classList.add("leaderboard-row-current-user-extra"); + userRow.innerHTML = ` + ${getDisplayedRank(extraUserEntry, 0)} + ${extraUserEntry.username ?? "-"} + ${formatTime(extraUserEntry.time)} min + ${extraUserEntry.score ?? "-"} + `; + + tableBody.appendChild(userRow); + } +} + +async function loadTopTenLeaderboard() { + const leaderboardService = new window.LeaderboardService(window.config); + const result = await leaderboardService.getLeaderboard(0, 10); + + if (!result.ok || !Array.isArray(result.body)) { + renderLeaderboard([]); + return; + } + + const auth = getLoggedInAuth(); + let extraUserEntry = null; + + if (auth && auth.username) { + const loggedInUsername = normalizeUsername(auth.username); + // Falls der Nutzer nicht in den Top-10 erscheint, wird sein bestes Ergebnis separat gezeigt. + const isInTopTen = result.body.some( + (entry) => normalizeUsername(entry.username) === loggedInUsername, + ); + + if (!isInTopTen) { + extraUserEntry = await getCurrentUserLeaderboardEntry(auth.username); + } + } + + renderLeaderboard(result.body, extraUserEntry); +} + +window.initLeaderboardPage = function initLeaderboardPage() { + loadTopTenLeaderboard().catch((error) => { + console.error("Fehler beim Laden des Leaderboards:", error); + renderLeaderboard([]); + }); +}; diff --git a/js/navigation.js b/js/navigation.js index 6111ac0..169c63a 100644 --- a/js/navigation.js +++ b/js/navigation.js @@ -23,6 +23,9 @@ document.addEventListener("DOMContentLoaded", () => { if (page === "login" && typeof window.initLoginPage === "function") { window.initLoginPage(); } + if (page === "leaderboard" && typeof window.initLeaderboardPage === "function") { + window.initLeaderboardPage(); + } }) .catch(error => { console.error("Fehler beim Laden von " + page + ":", error); diff --git a/pages/leaderboard.html b/pages/leaderboard.html index 7fbb136..99e3973 100644 --- a/pages/leaderboard.html +++ b/pages/leaderboard.html @@ -1,5 +1,19 @@ +

Leaderboard

-

Hier kannst du die besten Spieler und ihre Scores sehen.

+ + + + + + + + + + + + + +
RankUsertimeScore