Compare commits

...

7 Commits

Author SHA1 Message Date
ad64bae67b Merge pull request 'impressum' (#7) from impressum into main
Reviewed-on: #7
2026-05-18 14:10:56 +02:00
DST81
d91d42f759 Add Spiellogik in play.html und play.js 2026-05-16 17:26:49 +02:00
DST81
66cee7e5e2 Add branch leaderboard to impressum 2026-05-13 10:26:59 +02:00
DST81
90ab402ca6 Add play.js 2026-05-13 10:22:06 +02:00
DST81
184f933a57 Merge branch 'leaderboard' into impressum 2026-05-13 10:15:44 +02:00
DST81
40e61c16a2 Add Impressum 2026-05-10 23:54:07 +02:00
Trompi001
9f995e213a creating leaderboard 2026-04-22 10:23:35 +02:00
9 changed files with 842 additions and 26 deletions

View File

@ -137,9 +137,9 @@ p {
border-radius: 6px;
padding: 5px 10px;
}
#logo-img {
max-width: 500px;
width: 100%;
#logo_img {
max-width: 300px;
width: 50%;
height: auto;
}
#main-area {
@ -213,3 +213,67 @@ p {
}
/* Table styles leaderboard */
.leaderboard-table {
border-collapse: collapse;
width: 100%;
background: #ffffff;
border-radius: 10px;
overflow: hidden;
}
.leaderboard-table th,
.leaderboard-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.leaderboard-table thead th {
background: #1b1b2f;
color: #ffffff;
font-weight: 600;
letter-spacing: 0.02em;
}
.leaderboard-table tbody tr {
transition: background-color 0.2s ease;
}
.leaderboard-table tbody tr:nth-child(even) {
background-color: #f7f9fc;
}
.leaderboard-table tbody tr:hover {
background-color: #dbe6f7;
}
.leaderboard-table tbody tr.leaderboard-row-current-user,
.leaderboard-table tbody tr.leaderboard-row-current-user:nth-child(even),
.leaderboard-table tbody tr.leaderboard-row-current-user:hover {
background-color: #dff3df;
}
.leaderboard-table tbody tr.leaderboard-row-current-user td {
font-weight: 600;
}
.leaderboard-table tbody tr.leaderboard-row-gap td {
border-bottom: none;
padding: 0;
height: 16px;
background: transparent;
}
.leaderboard-table tbody tr.leaderboard-row-gap:hover,
.leaderboard-table tbody tr.leaderboard-row-gap:hover td {
background: transparent;
}
.leaderboard-table tbody tr.leaderboard-row-current-user-extra td {
border-top: 2px solid #b8dfb8;
}
.leaderboard-table tbody tr:last-child td {
border-bottom: none;
}

View File

@ -3,14 +3,41 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title >Lorem Ipsum - Das Spiel</title>
<title>Lorem Ipsum - Das Spiel</title>
<link rel="icon" type="image/png" href="image/icon_l.png">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="assets/bootstrap-5.3.8-dist/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
<!-- Kleine Korrektur direkt hier, falls Sie custom.css nicht sofort ändern wollen -->
<style>
/* Sicherstellen, dass der Main-Area den ganzen Platz einnimmt */
#main-area {
display: flex;
flex-direction: column;
flex: 1; /* Füllt den verbleibenden Platz im page-wrapper */
}
/* Content soll wachsen, Footer wird nach unten gedrückt */
#main-content {
flex: 1;
}
/* Footer immer unten im Flex-Container */
footer {
margin-top: auto;
}
/* Body Padding entfernen für sauberes Dashboard-Layout */
body {
padding: 0 !important;
overflow-x: hidden; /* Verhindert horizontales Scrollen durch Sidebar */
}
</style>
</head>
<body class="p-4">
<!-- class="p-4" entfernt -->
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
@ -18,7 +45,6 @@
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!--evtl. weglassen, da es eine Sidebar gibt -->
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class ="nav-item"><a class="nav-link" href="#" id="navbar-messages">Nachrichten</a></li>
@ -27,6 +53,7 @@
</div>
</div>
</nav>
<div id="page-wrapper">
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white p-3">
@ -35,19 +62,66 @@
<li class="nav-item"><a class="nav-link" href="#" id="nav-play">Spiel Starten</a></li>
<li class="nav-item"><a class="nav-link" href="#" id="nav-my-scores">Meine Scores</a></li>
<li class="nav-item"> <a class="nav-link" href="#" id="nav-leaderboard">Leaderboard</a></li>
</ul>
</nav>
<!--Main Area -->
<!-- Main Area: Enthält jetzt Topbar, Content UND Footer -->
<div id="main-area">
<!--Topbar-->
<!-- Topbar -->
<header id="topbar">
<h1 class="text-center mb-4">Dashboard</h1>
</header>
<!-- Content -->
<main class="container mt-4" id="main-content"></main>
<main class="container mt-4" id="main-content">
<!-- Hier werden die pages.html geladen -->
</main>
<!-- Footer: Jetzt INSIDE main-area, damit Flexbox funktioniert -->
<footer class="bg-dark text-black py-4 border-top border-secondary">
<div class="container">
<!-- Oberer Bereich -->
<div class="row mb-3">
<div class="col-12">Made with <span class="text-danger"></span> für das Modul Frontend
<h5 class="text-warning">Made with Bootstrap 5.3.8.</h5>
<p class="text-black small mb-0 fst-italic">
<em>„Unser Impressum ist länger als der Text, den Sie sich merken müssen.“</em>
</p>
</div>
</div>
<!-- Impressum -->
<div class="row mt-2">
<div class="col-12">
<span class="text-warning fw-bold d-block mb-2" style="font-size: 0.7rem;">Impressum</span>
<div class="text-black-50" style="font-size: 0.7rem; line-height: 1.4;">
<div class="row g-1">
<div class="col-12 col-md-6 col-lg-4">
<strong>Betreiber</strong>:&nbsp;Das Team FAD (Florin, Adi, Daniela)
</div>
<div class="col-12 col-md-6 col-lg-4">
<strong>Adresse</strong>:&nbsp;Irgendwo im Internet, Schweiz
</div>
<div class="col-12 col-md-6 col-lg-4">
<strong>Kontakt</strong>:&nbsp;schreibuns@lorem-ipsum-spiel.ch
</div>
<div class="col-12 col-md-6 col-lg-4">
<strong>Inhalt</strong>:&nbsp;Der letzte Committer
</div>
<div class="col-12 col-md-6 col-lg-4">
<strong>Datenschutz</strong>:&nbsp;Wir speichern nur Scores.
</div>
</div>
</div>
<div class="text-center mt-2">
<p class="mb-0 text-black-50" style="font-size: 0.65rem;">
&copy; 2026 Modul Frontend Projekt. Alle Rechte vorbehalten.
</p>
</div>
</div>
</div>
</div>
</footer>
</div>
</div>
@ -58,8 +132,9 @@
<script src="assets/src/service/score-service.js"></script>
<script src="assets/src/service/leaderboard-service.js"></script>
<script src="js/login.js"></script>
<script src="js/leaderboard.js"></script>
<!--Navigation Script -->
<script src="js/play.js"></script>
<script src="js/navigation.js"></script>
</body>
</html>

164
js/leaderboard.js Normal file
View File

@ -0,0 +1,164 @@
// Formatiert Sekunden als m:ss. Bei ungültigem Wert wird ein Platzhalter angezeigt.
function formatTime(seconds) {
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
return "-";
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
}
// Liefert den aktuellen Login-Kontext, falls Auth global verfügbar ist.
function getLoggedInAuth() {
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
return null;
}
const auth = window.AppAuth.getAuth();
if (!auth || !auth.username) {
return null;
}
return auth;
}
// Vereinheitlicht Benutzernamen für robuste Vergleiche (z. B. Groß-/Kleinschreibung).
function normalizeUsername(username) {
return String(username ?? "").trim().toLowerCase();
}
function getLoggedInUsername() {
const auth = getLoggedInAuth();
if (!auth) {
return null;
}
return normalizeUsername(auth.username);
}
// Nutzt den vom Backend gelieferten Rang, fallback auf die aktuelle Listenposition.
function getDisplayedRank(entry, index) {
const place = Number(entry?.place);
if (!Number.isNaN(place) && place > 0) {
return place;
}
return index + 1;
}
// Bestes Ergebnis: höchste Punktzahl, bei Gleichstand die geringere Zeit.
function getBestScoreEntry(entries) {
return entries
.slice()
.sort((a, b) => {
const scoreA = Number(a.score ?? 0);
const scoreB = Number(b.score ?? 0);
if (scoreB !== scoreA) {
return scoreB - scoreA;
}
const timeA = Number(a.time ?? Number.MAX_SAFE_INTEGER);
const timeB = Number(b.time ?? Number.MAX_SAFE_INTEGER);
return timeA - timeB;
})[0] ?? null;
}
async function getCurrentUserLeaderboardEntry(username) {
if (!window.ScoreService || !username) {
return null;
}
const scoreService = new window.ScoreService(window.config);
const result = await scoreService.getScoreByName(username);
if (!result.ok || !Array.isArray(result.body) || result.body.length === 0) {
return null;
}
return getBestScoreEntry(result.body);
}
// Rendert die Top-Liste und markiert den eingeloggten Nutzer visuell.
function renderLeaderboard(entries, extraUserEntry = null) {
const tableBody = document.getElementById("leaderboard-body");
if (!tableBody) {
return;
}
const loggedInUsername = getLoggedInUsername();
tableBody.innerHTML = "";
entries.forEach((entry, index) => {
const row = document.createElement("tr");
const rowUsername = normalizeUsername(entry.username);
if (loggedInUsername && rowUsername === loggedInUsername) {
row.classList.add("leaderboard-row-current-user");
}
row.innerHTML = `
<td>${getDisplayedRank(entry, index)}</td>
<td>${entry.username ?? "-"}</td>
<td>${formatTime(entry.time)} min</td>
<td>${entry.score ?? "-"}</td>
`;
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 = '<td colspan="4"></td>';
tableBody.appendChild(spacerRow);
const userRow = document.createElement("tr");
userRow.classList.add("leaderboard-row-current-user");
userRow.classList.add("leaderboard-row-current-user-extra");
userRow.innerHTML = `
<td>${getDisplayedRank(extraUserEntry, 0)}</td>
<td>${extraUserEntry.username ?? "-"}</td>
<td>${formatTime(extraUserEntry.time)} min</td>
<td>${extraUserEntry.score ?? "-"}</td>
`;
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([]);
});
};

View File

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

View File

@ -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");
});
});

379
js/play.js Normal file
View File

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

View File

@ -1,8 +1,7 @@
<div class="card">
<h2>Willkommen beim Lorem Ipsum Game</h2>
<p>Teste deine Fähigkeiten im Umgang mit Lorem Ipsum Texten! Je schneller und genauer du bist, desto höher ist dein Score.
<h4 class="card-title mb-3">Willkommen beim Lorem Ipsum Game</h2>
<p class="card-text text-uted fs-6 mb-4">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!</p>
<p> Wähle eine Option aus der Navigation, um zu starten.</p>
<img id="logo_img" src="image/Logo_loremIpsum.png" alt="Lorem Ipsum Game" class="img-fluid mt-3 d-block mx-auto">
<p class="card-text fs-6"> Wähle eine Option aus der Navigation, um zu starten.</p>
<img id="logo_img" src="image/Logo_loremIpsum.png" alt="Lorem Ipsum Game"class="img-fluid mt-3 d-block mx-auto">
</div>

View File

@ -1,5 +1,19 @@
<!-- Hauptbereich der Rangliste -->
<div class="card">
<h2 class="mb-3">Leaderboard</h2>
<p class="mt-2">Hier kannst du die besten Spieler und ihre Scores sehen.</p>
<!-- Tabellarische Darstellung der besten Eintraege -->
<table class="leaderboard-table">
<thead>
<!-- Spalten fuer Rang, Nutzer, Zeit und Punkte -->
<tr>
<th>Rank</th>
<th>User</th>
<th>time</th>
<th>Score</th>
</tr>
</thead>
<!-- Wird zur Laufzeit durch js/leaderboard.js befuellt -->
<tbody id="leaderboard-body"></tbody>
</table>
</div>

View File

@ -1,7 +1,99 @@
<div class= "card">
<h2 class="mb-3">Spiel Starten</h2>
<p class="mt-2">Hier kannst du das Spiel starten. Viel Erfolg!</p>
<!-- Spielseite: Die vier Phasen werden per play.js ein- und ausgeblendet. -->
<div class="game-container">
<!-- Status-Badge zeigt die aktuelle Spielphase: Bereit, Lernphase, Eingabe, Abgeschlossen. -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">Lorem Ipsum - Challenge you brain</h2>
<p class="text-muted mb-0">Merken Sie sich den Text so gut wie möglich.</p>
</div>
<div id="gameStatus" class="badge bg-secondary fs-6 px-3 py-2">
Bereit
</div>
</div>
<!-- Phase 1: Startbildschirm vor der Textanzeige. -->
<div id="phaseStart" class="card">
<div class="card-body text-center py-5">
<h3 class="mb-3">Sind Sie bereit?</h3>
<p class="mb-4" style="max-width: 600px; margin: 0 auto; font-size: 1.1rem;">
Sie sehen gleich einen Text für <strong style="color: #4a6fa5;">15 Sekunden</strong>.<br>
Versuchen Sie, sich so viele Wörter wie möglich zu merken!
</p>
<button id="btnStartGame" class="btn btn-lg px-5">
Spiel Starten
</button>
</div>
</div>
<!-- Phase 2: Merken. Der zufaellige Rundentext wird in #targetTextDisplay eingesetzt. -->
<div id="phaseMemorize" class="d-none">
<div class="card border-warning" style="border-left: 5px solid #ffd166; background-color: #fffbf0;">
<div class="card-body text-center py-4">
<h4 class="text-warning-emphasis mb-3" style="color: #b58900;">Lernphase läuft...</h4>
<p id="targetTextDisplay" class="lead fw-bold mb-4 fst-italic" style="color: #1b1b2f; font-size: 1.4rem; line-height: 1.6;">
<!-- Text wird per JS eingefügt -->
</p>
<div class="mt-3">
<span class="badge bg-danger fs-6 px-3 py-2 blink-animation">
Verbleibende Zeit: <span id="timerDisplay">15</span>s
</span>
</div>
</div>
</div>
</div>
<!-- Phase 3: Eingabe aus dem Gedaechtnis. Der Originaltext ist hier bewusst ausgeblendet. -->
<div id="phaseInput" class="d-none">
<div class="card">
<div class="card-body">
<label for="userTextInput" class="form-label fw-bold mb-2">Geben Sie den Text aus dem Gedächtnis ein:</label>
<textarea id="userTextInput" class="form-control mb-3" rows="8" placeholder="Tippen Sie hier den gemerkten Text ein..."></textarea>
<div class="d-grid">
<button id="btnSubmitScore" class="btn btn-success btn-lg" style="background-color: #28a745; border: none;">
Auswerten & Absenden
</button>
</div>
</div>
</div>
</div>
<!-- Phase 4: Auswertung inklusive Backend-Feedback und Vergleich Original/Eingabe. -->
<div id="phaseResult" class="d-none">
<div class="card text-center" style="border-top: 5px solid #28a745;">
<div class="card-body py-5">
<h3 class="text-success mb-2">Ergebnis</h3>
<div class="display-1 fw-bold my-4" id="resultScore" style="color: #4a6fa5;">0</div>
<p class="text-muted mb-5">Punkte (korrekte Wörter an der richtigen Position)</p>
<!-- Wird von saveScore() befuellt: gespeichert, nur lokal berechnet oder Fehler. -->
<div id="scoreSaveFeedback" class="alert d-none mb-4" role="alert"></div>
<div class="row g-4 text-start mb-5">
<div class="col-md-6">
<div class="p-3 h-100" style="background: #f8f9fa; border-radius: 8px;">
<small class="text-muted d-block mb-2 text-uppercase fw-bold" style="font-size: 0.75rem;">Originaltext</small>
<p class="mb-0 fst-italic" id="resultOriginal" style="font-size: 0.95rem; line-height: 1.5;"></p>
</div>
</div>
<div class="col-md-6">
<div class="p-3 h-100" style="background: #f8f9fa; border-radius: 8px;">
<small class="text-muted d-block mb-2 text-uppercase fw-bold" style="font-size: 0.75rem;">Ihre Eingabe</small>
<p class="mb-0 fst-italic" id="resultInput" style="font-size: 0.95rem; line-height: 1.5;"></p>
</div>
</div>
</div>
<div class="d-flex justify-content-center gap-3">
<button id="btnRestart" class="btn px-4">
Nochmal spielen
</button>
<button id="btnLeaderboard" class="btn px-4" style="background-color: #4a6fa5;">
Zum Leaderboard
</button>
</div>
</div>
</div>
</div>
</div>
<button class="btn btn-primary mt-3">Spiel starten</button>
<!-- Hier könnte der eigentliche Spielinhalt eingebunden werden -->
</div>