Merge pull request 'add prevention of ctrl+c ctrl + v' (#8) from prohibit-copy into main

Reviewed-on: #8
This commit is contained in:
Adrian Joost 2026-05-18 14:32:00 +02:00
commit 57ae0ccda5

View File

@ -1,379 +1,436 @@
(function() { (function () {
// --- Konfiguration --- // --- Konfiguration ---
const MEMORIZE_TIME_SECONDS = 15; const MEMORIZE_TIME_SECONDS = 15;
// Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann. // Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann.
const TEXT_PARTS = { const TEXT_PARTS = {
subjects: [ subjects: [
"Der flinke Entwickler", "Der flinke Entwickler",
"Die neugierige Studentin", "Die neugierige Studentin",
"Ein mueder Professor", "Ein mueder Professor",
"Das kleine Frontend", "Das kleine Frontend",
"Der mutige Browser", "Der mutige Browser",
"Eine schlaue Funktion", "Eine schlaue Funktion",
"Der vergessliche Server", "Der vergessliche Server",
"Die kreative Gruppe" "Die kreative Gruppe",
], ],
actions: [ actions: [
"sortiert leise", "sortiert leise",
"debuggt geduldig", "debuggt geduldig",
"vergleicht heimlich", "vergleicht heimlich",
"speichert vorsichtig", "speichert vorsichtig",
"rendert ploetzlich", "rendert ploetzlich",
"zaehlt konzentriert", "zaehlt konzentriert",
"testet neugierig", "testet neugierig",
"kompiliert langsam" "kompiliert langsam",
], ],
objects: [ objects: [
"sieben blaue Buttons", "sieben blaue Buttons",
"drei lange Variablen", "drei lange Variablen",
"neun goldene Woerter", "neun goldene Woerter",
"vier kaputte Formulare", "vier kaputte Formulare",
"acht schnelle Requests", "acht schnelle Requests",
"zwei leuchtende Karten", "zwei leuchtende Karten",
"fuenf stille Fehlermeldungen", "fuenf stille Fehlermeldungen",
"sechs winzige Icons" "sechs winzige Icons",
], ],
places: [ places: [
"im hellen Dashboard", "im hellen Dashboard",
"unter dem dunklen Navbar", "unter dem dunklen Navbar",
"neben dem alten Footer", "neben dem alten Footer",
"zwischen Login und Leaderboard", "zwischen Login und Leaderboard",
"vor dem ersten Kaffee", "vor dem ersten Kaffee",
"waehrend der Lernphase", "waehrend der Lernphase",
"hinter dem lokalen Server", "hinter dem lokalen Server",
"mitten im Semesterprojekt" "mitten im Semesterprojekt",
], ],
endings: [ endings: [
"Danach lacht der Code, weil alles endlich funktioniert.", "Danach lacht der Code, weil alles endlich funktioniert.",
"Am Ende merkt sich niemand die Semikolons, aber alle die Punkte.", "Am Ende merkt sich niemand die Semikolons, aber alle die Punkte.",
"Kurz darauf blinkt die Konsole und behauptet, sie sei unschuldig.", "Kurz darauf blinkt die Konsole und behauptet, sie sei unschuldig.",
"Spaeter landet der Score im Ranking und wartet auf Applaus.", "Spaeter landet der Score im Ranking und wartet auf Applaus.",
"Dabei bleibt die Seite ruhig, obwohl der Timer dramatisch tickt.", "Dabei bleibt die Seite ruhig, obwohl der Timer dramatisch tickt.",
"Zum Schluss gewinnt, wer die Woerter sauber in Reihenfolge bringt." "Zum Schluss gewinnt, wer die Woerter sauber in Reihenfolge bringt.",
] ],
};
let timerInterval;
let currentTime = 0;
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
let currentGameText = "";
let lastGeneratedText = "";
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde.
let phaseStart;
let phaseMemorize;
let phaseInput;
let phaseResult;
let targetTextDisplay;
let timerDisplay;
let userTextInput;
let resultScore;
let resultOriginal;
let resultInput;
let gameStatus;
let scoreSaveFeedback;
let btnSubmitScore;
// --- Funktionen ---
function getRandomItem(items) {
return items[Math.floor(Math.random() * items.length)];
}
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen.
function generateGameText() {
let generatedText = "";
do {
const firstSentence =
[
getRandomItem(TEXT_PARTS.subjects),
getRandomItem(TEXT_PARTS.actions),
getRandomItem(TEXT_PARTS.objects),
getRandomItem(TEXT_PARTS.places),
].join(" ") + ".";
const secondSentence =
[
getRandomItem(TEXT_PARTS.subjects),
getRandomItem(TEXT_PARTS.actions),
getRandomItem(TEXT_PARTS.objects),
getRandomItem(TEXT_PARTS.places),
].join(" ") + ".";
generatedText =
firstSentence +
" " +
secondSentence +
" " +
getRandomItem(TEXT_PARTS.endings);
} while (generatedText === lastGeneratedText);
lastGeneratedText = generatedText;
return generatedText;
}
function startGame() {
if (!phaseStart || !phaseMemorize) return;
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
phaseStart.classList.add("d-none");
phaseMemorize.classList.remove("d-none");
if (gameStatus) {
gameStatus.textContent = "Lernphase";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#ffd166";
gameStatus.style.color = "#1b1b2f";
}
currentGameText = generateGameText();
if (targetTextDisplay) targetTextDisplay.textContent = currentGameText;
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
currentTime = MEMORIZE_TIME_SECONDS;
if (timerDisplay) timerDisplay.textContent = currentTime;
timerInterval = setInterval(() => {
currentTime--;
if (timerDisplay) timerDisplay.textContent = currentTime;
if (currentTime <= 0) {
endMemorizePhase();
}
}, 1000);
}
function endMemorizePhase() {
clearInterval(timerInterval);
if (!phaseMemorize || !phaseInput) return;
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
phaseMemorize.classList.add("d-none");
phaseInput.classList.remove("d-none");
if (gameStatus) {
gameStatus.textContent = "Eingabe";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#4a6fa5";
gameStatus.style.color = "#fff";
}
if (userTextInput) {
userTextInput.value = "";
userTextInput.focus();
}
}
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
function normalizeWord(word) {
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
}
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
function getWords(text) {
return text.split(/\s+/).filter((word) => word.length > 0);
}
function calculateScore(original, input) {
if (!original || !input) return 0;
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
const cleanOriginal = getWords(original)
.map(normalizeWord)
.filter((word) => word.length > 0);
const cleanInput = getWords(input)
.map(normalizeWord)
.filter((word) => word.length > 0);
let correctWords = 0;
const limit = Math.min(cleanOriginal.length, cleanInput.length);
for (let i = 0; i < limit; i++) {
if (cleanInput[i] === cleanOriginal[i]) {
correctWords++;
}
}
return correctWords;
}
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
function createWordBadge(word, isCorrect) {
const badge = document.createElement("span");
badge.className =
"badge me-1 mb-1 " + (isCorrect ? "text-bg-success" : "text-bg-danger");
badge.textContent = word;
return badge;
}
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
function renderWordComparison(original, input) {
if (!resultOriginal || !resultInput) return;
const originalWords = getWords(original);
const inputWords = getWords(input);
resultOriginal.innerHTML = "";
resultInput.innerHTML = "";
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
originalWords.forEach((word, index) => {
const isCorrect =
normalizeWord(word) === normalizeWord(inputWords[index] || "");
resultOriginal.appendChild(createWordBadge(word, isCorrect));
});
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
inputWords.forEach((word, index) => {
const isCorrect =
normalizeWord(word) === normalizeWord(originalWords[index] || "");
resultInput.appendChild(createWordBadge(word, isCorrect));
});
}
function getAuth() {
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
return null;
}
return window.AppAuth.getAuth();
}
function getScoreService() {
if (!window.config || !window.ScoreService) {
return null;
}
return new window.ScoreService(window.config);
}
function showScoreSaveFeedback(message, type) {
if (!scoreSaveFeedback) return;
scoreSaveFeedback.className = "alert alert-" + type + " mb-4";
scoreSaveFeedback.textContent = message;
scoreSaveFeedback.classList.remove("d-none");
}
async function saveScore(scoreData) {
const auth = getAuth();
if (!auth || !auth.username || !auth.password) {
showScoreSaveFeedback(
"Score wurde nur lokal berechnet. Bitte einloggen, damit er im Leaderboard gespeichert wird.",
"warning",
);
return;
}
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
const scoreService = getScoreService();
if (!scoreService) {
showScoreSaveFeedback(
"Score-Service konnte nicht geladen werden.",
"danger",
);
return;
}
showScoreSaveFeedback("Score wird gespeichert...", "info");
const result = await scoreService.postScore(
auth.username,
auth.password,
scoreData.score,
scoreData.time,
scoreData.text,
scoreData.userWrittenText,
);
if (result.ok) {
const place =
result.body && result.body.place
? " Platz " + result.body.place + "."
: "";
showScoreSaveFeedback(
"Score erfolgreich gespeichert." + place,
"success",
);
return;
}
if (result.status === 401) {
showScoreSaveFeedback(
"Score konnte nicht gespeichert werden: Login ist nicht gültig.",
"danger",
);
return;
}
showScoreSaveFeedback(
"Score konnte nicht gespeichert werden (Status " + result.status + ").",
"danger",
);
}
async function submitScore() {
if (!userTextInput) return;
const userInput = userTextInput.value.trim();
if (!userInput) {
alert("Bitte geben Sie einen Text ein.");
return;
}
if (btnSubmitScore) {
btnSubmitScore.disabled = true;
btnSubmitScore.textContent = "Wird ausgewertet...";
}
const score = calculateScore(currentGameText, userInput);
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
if (phaseInput) phaseInput.classList.add("d-none");
if (phaseResult) phaseResult.classList.remove("d-none");
if (gameStatus) {
gameStatus.textContent = "Abgeschlossen";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#28a745";
gameStatus.style.color = "#fff";
}
if (resultScore) resultScore.textContent = score;
renderWordComparison(currentGameText, userInput);
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
const scoreData = {
score: score,
time: MEMORIZE_TIME_SECONDS,
text: currentGameText,
userWrittenText: userInput,
}; };
let timerInterval; console.log("Score bereit zum Senden:", scoreData);
let currentTime = 0;
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben. try {
let currentGameText = ""; await saveScore(scoreData);
let lastGeneratedText = ""; } catch (error) {
console.error("Fehler beim Speichern des Scores:", error);
showScoreSaveFeedback(
"Score konnte wegen eines technischen Fehlers nicht gespeichert werden.",
"danger",
);
} finally {
if (btnSubmitScore) {
btnSubmitScore.disabled = false;
btnSubmitScore.textContent = "Auswerten & Absenden";
}
}
}
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde. window.initPlayPage = function initPlayPage() {
let phaseStart; clearInterval(timerInterval);
let phaseMemorize;
let phaseInput;
let phaseResult;
let targetTextDisplay;
let timerDisplay;
let userTextInput;
let resultScore;
let resultOriginal;
let resultInput;
let gameStatus;
let scoreSaveFeedback;
let btnSubmitScore;
// --- Funktionen --- // Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
phaseStart = document.getElementById("phaseStart");
phaseMemorize = document.getElementById("phaseMemorize");
phaseInput = document.getElementById("phaseInput");
phaseResult = document.getElementById("phaseResult");
function getRandomItem(items) { targetTextDisplay = document.getElementById("targetTextDisplay");
return items[Math.floor(Math.random() * items.length)]; timerDisplay = document.getElementById("timerDisplay");
userTextInput = document.getElementById("userTextInput");
resultScore = document.getElementById("resultScore");
resultOriginal = document.getElementById("resultOriginal");
resultInput = document.getElementById("resultInput");
gameStatus = document.getElementById("gameStatus");
scoreSaveFeedback = document.getElementById("scoreSaveFeedback");
const btnStart = document.getElementById("btnStartGame");
btnSubmitScore = document.getElementById("btnSubmitScore");
const btnRestart = document.getElementById("btnRestart");
const btnLeaderboard = document.getElementById("btnLeaderboard");
if (btnStart) btnStart.addEventListener("click", startGame);
if (btnSubmitScore) btnSubmitScore.addEventListener("click", submitScore);
if (btnRestart)
btnRestart.addEventListener("click", () =>
window.loadPage("play", "nav-play"),
);
if (btnLeaderboard)
btnLeaderboard.addEventListener("click", () => {
const navLink = document.getElementById("nav-leaderboard");
if (navLink) {
navLink.click(); // Nutzt die bestehende Navigation inklusive Active-State.
} else {
console.warn("Sidebar Link #nav-leaderboard nicht gefunden.");
}
});
if (userTextInput) {
userTextInput.addEventListener("keydown", (e) => {
const key = e.key.toLowerCase();
if (
(e.ctrlKey || e.metaKey) &&
(key === "c" || key === "v" || key === "x" || key === "a")
) {
e.preventDefault();
alert("Yeah, cheating is not sooo nice...");
}
});
userTextInput.addEventListener("paste", (e) => e.preventDefault());
userTextInput.addEventListener("copy", (e) => e.preventDefault());
userTextInput.addEventListener("cut", (e) => e.preventDefault());
} }
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen. if (targetTextDisplay) {
function generateGameText() { targetTextDisplay.addEventListener("copy", (e) => e.preventDefault());
let generatedText = ""; targetTextDisplay.style.userSelect = "none";
do {
const firstSentence = [
getRandomItem(TEXT_PARTS.subjects),
getRandomItem(TEXT_PARTS.actions),
getRandomItem(TEXT_PARTS.objects),
getRandomItem(TEXT_PARTS.places)
].join(" ") + ".";
const secondSentence = [
getRandomItem(TEXT_PARTS.subjects),
getRandomItem(TEXT_PARTS.actions),
getRandomItem(TEXT_PARTS.objects),
getRandomItem(TEXT_PARTS.places)
].join(" ") + ".";
generatedText = firstSentence + " " + secondSentence + " " + getRandomItem(TEXT_PARTS.endings);
} while (generatedText === lastGeneratedText);
lastGeneratedText = generatedText;
return generatedText;
} }
};
function startGame() {
if (!phaseStart || !phaseMemorize) return;
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
phaseStart.classList.add('d-none');
phaseMemorize.classList.remove('d-none');
if(gameStatus) {
gameStatus.textContent = "Lernphase";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#ffd166";
gameStatus.style.color = "#1b1b2f";
}
currentGameText = generateGameText();
if(targetTextDisplay) targetTextDisplay.textContent = currentGameText;
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
currentTime = MEMORIZE_TIME_SECONDS;
if(timerDisplay) timerDisplay.textContent = currentTime;
timerInterval = setInterval(() => {
currentTime--;
if(timerDisplay) timerDisplay.textContent = currentTime;
if (currentTime <= 0) {
endMemorizePhase();
}
}, 1000);
}
function endMemorizePhase() {
clearInterval(timerInterval);
if (!phaseMemorize || !phaseInput) return;
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
phaseMemorize.classList.add('d-none');
phaseInput.classList.remove('d-none');
if(gameStatus) {
gameStatus.textContent = "Eingabe";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#4a6fa5";
gameStatus.style.color = "#fff";
}
if(userTextInput) {
userTextInput.value = "";
userTextInput.focus();
}
}
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
function normalizeWord(word) {
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
}
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
function getWords(text) {
return text.split(/\s+/).filter(word => word.length > 0);
}
function calculateScore(original, input) {
if (!original || !input) return 0;
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
const cleanOriginal = getWords(original).map(normalizeWord).filter(word => word.length > 0);
const cleanInput = getWords(input).map(normalizeWord).filter(word => word.length > 0);
let correctWords = 0;
const limit = Math.min(cleanOriginal.length, cleanInput.length);
for (let i = 0; i < limit; i++) {
if (cleanInput[i] === cleanOriginal[i]) {
correctWords++;
}
}
return correctWords;
}
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
function createWordBadge(word, isCorrect) {
const badge = document.createElement("span");
badge.className = "badge me-1 mb-1 " + (isCorrect ? "text-bg-success" : "text-bg-danger");
badge.textContent = word;
return badge;
}
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
function renderWordComparison(original, input) {
if (!resultOriginal || !resultInput) return;
const originalWords = getWords(original);
const inputWords = getWords(input);
resultOriginal.innerHTML = "";
resultInput.innerHTML = "";
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
originalWords.forEach((word, index) => {
const isCorrect = normalizeWord(word) === normalizeWord(inputWords[index] || "");
resultOriginal.appendChild(createWordBadge(word, isCorrect));
});
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
inputWords.forEach((word, index) => {
const isCorrect = normalizeWord(word) === normalizeWord(originalWords[index] || "");
resultInput.appendChild(createWordBadge(word, isCorrect));
});
}
function getAuth() {
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
return null;
}
return window.AppAuth.getAuth();
}
function getScoreService() {
if (!window.config || !window.ScoreService) {
return null;
}
return new window.ScoreService(window.config);
}
function showScoreSaveFeedback(message, type) {
if (!scoreSaveFeedback) return;
scoreSaveFeedback.className = "alert alert-" + type + " mb-4";
scoreSaveFeedback.textContent = message;
scoreSaveFeedback.classList.remove("d-none");
}
async function saveScore(scoreData) {
const auth = getAuth();
if (!auth || !auth.username || !auth.password) {
showScoreSaveFeedback(
"Score wurde nur lokal berechnet. Bitte einloggen, damit er im Leaderboard gespeichert wird.",
"warning"
);
return;
}
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
const scoreService = getScoreService();
if (!scoreService) {
showScoreSaveFeedback("Score-Service konnte nicht geladen werden.", "danger");
return;
}
showScoreSaveFeedback("Score wird gespeichert...", "info");
const result = await scoreService.postScore(
auth.username,
auth.password,
scoreData.score,
scoreData.time,
scoreData.text,
scoreData.userWrittenText
);
if (result.ok) {
const place = result.body && result.body.place ? " Platz " + result.body.place + "." : "";
showScoreSaveFeedback("Score erfolgreich gespeichert." + place, "success");
return;
}
if (result.status === 401) {
showScoreSaveFeedback("Score konnte nicht gespeichert werden: Login ist nicht gültig.", "danger");
return;
}
showScoreSaveFeedback("Score konnte nicht gespeichert werden (Status " + result.status + ").", "danger");
}
async function submitScore() {
if (!userTextInput) return;
const userInput = userTextInput.value.trim();
if (!userInput) {
alert("Bitte geben Sie einen Text ein.");
return;
}
if (btnSubmitScore) {
btnSubmitScore.disabled = true;
btnSubmitScore.textContent = "Wird ausgewertet...";
}
const score = calculateScore(currentGameText, userInput);
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
if (phaseInput) phaseInput.classList.add('d-none');
if (phaseResult) phaseResult.classList.remove('d-none');
if(gameStatus) {
gameStatus.textContent = "Abgeschlossen";
gameStatus.className = "badge fs-6 px-3 py-2";
gameStatus.style.backgroundColor = "#28a745";
gameStatus.style.color = "#fff";
}
if (resultScore) resultScore.textContent = score;
renderWordComparison(currentGameText, userInput);
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
const scoreData = {
score: score,
time: MEMORIZE_TIME_SECONDS,
text: currentGameText,
userWrittenText: userInput
};
console.log("Score bereit zum Senden:", scoreData);
try {
await saveScore(scoreData);
} catch (error) {
console.error("Fehler beim Speichern des Scores:", error);
showScoreSaveFeedback("Score konnte wegen eines technischen Fehlers nicht gespeichert werden.", "danger");
} finally {
if (btnSubmitScore) {
btnSubmitScore.disabled = false;
btnSubmitScore.textContent = "Auswerten & Absenden";
}
}
}
window.initPlayPage = function initPlayPage() {
clearInterval(timerInterval);
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
phaseStart = document.getElementById('phaseStart');
phaseMemorize = document.getElementById('phaseMemorize');
phaseInput = document.getElementById('phaseInput');
phaseResult = document.getElementById('phaseResult');
targetTextDisplay = document.getElementById('targetTextDisplay');
timerDisplay = document.getElementById('timerDisplay');
userTextInput = document.getElementById('userTextInput');
resultScore = document.getElementById('resultScore');
resultOriginal = document.getElementById('resultOriginal');
resultInput = document.getElementById('resultInput');
gameStatus = document.getElementById('gameStatus');
scoreSaveFeedback = document.getElementById('scoreSaveFeedback');
const btnStart = document.getElementById('btnStartGame');
btnSubmitScore = document.getElementById('btnSubmitScore');
const btnRestart = document.getElementById('btnRestart');
const btnLeaderboard = document.getElementById('btnLeaderboard');
if (btnStart) btnStart.addEventListener('click', startGame);
if (btnSubmitScore) btnSubmitScore.addEventListener('click', submitScore);
if (btnRestart) btnRestart.addEventListener('click', () => window.loadPage("play", "nav-play"));
if (btnLeaderboard) btnLeaderboard.addEventListener('click', () => {
const navLink = document.getElementById('nav-leaderboard');
if (navLink) {
navLink.click(); // Nutzt die bestehende Navigation inklusive Active-State.
} else {
console.warn("Sidebar Link #nav-leaderboard nicht gefunden.");
}
});
};
})(); })();