Compare commits
7 Commits
3b16df35a4
...
ad64bae67b
| Author | SHA1 | Date | |
|---|---|---|---|
| ad64bae67b | |||
|
|
d91d42f759 | ||
|
|
66cee7e5e2 | ||
|
|
90ab402ca6 | ||
|
|
184f933a57 | ||
|
|
40e61c16a2 | ||
|
|
9f995e213a |
@ -137,9 +137,9 @@ p {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
#logo-img {
|
#logo_img {
|
||||||
max-width: 500px;
|
max-width: 300px;
|
||||||
width: 100%;
|
width: 50%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
#main-area {
|
#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;
|
||||||
|
}
|
||||||
91
index.html
91
index.html
@ -3,14 +3,41 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="icon" type="image/png" href="image/icon_l.png">
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="assets/bootstrap-5.3.8-dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="assets/bootstrap-5.3.8-dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="assets/css/custom.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>
|
</head>
|
||||||
|
|
||||||
<body class="p-4">
|
<!-- class="p-4" entfernt -->
|
||||||
|
<body>
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
<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">
|
<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>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<!--evtl. weglassen, da es eine Sidebar gibt -->
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
<li class ="nav-item"><a class="nav-link" href="#" id="navbar-messages">Nachrichten</a></li>
|
<li class ="nav-item"><a class="nav-link" href="#" id="navbar-messages">Nachrichten</a></li>
|
||||||
@ -27,6 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="page-wrapper">
|
<div id="page-wrapper">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav id="sidebar" class="bg-dark text-white p-3">
|
<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-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-my-scores">Meine Scores</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link" href="#" id="nav-leaderboard">Leaderboard</a></li>
|
<li class="nav-item"> <a class="nav-link" href="#" id="nav-leaderboard">Leaderboard</a></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!--Main Area -->
|
<!-- Main Area: Enthält jetzt Topbar, Content UND Footer -->
|
||||||
<div id="main-area">
|
<div id="main-area">
|
||||||
<!--Topbar-->
|
<!-- Topbar -->
|
||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
<h1 class="text-center mb-4">Dashboard</h1>
|
<h1 class="text-center mb-4">Dashboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- 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>: Das Team FAD (Florin, Adi, Daniela)
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<strong>Adresse</strong>: Irgendwo im Internet, Schweiz
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<strong>Kontakt</strong>: schreibuns@lorem-ipsum-spiel.ch
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<strong>Inhalt</strong>: Der letzte Committer
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<strong>Datenschutz</strong>: 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;">
|
||||||
|
© 2026 Modul Frontend Projekt. Alle Rechte vorbehalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -58,8 +132,9 @@
|
|||||||
<script src="assets/src/service/score-service.js"></script>
|
<script src="assets/src/service/score-service.js"></script>
|
||||||
<script src="assets/src/service/leaderboard-service.js"></script>
|
<script src="assets/src/service/leaderboard-service.js"></script>
|
||||||
<script src="js/login.js"></script>
|
<script src="js/login.js"></script>
|
||||||
|
<script src="js/leaderboard.js"></script>
|
||||||
<!--Navigation Script -->
|
<!--Navigation Script -->
|
||||||
|
<script src="js/play.js"></script>
|
||||||
<script src="js/navigation.js"></script>
|
<script src="js/navigation.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
164
js/leaderboard.js
Normal file
164
js/leaderboard.js
Normal 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([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
27
js/login.js
27
js/login.js
@ -71,7 +71,7 @@
|
|||||||
logoutButton.disabled = false;
|
logoutButton.disabled = false;
|
||||||
deleteAccountButton.disabled = false;
|
deleteAccountButton.disabled = false;
|
||||||
currentSessionBox.classList.remove("d-none");
|
currentSessionBox.classList.remove("d-none");
|
||||||
authFormsRow.classList.add("d-none");
|
authFormsRow.classList.remove("d-none");
|
||||||
} else {
|
} else {
|
||||||
sessionText.textContent = "Nicht eingeloggt.";
|
sessionText.textContent = "Nicht eingeloggt.";
|
||||||
logoutButton.disabled = true;
|
logoutButton.disabled = true;
|
||||||
@ -100,6 +100,7 @@
|
|||||||
|
|
||||||
const usernameInput = document.getElementById("login-username");
|
const usernameInput = document.getElementById("login-username");
|
||||||
const passwordInput = document.getElementById("login-password");
|
const passwordInput = document.getElementById("login-password");
|
||||||
|
const submitButton = event.submitter;
|
||||||
const username = usernameInput.value.trim();
|
const username = usernameInput.value.trim();
|
||||||
const password = passwordInput.value.trim();
|
const password = passwordInput.value.trim();
|
||||||
|
|
||||||
@ -108,7 +109,29 @@
|
|||||||
return;
|
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) {
|
if (result.ok) {
|
||||||
saveAuth(username, password);
|
saveAuth(username, password);
|
||||||
setFeedback("Login erfolgreich.", "success");
|
setFeedback("Login erfolgreich.", "success");
|
||||||
|
|||||||
@ -23,6 +23,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (page === "login" && typeof window.initLoginPage === "function") {
|
if (page === "login" && typeof window.initLoginPage === "function") {
|
||||||
window.initLoginPage();
|
window.initLoginPage();
|
||||||
}
|
}
|
||||||
|
if (page === "leaderboard" && typeof window.initLeaderboardPage === "function") {
|
||||||
|
window.initLeaderboardPage();
|
||||||
|
}
|
||||||
|
if (page === "play" && typeof window.initPlayPage === "function") {
|
||||||
|
window.initPlayPage();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Fehler beim Laden von " + page + ":", error);
|
console.error("Fehler beim Laden von " + page + ":", error);
|
||||||
@ -46,4 +52,4 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
//Startseite laden
|
//Startseite laden
|
||||||
loadPage("home", "nav-home");
|
loadPage("home", "nav-home");
|
||||||
});
|
});
|
||||||
|
|||||||
379
js/play.js
Normal file
379
js/play.js
Normal 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.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
@ -1,8 +1,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Willkommen beim Lorem Ipsum Game</h2>
|
<h4 class="card-title mb-3">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.
|
<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>
|
Viel Spaß beim Spielen!</p>
|
||||||
|
<p class="card-text fs-6"> Wähle eine Option aus der Navigation, um zu starten.</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">
|
||||||
<img id="logo_img" src="image/Logo_loremIpsum.png" alt="Lorem Ipsum Game" class="img-fluid mt-3 d-block mx-auto">
|
|
||||||
</div>
|
</div>
|
||||||
@ -1,5 +1,19 @@
|
|||||||
|
<!-- Hauptbereich der Rangliste -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="mb-3">Leaderboard</h2>
|
<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>
|
</div>
|
||||||
|
|||||||
104
pages/play.html
104
pages/play.html
@ -1,7 +1,99 @@
|
|||||||
<div class= "card">
|
<!-- Spielseite: Die vier Phasen werden per play.js ein- und ausgeblendet. -->
|
||||||
<h2 class="mb-3">Spiel Starten</h2>
|
<div class="game-container">
|
||||||
<p class="mt-2">Hier kannst du das Spiel starten. Viel Erfolg!</p>
|
|
||||||
|
<!-- 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>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user