This commit is contained in:
242
data/index.html
242
data/index.html
@@ -44,7 +44,6 @@
|
||||
|
||||
<div class="header">
|
||||
<h1>🏊♀️ NinjaCross Timer</h1>
|
||||
<p>Dein professioneller Zeitmesser für Ninjacross Wettkämpfe</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-display" class="learning-mode" style="display: none">
|
||||
@@ -72,9 +71,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="best-times">
|
||||
<h3>🏆 Lokales Leaderboard</h3>
|
||||
<div id="leaderboard-container"></div>
|
||||
<div class="leaderboards-row">
|
||||
<div class="best-times" id="best-times-1">
|
||||
<h3 id="lb-title-1">🏊♀️ Bahn 1 — Letzte Zeiten</h3>
|
||||
<div id="leaderboard-container-1" class="leaderboard-list"></div>
|
||||
</div>
|
||||
<div class="best-times" id="best-times-2">
|
||||
<h3 id="lb-title-2">🏊♂️ Bahn 2 — Letzte Zeiten</h3>
|
||||
<div id="leaderboard-container-2" class="leaderboard-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -90,7 +95,7 @@
|
||||
let learningButton = "";
|
||||
let name1 = "";
|
||||
let name2 = "";
|
||||
let leaderboardData = [];
|
||||
let leaderboardData = null;
|
||||
|
||||
// Lane Configuration
|
||||
let laneConfigType = 0; // 0=Identical, 1=Different
|
||||
@@ -329,6 +334,109 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Passt "Bereit" so an, dass es die Status-Box maximal ausfüllt.
|
||||
// Nutzt echte DOM-Messung via unsichtbarem Span → berechnet den
|
||||
// Skalierungsfaktor und setzt font-size pixelgenau.
|
||||
const fitReadyCache = { 1: { w: 0, h: 0, fs: 0 }, 2: { w: 0, h: 0, fs: 0 } };
|
||||
function fitReadyText(statusEl, laneEl, laneNum) {
|
||||
// Wir messen die Status-Box selbst — sie wurde vorher bereits
|
||||
// positioniert (top/bottom/width gesetzt), hat also ihre finale Größe.
|
||||
const sw = statusEl.clientWidth;
|
||||
const sh = statusEl.clientHeight;
|
||||
if (!sw || !sh) return;
|
||||
|
||||
const cache = fitReadyCache[laneNum];
|
||||
if (cache.w === sw && cache.h === sh) {
|
||||
statusEl.style.fontSize = cache.fs + "px";
|
||||
return;
|
||||
}
|
||||
|
||||
// Innenraum der Status-Box (nach Padding)
|
||||
const cs = window.getComputedStyle(statusEl);
|
||||
const pL = parseFloat(cs.paddingLeft) || 0;
|
||||
const pR = parseFloat(cs.paddingRight) || 0;
|
||||
const pT = parseFloat(cs.paddingTop) || 0;
|
||||
const pB = parseFloat(cs.paddingBottom) || 0;
|
||||
const availW = sw - pL - pR - 6;
|
||||
const availH = sh - pT - pB - 6;
|
||||
if (availW <= 0 || availH <= 0) return;
|
||||
|
||||
// Unsichtbarer Messspan im selben Font
|
||||
let m = fitReadyText.m;
|
||||
if (!m) {
|
||||
m = document.createElement("span");
|
||||
m.style.cssText =
|
||||
"position:absolute;visibility:hidden;white-space:nowrap;" +
|
||||
"left:-99999px;top:0;font-family:'Segoe UI',Arial,sans-serif;" +
|
||||
"font-weight:600;line-height:1;padding:0;margin:0";
|
||||
m.textContent = "Bereit";
|
||||
document.body.appendChild(m);
|
||||
fitReadyText.m = m;
|
||||
}
|
||||
const refSize = 200;
|
||||
m.style.fontSize = refSize + "px";
|
||||
const textW = m.offsetWidth || 1;
|
||||
const textH = m.offsetHeight || 1;
|
||||
|
||||
// Skalierungsfaktor so wählen, dass Breite UND Höhe passen
|
||||
const scale = Math.min(availW / textW, availH / textH);
|
||||
const finalFs = Math.max(20, Math.floor(refSize * scale));
|
||||
|
||||
cache.w = sw;
|
||||
cache.h = sh;
|
||||
cache.fs = finalFs;
|
||||
statusEl.style.fontSize = finalFs + "px";
|
||||
}
|
||||
|
||||
// Passt die Timer-Zeit (Courier-Monospace) so an, dass sie den Platz
|
||||
// zwischen h2 und Status maximal ausnutzt.
|
||||
const fitTimeCache = {
|
||||
1: { len: 0, lw: 0, lh: 0, fs: 0 },
|
||||
2: { len: 0, lw: 0, lh: 0, fs: 0 },
|
||||
};
|
||||
function fitTimeText(timeEl, laneEl, laneNum) {
|
||||
const text = timeEl.textContent;
|
||||
const len = text.length;
|
||||
const lw = laneEl.clientWidth;
|
||||
const lh = laneEl.clientHeight;
|
||||
if (!lw || !lh) return;
|
||||
|
||||
const cache = fitTimeCache[laneNum];
|
||||
if (cache.len === len && cache.lw === lw && cache.lh === lh) {
|
||||
timeEl.style.fontSize = cache.fs + "px";
|
||||
return;
|
||||
}
|
||||
|
||||
let m = fitTimeText.m;
|
||||
if (!m) {
|
||||
m = document.createElement("span");
|
||||
m.style.cssText =
|
||||
"position:absolute;visibility:hidden;white-space:nowrap;" +
|
||||
"left:-99999px;top:0;font-family:'Courier New',monospace;" +
|
||||
"font-weight:bold;line-height:1;padding:0;margin:0";
|
||||
document.body.appendChild(m);
|
||||
fitTimeText.m = m;
|
||||
}
|
||||
const refSize = 200;
|
||||
m.style.fontSize = refSize + "px";
|
||||
m.textContent = text;
|
||||
const textW = m.offsetWidth || 1;
|
||||
const textH = m.offsetHeight || 1;
|
||||
|
||||
// Aggressiv: 92% Breite, 62% Höhe (h2 oben + Status unten reserviert)
|
||||
const availW = lw * 0.92;
|
||||
const availH = lh * 0.62;
|
||||
|
||||
const scale = Math.min(availW / textW, availH / textH);
|
||||
const fs = Math.max(30, Math.floor(refSize * scale));
|
||||
|
||||
cache.len = len;
|
||||
cache.lw = lw;
|
||||
cache.lh = lh;
|
||||
cache.fs = fs;
|
||||
timeEl.style.fontSize = fs + "px";
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (seconds === 0) return "00.00";
|
||||
|
||||
@@ -353,70 +461,70 @@
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
const response = await fetch("/api/leaderboard");
|
||||
const data = await response.json();
|
||||
leaderboardData = data.leaderboard || [];
|
||||
leaderboardData = await response.json();
|
||||
updateLeaderboardDisplay();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden des Leaderboards:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLeaderboardDisplay() {
|
||||
const container = document.getElementById("leaderboard-container");
|
||||
container.innerHTML = "";
|
||||
function createEntryElement(entry) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "leaderboard-entry";
|
||||
|
||||
if (leaderboardData.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="no-times">Noch keine Zeiten erfasst</div>';
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "name";
|
||||
nameSpan.textContent = entry.name || "Unbekannt";
|
||||
|
||||
const timeSpan = document.createElement("span");
|
||||
timeSpan.className = "time";
|
||||
timeSpan.textContent = entry.timeFormatted;
|
||||
|
||||
div.appendChild(nameSpan);
|
||||
div.appendChild(timeSpan);
|
||||
return div;
|
||||
}
|
||||
|
||||
function fillLeaderboardContainer(container, entries) {
|
||||
container.innerHTML = "";
|
||||
if (!entries || entries.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "no-times";
|
||||
empty.textContent = "Noch keine Zeiten";
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
entries.forEach((e) => container.appendChild(createEntryElement(e)));
|
||||
}
|
||||
|
||||
function updateLeaderboardDisplay() {
|
||||
const box1 = document.getElementById("best-times-1");
|
||||
const box2 = document.getElementById("best-times-2");
|
||||
const container1 = document.getElementById("leaderboard-container-1");
|
||||
const container2 = document.getElementById("leaderboard-container-2");
|
||||
const title1 = document.getElementById("lb-title-1");
|
||||
const title2 = document.getElementById("lb-title-2");
|
||||
|
||||
if (!leaderboardData) {
|
||||
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";
|
||||
// Reset Layout-Klassen
|
||||
box1.classList.remove("best-times--full");
|
||||
box2.style.display = "";
|
||||
|
||||
leaderboardData.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 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);
|
||||
if (leaderboardData.mode === "different") {
|
||||
// Unterschiedliche Lanes: eigene History pro Bahn unter jeder Lane
|
||||
title1.textContent = "🏊♀️ Bahn 1 — Letzte Zeiten";
|
||||
title2.textContent = "🏊♂️ Bahn 2 — Letzte Zeiten";
|
||||
fillLeaderboardContainer(container1, leaderboardData.lane1);
|
||||
fillLeaderboardContainer(container2, leaderboardData.lane2);
|
||||
} else {
|
||||
// Identische Lanes: ein gemeinsames Leaderboard über beide Spalten
|
||||
title1.textContent = "🏆 Letzte Zeiten";
|
||||
box1.classList.add("best-times--full");
|
||||
box2.style.display = "none";
|
||||
fillLeaderboardContainer(container1, leaderboardData.entries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +592,7 @@
|
||||
s1.style.display = "flex";
|
||||
s1.style.alignItems = "center";
|
||||
s1.style.justifyContent = "center";
|
||||
s1.style.fontSize = "clamp(2rem, 8vw, 8rem)";
|
||||
fitReadyText(s1, lane1Element, 1);
|
||||
} else {
|
||||
// Bei anderen Status (running, finished, etc.) zeige Zeit wieder an
|
||||
time1Element.style.display = "";
|
||||
@@ -522,12 +630,13 @@
|
||||
s1.style.fontSize = "";
|
||||
s1.style.left = "";
|
||||
s1.style.bottom = "";
|
||||
fitTimeText(time1Element, lane1Element, 1);
|
||||
}
|
||||
}
|
||||
|
||||
switch (status1) {
|
||||
case "ready":
|
||||
s1.textContent = "Bereit für den Start!";
|
||||
s1.textContent = "Bereit";
|
||||
break;
|
||||
case "running":
|
||||
s1.textContent = "Läuft - Gib alles!";
|
||||
@@ -587,7 +696,7 @@
|
||||
s2.style.display = "flex";
|
||||
s2.style.alignItems = "center";
|
||||
s2.style.justifyContent = "center";
|
||||
s2.style.fontSize = "clamp(2rem, 8vw, 8rem)";
|
||||
fitReadyText(s2, lane2Element, 2);
|
||||
} else {
|
||||
// Bei anderen Status (running, finished, etc.) zeige Zeit wieder an
|
||||
time2Element.style.display = "";
|
||||
@@ -625,12 +734,13 @@
|
||||
s2.style.fontSize = "";
|
||||
s2.style.left = "";
|
||||
s2.style.bottom = "";
|
||||
fitTimeText(time2Element, lane2Element, 2);
|
||||
}
|
||||
}
|
||||
|
||||
switch (status2) {
|
||||
case "ready":
|
||||
s2.textContent = "Bereit für den Start!";
|
||||
s2.textContent = "Bereit";
|
||||
break;
|
||||
case "running":
|
||||
s2.textContent = "Läuft - Gib alles!";
|
||||
@@ -757,6 +867,14 @@
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
fitReadyCache[1].w = 0;
|
||||
fitReadyCache[2].w = 0;
|
||||
fitTimeCache[1].lw = 0;
|
||||
fitTimeCache[2].lw = 0;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
// Initial load
|
||||
syncFromBackend();
|
||||
loadLaneConfig();
|
||||
|
||||
Reference in New Issue
Block a user