This commit is contained in:
111
data/index.css
111
data/index.css
@@ -472,10 +472,13 @@ body {
|
||||
}
|
||||
|
||||
.status.large-status.ready {
|
||||
font-size: clamp(2rem, 8vw, 8rem) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
padding: 8px 12px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status.finished {
|
||||
@@ -538,41 +541,55 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.best-times {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: clamp(10px, 1.5vh, 15px);
|
||||
margin: 1vh 0 0 0;
|
||||
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;
|
||||
.leaderboards-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: clamp(12px, 2vh, 20px);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(15px, 2vw, 30px);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0 2vw;
|
||||
margin-top: 0.5vh;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#leaderboard-container {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@media (max-width: 768px) {
|
||||
.leaderboards-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: clamp(15px, 3vw, 30px);
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.best-times {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: clamp(6px, 1vh, 10px);
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: clamp(4px, 0.8vh, 8px);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.best-times--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.leaderboard-list {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(4px, 0.8vh, 8px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.best-times h3 {
|
||||
font-size: clamp(0.9rem, 1.8vw, 1.1rem);
|
||||
font-size: clamp(0.7rem, 1.2vw, 0.85rem);
|
||||
margin: 0 auto;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
@@ -592,53 +609,58 @@ body {
|
||||
}
|
||||
|
||||
/* Leaderboard Styles */
|
||||
#leaderboard-container {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: clamp(8px, 1vh, 12px) 0;
|
||||
font-size: clamp(1.1rem, 2.2vw, 1.4rem);
|
||||
margin: 0;
|
||||
font-size: clamp(0.7rem, 1.1vw, 0.9rem);
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: clamp(12px, 2vh, 16px) clamp(16px, 3vw, 24px);
|
||||
border-radius: 10px;
|
||||
padding: clamp(4px, 0.8vh, 7px) clamp(8px, 1.2vw, 12px);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 50px;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.leaderboard-entry:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.leaderboard-entry .rank {
|
||||
color: #ffd700;
|
||||
font-weight: bold;
|
||||
min-width: 30px;
|
||||
font-size: clamp(1.2rem, 2.4vw, 1.5rem);
|
||||
min-width: 20px;
|
||||
font-size: clamp(0.75rem, 1.2vw, 0.95rem);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leaderboard-entry .name {
|
||||
flex: 1;
|
||||
margin: 0 15px;
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: clamp(0.7rem, 1.1vw, 0.9rem);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.leaderboard-entry .time {
|
||||
color: #00ff88;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 80px;
|
||||
min-width: 70px;
|
||||
text-align: right;
|
||||
font-size: clamp(1rem, 1.8vw, 1.3rem);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leaderboard-entry.gold {
|
||||
@@ -703,6 +725,7 @@ body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.learning-mode {
|
||||
background: rgba(245, 157, 15, 0.2);
|
||||
border: 2px solid #f59d0f;
|
||||
|
||||
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