Button Simmulator, Frontend änderungen
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
Carsten Graf
2026-04-11 20:24:39 +02:00
parent 05166b443b
commit 0223cceef8
19 changed files with 1200 additions and 152 deletions

View File

@@ -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;

View File

@@ -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();