+ `;
+
+ 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(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/login.js b/js/login.js
index 73aaf38..03d97ed 100644
--- a/js/login.js
+++ b/js/login.js
@@ -71,7 +71,7 @@
logoutButton.disabled = false;
deleteAccountButton.disabled = false;
currentSessionBox.classList.remove("d-none");
- authFormsRow.classList.add("d-none");
+ authFormsRow.classList.remove("d-none");
} else {
sessionText.textContent = "Nicht eingeloggt.";
logoutButton.disabled = true;
@@ -100,6 +100,7 @@
const usernameInput = document.getElementById("login-username");
const passwordInput = document.getElementById("login-password");
+ const submitButton = event.submitter;
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
@@ -108,7 +109,29 @@
return;
}
- const result = await userService.getUser(username, password);
+ if (submitButton) {
+ submitButton.disabled = true;
+ submitButton.textContent = "Einloggen...";
+ }
+
+ let result;
+ try {
+ result = await userService.getUser(username, password);
+ } catch (error) {
+ console.error("Login fehlgeschlagen:", error);
+ setFeedback("Login fehlgeschlagen: Backend ist nicht erreichbar.", "danger");
+ if (submitButton) {
+ submitButton.disabled = false;
+ submitButton.textContent = "Einloggen";
+ }
+ return;
+ }
+
+ if (submitButton) {
+ submitButton.disabled = false;
+ submitButton.textContent = "Einloggen";
+ }
+
if (result.ok) {
saveAuth(username, password);
setFeedback("Login erfolgreich.", "success");
diff --git a/js/navigation.js b/js/navigation.js
index 6111ac0..622579e 100644
--- a/js/navigation.js
+++ b/js/navigation.js
@@ -23,6 +23,12 @@ document.addEventListener("DOMContentLoaded", () => {
if (page === "login" && typeof window.initLoginPage === "function") {
window.initLoginPage();
}
+ if (page === "leaderboard" && typeof window.initLeaderboardPage === "function") {
+ window.initLeaderboardPage();
+ }
+ if (page === "play" && typeof window.initPlayPage === "function") {
+ window.initPlayPage();
+ }
})
.catch(error => {
console.error("Fehler beim Laden von " + page + ":", error);
@@ -46,4 +52,4 @@ document.addEventListener("DOMContentLoaded", () => {
//Startseite laden
loadPage("home", "nav-home");
-});
\ No newline at end of file
+});
diff --git a/js/play.js b/js/play.js
new file mode 100644
index 0000000..a73dd06
--- /dev/null
+++ b/js/play.js
@@ -0,0 +1,379 @@
+(function() {
+ // --- Konfiguration ---
+ const MEMORIZE_TIME_SECONDS = 15;
+
+ // Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann.
+ const TEXT_PARTS = {
+ subjects: [
+ "Der flinke Entwickler",
+ "Die neugierige Studentin",
+ "Ein mueder Professor",
+ "Das kleine Frontend",
+ "Der mutige Browser",
+ "Eine schlaue Funktion",
+ "Der vergessliche Server",
+ "Die kreative Gruppe"
+ ],
+ actions: [
+ "sortiert leise",
+ "debuggt geduldig",
+ "vergleicht heimlich",
+ "speichert vorsichtig",
+ "rendert ploetzlich",
+ "zaehlt konzentriert",
+ "testet neugierig",
+ "kompiliert langsam"
+ ],
+ objects: [
+ "sieben blaue Buttons",
+ "drei lange Variablen",
+ "neun goldene Woerter",
+ "vier kaputte Formulare",
+ "acht schnelle Requests",
+ "zwei leuchtende Karten",
+ "fuenf stille Fehlermeldungen",
+ "sechs winzige Icons"
+ ],
+ places: [
+ "im hellen Dashboard",
+ "unter dem dunklen Navbar",
+ "neben dem alten Footer",
+ "zwischen Login und Leaderboard",
+ "vor dem ersten Kaffee",
+ "waehrend der Lernphase",
+ "hinter dem lokalen Server",
+ "mitten im Semesterprojekt"
+ ],
+ endings: [
+ "Danach lacht der Code, weil alles endlich funktioniert.",
+ "Am Ende merkt sich niemand die Semikolons, aber alle die Punkte.",
+ "Kurz darauf blinkt die Konsole und behauptet, sie sei unschuldig.",
+ "Spaeter landet der Score im Ranking und wartet auf Applaus.",
+ "Dabei bleibt die Seite ruhig, obwohl der Timer dramatisch tickt.",
+ "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
+ };
+
+ 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.");
+ }
+ });
+ };
+
+})();
diff --git a/pages/home.html b/pages/home.html
index 731056c..585c591 100644
--- a/pages/home.html
+++ b/pages/home.html
@@ -1,8 +1,7 @@
-
Willkommen beim Lorem Ipsum Game
-
Teste deine Fähigkeiten im Umgang mit Lorem Ipsum Texten! Je schneller und genauer du bist, desto höher ist dein Score.
+
Willkommen beim Lorem Ipsum Game
+
Teste deine Fähigkeiten im Umgang mit Lorem Ipsum Texten! Je schneller und genauer du bist, desto höher ist dein Score.
Viel Spaß beim Spielen!
-
-
Wähle eine Option aus der Navigation, um zu starten.
-
+
Wähle eine Option aus der Navigation, um zu starten.
+
\ No newline at end of file
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.