docs: Add comprehensive German comments to frontend codebase
This commit is contained in:
parent
9beec6e113
commit
8aed9d5346
@ -1,9 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Service-Klasse für Duelle/Herausforderungen (Challenge Service).
|
||||||
|
* Ermöglicht das Erstellen und Abschließen von direkt verlinkten Challenges im Backend.
|
||||||
|
*/
|
||||||
class ChallengeService {
|
class ChallengeService {
|
||||||
|
/**
|
||||||
|
* Erstellt eine Instanz des ChallengeService.
|
||||||
|
* @param {Object} config - Das globale Konfigurationsobjekt.
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.baseUrl = config.API_BASE_URL;
|
this.baseUrl = config.API_BASE_URL;
|
||||||
this.urlTail = "challenges";
|
this.urlTail = "challenges";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine neue Herausforderung für einen gegnerischen Spieler.
|
||||||
|
* @param {string} username - Name des herausfordernden Spielers.
|
||||||
|
* @param {string} password - Passwort des herausfordernden Spielers.
|
||||||
|
* @param {string} opponent - Name des herausgeforderten Spielers.
|
||||||
|
* @param {string} text - Der Rundentext und optionale Zusatznachrichten.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit den erstellten Challenge-Details im Body.
|
||||||
|
*/
|
||||||
async postChallenge(username, password, opponent, text) {
|
async postChallenge(username, password, opponent, text) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -32,6 +48,17 @@ class ChallengeService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schließt eine bestehende Herausforderung ab, indem das Spielergebnis übermittelt wird.
|
||||||
|
* @param {string} username - Name des Spielers, der die Challenge abschließt.
|
||||||
|
* @param {string} password - Passwort des Spielers.
|
||||||
|
* @param {string|number} challengeId - Eindeutige ID der Challenge im Backend.
|
||||||
|
* @param {number} score - Die erreichte Punktzahl.
|
||||||
|
* @param {number} time - Die benötigte Zeit in Sekunden.
|
||||||
|
* @param {string} text - Der Originaltext der Challenge-Runde.
|
||||||
|
* @param {string} userWrittenText - Der vom Spieler eingegebene Text.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Bestätigung und dem berechneten Ergebnis.
|
||||||
|
*/
|
||||||
async completeChallenge(username, password, challengeId, score, time, text, userWrittenText) {
|
async completeChallenge(username, password, challengeId, score, time, text, userWrittenText) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}/${challengeId}/complete`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}/${challengeId}/complete`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -63,4 +90,6 @@ class ChallengeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global verfügbar machen
|
||||||
window.ChallengeService = ChallengeService;
|
window.ChallengeService = ChallengeService;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
const config = {
|
/**
|
||||||
API_BASE_URL: "https://webdev.iten-web.ch/10001/api/"
|
* Konfiguration für die Backend-Verbindung.
|
||||||
}
|
* Enthält die Basis-URL für alle API-Anfragen der Anwendung.
|
||||||
|
*/
|
||||||
window.config = config;
|
const config = {
|
||||||
|
// Die Basis-URL des Backends, an die alle REST-Anfragen gesendet werden.
|
||||||
|
API_BASE_URL: "https://webdev.iten-web.ch/10001/api/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globales Konfigurationsobjekt im window-Scope registrieren,
|
||||||
|
// damit alle anderen Service-Klassen darauf zugreifen können.
|
||||||
|
window.config = config;
|
||||||
@ -1,9 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Service-Klasse für die Bestenliste (Leaderboard Service).
|
||||||
|
* Kapselt den REST-API-Aufruf zur Abfrage der globalen Highscore-Liste.
|
||||||
|
*/
|
||||||
class LeaderboardService {
|
class LeaderboardService {
|
||||||
|
/**
|
||||||
|
* Erstellt eine Instanz des LeaderboardService.
|
||||||
|
* @param {Object} config - Das globale Konfigurationsobjekt.
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.baseUrl = config.API_BASE_URL;
|
this.baseUrl = config.API_BASE_URL;
|
||||||
this.urlTail = "leaderboard";
|
this.urlTail = "leaderboard";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruft einen Ausschnitt der globalen Bestenliste ab (Paging).
|
||||||
|
* @param {number} offset - Der Startindex der abzufragenden Einträge (0-basiert).
|
||||||
|
* @param {number} limit - Die maximale Anzahl der zurückzugebenden Einträge (z.B. 10 für Top 10).
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Liste der Top-Spieler im Body.
|
||||||
|
*/
|
||||||
async getLeaderboard(offset, limit) {
|
async getLeaderboard(offset, limit) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${this.baseUrl}${this.urlTail}?offset=${offset}&limit=${limit}`,
|
`${this.baseUrl}${this.urlTail}?offset=${offset}&limit=${limit}`,
|
||||||
@ -26,4 +40,6 @@ class LeaderboardService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global verfügbar machen
|
||||||
window.LeaderboardService = LeaderboardService;
|
window.LeaderboardService = LeaderboardService;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Service-Klasse für das Nachrichtensystem (Message Service).
|
||||||
|
* Ermöglicht den Abruf und Versand von Systemnachrichten und Herausforderungen (Challenges)
|
||||||
|
* sowie das Ändern des Gelesen-Status von Nachrichten.
|
||||||
|
*/
|
||||||
class MessageService {
|
class MessageService {
|
||||||
|
/**
|
||||||
|
* Erstellt eine Instanz des MessageService.
|
||||||
|
* @param {Object} config - Das globale Konfigurationsobjekt.
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.baseUrl = config.API_BASE_URL;
|
this.baseUrl = config.API_BASE_URL;
|
||||||
this.urlTail = "messages";
|
this.urlTail = "messages";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt alle Nachrichten (eingehend und ausgehend) für den authentifizierten Benutzer.
|
||||||
|
* @param {string} username - Der Benutzername.
|
||||||
|
* @param {string} password - Das Passwort des Benutzers.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Liste aller Nachrichten im Body.
|
||||||
|
*/
|
||||||
async getMessages(username, password) {
|
async getMessages(username, password) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -27,6 +42,15 @@ class MessageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Nachricht (oder eine eingebettete Challenge) an einen anderen Benutzer.
|
||||||
|
* @param {string} username - Der Benutzername des Senders.
|
||||||
|
* @param {string} password - Das Passwort des Senders.
|
||||||
|
* @param {string} recipient - Der Empfänger der Nachricht.
|
||||||
|
* @param {string} type - Der Typ der Nachricht (z. B. "standard" oder "challenge").
|
||||||
|
* @param {string} text - Der eigentliche Textinhalt (kann eingebettete Challenge-JSON-Daten enthalten).
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Antwort des Servers im Body.
|
||||||
|
*/
|
||||||
async postMessage(username, password, recipient, type, text) {
|
async postMessage(username, password, recipient, type, text) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -56,6 +80,13 @@ class MessageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert eine spezifische Nachricht als gelesen.
|
||||||
|
* @param {string} username - Der Benutzername des Empfängers.
|
||||||
|
* @param {string} password - Das Passwort des Empfängers.
|
||||||
|
* @param {string|number} messageId - Eindeutige ID der Nachricht.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit dem Resultat der PATCH-Anfrage.
|
||||||
|
*/
|
||||||
async markMessageAsRead(username, password, messageId) {
|
async markMessageAsRead(username, password, messageId) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}/${messageId}/read`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}/${messageId}/read`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@ -79,6 +110,12 @@ class MessageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert alle ungelesenen Nachrichten des Benutzers auf einmal als gelesen.
|
||||||
|
* @param {string} username - Der Benutzername.
|
||||||
|
* @param {string} password - Das Passwort des Benutzers.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit dem Status der Bulk-Operation.
|
||||||
|
*/
|
||||||
async markAllMessagesAsRead(username, password) {
|
async markAllMessagesAsRead(username, password) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}/read`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}/read`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@ -103,4 +140,6 @@ class MessageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global verfügbar machen
|
||||||
window.MessageService = MessageService;
|
window.MessageService = MessageService;
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Service-Klasse für die Spielstände (Score Service).
|
||||||
|
* Kapselt alle REST-Anfragen an das Backend zur Speicherung und zum Laden von Scores.
|
||||||
|
*/
|
||||||
class ScoreService {
|
class ScoreService {
|
||||||
|
/**
|
||||||
|
* Erstellt eine Instanz des ScoreService.
|
||||||
|
* @param {Object} config - Das globale Konfigurationsobjekt.
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.baseUrl = config.API_BASE_URL;
|
this.baseUrl = config.API_BASE_URL;
|
||||||
this.urlTail = "score";
|
this.urlTail = "score";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt alle Scores eines bestimmten Benutzers.
|
||||||
|
* @param {string} username - Der Name des Benutzers, dessen Scores geladen werden sollen.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Liste aller Benutzer-Scores im Body.
|
||||||
|
*/
|
||||||
async getScoreByName(username) {
|
async getScoreByName(username) {
|
||||||
// Note: When user does not exist, we get a 200 with empty array, not a 404
|
// Hinweis: Wenn der Benutzer nicht existiert, liefert das Backend einen Status 200
|
||||||
|
// mit einem leeren Array statt eines 404-Fehlers.
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}/${username}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}/${username}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
@ -23,6 +37,16 @@ class ScoreService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert einen neu erzielten Score im Backend.
|
||||||
|
* @param {string} username - Benutzername des Spielers.
|
||||||
|
* @param {string} password - Passwort des Spielers zur Autorisierung.
|
||||||
|
* @param {number} score - Die erreichte Punktzahl (Anzahl korrekter Wörter).
|
||||||
|
* @param {number} time - Die für die Eingabe benötigte Zeit in Sekunden.
|
||||||
|
* @param {string} text - Der vorgegebene Originaltext der Spielrunde.
|
||||||
|
* @param {string} userWrittenText - Der vom Benutzer eingegebene Text.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Antwort des Servers (inkl. vergebenem Rang im Body).
|
||||||
|
*/
|
||||||
async postScore(username, password, score, time, text, userWrittenText) {
|
async postScore(username, password, score, time, text, userWrittenText) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -53,6 +77,13 @@ class ScoreService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht ein bestimmtes Score-Ergebnis anhand seiner ID (falls vom Backend unterstützt).
|
||||||
|
* @param {string} username - Benutzername des Spielers.
|
||||||
|
* @param {string} password - Passwort des Spielers zur Autorisierung.
|
||||||
|
* @param {string|number} scoreId - Die eindeutige ID des zu löschenden Scores.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit dem Resultat der Löschung.
|
||||||
|
*/
|
||||||
async deleteScore(username, password, scoreId) {
|
async deleteScore(username, password, scoreId) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}/${scoreId}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}/${scoreId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -76,4 +107,6 @@ class ScoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global verfügbar machen
|
||||||
window.ScoreService = ScoreService;
|
window.ScoreService = ScoreService;
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Service-Klasse für die Benutzerverwaltung (User Service).
|
||||||
|
* Kapselt alle REST-Anfragen an das Backend bezüglich Login, Registrierung,
|
||||||
|
* Abfrage aller Benutzer und Löschung von Konten.
|
||||||
|
*/
|
||||||
class UserService {
|
class UserService {
|
||||||
|
/**
|
||||||
|
* Erstellt eine Instanz des UserService.
|
||||||
|
* @param {Object} config - Das globale Konfigurationsobjekt (enthält API_BASE_URL).
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.baseUrl = config.API_BASE_URL;
|
this.baseUrl = config.API_BASE_URL;
|
||||||
this.urlTail = "user";
|
this.urlTail = "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentifiziert einen Benutzer und ruft dessen Kontoinformationen ab.
|
||||||
|
* @param {string} username - Der Benutzername.
|
||||||
|
* @param {string} password - Das Passwort des Benutzers.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit dem HTTP-Status, dem OK-Flag und den Benutzerdaten (Body).
|
||||||
|
*/
|
||||||
async getUser(username, password) {
|
async getUser(username, password) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
// Authentifizierung erfolgt über benutzerdefinierte HTTP-Header
|
||||||
"X-Username": username,
|
"X-Username": username,
|
||||||
"X-Password": password,
|
"X-Password": password,
|
||||||
},
|
},
|
||||||
@ -26,6 +42,12 @@ class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruft eine Liste aller im System registrierten Benutzer ab (z. B. für die Challenge-Auswahl).
|
||||||
|
* @param {string} username - Der Benutzername des anfragenden Benutzers.
|
||||||
|
* @param {string} password - Das Passwort des anfragenden Benutzers.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit der Benutzerliste im Response-Body.
|
||||||
|
*/
|
||||||
async getUsers(username, password) {
|
async getUsers(username, password) {
|
||||||
const response = await fetch(`${this.baseUrl}users`, {
|
const response = await fetch(`${this.baseUrl}users`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -48,6 +70,12 @@ class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht das eigene Benutzerkonto unwiderruflich aus dem System.
|
||||||
|
* @param {string} username - Der Benutzername des zu löschenden Kontos.
|
||||||
|
* @param {string} password - Das Passwort des Kontos.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit dem Status der Löschoperation.
|
||||||
|
*/
|
||||||
async deleteUser(username, password) {
|
async deleteUser(username, password) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -70,6 +98,11 @@ class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert einen neuen Benutzer im System. Das Passwort wird vom Backend generiert und zurückgegeben.
|
||||||
|
* @param {string} username - Der gewünschte neue Benutzername.
|
||||||
|
* @returns {Promise<Object>} Ein Promise mit den erstellten Account-Daten (inkl. generiertem Passwort) im Body.
|
||||||
|
*/
|
||||||
async postUser(username) {
|
async postUser(username) {
|
||||||
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -96,4 +129,6 @@ class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global verfügbar machen
|
||||||
window.UserService = UserService;
|
window.UserService = UserService;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,16 @@
|
|||||||
// Formatiert Sekunden als m:ss. Bei ungültigem Wert wird ein Platzhalter angezeigt.
|
/**
|
||||||
|
* Bestenliste (Leaderboard) verwalten und anzeigen.
|
||||||
|
* Dieses Modul lädt die Top-10-Spieler vom Server. Falls der aktuell angemeldete
|
||||||
|
* Benutzer nicht in diesen Top-10 vertreten ist, lädt es zusätzlich dessen
|
||||||
|
* persönliches Bestergebnis und zeigt dieses optisch getrennt darunter an.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert Sekunden in ein lesbares MM:SS Format (z.B. 75 Sekunden -> "1:15").
|
||||||
|
* Bei ungültigen Werten wird ein Strich ausgegeben.
|
||||||
|
* @param {number} seconds - Anzahl Sekunden.
|
||||||
|
* @returns {string} Formatiertes Zeit-String.
|
||||||
|
*/
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds) {
|
||||||
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
|
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
|
||||||
return "-";
|
return "-";
|
||||||
@ -9,7 +21,10 @@ function formatTime(seconds) {
|
|||||||
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liefert den aktuellen Login-Kontext, falls Auth global verfügbar ist.
|
/**
|
||||||
|
* Holt die aktuellen Authentifizierungsdaten aus dem globalen Auth-Modul.
|
||||||
|
* @returns {Object|null} Auth-Objekt (mit username) oder null.
|
||||||
|
*/
|
||||||
function getLoggedInAuth() {
|
function getLoggedInAuth() {
|
||||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||||
return null;
|
return null;
|
||||||
@ -23,11 +38,19 @@ function getLoggedInAuth() {
|
|||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vereinheitlicht Benutzernamen für robuste Vergleiche (z. B. Groß-/Kleinschreibung).
|
/**
|
||||||
|
* Normalisiert einen Benutzernamen (Trimming und Kleinschreibung) für robuste Vergleiche.
|
||||||
|
* @param {string} username - Der Benutzername.
|
||||||
|
* @returns {string} Der bereinigte Benutzername.
|
||||||
|
*/
|
||||||
function normalizeUsername(username) {
|
function normalizeUsername(username) {
|
||||||
return String(username ?? "").trim().toLowerCase();
|
return String(username ?? "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den normalisierten Benutzernamen des aktuell angemeldeten Benutzers.
|
||||||
|
* @returns {string|null} Der bereinigte Name oder null.
|
||||||
|
*/
|
||||||
function getLoggedInUsername() {
|
function getLoggedInUsername() {
|
||||||
const auth = getLoggedInAuth();
|
const auth = getLoggedInAuth();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
@ -37,7 +60,13 @@ function getLoggedInUsername() {
|
|||||||
return normalizeUsername(auth.username);
|
return normalizeUsername(auth.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nutzt den vom Backend gelieferten Rang, fallback auf die aktuelle Listenposition.
|
/**
|
||||||
|
* Ermittelt den anzuzeigenden Rang eines Bestenlisten-Eintrags.
|
||||||
|
* Nutzt vorrangig den vom Backend gelieferten Platz ("place"), andernfalls den Listenindex.
|
||||||
|
* @param {Object} entry - Der Listeneintrag.
|
||||||
|
* @param {number} index - Der Listenindex.
|
||||||
|
* @returns {number} Der anzuzeigende Rang.
|
||||||
|
*/
|
||||||
function getDisplayedRank(entry, index) {
|
function getDisplayedRank(entry, index) {
|
||||||
const place = Number(entry?.place);
|
const place = Number(entry?.place);
|
||||||
if (!Number.isNaN(place) && place > 0) {
|
if (!Number.isNaN(place) && place > 0) {
|
||||||
@ -47,7 +76,12 @@ function getDisplayedRank(entry, index) {
|
|||||||
return index + 1;
|
return index + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bestes Ergebnis: höchste Punktzahl, bei Gleichstand die geringere Zeit.
|
/**
|
||||||
|
* Ermittelt das beste Ergebnis aus einer Liste von Scores.
|
||||||
|
* Sortiert nach Score (absteigend) und bei Gleichstand nach Zeit (aufsteigend).
|
||||||
|
* @param {Array<Object>} entries - Eine Liste von Score-Einträgen.
|
||||||
|
* @returns {Object|null} Der beste Score-Eintrag.
|
||||||
|
*/
|
||||||
function getBestScoreEntry(entries) {
|
function getBestScoreEntry(entries) {
|
||||||
return entries
|
return entries
|
||||||
.slice()
|
.slice()
|
||||||
@ -64,6 +98,11 @@ function getBestScoreEntry(entries) {
|
|||||||
})[0] ?? null;
|
})[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt das beste Ergebnis des aktuell angemeldeten Benutzers aus dem Backend.
|
||||||
|
* @param {string} username - Der Benutzername des eingeloggten Spielers.
|
||||||
|
* @returns {Promise<Object|null>} Das beste Spielergebnis oder null.
|
||||||
|
*/
|
||||||
async function getCurrentUserLeaderboardEntry(username) {
|
async function getCurrentUserLeaderboardEntry(username) {
|
||||||
if (!window.ScoreService || !username) {
|
if (!window.ScoreService || !username) {
|
||||||
return null;
|
return null;
|
||||||
@ -79,7 +118,14 @@ async function getCurrentUserLeaderboardEntry(username) {
|
|||||||
return getBestScoreEntry(result.body);
|
return getBestScoreEntry(result.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rendert die Top-Liste und markiert den eingeloggten Nutzer visuell.
|
/**
|
||||||
|
* Rendert die Bestenliste im DOM.
|
||||||
|
* Hebt die Zeile des aktuell angemeldeten Benutzers farblich hervor.
|
||||||
|
* Falls extraUserEntry übergeben wird (wenn der User nicht in den Top-10 ist),
|
||||||
|
* wird am Ende eine Trennzeile und der Eintrag des Benutzers angehängt.
|
||||||
|
* @param {Array<Object>} entries - Die Top-10 Leaderboard-Einträge.
|
||||||
|
* @param {Object|null} extraUserEntry - Der separate Bestenlisten-Eintrag des angemeldeten Benutzers.
|
||||||
|
*/
|
||||||
function renderLeaderboard(entries, extraUserEntry = null) {
|
function renderLeaderboard(entries, extraUserEntry = null) {
|
||||||
const tableBody = document.getElementById("leaderboard-body");
|
const tableBody = document.getElementById("leaderboard-body");
|
||||||
if (!tableBody) {
|
if (!tableBody) {
|
||||||
@ -90,10 +136,12 @@ function renderLeaderboard(entries, extraUserEntry = null) {
|
|||||||
|
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
|
|
||||||
|
// Rendern der Top-10
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry, index) => {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
const rowUsername = normalizeUsername(entry.username);
|
const rowUsername = normalizeUsername(entry.username);
|
||||||
|
|
||||||
|
// Zeile grün hervorheben, wenn es sich um den eigenen Account handelt
|
||||||
if (loggedInUsername && rowUsername === loggedInUsername) {
|
if (loggedInUsername && rowUsername === loggedInUsername) {
|
||||||
row.classList.add("leaderboard-row-current-user");
|
row.classList.add("leaderboard-row-current-user");
|
||||||
}
|
}
|
||||||
@ -108,13 +156,15 @@ function renderLeaderboard(entries, extraUserEntry = null) {
|
|||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wenn der User eingeloggt ist, aber nicht in den Top-10 vertreten war, hängen wir ihn unten an
|
||||||
if (extraUserEntry) {
|
if (extraUserEntry) {
|
||||||
// Trennt Top-10 und eigenen Eintrag optisch, wenn der Nutzer nicht in den Top-10 ist.
|
// Optische Lücke (Leerzeile) einfügen
|
||||||
const spacerRow = document.createElement("tr");
|
const spacerRow = document.createElement("tr");
|
||||||
spacerRow.classList.add("leaderboard-row-gap");
|
spacerRow.classList.add("leaderboard-row-gap");
|
||||||
spacerRow.innerHTML = '<td colspan="4"></td>';
|
spacerRow.innerHTML = '<td colspan="4"></td>';
|
||||||
tableBody.appendChild(spacerRow);
|
tableBody.appendChild(spacerRow);
|
||||||
|
|
||||||
|
// Zeile des angemeldeten Benutzers anhängen
|
||||||
const userRow = document.createElement("tr");
|
const userRow = document.createElement("tr");
|
||||||
userRow.classList.add("leaderboard-row-current-user");
|
userRow.classList.add("leaderboard-row-current-user");
|
||||||
userRow.classList.add("leaderboard-row-current-user-extra");
|
userRow.classList.add("leaderboard-row-current-user-extra");
|
||||||
@ -129,8 +179,13 @@ function renderLeaderboard(entries, extraUserEntry = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Top-10-Bestenliste vom Backend und prüft,
|
||||||
|
* ob der angemeldete Benutzer separat geladen werden muss.
|
||||||
|
*/
|
||||||
async function loadTopTenLeaderboard() {
|
async function loadTopTenLeaderboard() {
|
||||||
const leaderboardService = new window.LeaderboardService(window.config);
|
const leaderboardService = new window.LeaderboardService(window.config);
|
||||||
|
// Holt die Bestenliste beginnend bei Rang 1 (Offset 0) mit maximal 10 Einträgen
|
||||||
const result = await leaderboardService.getLeaderboard(0, 10);
|
const result = await leaderboardService.getLeaderboard(0, 10);
|
||||||
|
|
||||||
if (!result.ok || !Array.isArray(result.body)) {
|
if (!result.ok || !Array.isArray(result.body)) {
|
||||||
@ -143,11 +198,12 @@ async function loadTopTenLeaderboard() {
|
|||||||
|
|
||||||
if (auth && auth.username) {
|
if (auth && auth.username) {
|
||||||
const loggedInUsername = normalizeUsername(auth.username);
|
const loggedInUsername = normalizeUsername(auth.username);
|
||||||
// Falls der Nutzer nicht in den Top-10 erscheint, wird sein bestes Ergebnis separat gezeigt.
|
// Prüfen, ob der angemeldete Benutzer bereits in den Top-10 vorhanden ist
|
||||||
const isInTopTen = result.body.some(
|
const isInTopTen = result.body.some(
|
||||||
(entry) => normalizeUsername(entry.username) === loggedInUsername,
|
(entry) => normalizeUsername(entry.username) === loggedInUsername,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wenn er nicht in den Top-10 ist, rufen wir sein bestes Ergebnis separat ab
|
||||||
if (!isInTopTen) {
|
if (!isInTopTen) {
|
||||||
extraUserEntry = await getCurrentUserLeaderboardEntry(auth.username);
|
extraUserEntry = await getCurrentUserLeaderboardEntry(auth.username);
|
||||||
}
|
}
|
||||||
@ -156,9 +212,14 @@ async function loadTopTenLeaderboard() {
|
|||||||
renderLeaderboard(result.body, extraUserEntry);
|
renderLeaderboard(result.body, extraUserEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Initialisierungsfunktion, die von navigation.js aufgerufen wird,
|
||||||
|
* sobald die leaderboard.html-Teilseite geladen wurde.
|
||||||
|
*/
|
||||||
window.initLeaderboardPage = function initLeaderboardPage() {
|
window.initLeaderboardPage = function initLeaderboardPage() {
|
||||||
loadTopTenLeaderboard().catch((error) => {
|
loadTopTenLeaderboard().catch((error) => {
|
||||||
console.error("Fehler beim Laden des Leaderboards:", error);
|
console.error("Fehler beim Laden des Leaderboards:", error);
|
||||||
renderLeaderboard([]);
|
renderLeaderboard([]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
94
js/login.js
94
js/login.js
@ -1,6 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Benutzer-Authentifizierung und Account-Verwaltung.
|
||||||
|
* Verwaltet den Login-Status über den LocalStorage, steuert die Anzeige im Header,
|
||||||
|
* verarbeitet Login- und Registrierungsformulare und ermöglicht das Löschen des Kontos.
|
||||||
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
|
// Der Schlüssel, unter dem die Benutzerdaten im LocalStorage abgelegt werden.
|
||||||
const AUTH_STORAGE_KEY = "loremIpsumAuth";
|
const AUTH_STORAGE_KEY = "loremIpsumAuth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die aktuellen Anmeldedaten aus dem LocalStorage des Browsers.
|
||||||
|
* @returns {Object|null} Ein Objekt mit {username, password} oder null, falls nicht angemeldet oder ungültig.
|
||||||
|
*/
|
||||||
function readAuth() {
|
function readAuth() {
|
||||||
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
|
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@ -9,15 +19,22 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
// Validieren, dass die Pflichtfelder vorhanden sind
|
||||||
if (!parsed.username || !parsed.password) {
|
if (!parsed.username || !parsed.password) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
|
// Fehler beim JSON-Parsen (z.B. manipulierte Daten) -> als nicht angemeldet behandeln
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert die Benutzerdaten verschlüsselt als JSON-String im LocalStorage.
|
||||||
|
* @param {string} username - Der Benutzername.
|
||||||
|
* @param {string} password - Das generierte/eingegebene Passwort.
|
||||||
|
*/
|
||||||
function saveAuth(username, password) {
|
function saveAuth(username, password) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
AUTH_STORAGE_KEY,
|
AUTH_STORAGE_KEY,
|
||||||
@ -25,24 +42,37 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt die Anmeldedaten aus dem LocalStorage (Logout).
|
||||||
|
*/
|
||||||
function clearAuth() {
|
function clearAuth() {
|
||||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert die Anzeige des Benutzernamens im Header und den Login-Button.
|
||||||
|
*/
|
||||||
function updateHeaderUsername() {
|
function updateHeaderUsername() {
|
||||||
const usernameDisplay = document.getElementById("username-display");
|
const usernameDisplay = document.getElementById("username-display");
|
||||||
const navbarLogin = document.getElementById("navbar-login");
|
const navbarLogin = document.getElementById("navbar-login");
|
||||||
const auth = readAuth();
|
const auth = readAuth();
|
||||||
|
|
||||||
|
// Text im Header anpassen ("User: Guest" oder "User: <Name>")
|
||||||
if (usernameDisplay) {
|
if (usernameDisplay) {
|
||||||
usernameDisplay.textContent = auth ? "User: " + auth.username : "User: Guest";
|
usernameDisplay.textContent = auth ? "User: " + auth.username : "User: Guest";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text des Login-Links in der Navbar anpassen
|
||||||
if (navbarLogin) {
|
if (navbarLogin) {
|
||||||
navbarLogin.textContent = auth ? auth.username : "Login / Registrieren";
|
navbarLogin.textContent = auth ? auth.username : "Login / Registrieren";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt dem Benutzer Feedback (z. B. Fehlermeldungen) auf der Login-Seite an.
|
||||||
|
* @param {string} message - Die anzuzeigende Nachricht.
|
||||||
|
* @param {string} type - Der Bootstrap-Alert-Typ (danger, warning, success, info).
|
||||||
|
*/
|
||||||
function setFeedback(message, type) {
|
function setFeedback(message, type) {
|
||||||
const feedback = document.getElementById("auth-feedback");
|
const feedback = document.getElementById("auth-feedback");
|
||||||
if (!feedback) {
|
if (!feedback) {
|
||||||
@ -51,9 +81,13 @@
|
|||||||
|
|
||||||
feedback.className = "alert alert-" + type;
|
feedback.className = "alert alert-" + type;
|
||||||
feedback.textContent = message;
|
feedback.textContent = message;
|
||||||
feedback.classList.remove("d-none");
|
feedback.classList.remove("d-none"); // Alert einblenden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert den Zustand der Session-Box (Anzeige der aktuellen Anmeldung).
|
||||||
|
* Blendet die Forms aus bzw. ein und aktiviert/deaktiviert die Buttons.
|
||||||
|
*/
|
||||||
function updateSessionBox() {
|
function updateSessionBox() {
|
||||||
const sessionText = document.getElementById("current-session-text");
|
const sessionText = document.getElementById("current-session-text");
|
||||||
const logoutButton = document.getElementById("logout-button");
|
const logoutButton = document.getElementById("logout-button");
|
||||||
@ -67,12 +101,14 @@
|
|||||||
|
|
||||||
const auth = readAuth();
|
const auth = readAuth();
|
||||||
if (auth) {
|
if (auth) {
|
||||||
|
// Wenn eingeloggt: Session-Info anzeigen und Buttons aktivieren
|
||||||
sessionText.textContent = "Eingeloggt als " + auth.username + ".";
|
sessionText.textContent = "Eingeloggt als " + auth.username + ".";
|
||||||
logoutButton.disabled = false;
|
logoutButton.disabled = false;
|
||||||
deleteAccountButton.disabled = false;
|
deleteAccountButton.disabled = false;
|
||||||
currentSessionBox.classList.remove("d-none");
|
currentSessionBox.classList.remove("d-none");
|
||||||
authFormsRow.classList.remove("d-none");
|
authFormsRow.classList.remove("d-none");
|
||||||
} else {
|
} else {
|
||||||
|
// Wenn nicht eingeloggt: Info setzen und Session-Box verbergen
|
||||||
sessionText.textContent = "Nicht eingeloggt.";
|
sessionText.textContent = "Nicht eingeloggt.";
|
||||||
logoutButton.disabled = true;
|
logoutButton.disabled = true;
|
||||||
deleteAccountButton.disabled = true;
|
deleteAccountButton.disabled = true;
|
||||||
@ -81,6 +117,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion zum Instanziieren des UserService.
|
||||||
|
* @returns {UserService|null} Eine UserService-Instanz oder null.
|
||||||
|
*/
|
||||||
function getUserService() {
|
function getUserService() {
|
||||||
if (!window.config || !window.UserService) {
|
if (!window.config || !window.UserService) {
|
||||||
return null;
|
return null;
|
||||||
@ -89,6 +129,11 @@
|
|||||||
return new window.UserService(window.config);
|
return new window.UserService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt das Absenden des Login-Formulars.
|
||||||
|
* Authentifiziert den Benutzer beim Backend und speichert die Session bei Erfolg.
|
||||||
|
* @param {Event} event - Das Submit-Event des Formulars.
|
||||||
|
*/
|
||||||
async function handleLoginSubmit(event) {
|
async function handleLoginSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@ -100,7 +145,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 submitButton = event.submitter; // Der geklickte Button
|
||||||
const username = usernameInput.value.trim();
|
const username = usernameInput.value.trim();
|
||||||
const password = passwordInput.value.trim();
|
const password = passwordInput.value.trim();
|
||||||
|
|
||||||
@ -109,6 +154,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button während der Anfrage sperren und Ladeindikator zeigen
|
||||||
if (submitButton) {
|
if (submitButton) {
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
submitButton.textContent = "Einloggen...";
|
submitButton.textContent = "Einloggen...";
|
||||||
@ -127,19 +173,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button wieder freigeben
|
||||||
if (submitButton) {
|
if (submitButton) {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = "Einloggen";
|
submitButton.textContent = "Einloggen";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
// Login erfolgreich: Daten speichern, Ansicht aktualisieren und zur Startseite leiten
|
||||||
saveAuth(username, password);
|
saveAuth(username, password);
|
||||||
setFeedback("Login erfolgreich.", "success");
|
setFeedback("Login erfolgreich.", "success");
|
||||||
updateSessionBox();
|
updateSessionBox();
|
||||||
updateHeaderUsername();
|
updateHeaderUsername();
|
||||||
|
|
||||||
|
// Falls das Navigationsmenü geöffnet ist, dessen Status aktualisieren
|
||||||
if (typeof window.updateMessagesNavState === "function") {
|
if (typeof window.updateMessagesNavState === "function") {
|
||||||
window.updateMessagesNavState();
|
window.updateMessagesNavState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kurze Verzögerung für visuelles Feedback vor dem Seitenwechsel
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (typeof window.loadPage === "function") {
|
if (typeof window.loadPage === "function") {
|
||||||
window.loadPage("home", "nav-home");
|
window.loadPage("home", "nav-home");
|
||||||
@ -156,6 +208,12 @@
|
|||||||
setFeedback("Login fehlgeschlagen (Status " + result.status + ").", "danger");
|
setFeedback("Login fehlgeschlagen (Status " + result.status + ").", "danger");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt das Absenden des Registrierungsformulars.
|
||||||
|
* Sendet den gewünschten Benutzernamen ans Backend, welches das Passwort generiert.
|
||||||
|
* Zeigt das Passwort anschließend in einem Bootstrap-Modal an, da es nur einmalig sichtbar ist.
|
||||||
|
* @param {Event} event - Das Submit-Event des Formulars.
|
||||||
|
*/
|
||||||
async function handleRegisterSubmit(event) {
|
async function handleRegisterSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@ -175,9 +233,11 @@
|
|||||||
|
|
||||||
const result = await userService.postUser(username);
|
const result = await userService.postUser(username);
|
||||||
if (result.ok && result.body) {
|
if (result.ok && result.body) {
|
||||||
|
// Registrierung erfolgreich: Antwortdaten auslesen
|
||||||
const createdName = result.body.name || username;
|
const createdName = result.body.name || username;
|
||||||
const createdPassword = result.body.password || "";
|
const createdPassword = result.body.password || "";
|
||||||
|
|
||||||
|
// Direkt einloggen mit den erhaltenen Daten
|
||||||
saveAuth(createdName, createdPassword);
|
saveAuth(createdName, createdPassword);
|
||||||
updateSessionBox();
|
updateSessionBox();
|
||||||
updateHeaderUsername();
|
updateHeaderUsername();
|
||||||
@ -185,17 +245,18 @@
|
|||||||
window.updateMessagesNavState();
|
window.updateMessagesNavState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal mit Daten füllen und anzeigen
|
// Modal mit den generierten Zugangsdaten befüllen
|
||||||
const modalUsername = document.getElementById("modal-username");
|
const modalUsername = document.getElementById("modal-username");
|
||||||
const modalPassword = document.getElementById("modal-password");
|
const modalPassword = document.getElementById("modal-password");
|
||||||
if (modalUsername) modalUsername.textContent = createdName;
|
if (modalUsername) modalUsername.textContent = createdName;
|
||||||
if (modalPassword) modalPassword.textContent = createdPassword;
|
if (modalPassword) modalPassword.textContent = createdPassword;
|
||||||
|
|
||||||
|
// Das Passwort-Modal anzeigen
|
||||||
const passwordModalElement = document.getElementById("password-modal");
|
const passwordModalElement = document.getElementById("password-modal");
|
||||||
if (passwordModalElement) {
|
if (passwordModalElement) {
|
||||||
const passwordModal = new window.bootstrap.Modal(passwordModalElement);
|
const passwordModal = new window.bootstrap.Modal(passwordModalElement);
|
||||||
|
|
||||||
// Listener für Modals-Schließen: zur Startseite navigieren
|
// Nach Schließen des Modals automatisch auf die Startseite weiterleiten
|
||||||
passwordModalElement.addEventListener("hidden.bs.modal", function handleClose() {
|
passwordModalElement.addEventListener("hidden.bs.modal", function handleClose() {
|
||||||
if (typeof window.loadPage === "function") {
|
if (typeof window.loadPage === "function") {
|
||||||
window.loadPage("home", "nav-home");
|
window.loadPage("home", "nav-home");
|
||||||
@ -205,7 +266,7 @@
|
|||||||
|
|
||||||
passwordModal.show();
|
passwordModal.show();
|
||||||
} else {
|
} else {
|
||||||
// Fallback wenn Modal nicht gefunden
|
// Fallback, falls das Bootstrap-Modal im DOM nicht gefunden wurde
|
||||||
setFeedback(
|
setFeedback(
|
||||||
"Account erstellt. Username: " + createdName + ", Passwort: " + createdPassword,
|
"Account erstellt. Username: " + createdName + ", Passwort: " + createdPassword,
|
||||||
"success",
|
"success",
|
||||||
@ -217,6 +278,7 @@
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formularfelder für eventuelle spätere Logins vorausfüllen
|
||||||
const loginUsernameInput = document.getElementById("login-username");
|
const loginUsernameInput = document.getElementById("login-username");
|
||||||
const loginPasswordInput = document.getElementById("login-password");
|
const loginPasswordInput = document.getElementById("login-password");
|
||||||
if (loginUsernameInput && loginPasswordInput) {
|
if (loginUsernameInput && loginPasswordInput) {
|
||||||
@ -227,6 +289,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fehlerbehandlung
|
||||||
if (result.status === 400) {
|
if (result.status === 400) {
|
||||||
const errorMessage = result.body && result.body.message
|
const errorMessage = result.body && result.body.message
|
||||||
? result.body.message
|
? result.body.message
|
||||||
@ -241,6 +304,9 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loggt den aktuellen Benutzer aus (löscht Auth-Daten aus LocalStorage und aktualisiert UI).
|
||||||
|
*/
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
setFeedback("Du wurdest ausgeloggt.", "info");
|
setFeedback("Du wurdest ausgeloggt.", "info");
|
||||||
@ -251,6 +317,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt das Löschen des eigenen Accounts.
|
||||||
|
* Fordert eine Sicherheitsbestätigung an, sendet die Löschanfrage ans Backend
|
||||||
|
* und führt bei Erfolg einen automatischen Logout aus.
|
||||||
|
*/
|
||||||
async function handleDeleteAccount() {
|
async function handleDeleteAccount() {
|
||||||
const auth = readAuth();
|
const auth = readAuth();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
@ -258,6 +329,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sicherheitsabfrage im Browser
|
||||||
const wantsDelete = window.confirm(
|
const wantsDelete = window.confirm(
|
||||||
"Möchtest du den Account \"" + auth.username + "\" wirklich löschen?",
|
"Möchtest du den Account \"" + auth.username + "\" wirklich löschen?",
|
||||||
);
|
);
|
||||||
@ -273,6 +345,7 @@
|
|||||||
|
|
||||||
const result = await userService.deleteUser(auth.username, auth.password);
|
const result = await userService.deleteUser(auth.username, auth.password);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
// Account erfolgreich gelöscht: Lokal ausloggen und Feedback geben
|
||||||
clearAuth();
|
clearAuth();
|
||||||
updateSessionBox();
|
updateSessionBox();
|
||||||
updateHeaderUsername();
|
updateHeaderUsername();
|
||||||
@ -294,6 +367,10 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert die Login-Seite.
|
||||||
|
* Bindet alle Event-Listener an die Formulare und Buttons nach dem Laden des HTML-Inhalts.
|
||||||
|
*/
|
||||||
function initLoginPage() {
|
function initLoginPage() {
|
||||||
const loginForm = document.getElementById("login-form");
|
const loginForm = document.getElementById("login-form");
|
||||||
const registerForm = document.getElementById("register-form");
|
const registerForm = document.getElementById("register-form");
|
||||||
@ -309,15 +386,22 @@
|
|||||||
logoutButton.addEventListener("click", handleLogout);
|
logoutButton.addEventListener("click", handleLogout);
|
||||||
deleteAccountButton.addEventListener("click", handleDeleteAccount);
|
deleteAccountButton.addEventListener("click", handleDeleteAccount);
|
||||||
|
|
||||||
|
// Initialen UI-Zustand herstellen
|
||||||
updateSessionBox();
|
updateSessionBox();
|
||||||
updateHeaderUsername();
|
updateHeaderUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bindet die Initialisierungsfunktion an das globale Fensterobjekt für navigation.js
|
||||||
window.initLoginPage = initLoginPage;
|
window.initLoginPage = initLoginPage;
|
||||||
|
|
||||||
|
// Stellt Authentifizierungsfunktionen global bereit, damit andere Module (z. B. play.js)
|
||||||
|
// prüfen können, ob und wer eingeloggt ist.
|
||||||
window.AppAuth = {
|
window.AppAuth = {
|
||||||
getAuth: readAuth,
|
getAuth: readAuth,
|
||||||
clearAuth: clearAuth,
|
clearAuth: clearAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Beim allerersten Laden der Anwendung direkt den Header anpassen
|
||||||
document.addEventListener("DOMContentLoaded", updateHeaderUsername);
|
document.addEventListener("DOMContentLoaded", updateHeaderUsername);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
264
js/messages.js
264
js/messages.js
@ -1,9 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Nachrichten-Dashboard und Duell-System (Challenges).
|
||||||
|
* Dieses Modul verwaltet den Abruf und Versand von Textnachrichten, die Gruppierung von
|
||||||
|
* Nachrichten zu Duell-Konversationen, das Erstellen neuer Herausforderungen mit zufälligen
|
||||||
|
* Texten und die visuelle Aufbereitung von Spielergebnissen (Gewonnen/Verloren/Unentschieden).
|
||||||
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
|
// --- Konstanten & Konfiguration ---
|
||||||
const MESSAGE_TYPE_CHALLENGE = "challenge";
|
const MESSAGE_TYPE_CHALLENGE = "challenge";
|
||||||
const MESSAGE_TYPE_CHALLENGE_RESULT = "challenge-result";
|
const MESSAGE_TYPE_CHALLENGE_RESULT = "challenge-result";
|
||||||
|
|
||||||
|
// Abfrage-Intervall für neue Nachrichten (30 Sekunden)
|
||||||
const MESSAGE_POLL_INTERVAL_MS = 30000;
|
const MESSAGE_POLL_INTERVAL_MS = 30000;
|
||||||
|
|
||||||
|
// Key für die aktive Challenge im SessionStorage
|
||||||
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
|
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
|
||||||
|
|
||||||
|
// Eindeutiges Kennzeichen für JSON-strukturierte Challenge-Daten innerhalb normaler Textnachrichten
|
||||||
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
|
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
|
||||||
|
|
||||||
|
// Satzteile zur Generierung der zu merkenden Sätze in Duellen
|
||||||
const CHALLENGE_TEXT_PARTS = {
|
const CHALLENGE_TEXT_PARTS = {
|
||||||
subjects: [
|
subjects: [
|
||||||
"Der flinke Entwickler",
|
"Der flinke Entwickler",
|
||||||
@ -55,14 +70,24 @@
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentMessages = [];
|
// --- Modulweiter Status ---
|
||||||
let currentUsers = [];
|
let currentMessages = []; // Cache für alle geladenen Nachrichten
|
||||||
let messagePollingInterval = null;
|
let currentUsers = []; // Liste aller anderen Benutzer im System
|
||||||
|
let messagePollingInterval = null; // ID des Polling-Timers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt ein zufälliges Element aus einem Array.
|
||||||
|
* @param {Array} items - Das Quell-Array.
|
||||||
|
*/
|
||||||
function getRandomChallengeTextPart(items) {
|
function getRandomChallengeTextPart(items) {
|
||||||
return items[Math.floor(Math.random() * items.length)];
|
return items[Math.floor(Math.random() * items.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen grammatikalisch korrekten, zufälligen Text für ein Duell.
|
||||||
|
* Besteht aus zwei Hauptsätzen und einem Schlusssatz.
|
||||||
|
* @returns {string} Der generierte Challenge-Text.
|
||||||
|
*/
|
||||||
function generateChallengeText() {
|
function generateChallengeText() {
|
||||||
const firstSentence =
|
const firstSentence =
|
||||||
[
|
[
|
||||||
@ -87,6 +112,12 @@
|
|||||||
getRandomChallengeTextPart(CHALLENGE_TEXT_PARTS.endings);
|
getRandomChallengeTextPart(CHALLENGE_TEXT_PARTS.endings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Nachrichtentext auf, in den die Spieldaten (JSON) eingebettet sind.
|
||||||
|
* @param {Object} challengeData - Die strukturierten Spieldaten.
|
||||||
|
* @param {string} displayText - Der für den Nutzer lesbare Textteil der Nachricht.
|
||||||
|
* @returns {string} Der präparierte Gesamttext für die API.
|
||||||
|
*/
|
||||||
function buildEmbeddedChallengeText(challengeData, displayText) {
|
function buildEmbeddedChallengeText(challengeData, displayText) {
|
||||||
return CHALLENGE_DATA_PREFIX +
|
return CHALLENGE_DATA_PREFIX +
|
||||||
JSON.stringify(challengeData) +
|
JSON.stringify(challengeData) +
|
||||||
@ -95,6 +126,9 @@
|
|||||||
displayText;
|
displayText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt die aktuellen Zugangsdaten aus dem AppAuth-Modul.
|
||||||
|
*/
|
||||||
function getAuth() {
|
function getAuth() {
|
||||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||||
return null;
|
return null;
|
||||||
@ -108,10 +142,16 @@
|
|||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bereinigt Benutzernamen für Vergleiche.
|
||||||
|
*/
|
||||||
function normalizeUsername(username) {
|
function normalizeUsername(username) {
|
||||||
return String(username || "").trim().toLowerCase();
|
return String(username || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des MessageService.
|
||||||
|
*/
|
||||||
function getMessageService() {
|
function getMessageService() {
|
||||||
if (!window.config || !window.MessageService) {
|
if (!window.config || !window.MessageService) {
|
||||||
return null;
|
return null;
|
||||||
@ -120,6 +160,9 @@
|
|||||||
return new window.MessageService(window.config);
|
return new window.MessageService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des UserService.
|
||||||
|
*/
|
||||||
function getUserService() {
|
function getUserService() {
|
||||||
if (!window.config || !window.UserService) {
|
if (!window.config || !window.UserService) {
|
||||||
return null;
|
return null;
|
||||||
@ -128,6 +171,9 @@
|
|||||||
return new window.UserService(window.config);
|
return new window.UserService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des ChallengeService.
|
||||||
|
*/
|
||||||
function getChallengeService() {
|
function getChallengeService() {
|
||||||
if (!window.config || !window.ChallengeService) {
|
if (!window.config || !window.ChallengeService) {
|
||||||
return null;
|
return null;
|
||||||
@ -136,9 +182,15 @@
|
|||||||
return new window.ChallengeService(window.config);
|
return new window.ChallengeService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert ein vom Server empfangenes Nachrichtenobjekt.
|
||||||
|
* Extrahiert ggf. eingebettete JSON-Challenge-Daten und vereinheitlicht
|
||||||
|
* abweichende Backend-Feldnamen (z.B. date/time/createdAt).
|
||||||
|
* @param {Object} message - Das rohe Nachrichtenobjekt.
|
||||||
|
* @returns {Object} Die normalisierte Nachricht.
|
||||||
|
*/
|
||||||
function normalizeMessage(message) {
|
function normalizeMessage(message) {
|
||||||
// Das Backend und unser Nachrichten-Fallback liefern Challenge-Daten leicht unterschiedlich.
|
// Verschiedene mögliche Text-Eigenschaften des Backends prüfen
|
||||||
// Hier werden beide Formen in ein einheitliches Message-Objekt gebracht.
|
|
||||||
const textCandidates = [
|
const textCandidates = [
|
||||||
message.text,
|
message.text,
|
||||||
message.content,
|
message.content,
|
||||||
@ -146,14 +198,20 @@
|
|||||||
message.challenge?.text,
|
message.challenge?.text,
|
||||||
message.result?.text,
|
message.result?.text,
|
||||||
];
|
];
|
||||||
|
// Findet den Text, der unsere eingebetteten JSON-Daten enthält, oder den ersten nicht-leeren Text
|
||||||
const rawText = textCandidates.find((value) => String(value ?? "").includes(CHALLENGE_DATA_PREFIX))
|
const rawText = textCandidates.find((value) => String(value ?? "").includes(CHALLENGE_DATA_PREFIX))
|
||||||
?? textCandidates.find((value) => value !== null && value !== undefined)
|
?? textCandidates.find((value) => value !== null && value !== undefined)
|
||||||
?? "";
|
?? "";
|
||||||
|
|
||||||
|
// Versuchen, eingebettetes JSON aus dem Text zu parsen
|
||||||
const embeddedChallenge = extractEmbeddedChallenge(rawText);
|
const embeddedChallenge = extractEmbeddedChallenge(rawText);
|
||||||
const type = message.type ?? MESSAGE_TYPE_CHALLENGE;
|
const type = message.type ?? MESSAGE_TYPE_CHALLENGE;
|
||||||
|
|
||||||
const backendChallenge = message.challenge
|
const backendChallenge = message.challenge
|
||||||
?? message.result
|
?? message.result
|
||||||
?? (type === MESSAGE_TYPE_CHALLENGE || type === MESSAGE_TYPE_CHALLENGE_RESULT ? message : null);
|
?? (type === MESSAGE_TYPE_CHALLENGE || type === MESSAGE_TYPE_CHALLENGE_RESULT ? message : null);
|
||||||
|
|
||||||
|
// Zusammenführen von Backend-Challenge-Daten und lokal extrahierten Daten
|
||||||
const challenge = embeddedChallenge.challenge
|
const challenge = embeddedChallenge.challenge
|
||||||
? { ...(backendChallenge ?? {}), ...embeddedChallenge.challenge }
|
? { ...(backendChallenge ?? {}), ...embeddedChallenge.challenge }
|
||||||
: backendChallenge;
|
: backendChallenge;
|
||||||
@ -163,16 +221,20 @@
|
|||||||
sender: message.sender ?? message.from ?? "",
|
sender: message.sender ?? message.from ?? "",
|
||||||
recipient: message.recipient ?? message.to ?? "",
|
recipient: message.recipient ?? message.to ?? "",
|
||||||
type: type,
|
type: type,
|
||||||
text: embeddedChallenge.text,
|
text: embeddedChallenge.text, // Nur der lesbare Teil
|
||||||
read: Boolean(message.read),
|
read: Boolean(message.read),
|
||||||
createdAt: message.createdAt ?? message.time ?? message.date ?? "",
|
createdAt: message.createdAt ?? message.time ?? message.date ?? "",
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert ein eingebettetes JSON-Objekt aus einem Nachrichtentext.
|
||||||
|
* Sucht nach dem Muster: [[loremIpsumChallenge: {JSON} ]]
|
||||||
|
* @param {string} text - Der rohe Nachrichtentext.
|
||||||
|
* @returns {Object} Ein Objekt mit dem bereinigten Anzeigetext und dem extrahierten JSON-Objekt (oder null).
|
||||||
|
*/
|
||||||
function extractEmbeddedChallenge(text) {
|
function extractEmbeddedChallenge(text) {
|
||||||
// Fallback fuer Challenge-Daten, die im Nachrichtentext mitgesendet werden.
|
|
||||||
// So koennen wir bestehende Message-Endpunkte nutzen, ohne neue Backend-Felder zu verlangen.
|
|
||||||
const rawText = String(text ?? "");
|
const rawText = String(text ?? "");
|
||||||
const startIndex = rawText.indexOf(CHALLENGE_DATA_PREFIX);
|
const startIndex = rawText.indexOf(CHALLENGE_DATA_PREFIX);
|
||||||
if (startIndex === -1) {
|
if (startIndex === -1) {
|
||||||
@ -190,7 +252,9 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON-String ausschneiden
|
||||||
const json = rawText.slice(startIndex + CHALLENGE_DATA_PREFIX.length, endIndex);
|
const json = rawText.slice(startIndex + CHALLENGE_DATA_PREFIX.length, endIndex);
|
||||||
|
// Den JSON-Teil aus dem Text entfernen, damit der Empfänger ihn nicht sieht
|
||||||
const displayText = (
|
const displayText = (
|
||||||
rawText.slice(0, startIndex) +
|
rawText.slice(0, startIndex) +
|
||||||
rawText.slice(endIndex + 2)
|
rawText.slice(endIndex + 2)
|
||||||
@ -202,6 +266,7 @@
|
|||||||
challenge: JSON.parse(json),
|
challenge: JSON.parse(json),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
// Falls JSON beschädigt ist, den Text unverändert zurückgeben
|
||||||
return {
|
return {
|
||||||
text: rawText,
|
text: rawText,
|
||||||
challenge: null,
|
challenge: null,
|
||||||
@ -209,10 +274,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt die eindeutige ID einer Challenge aus verschiedenen Backend-Datenstrukturen.
|
||||||
|
*/
|
||||||
function getChallengeId(challenge) {
|
function getChallengeId(challenge) {
|
||||||
return challenge?.challengeId ?? challenge?.challenge_id ?? challenge?.id ?? null;
|
return challenge?.challengeId ?? challenge?.challenge_id ?? challenge?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Namen des Herausforderers.
|
||||||
|
*/
|
||||||
function getChallengeChallenger(challenge, fallbackName) {
|
function getChallengeChallenger(challenge, fallbackName) {
|
||||||
return challenge?.challenger
|
return challenge?.challenger
|
||||||
?? challenge?.challengerName
|
?? challenge?.challengerName
|
||||||
@ -222,6 +293,9 @@
|
|||||||
?? "";
|
?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Namen des Herausgeforderten (Gegners).
|
||||||
|
*/
|
||||||
function getChallengeOpponent(challenge, fallbackName) {
|
function getChallengeOpponent(challenge, fallbackName) {
|
||||||
return challenge?.opponent
|
return challenge?.opponent
|
||||||
?? challenge?.opponentName
|
?? challenge?.opponentName
|
||||||
@ -232,6 +306,9 @@
|
|||||||
?? "";
|
?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Score des Gegners.
|
||||||
|
*/
|
||||||
function getOpponentScore(challenge) {
|
function getOpponentScore(challenge) {
|
||||||
return challenge?.opponentScore
|
return challenge?.opponentScore
|
||||||
?? challenge?.challengedScore
|
?? challenge?.challengedScore
|
||||||
@ -239,6 +316,9 @@
|
|||||||
?? null;
|
?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den vorgegebenen Rundentext der Challenge.
|
||||||
|
*/
|
||||||
function getChallengeText(challenge) {
|
function getChallengeText(challenge) {
|
||||||
return challenge?.challengeText
|
return challenge?.challengeText
|
||||||
?? challenge?.roundText
|
?? challenge?.roundText
|
||||||
@ -246,10 +326,18 @@
|
|||||||
?? null;
|
?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob ein Wert eine gültige Punktzahl darstellt.
|
||||||
|
*/
|
||||||
function hasScore(value) {
|
function hasScore(value) {
|
||||||
return value !== null && value !== undefined && value !== "";
|
return value !== null && value !== undefined && value !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bestimmt die Rolle des aktuell angemeldeten Benutzers in einer Challenge.
|
||||||
|
* @param {Object} message - Die normalisierte Nachricht.
|
||||||
|
* @returns {string|null} "opponent" (Geforderter), "challenger" (Herausforderer) oder null.
|
||||||
|
*/
|
||||||
function getChallengeRole(message) {
|
function getChallengeRole(message) {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const challenge = message.challenge;
|
const challenge = message.challenge;
|
||||||
@ -272,6 +360,10 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob der Geforderte (Opponent) die Challenge annehmen und spielen darf.
|
||||||
|
* Das ist der Fall, wenn er noch keinen Score hat und auch der Herausforderer noch nicht gespielt hat.
|
||||||
|
*/
|
||||||
function canOpponentAcceptChallenge(message) {
|
function canOpponentAcceptChallenge(message) {
|
||||||
if (message.type !== MESSAGE_TYPE_CHALLENGE || !message.challenge) {
|
if (message.type !== MESSAGE_TYPE_CHALLENGE || !message.challenge) {
|
||||||
return false;
|
return false;
|
||||||
@ -284,6 +376,11 @@
|
|||||||
return role === "opponent" && !hasScore(opponentScore) && !hasScore(challenge.challengerScore);
|
return role === "opponent" && !hasScore(opponentScore) && !hasScore(challenge.challengerScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob der Herausforderer (Challenger) seine Runde spielen darf.
|
||||||
|
* Das ist der Fall, wenn der Gegner (Opponent) bereits gespielt hat,
|
||||||
|
* der Herausforderer selbst aber noch nicht.
|
||||||
|
*/
|
||||||
function canChallengerPlayChallenge(message) {
|
function canChallengerPlayChallenge(message) {
|
||||||
const role = getChallengeRole(message);
|
const role = getChallengeRole(message);
|
||||||
const challenge = message.challenge;
|
const challenge = message.challenge;
|
||||||
@ -293,9 +390,12 @@
|
|||||||
&& !hasScore(challenge.challengerScore);
|
&& !hasScore(challenge.challengerScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Beschriftungs- und Aktivierungsstatus des Buttons für eine Challenge.
|
||||||
|
* @param {Object} message - Die normalisierte Nachricht.
|
||||||
|
* @returns {Object|null} Button-Konfiguration mit {disabled, label, role} oder null.
|
||||||
|
*/
|
||||||
function getChallengeButtonState(message) {
|
function getChallengeButtonState(message) {
|
||||||
// Bestimmt den sichtbaren Status einer Challenge aus Sicht des eingeloggten Users.
|
|
||||||
// Dadurch gibt es pro Challenge genau einen aktiven oder deaktivierten Button.
|
|
||||||
const challenge = message.challenge;
|
const challenge = message.challenge;
|
||||||
const role = getChallengeRole(message);
|
const role = getChallengeRole(message);
|
||||||
if (!challenge || !role) {
|
if (!challenge || !role) {
|
||||||
@ -306,6 +406,7 @@
|
|||||||
const challengerHasScore = hasScore(challenge.challengerScore);
|
const challengerHasScore = hasScore(challenge.challengerScore);
|
||||||
const opponentHasScore = hasScore(opponentScore);
|
const opponentHasScore = hasScore(opponentScore);
|
||||||
|
|
||||||
|
// Szenario 1: Beide haben bereits gespielt -> Duell vorbei
|
||||||
if (challengerHasScore && opponentHasScore) {
|
if (challengerHasScore && opponentHasScore) {
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@ -313,6 +414,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Szenario 2: Der angemeldete User wurde herausgefordert (Rolle Opponent)
|
||||||
if (role === "opponent") {
|
if (role === "opponent") {
|
||||||
if (!opponentHasScore && !challengerHasScore) {
|
if (!opponentHasScore && !challengerHasScore) {
|
||||||
return {
|
return {
|
||||||
@ -328,7 +430,9 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Szenario 3: Der angemeldete User hat das Duell gestartet (Rolle Challenger)
|
||||||
if (role === "challenger") {
|
if (role === "challenger") {
|
||||||
|
// Herausforderer spielt als zweites, nachdem der Gegner vorgelegt hat
|
||||||
if (opponentHasScore && !challengerHasScore) {
|
if (opponentHasScore && !challengerHasScore) {
|
||||||
return {
|
return {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -337,6 +441,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gegner hat noch nicht reagiert
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
label: "Warte auf Gegner",
|
label: "Warte auf Gegner",
|
||||||
@ -346,15 +451,20 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die Spielrunde für eine Challenge.
|
||||||
|
* Speichert die Challenge-Daten im SessionStorage ab und leitet auf die Spielseite weiter.
|
||||||
|
* @param {Object} message - Die Challenge-Nachricht.
|
||||||
|
* @param {string} role - Die zugewiesene Spielrolle ("opponent" / "challenger").
|
||||||
|
*/
|
||||||
function startChallenge(message, role) {
|
function startChallenge(message, role) {
|
||||||
// Der aktive Challenge-Kontext wird nur fuer die naechste Spielrunde im Session Storage abgelegt.
|
|
||||||
// play.js liest diesen Zustand aus und weiss dadurch, ob es die erste oder finale Runde ist.
|
|
||||||
const challenge = message.challenge;
|
const challenge = message.challenge;
|
||||||
const challenger = getChallengeChallenger(challenge, message.sender);
|
const challenger = getChallengeChallenger(challenge, message.sender);
|
||||||
const opponent = getChallengeOpponent(challenge, message.recipient);
|
const opponent = getChallengeOpponent(challenge, message.recipient);
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const otherUser = role === "opponent" ? challenger : opponent;
|
const otherUser = role === "opponent" ? challenger : opponent;
|
||||||
|
|
||||||
|
// Challenge-Kontext sichern, damit play.js weiß, in welchem Modus gestartet wird
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
ACTIVE_CHALLENGE_STORAGE_KEY,
|
ACTIVE_CHALLENGE_STORAGE_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -368,11 +478,15 @@
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dynamisch auf die Spielseite navigieren
|
||||||
if (typeof window.loadPage === "function") {
|
if (typeof window.loadPage === "function") {
|
||||||
window.loadPage("play", "nav-play");
|
window.loadPage("play", "nav-play");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert ein Benutzerobjekt/String zu einem Namen.
|
||||||
|
*/
|
||||||
function normalizeUser(user) {
|
function normalizeUser(user) {
|
||||||
if (typeof user === "string") {
|
if (typeof user === "string") {
|
||||||
return user;
|
return user;
|
||||||
@ -381,6 +495,9 @@
|
|||||||
return user?.name ?? user?.username ?? "";
|
return user?.name ?? user?.username ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert einen ISO-Zeitstempel in de-CH Lokale Format (z.B. "09.06. 18:07").
|
||||||
|
*/
|
||||||
function formatMessageTime(value) {
|
function formatMessageTime(value) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
@ -395,6 +512,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blendet eine Feedbackmeldung ein.
|
||||||
|
*/
|
||||||
function setFeedback(message, type) {
|
function setFeedback(message, type) {
|
||||||
const feedback = document.getElementById("messages-feedback");
|
const feedback = document.getElementById("messages-feedback");
|
||||||
if (!feedback) {
|
if (!feedback) {
|
||||||
@ -406,6 +526,9 @@
|
|||||||
feedback.classList.remove("d-none");
|
feedback.classList.remove("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktiviert oder deaktiviert alle Formular-Interaktionen in der Inbox (während Ladezeiten).
|
||||||
|
*/
|
||||||
function setFormEnabled(enabled) {
|
function setFormEnabled(enabled) {
|
||||||
const formElements = document.querySelectorAll(
|
const formElements = document.querySelectorAll(
|
||||||
"#challenge-form button, #challenge-form select, #challenge-form textarea, #mark-read-button, #refresh-messages-button",
|
"#challenge-form button, #challenge-form select, #challenge-form textarea, #mark-read-button, #refresh-messages-button",
|
||||||
@ -416,6 +539,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert den visuellen Zustand des Nachrichten-Links in der Navbar.
|
||||||
|
* Falls ungelesene Nachrichten für den Benutzer vorliegen, wird der Link rot hinterlegt.
|
||||||
|
* @param {Array<Object>} messages - Die Liste der Nachrichten.
|
||||||
|
*/
|
||||||
function updateMessagesNavState(messages = currentMessages) {
|
function updateMessagesNavState(messages = currentMessages) {
|
||||||
const navLink = document.getElementById("navbar-messages");
|
const navLink = document.getElementById("navbar-messages");
|
||||||
if (!navLink) {
|
if (!navLink) {
|
||||||
@ -424,14 +552,19 @@
|
|||||||
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const ownName = normalizeUsername(auth?.username);
|
const ownName = normalizeUsername(auth?.username);
|
||||||
|
// Prüfen, ob eine eingehende Nachricht ungelesen ist
|
||||||
const hasUnreadMessages = messages.some((message) => {
|
const hasUnreadMessages = messages.some((message) => {
|
||||||
const isIncoming = normalizeUsername(message.recipient) === ownName
|
const isIncoming = normalizeUsername(message.recipient) === ownName
|
||||||
|| normalizeUsername(message.sender) !== ownName;
|
|| normalizeUsername(message.sender) !== ownName;
|
||||||
return isIncoming && !message.read;
|
return isIncoming && !message.read;
|
||||||
});
|
});
|
||||||
|
// Toggle der CSS-Klasse "has-unread-messages"
|
||||||
navLink.classList.toggle("has-unread-messages", hasUnreadMessages);
|
navLink.classList.toggle("has-unread-messages", hasUnreadMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert eine Nachricht beim Anklicken als gelesen und sendet dies an den Server.
|
||||||
|
*/
|
||||||
async function markMessageReadOnClick(message, item) {
|
async function markMessageReadOnClick(message, item) {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
@ -445,6 +578,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimistisches UI-Update: Sofort als gelesen markieren
|
||||||
message.read = true;
|
message.read = true;
|
||||||
item.classList.remove("message-item-unread");
|
item.classList.remove("message-item-unread");
|
||||||
updateMessagesNavState(currentMessages);
|
updateMessagesNavState(currentMessages);
|
||||||
@ -456,6 +590,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
// Rollback bei Fehler im Backend
|
||||||
message.read = false;
|
message.read = false;
|
||||||
item.classList.add("message-item-unread");
|
item.classList.add("message-item-unread");
|
||||||
updateMessagesNavState(currentMessages);
|
updateMessagesNavState(currentMessages);
|
||||||
@ -463,15 +598,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert alle ungelesenen Nachrichten einer Challenge-Gruppe beim Klicken auf gelesen.
|
||||||
|
*/
|
||||||
async function markMessageGroupReadOnClick(messages, item) {
|
async function markMessageGroupReadOnClick(messages, item) {
|
||||||
// Eine gruppierte Challenge-Karte kann mehrere einzelne Nachrichten enthalten.
|
|
||||||
// Beim Anklicken werden deshalb alle ungelesenen eingehenden Nachrichten der Gruppe markiert.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
if (!auth || !messageService) {
|
if (!auth || !messageService) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alle ungelesenen eingehenden Nachrichten der Gruppe filtern
|
||||||
const unreadIncomingMessages = messages.filter((message) => {
|
const unreadIncomingMessages = messages.filter((message) => {
|
||||||
const isIncoming = normalizeUsername(message.recipient) === normalizeUsername(auth.username)
|
const isIncoming = normalizeUsername(message.recipient) === normalizeUsername(auth.username)
|
||||||
|| normalizeUsername(message.sender) !== normalizeUsername(auth.username);
|
|| normalizeUsername(message.sender) !== normalizeUsername(auth.username);
|
||||||
@ -482,12 +619,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimistisches Update
|
||||||
unreadIncomingMessages.forEach((message) => {
|
unreadIncomingMessages.forEach((message) => {
|
||||||
message.read = true;
|
message.read = true;
|
||||||
});
|
});
|
||||||
item.classList.remove("message-item-unread");
|
item.classList.remove("message-item-unread");
|
||||||
updateMessagesNavState(currentMessages);
|
updateMessagesNavState(currentMessages);
|
||||||
|
|
||||||
|
// Alle Requests parallel an das Backend senden
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
unreadIncomingMessages.map((message) =>
|
unreadIncomingMessages.map((message) =>
|
||||||
messageService.markMessageAsRead(auth.username, auth.password, message.id),
|
messageService.markMessageAsRead(auth.username, auth.password, message.id),
|
||||||
@ -495,6 +634,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (results.some((result) => !result.ok)) {
|
if (results.some((result) => !result.ok)) {
|
||||||
|
// Rollback bei Fehlern
|
||||||
unreadIncomingMessages.forEach((message) => {
|
unreadIncomingMessages.forEach((message) => {
|
||||||
message.read = false;
|
message.read = false;
|
||||||
});
|
});
|
||||||
@ -504,22 +644,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion zur Ermittlung des Zeitstempels einer Nachricht.
|
||||||
|
*/
|
||||||
function getMessageTimeValue(message) {
|
function getMessageTimeValue(message) {
|
||||||
const time = new Date(message.createdAt).getTime();
|
const time = new Date(message.createdAt).getTime();
|
||||||
return Number.isNaN(time) ? 0 : time;
|
return Number.isNaN(time) ? 0 : time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt die Teildaten aus verschiedenen Nachrichten einer Challenge zusammen.
|
||||||
|
* Wenn z.B. Nachricht 1 die ID und den Text liefert, und Nachricht 2 den Gegner-Score,
|
||||||
|
* enthält das Ergebnis-Objekt alle Eigenschaften.
|
||||||
|
* @param {Array<Object>} messages - Die sortierte Liste der Challenge-Nachrichten.
|
||||||
|
* @returns {Object} Ein zusammengeführtes Datenobjekt der Challenge.
|
||||||
|
*/
|
||||||
function mergeChallengeData(messages) {
|
function mergeChallengeData(messages) {
|
||||||
// Fuer die Challenge-Karte werden Teildaten aus mehreren Nachrichten zusammengefuehrt
|
|
||||||
// (z. B. zuerst Challenge-ID, spaeter Gegner-Score, am Schluss Gewinner).
|
|
||||||
return messages
|
return messages
|
||||||
.slice()
|
.slice()
|
||||||
|
// Chronologisch aufsteigend sortieren für korrektes Überschreiben
|
||||||
.sort((a, b) => getMessageTimeValue(a) - getMessageTimeValue(b))
|
.sort((a, b) => getMessageTimeValue(a) - getMessageTimeValue(b))
|
||||||
.reduce((merged, message) => {
|
.reduce((merged, message) => {
|
||||||
if (!message.challenge) {
|
if (!message.challenge) {
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alle nicht-leeren Werte in das Sammelobjekt übernehmen
|
||||||
Object.keys(message.challenge).forEach((key) => {
|
Object.keys(message.challenge).forEach((key) => {
|
||||||
const value = message.challenge[key];
|
const value = message.challenge[key];
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
@ -531,14 +681,21 @@
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gruppiert eine Liste von Nachrichten.
|
||||||
|
* Normale Nachrichten bleiben eigenständig, während Nachrichten mit derselben Challenge-ID
|
||||||
|
* zu einer einzigen "Challenge-Gruppe" zusammengefasst werden. Das verhindert Inbox-Spam.
|
||||||
|
* @param {Array<Object>} messages - Liste aller Nachrichten.
|
||||||
|
* @returns {Array<Object>} Die gruppierten Einträge, absteigend nach Aktualität sortiert.
|
||||||
|
*/
|
||||||
function groupMessagesByChallenge(messages) {
|
function groupMessagesByChallenge(messages) {
|
||||||
// Nachrichten mit gleicher Challenge-ID werden zu einer Karte gruppiert,
|
|
||||||
// damit alte Zwischen-Nachrichten nicht einzeln die Inbox ueberladen.
|
|
||||||
const groupsByChallenge = new Map();
|
const groupsByChallenge = new Map();
|
||||||
const standaloneGroups = [];
|
const standaloneGroups = [];
|
||||||
|
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
const challengeId = getChallengeId(message.challenge);
|
const challengeId = getChallengeId(message.challenge);
|
||||||
|
|
||||||
|
// Standardnachrichten ohne Challenge-ID werden direkt verarbeitet
|
||||||
if (challengeId === null || challengeId === undefined) {
|
if (challengeId === null || challengeId === undefined) {
|
||||||
standaloneGroups.push({
|
standaloneGroups.push({
|
||||||
kind: "message",
|
kind: "message",
|
||||||
@ -548,6 +705,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Challenge-Nachrichten werden nach ID gruppiert
|
||||||
const key = String(challengeId);
|
const key = String(challengeId);
|
||||||
if (!groupsByChallenge.has(key)) {
|
if (!groupsByChallenge.has(key)) {
|
||||||
groupsByChallenge.set(key, []);
|
groupsByChallenge.set(key, []);
|
||||||
@ -555,11 +713,14 @@
|
|||||||
groupsByChallenge.get(key).push(message);
|
groupsByChallenge.get(key).push(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Die Gruppierungen konsolidieren
|
||||||
const challengeGroups = Array.from(groupsByChallenge.values()).map((groupMessages) => {
|
const challengeGroups = Array.from(groupsByChallenge.values()).map((groupMessages) => {
|
||||||
|
// Absteigend sortieren, damit das neueste Element die Basis bildet
|
||||||
const sortedMessages = groupMessages
|
const sortedMessages = groupMessages
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => getMessageTimeValue(b) - getMessageTimeValue(a));
|
.sort((a, b) => getMessageTimeValue(b) - getMessageTimeValue(a));
|
||||||
const latest = { ...sortedMessages[0] };
|
const latest = { ...sortedMessages[0] };
|
||||||
|
// Spieldaten aller Nachrichten der Gruppe verschmelzen
|
||||||
latest.challenge = mergeChallengeData(sortedMessages);
|
latest.challenge = mergeChallengeData(sortedMessages);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -569,11 +730,17 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Zusammenführen und absteigend nach Datum der neuesten Nachricht sortieren
|
||||||
return standaloneGroups
|
return standaloneGroups
|
||||||
.concat(challengeGroups)
|
.concat(challengeGroups)
|
||||||
.sort((a, b) => getMessageTimeValue(b.latest) - getMessageTimeValue(a.latest));
|
.sort((a, b) => getMessageTimeValue(b.latest) - getMessageTimeValue(a.latest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert die Benutzerliste im Sidebar-Bereich der Inbox zum schnellen Herausfordern.
|
||||||
|
* Befüllt auch das Empfänger-Dropdown im Formular.
|
||||||
|
* @param {Array<string|Object>} users - Liste aller User.
|
||||||
|
*/
|
||||||
function renderUserList(users) {
|
function renderUserList(users) {
|
||||||
const userList = document.getElementById("messages-user-list");
|
const userList = document.getElementById("messages-user-list");
|
||||||
const recipientSelect = document.getElementById("challenge-recipient");
|
const recipientSelect = document.getElementById("challenge-recipient");
|
||||||
@ -583,6 +750,8 @@
|
|||||||
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const ownName = normalizeUsername(auth?.username);
|
const ownName = normalizeUsername(auth?.username);
|
||||||
|
|
||||||
|
// Benutzernamen extrahieren, bereinigen, Duplikate entfernen und eigenen Namen filtern
|
||||||
const uniqueUsers = Array.from(new Set(users.map(normalizeUser)))
|
const uniqueUsers = Array.from(new Set(users.map(normalizeUser)))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((username) => normalizeUsername(username) !== ownName)
|
.filter((username) => normalizeUsername(username) !== ownName)
|
||||||
@ -600,11 +769,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Für jeden Benutzer einen Button in der Liste und eine Option im Select erstellen
|
||||||
uniqueUsers.forEach((username) => {
|
uniqueUsers.forEach((username) => {
|
||||||
const userButton = document.createElement("button");
|
const userButton = document.createElement("button");
|
||||||
userButton.type = "button";
|
userButton.type = "button";
|
||||||
userButton.className = "messages-user-button";
|
userButton.className = "messages-user-button";
|
||||||
userButton.textContent = username;
|
userButton.textContent = username;
|
||||||
|
// Bei Klick den User als Empfänger auswählen und Fokus ins Textfeld setzen
|
||||||
userButton.addEventListener("click", () => {
|
userButton.addEventListener("click", () => {
|
||||||
recipientSelect.value = username;
|
recipientSelect.value = username;
|
||||||
document.getElementById("challenge-text")?.focus();
|
document.getElementById("challenge-text")?.focus();
|
||||||
@ -618,8 +789,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert die Nachrichtenliste (Inbox) im DOM.
|
||||||
|
* Erstellt Standard-Nachrichtenkarten oder komplexe Challenge-Karten.
|
||||||
|
* @param {Array<Object>} messages - Die anzuzeigenden Nachrichten.
|
||||||
|
*/
|
||||||
function renderMessages(messages = currentMessages) {
|
function renderMessages(messages = currentMessages) {
|
||||||
// Rendert normale Nachrichten einzeln und Challenge-Nachrichten als gruppierte Karten.
|
|
||||||
const messageList = document.getElementById("message-list");
|
const messageList = document.getElementById("message-list");
|
||||||
if (!messageList) {
|
if (!messageList) {
|
||||||
return;
|
return;
|
||||||
@ -637,11 +812,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nachrichten nach Gruppierung rendern
|
||||||
groupMessagesByChallenge(messages)
|
groupMessagesByChallenge(messages)
|
||||||
.forEach((messageGroup) => {
|
.forEach((messageGroup) => {
|
||||||
const message = messageGroup.latest;
|
const message = messageGroup.latest;
|
||||||
const item = document.createElement("article");
|
const item = document.createElement("article");
|
||||||
const isOutgoing = normalizeUsername(message.sender) === ownName;
|
const isOutgoing = normalizeUsername(message.sender) === ownName;
|
||||||
|
// Prüfen, ob mindestens eine Nachricht in der Gruppe ungelesen ist
|
||||||
const hasUnreadIncoming = messageGroup.messages.some((groupMessage) => {
|
const hasUnreadIncoming = messageGroup.messages.some((groupMessage) => {
|
||||||
const isIncoming = normalizeUsername(groupMessage.recipient) === ownName
|
const isIncoming = normalizeUsername(groupMessage.recipient) === ownName
|
||||||
|| normalizeUsername(groupMessage.sender) !== ownName;
|
|| normalizeUsername(groupMessage.sender) !== ownName;
|
||||||
@ -654,9 +831,11 @@
|
|||||||
}
|
}
|
||||||
if (hasUnreadIncoming) {
|
if (hasUnreadIncoming) {
|
||||||
item.classList.add("message-item-unread");
|
item.classList.add("message-item-unread");
|
||||||
|
// Bei Klick alle ungelesenen Nachrichten dieser Gruppe als gelesen markieren
|
||||||
item.addEventListener("click", () => markMessageGroupReadOnClick(messageGroup.messages, item));
|
item.addEventListener("click", () => markMessageGroupReadOnClick(messageGroup.messages, item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Absender-/Empfängerzeile bestimmen
|
||||||
const fromToText = messageGroup.kind === "challenge"
|
const fromToText = messageGroup.kind === "challenge"
|
||||||
? "Challenge"
|
? "Challenge"
|
||||||
: isOutgoing
|
: isOutgoing
|
||||||
@ -678,6 +857,7 @@
|
|||||||
meta.append(sender, time);
|
meta.append(sender, time);
|
||||||
item.append(meta, text);
|
item.append(meta, text);
|
||||||
|
|
||||||
|
// Bei Challenge-Gruppen mit mehreren Nachrichten einen "Verlauf einblenden"-Button anbieten
|
||||||
if (messageGroup.kind === "challenge" && messageGroup.messages.length > 1) {
|
if (messageGroup.kind === "challenge" && messageGroup.messages.length > 1) {
|
||||||
const toggleButton = document.createElement("button");
|
const toggleButton = document.createElement("button");
|
||||||
toggleButton.type = "button";
|
toggleButton.type = "button";
|
||||||
@ -704,7 +884,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
toggleButton.addEventListener("click", (event) => {
|
toggleButton.addEventListener("click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation(); // Verhindert das Auslösen des Gelesen-Markierens der ganzen Karte
|
||||||
const isCollapsed = thread.classList.toggle("d-none");
|
const isCollapsed = thread.classList.toggle("d-none");
|
||||||
toggleButton.textContent = isCollapsed ? "Verlauf anzeigen" : "Verlauf ausblenden";
|
toggleButton.textContent = isCollapsed ? "Verlauf anzeigen" : "Verlauf ausblenden";
|
||||||
});
|
});
|
||||||
@ -713,10 +893,12 @@
|
|||||||
item.appendChild(thread);
|
item.appendChild(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls die Challenge beendet ist (beide Scores vorhanden), eine Ergebnis-Grafik zeichnen
|
||||||
if (message.challenge && hasScore(message.challenge.challengerScore) && hasScore(message.challenge.opponentScore)) {
|
if (message.challenge && hasScore(message.challenge.challengerScore) && hasScore(message.challenge.opponentScore)) {
|
||||||
item.appendChild(createChallengeResultGraphic(message.challenge));
|
item.appendChild(createChallengeResultGraphic(message.challenge));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ggf. Button für "Annehmen" oder "Spielen" einblenden
|
||||||
const challengeButtonState = getChallengeButtonState(message);
|
const challengeButtonState = getChallengeButtonState(message);
|
||||||
if (challengeButtonState) {
|
if (challengeButtonState) {
|
||||||
const challengeButton = document.createElement("button");
|
const challengeButton = document.createElement("button");
|
||||||
@ -734,8 +916,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die HTML-Ergebnisgrafik für eine beendete Challenge zusammen.
|
||||||
|
* @param {Object} result - Die zusammengeführten Challenge-Daten.
|
||||||
|
* @returns {HTMLElement} Die erstellte Ergebnis-Komponente.
|
||||||
|
*/
|
||||||
function createChallengeResultGraphic(result) {
|
function createChallengeResultGraphic(result) {
|
||||||
// Baut die kompakte Ergebnisgrafik mit passendem Bild aus Sicht des aktuellen Users.
|
|
||||||
const graphic = document.createElement("div");
|
const graphic = document.createElement("div");
|
||||||
graphic.className = "challenge-result-graphic";
|
graphic.className = "challenge-result-graphic";
|
||||||
|
|
||||||
@ -743,6 +929,8 @@
|
|||||||
const isDraw = result.winner === null || result.winner === "draw";
|
const isDraw = result.winner === null || result.winner === "draw";
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const isOwnWin = !isDraw && normalizeUsername(winnerName) === normalizeUsername(auth?.username);
|
const isOwnWin = !isDraw && normalizeUsername(winnerName) === normalizeUsername(auth?.username);
|
||||||
|
|
||||||
|
// Status bestimmen aus Sicht des eingeloggten Users (Sieg / Niederlage / Draw)
|
||||||
const outcome = isDraw ? "draw" : isOwnWin ? "win" : "loss";
|
const outcome = isDraw ? "draw" : isOwnWin ? "win" : "loss";
|
||||||
const outcomeText = outcome === "draw"
|
const outcomeText = outcome === "draw"
|
||||||
? "Unentschieden"
|
? "Unentschieden"
|
||||||
@ -792,6 +980,9 @@
|
|||||||
return graphic;
|
return graphic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Liste aller registrierten Benutzer vom Server.
|
||||||
|
*/
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const userService = getUserService();
|
const userService = getUserService();
|
||||||
@ -810,13 +1001,18 @@
|
|||||||
renderUserList(result.body);
|
renderUserList(result.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt alle Nachrichten vom Backend und aktualisiert die Ansichten.
|
||||||
|
* Schaltet bei fehlendem Login auf den Platzhalter-Zustand um.
|
||||||
|
* @param {Object} options - Z. B. { showFeedback: true } für Aktualisierungsmeldung.
|
||||||
|
*/
|
||||||
async function loadMessages(options = {}) {
|
async function loadMessages(options = {}) {
|
||||||
// Laedt Nachrichten vom Backend und blendet die Seite bei fehlendem Login in einen Hinweiszustand.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
const loggedInDiv = document.getElementById("messages-content");
|
const loggedInDiv = document.getElementById("messages-content");
|
||||||
const loggedOutDiv = document.getElementById("messages-login-placeholder");
|
const loggedOutDiv = document.getElementById("messages-login-placeholder");
|
||||||
|
|
||||||
|
// Falls nicht angemeldet: Platzhalter einblenden, Daten leeren
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
if (loggedInDiv) loggedInDiv.classList.add("d-none");
|
if (loggedInDiv) loggedInDiv.classList.add("d-none");
|
||||||
if (loggedOutDiv) loggedOutDiv.classList.remove("d-none");
|
if (loggedOutDiv) loggedOutDiv.classList.remove("d-none");
|
||||||
@ -828,6 +1024,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls angemeldet: Dashboard zeigen
|
||||||
if (loggedInDiv) loggedInDiv.classList.remove("d-none");
|
if (loggedInDiv) loggedInDiv.classList.remove("d-none");
|
||||||
if (loggedOutDiv) loggedOutDiv.classList.add("d-none");
|
if (loggedOutDiv) loggedOutDiv.classList.add("d-none");
|
||||||
|
|
||||||
@ -847,6 +1044,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nachrichten normalisieren, im Cache sichern und rendern
|
||||||
currentMessages = result.body.map(normalizeMessage);
|
currentMessages = result.body.map(normalizeMessage);
|
||||||
renderMessages();
|
renderMessages();
|
||||||
updateMessagesNavState();
|
updateMessagesNavState();
|
||||||
@ -857,8 +1055,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein neues Duell (Challenge). Generiert den Spieltext,
|
||||||
|
* bettet ihn als JSON ein und sendet die Nachricht ab.
|
||||||
|
*/
|
||||||
async function handleChallengeSubmit(event) {
|
async function handleChallengeSubmit(event) {
|
||||||
// Erstellt eine Challenge. Der Gegner spielt zuerst; der weitere Ablauf wird ueber Nachrichten gesteuert.
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
@ -877,7 +1078,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Challenge-Satz generieren
|
||||||
const challengeText = generateChallengeText();
|
const challengeText = generateChallengeText();
|
||||||
|
// JSON-Metadaten einbetten
|
||||||
const challengeMessage = buildEmbeddedChallengeText(
|
const challengeMessage = buildEmbeddedChallengeText(
|
||||||
{ challengeText: challengeText },
|
{ challengeText: challengeText },
|
||||||
text,
|
text,
|
||||||
@ -895,14 +1098,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reste im Storage säubern
|
||||||
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
||||||
setFeedback(
|
setFeedback(
|
||||||
"Challenge an " + recipient + " wurde gesendet. Der Gegner spielt zuerst; danach bekommst du sein Resultat.",
|
"Challenge an " + recipient + " wurde gesendet. Der Gegner spielt zuerst; danach bekommst du sein Resultat.",
|
||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
|
textInput.value = ""; // Textfeld leeren
|
||||||
await loadMessages({ showFeedback: false });
|
await loadMessages({ showFeedback: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert alle Nachrichten auf einmal als gelesen.
|
||||||
|
*/
|
||||||
async function handleMarkRead() {
|
async function handleMarkRead() {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
@ -913,6 +1121,7 @@
|
|||||||
|
|
||||||
const result = await messageService.markAllMessagesAsRead(auth.username, auth.password);
|
const result = await messageService.markAllMessagesAsRead(auth.username, auth.password);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
// Fallback: Einzeln nacheinander als gelesen markieren, falls die Bulk-API fehlschlägt
|
||||||
const unreadMessages = currentMessages.filter((message) => !message.read);
|
const unreadMessages = currentMessages.filter((message) => !message.read);
|
||||||
const readResults = await Promise.all(
|
const readResults = await Promise.all(
|
||||||
unreadMessages.map((message) =>
|
unreadMessages.map((message) =>
|
||||||
@ -930,6 +1139,10 @@
|
|||||||
await loadMessages({ showFeedback: false });
|
await loadMessages({ showFeedback: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert die Nachrichten-Seite (Events binden).
|
||||||
|
* Wird von navigation.js nach dem Laden von messages.html aufgerufen.
|
||||||
|
*/
|
||||||
async function initMessagesPage() {
|
async function initMessagesPage() {
|
||||||
const challengeForm = document.getElementById("challenge-form");
|
const challengeForm = document.getElementById("challenge-form");
|
||||||
const markReadButton = document.getElementById("mark-read-button");
|
const markReadButton = document.getElementById("mark-read-button");
|
||||||
@ -950,7 +1163,10 @@
|
|||||||
await loadMessages({ showFeedback: false });
|
await loadMessages({ showFeedback: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialisierungsfunktion global registrieren
|
||||||
window.initMessagesPage = initMessagesPage;
|
window.initMessagesPage = initMessagesPage;
|
||||||
|
|
||||||
|
// Statusaktualisierung global anbieten
|
||||||
window.updateMessagesNavState = function () {
|
window.updateMessagesNavState = function () {
|
||||||
loadMessages({ showFeedback: false }).catch((error) => {
|
loadMessages({ showFeedback: false }).catch((error) => {
|
||||||
console.error("Nachrichtenstatus konnte nicht geladen werden:", error);
|
console.error("Nachrichtenstatus konnte nicht geladen werden:", error);
|
||||||
@ -958,6 +1174,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Polling starten, um im Hintergrund alle 30s den Status (z.B. ungelesene Nachrichten) zu prüfen
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
window.updateMessagesNavState();
|
window.updateMessagesNavState();
|
||||||
if (!messagePollingInterval) {
|
if (!messagePollingInterval) {
|
||||||
@ -968,3 +1185,4 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
;
|
||||||
|
|||||||
@ -1,28 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Navigation und dynamisches Laden von Einzelseiten (Single Page App Verhalten).
|
||||||
|
* Dieses Skript reagiert auf Klicks im Menü/Sidebar, lädt die gewünschte HTML-Teilseite
|
||||||
|
* per Fetch-API nach und führt die jeweilige Seite-Initialisierungsfunktion aus.
|
||||||
|
*/
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die "active"-Klasse im Menü für die ausgewählte Seite,
|
||||||
|
* damit der Benutzer sieht, in welchem Bereich er sich befindet.
|
||||||
|
* @param {string} id - Die ID des Menü-Links (z.B. "nav-home").
|
||||||
|
*/
|
||||||
function setActiveMenu(id) {
|
function setActiveMenu(id) {
|
||||||
//Alle Sidebar-Links zurücksetzen
|
// Alle Sidebar-Links zurücksetzen
|
||||||
document.querySelectorAll("#sidebar .nav-link").forEach(link => link.classList.remove("active"));
|
document.querySelectorAll("#sidebar .nav-link").forEach(link => link.classList.remove("active"));
|
||||||
// Alle Navbar-Links zurücksetzen
|
// Alle Navbar-Links zurücksetzen
|
||||||
document.querySelectorAll(".navbar-nav .nav-link").forEach(link => link.classList.remove("active"));
|
document.querySelectorAll(".navbar-nav .nav-link").forEach(link => link.classList.remove("active"));
|
||||||
|
|
||||||
//Aktiven Link setzen
|
// Aktiven Link anhand seiner ID suchen und hervorheben
|
||||||
const activeLink = document.getElementById(id);
|
const activeLink = document.getElementById(id);
|
||||||
if (activeLink) activeLink.classList.add("active");
|
if (activeLink) activeLink.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Laedt Teilseiten dynamisch in den Hauptbereich.
|
/**
|
||||||
// Danach wird die passende Init-Funktion aufgerufen, weil die Elemente erst nach dem Laden existieren.
|
* Lädt eine Teilseite (.html) dynamisch aus dem "pages/"-Ordner in den Hauptbereich.
|
||||||
|
* Nach dem Laden werden die DOM-Elemente initialisiert (z.B. Event-Listener gebunden),
|
||||||
|
* indem die entsprechende globale "init..."-Funktion aufgerufen wird.
|
||||||
|
* @param {string} page - Der Name der HTML-Datei (ohne Endung, z.B. "play").
|
||||||
|
* @param {string} menuId - Die ID des zugehörigen Menüpunkts zur visuellen Aktivierung.
|
||||||
|
*/
|
||||||
window.loadPage = function loadPage(page, menuId) {
|
window.loadPage = function loadPage(page, menuId) {
|
||||||
|
// fetch lädt das HTML-Snippet. cache: "no-store" verhindert veraltete Stände im Browser-Cache.
|
||||||
fetch("pages/" + page + ".html", { cache: "no-store" })
|
fetch("pages/" + page + ".html", { cache: "no-store" })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("HTTP " + response.status);
|
throw new Error("HTTP-Fehler " + response.status);
|
||||||
}
|
}
|
||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// Das geladene HTML-Fragment in den Haupt-Container rendern
|
||||||
document.getElementById("main-content").innerHTML = data;
|
document.getElementById("main-content").innerHTML = data;
|
||||||
|
|
||||||
|
// Aktiven Menüpunkt visuell kennzeichnen
|
||||||
setActiveMenu(menuId);
|
setActiveMenu(menuId);
|
||||||
|
|
||||||
|
// Da das DOM nun neu aufgebaut wurde, müssen wir die Event-Handler
|
||||||
|
// für die jeweilige Seite neu registrieren (Init-Funktionen).
|
||||||
if (page === "login" && typeof window.initLoginPage === "function") {
|
if (page === "login" && typeof window.initLoginPage === "function") {
|
||||||
window.initLoginPage();
|
window.initLoginPage();
|
||||||
}
|
}
|
||||||
@ -41,11 +64,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Fehler beim Laden von " + page + ":", error);
|
console.error("Fehler beim Laden von " + page + ":", error);
|
||||||
|
// Fehlermeldung für den Benutzer in den Hauptbereich einbetten
|
||||||
document.getElementById("main-content").innerHTML = "<div class='alert alert-danger'>Fehler beim Laden der Seite: " + error.message + "</div>";
|
document.getElementById("main-content").innerHTML = "<div class='alert alert-danger'>Fehler beim Laden der Seite: " + error.message + "</div>";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigation wird zentral verdrahtet, damit alle Seiten ueber denselben Lade-Mechanismus laufen.
|
// --- Menü-Verkabelung ---
|
||||||
|
// Alle Navigations-Elemente aus dem DOM abrufen
|
||||||
const navHome = document.getElementById("nav-home");
|
const navHome = document.getElementById("nav-home");
|
||||||
const navPlay = document.getElementById("nav-play");
|
const navPlay = document.getElementById("nav-play");
|
||||||
const navMyScores = document.getElementById("nav-my-scores");
|
const navMyScores = document.getElementById("nav-my-scores");
|
||||||
@ -53,14 +78,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const navbarLogin = document.getElementById("navbar-login");
|
const navbarLogin = document.getElementById("navbar-login");
|
||||||
const navbarMessages = document.getElementById("navbar-messages");
|
const navbarMessages = document.getElementById("navbar-messages");
|
||||||
|
|
||||||
|
// Standard-Verhalten der Links (Seiten-Reload) verhindern und loadPage() aufrufen
|
||||||
if (navHome) navHome.onclick = (e) => { e.preventDefault(); loadPage("home", "nav-home"); };
|
if (navHome) navHome.onclick = (e) => { e.preventDefault(); loadPage("home", "nav-home"); };
|
||||||
if (navPlay) navPlay.onclick = (e) => { e.preventDefault(); loadPage("play", "nav-play"); };
|
if (navPlay) navPlay.onclick = (e) => { e.preventDefault(); loadPage("play", "nav-play"); };
|
||||||
if (navMyScores) navMyScores.onclick = (e) => { e.preventDefault(); loadPage("scores", "nav-my-scores"); };
|
if (navMyScores) navMyScores.onclick = (e) => { e.preventDefault(); loadPage("scores", "nav-my-scores"); };
|
||||||
if (navLeaderboard) navLeaderboard.onclick = (e) => { e.preventDefault(); loadPage("leaderboard", "nav-leaderboard"); };
|
if (navLeaderboard) navLeaderboard.onclick = (e) => { e.preventDefault(); loadPage("leaderboard", "nav-leaderboard"); };
|
||||||
if (navbarLogin) navbarLogin.onclick = (e) => { e.preventDefault(); loadPage("login", "navbar-login"); };
|
if (navbarLogin) navbarLogin.onclick = (e) => { e.preventDefault(); loadPage("login", "navbar-login"); };
|
||||||
if (navbarMessages) navbarMessages.onclick = (e) => { e.preventDefault(); loadPage("messages", "navbar-messages"); };
|
if (navbarMessages) navbarMessages.onclick = (e) => { e.preventDefault(); loadPage("messages", "navbar-messages"); };
|
||||||
|
|
||||||
|
// Prüfen, ob neue ungelesene Nachrichten vorliegen, um den Nachrichten-Link rot zu markieren
|
||||||
if (typeof window.updateMessagesNavState === "function") window.updateMessagesNavState();
|
if (typeof window.updateMessagesNavState === "function") window.updateMessagesNavState();
|
||||||
|
|
||||||
//Startseite laden
|
// Beim allerersten Aufruf der App die Startseite ("home") laden
|
||||||
loadPage("home", "nav-home");
|
loadPage("home", "nav-home");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
290
js/play.js
290
js/play.js
@ -1,10 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Spiellogik (Lorem Ipsum Merk- und Schreibspiel).
|
||||||
|
* Dieses Modul verwaltet die verschiedenen Phasen des Spiels (Startansicht, Einprägen, Eingabe, Auswertung),
|
||||||
|
* berechnet die Punktzahl durch Wortvergleich und speichert das Ergebnis im Backend.
|
||||||
|
* Unterstützt auch das Duell-System (Challenges) zwischen zwei Benutzern.
|
||||||
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
// --- Konfiguration ---
|
// --- Konfiguration & Konstanten ---
|
||||||
|
|
||||||
|
// Die Zeit in Sekunden, die der Spieler hat, um sich den Text einzuprägen.
|
||||||
const MEMORIZE_TIME_SECONDS = 15;
|
const MEMORIZE_TIME_SECONDS = 15;
|
||||||
|
|
||||||
|
// Storage-Schlüssel, unter dem eine aktive Challenge im SessionStorage zwischengespeichert wird.
|
||||||
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
|
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
|
||||||
|
|
||||||
|
// Präfix zur Kennzeichnung von eingebetteten JSON-Challenge-Daten in Textnachrichten.
|
||||||
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
|
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
|
||||||
|
|
||||||
// Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann.
|
// Bausteine für den zufälligen 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",
|
||||||
@ -56,37 +68,48 @@
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
let timerInterval;
|
// --- Spielstatus Variablen ---
|
||||||
let currentTime = 0;
|
let timerInterval; // Intervall-ID für den Memorize-Timer
|
||||||
let inputStartMs = null;
|
let currentTime = 0; // Aktueller Countdown-Stand
|
||||||
|
let inputStartMs = null; // Zeitstempel (ms), wann der Benutzer mit der Eingabe begonnen hat
|
||||||
|
|
||||||
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
|
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
|
||||||
let currentGameText = "";
|
let currentGameText = "";
|
||||||
let lastGeneratedText = "";
|
let lastGeneratedText = ""; // Verhindert direkt aufeinanderfolgende gleiche Texte
|
||||||
|
|
||||||
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde.
|
// DOM-Referenzen (werden in initPlayPage gesucht, da play.html dynamisch nachgeladen wird)
|
||||||
let phaseStart;
|
let phaseStart; // Start-Container (mit "Starten"-Button)
|
||||||
let phaseMemorize;
|
let phaseMemorize; // Einpräge-Container (zeigt den Text und Countdown)
|
||||||
let phaseInput;
|
let phaseInput; // Eingabe-Container (Textarea)
|
||||||
let phaseResult;
|
let phaseResult; // Ergebnis-Container (Vergleich und erreichte Punkte)
|
||||||
let targetTextDisplay;
|
|
||||||
let timerDisplay;
|
let targetTextDisplay; // Element zur Anzeige des zu merkenden Textes
|
||||||
let userTextInput;
|
let timerDisplay; // Element zur Anzeige des Countdowns
|
||||||
let resultScore;
|
let userTextInput; // Textarea für die Benutzereingabe
|
||||||
let resultOriginal;
|
let resultScore; // Element zur Anzeige der Punktzahl im Ergebnis
|
||||||
let resultInput;
|
let resultOriginal; // Container für farblich markierten Originaltext
|
||||||
let gameStatus;
|
let resultInput; // Container für farblich markierten eingegebenen Text
|
||||||
let scoreSaveFeedback;
|
let gameStatus; // Badge-Element zur Statusanzeige (z.B. "Lernphase", "Eingabe")
|
||||||
let btnSubmitScore;
|
let scoreSaveFeedback; // Container für Rückmeldungen beim Speichern
|
||||||
let activeChallenge = null;
|
let btnSubmitScore; // Button "Auswerten & Absenden"
|
||||||
|
let activeChallenge = null; // Enthält geladene Challenge-Daten (falls Spiel aus Inbox gestartet)
|
||||||
|
|
||||||
// --- Funktionen ---
|
// --- Hilfsfunktionen ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wählt ein zufälliges Element aus einem Array aus.
|
||||||
|
* @param {Array} items - Das Quell-Array.
|
||||||
|
* @returns {*} Ein zufälliges Element.
|
||||||
|
*/
|
||||||
function getRandomItem(items) {
|
function getRandomItem(items) {
|
||||||
return items[Math.floor(Math.random() * items.length)];
|
return items[Math.floor(Math.random() * items.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen.
|
/**
|
||||||
|
* Erstellt pro Runde zwei zufällige Hauptsätze plus einen Schlusssatz.
|
||||||
|
* Stellt sicher, dass sich zwei aufeinanderfolgende Runden-Texte unterscheiden.
|
||||||
|
* @returns {string} Ein generierter Satz.
|
||||||
|
*/
|
||||||
function generateGameText() {
|
function generateGameText() {
|
||||||
let generatedText = "";
|
let generatedText = "";
|
||||||
|
|
||||||
@ -119,15 +142,19 @@
|
|||||||
return generatedText;
|
return generatedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet eine neue Spielrunde. Blendet die Startphase aus und startet den Countdown.
|
||||||
|
*/
|
||||||
function startGame() {
|
function startGame() {
|
||||||
if (!phaseStart || !phaseMemorize) return;
|
if (!phaseStart || !phaseMemorize) return;
|
||||||
|
|
||||||
inputStartMs = null;
|
inputStartMs = null; // Zeitstempel zurücksetzen
|
||||||
|
|
||||||
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
|
// Startansicht ausblenden und den Einpräge-Bildschirm anzeigen
|
||||||
phaseStart.classList.add("d-none");
|
phaseStart.classList.add("d-none");
|
||||||
phaseMemorize.classList.remove("d-none");
|
phaseMemorize.classList.remove("d-none");
|
||||||
|
|
||||||
|
// Status-Badge aktualisieren
|
||||||
if (gameStatus) {
|
if (gameStatus) {
|
||||||
gameStatus.textContent = "Lernphase";
|
gameStatus.textContent = "Lernphase";
|
||||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||||
@ -135,10 +162,11 @@
|
|||||||
gameStatus.style.color = "#1b1b2f";
|
gameStatus.style.color = "#1b1b2f";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text ermitteln (aus Challenge oder neu generiert) und anzeigen
|
||||||
currentGameText = getRoundText();
|
currentGameText = getRoundText();
|
||||||
if (targetTextDisplay) targetTextDisplay.textContent = currentGameText;
|
if (targetTextDisplay) targetTextDisplay.textContent = currentGameText;
|
||||||
|
|
||||||
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
|
// Countdown-Timer starten
|
||||||
currentTime = MEMORIZE_TIME_SECONDS;
|
currentTime = MEMORIZE_TIME_SECONDS;
|
||||||
if (timerDisplay) timerDisplay.textContent = currentTime;
|
if (timerDisplay) timerDisplay.textContent = currentTime;
|
||||||
|
|
||||||
@ -146,21 +174,26 @@
|
|||||||
currentTime--;
|
currentTime--;
|
||||||
if (timerDisplay) timerDisplay.textContent = currentTime;
|
if (timerDisplay) timerDisplay.textContent = currentTime;
|
||||||
|
|
||||||
|
// Nach Ablauf des Timers automatisch in die Eingabephase wechseln
|
||||||
if (currentTime <= 0) {
|
if (currentTime <= 0) {
|
||||||
endMemorizePhase();
|
endMemorizePhase();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beendet die Einprägephase, stoppt den Timer und öffnet das Eingabefeld.
|
||||||
|
*/
|
||||||
function endMemorizePhase() {
|
function endMemorizePhase() {
|
||||||
clearInterval(timerInterval);
|
clearInterval(timerInterval);
|
||||||
|
|
||||||
if (!phaseMemorize || !phaseInput) return;
|
if (!phaseMemorize || !phaseInput) return;
|
||||||
|
|
||||||
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
|
// Text ausblenden (wichtig gegen Abschreiben!) und Eingabefeld zeigen
|
||||||
phaseMemorize.classList.add("d-none");
|
phaseMemorize.classList.add("d-none");
|
||||||
phaseInput.classList.remove("d-none");
|
phaseInput.classList.remove("d-none");
|
||||||
|
|
||||||
|
// Status-Badge anpassen
|
||||||
if (gameStatus) {
|
if (gameStatus) {
|
||||||
gameStatus.textContent = "Eingabe";
|
gameStatus.textContent = "Eingabe";
|
||||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||||
@ -168,15 +201,22 @@
|
|||||||
gameStatus.style.color = "#fff";
|
gameStatus.style.color = "#fff";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fokus ins Textfeld setzen, damit der Spieler sofort tippen kann
|
||||||
if (userTextInput) {
|
if (userTextInput) {
|
||||||
userTextInput.value = "";
|
userTextInput.value = "";
|
||||||
userTextInput.focus();
|
userTextInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zeitmessung für die Eingabedauer starten
|
||||||
inputStartMs = Date.now();
|
inputStartMs = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
|
/**
|
||||||
|
* Normalisiert ein einzelnes Wort für den fehlertoleranten Vergleich.
|
||||||
|
* Konvertiert zu Kleinbuchstaben, löst Umlaute/ß auf und entfernt Satzzeichen.
|
||||||
|
* @param {string} word - Das zu bereinigende Wort.
|
||||||
|
* @returns {string} Das normalisierte Wort.
|
||||||
|
*/
|
||||||
function normalizeWord(word) {
|
function normalizeWord(word) {
|
||||||
return word
|
return word
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@ -184,18 +224,31 @@
|
|||||||
.replace(/ö/g, "oe")
|
.replace(/ö/g, "oe")
|
||||||
.replace(/ü/g, "ue")
|
.replace(/ü/g, "ue")
|
||||||
.replace(/ß/g, "ss")
|
.replace(/ß/g, "ss")
|
||||||
|
// Entfernt Satz- und Sonderzeichen
|
||||||
.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
|
/**
|
||||||
|
* Zerlegt einen Text anhand von Leerzeichen in ein Array von Wörtern.
|
||||||
|
* Leere Elemente (z.B. durch mehrfache Leerzeichen) werden ausgefiltert.
|
||||||
|
* @param {string} text - Der Eingabetext.
|
||||||
|
* @returns {Array<string>} Liste der Wörter.
|
||||||
|
*/
|
||||||
function getWords(text) {
|
function getWords(text) {
|
||||||
return text.split(/\s+/).filter((word) => word.length > 0);
|
return text.split(/\s+/).filter((word) => word.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Punktzahl auf Basis der korrekt geschriebenen Wörter.
|
||||||
|
* Es wird die exakte Position der Wörter verglichen (Wort an Index i).
|
||||||
|
* @param {string} original - Der vorgegebene Originaltext.
|
||||||
|
* @param {string} input - Die Eingabe des Spielers.
|
||||||
|
* @returns {number} Anzahl der übereinstimmenden Wörter.
|
||||||
|
*/
|
||||||
function calculateScore(original, input) {
|
function calculateScore(original, input) {
|
||||||
if (!original || !input) return 0;
|
if (!original || !input) return 0;
|
||||||
|
|
||||||
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
|
// Beide Texte in normalisierte Wort-Listen zerlegen
|
||||||
const cleanOriginal = getWords(original)
|
const cleanOriginal = getWords(original)
|
||||||
.map(normalizeWord)
|
.map(normalizeWord)
|
||||||
.filter((word) => word.length > 0);
|
.filter((word) => word.length > 0);
|
||||||
@ -204,6 +257,7 @@
|
|||||||
.filter((word) => word.length > 0);
|
.filter((word) => word.length > 0);
|
||||||
|
|
||||||
let correctWords = 0;
|
let correctWords = 0;
|
||||||
|
// Nur bis zum kürzeren der beiden Texte vergleichen, um Out-of-Bounds zu vermeiden
|
||||||
const limit = Math.min(cleanOriginal.length, cleanInput.length);
|
const limit = Math.min(cleanOriginal.length, cleanInput.length);
|
||||||
|
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
@ -215,7 +269,12 @@
|
|||||||
return correctWords;
|
return correctWords;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
|
/**
|
||||||
|
* Erstellt ein farbiges Badge-Element für die Ergebnisanzeige.
|
||||||
|
* @param {string} word - Das darzustellende Wort.
|
||||||
|
* @param {boolean} isCorrect - Bestimmt die Farbe (Grün für korrekt, Rot für falsch).
|
||||||
|
* @returns {HTMLElement} Das erstellte span-Element.
|
||||||
|
*/
|
||||||
function createWordBadge(word, isCorrect) {
|
function createWordBadge(word, isCorrect) {
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className =
|
badge.className =
|
||||||
@ -224,7 +283,11 @@
|
|||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
|
/**
|
||||||
|
* Visualisiert den Wortvergleich zwischen Original und Eingabe im Ergebnisbereich.
|
||||||
|
* @param {string} original - Der Originaltext.
|
||||||
|
* @param {string} input - Der eingegebene Text.
|
||||||
|
*/
|
||||||
function renderWordComparison(original, input) {
|
function renderWordComparison(original, input) {
|
||||||
if (!resultOriginal || !resultInput) return;
|
if (!resultOriginal || !resultInput) return;
|
||||||
|
|
||||||
@ -234,14 +297,14 @@
|
|||||||
resultOriginal.innerHTML = "";
|
resultOriginal.innerHTML = "";
|
||||||
resultInput.innerHTML = "";
|
resultInput.innerHTML = "";
|
||||||
|
|
||||||
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
|
// Originaltext-Badges: Rot markieren, wenn an dieser Position das Wort fehlt oder falsch eingegeben wurde
|
||||||
originalWords.forEach((word, index) => {
|
originalWords.forEach((word, index) => {
|
||||||
const isCorrect =
|
const isCorrect =
|
||||||
normalizeWord(word) === normalizeWord(inputWords[index] || "");
|
normalizeWord(word) === normalizeWord(inputWords[index] || "");
|
||||||
resultOriginal.appendChild(createWordBadge(word, isCorrect));
|
resultOriginal.appendChild(createWordBadge(word, isCorrect));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
|
// Eingegebene Badges: Rot markieren, wenn das Wort an dieser Stelle nicht zum Original passt
|
||||||
inputWords.forEach((word, index) => {
|
inputWords.forEach((word, index) => {
|
||||||
const isCorrect =
|
const isCorrect =
|
||||||
normalizeWord(word) === normalizeWord(originalWords[index] || "");
|
normalizeWord(word) === normalizeWord(originalWords[index] || "");
|
||||||
@ -249,6 +312,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt die aktuellen Authentifizierungsdaten.
|
||||||
|
*/
|
||||||
function getAuth() {
|
function getAuth() {
|
||||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||||
return null;
|
return null;
|
||||||
@ -257,6 +323,9 @@
|
|||||||
return window.AppAuth.getAuth();
|
return window.AppAuth.getAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des ScoreService.
|
||||||
|
*/
|
||||||
function getScoreService() {
|
function getScoreService() {
|
||||||
if (!window.config || !window.ScoreService) {
|
if (!window.config || !window.ScoreService) {
|
||||||
return null;
|
return null;
|
||||||
@ -265,6 +334,9 @@
|
|||||||
return new window.ScoreService(window.config);
|
return new window.ScoreService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des ChallengeService.
|
||||||
|
*/
|
||||||
function getChallengeService() {
|
function getChallengeService() {
|
||||||
if (!window.config || !window.ChallengeService) {
|
if (!window.config || !window.ChallengeService) {
|
||||||
return null;
|
return null;
|
||||||
@ -273,6 +345,9 @@
|
|||||||
return new window.ChallengeService(window.config);
|
return new window.ChallengeService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt eine Instanz des MessageService.
|
||||||
|
*/
|
||||||
function getMessageService() {
|
function getMessageService() {
|
||||||
if (!window.config || !window.MessageService) {
|
if (!window.config || !window.MessageService) {
|
||||||
return null;
|
return null;
|
||||||
@ -281,9 +356,13 @@
|
|||||||
return new window.MessageService(window.config);
|
return new window.MessageService(window.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest eine im SessionStorage hinterlegte aktive Challenge aus.
|
||||||
|
* @returns {Object|null} Challenge-Datenobjekt oder null.
|
||||||
|
*/
|
||||||
function readActiveChallenge() {
|
function readActiveChallenge() {
|
||||||
// Challenge-Runden werden aus der Nachrichten-Seite gestartet.
|
// Challenge-Runden werden auf der Nachrichten-Seite gestartet.
|
||||||
// Der dafuer gespeicherte Kontext entscheidet, welcher API-/Nachrichten-Flow nach dem Spiel laeuft.
|
// Der gespeicherte Kontext entscheidet über den API-Fluss nach Spielende.
|
||||||
const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null;
|
return null;
|
||||||
@ -301,44 +380,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob aktuell eine aktive Herausforderung vorliegt.
|
||||||
|
*/
|
||||||
function hasActiveChallenge() {
|
function hasActiveChallenge() {
|
||||||
return Boolean(activeChallenge && activeChallenge.id !== undefined && activeChallenge.id !== null);
|
return Boolean(activeChallenge && activeChallenge.id !== undefined && activeChallenge.id !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob es sich um die zweite Runde einer Challenge handelt.
|
||||||
|
* In der zweiten Runde spielt der Herausforderer gegen den Score des Gegners.
|
||||||
|
*/
|
||||||
function isChallengeSecondRound() {
|
function isChallengeSecondRound() {
|
||||||
// In der zweiten Runde spielt der Herausforderer gegen den bereits bekannten Gegner-Score.
|
|
||||||
return hasActiveChallenge()
|
return hasActiveChallenge()
|
||||||
&& activeChallenge.role === "challenger"
|
&& activeChallenge.role === "challenger"
|
||||||
&& activeChallenge.opponentScore !== null
|
&& activeChallenge.opponentScore !== null
|
||||||
&& activeChallenge.opponentScore !== undefined;
|
&& activeChallenge.opponentScore !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob es sich um die erste Runde einer Challenge handelt.
|
||||||
|
* In der ersten Runde spielt der herausgeforderte User und sendet danach sein Ergebnis.
|
||||||
|
*/
|
||||||
function isChallengeFirstRound() {
|
function isChallengeFirstRound() {
|
||||||
// In der ersten Runde spielt der herausgeforderte User und sendet sein Resultat an den Herausforderer.
|
|
||||||
return hasActiveChallenge() && activeChallenge.role === "opponent";
|
return hasActiveChallenge() && activeChallenge.role === "opponent";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schreibt den aktuellen Challenge-Kontext in den SessionStorage.
|
||||||
|
*/
|
||||||
function writeActiveChallenge(challenge) {
|
function writeActiveChallenge(challenge) {
|
||||||
sessionStorage.setItem(ACTIVE_CHALLENGE_STORAGE_KEY, JSON.stringify(challenge));
|
sessionStorage.setItem(ACTIVE_CHALLENGE_STORAGE_KEY, JSON.stringify(challenge));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Text für die Spielrunde.
|
||||||
|
* Bei Challenges müssen beide Spieler denselben Text erhalten, welcher aus dem Kontext ausgelesen wird.
|
||||||
|
* @returns {string} Der Rundentext.
|
||||||
|
*/
|
||||||
function getRoundText() {
|
function getRoundText() {
|
||||||
// Bei Challenges spielen beide User mit demselben Text aus dem Challenge-Kontext.
|
|
||||||
// Falls alte Challenges diesen Text noch nicht haben, gibt es weiterhin einen lokalen Fallback.
|
|
||||||
if (!activeChallenge) {
|
if (!activeChallenge) {
|
||||||
return generateGameText();
|
return generateGameText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wenn ein fester Challenge-Text existiert, wird dieser verwendet
|
||||||
if (typeof activeChallenge.challengeText === "string" && activeChallenge.challengeText.trim()) {
|
if (typeof activeChallenge.challengeText === "string" && activeChallenge.challengeText.trim()) {
|
||||||
return activeChallenge.challengeText;
|
return activeChallenge.challengeText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls bei einer alten Challenge kein Text hinterlegt war, generieren wir einen und speichern ihn im Kontext
|
||||||
const generatedText = generateGameText();
|
const generatedText = generateGameText();
|
||||||
activeChallenge.challengeText = generatedText;
|
activeChallenge.challengeText = generatedText;
|
||||||
writeActiveChallenge(activeChallenge);
|
writeActiveChallenge(activeChallenge);
|
||||||
return generatedText;
|
return generatedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Feedback zum Speicherstatus des Scores (z. B. "Score erfolgreich gespeichert").
|
||||||
|
* @param {string} message - Der Feedbacktext.
|
||||||
|
* @param {string} type - Der Bootstrap-Alert-Typ (danger, warning, success, info).
|
||||||
|
*/
|
||||||
function showScoreSaveFeedback(message, type) {
|
function showScoreSaveFeedback(message, type) {
|
||||||
if (!scoreSaveFeedback) return;
|
if (!scoreSaveFeedback) return;
|
||||||
|
|
||||||
@ -347,9 +448,11 @@
|
|||||||
scoreSaveFeedback.classList.remove("d-none");
|
scoreSaveFeedback.classList.remove("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert das Ergebnis eines normalen Spiels im Backend.
|
||||||
|
* @param {Object} scoreData - Das Score-Datenobjekt.
|
||||||
|
*/
|
||||||
async function saveScore(scoreData) {
|
async function saveScore(scoreData) {
|
||||||
// Normale Spielrunden werden direkt im Score-/Leaderboard-Backend gespeichert.
|
|
||||||
// Challenge-Runden verwenden je nach Rolle einen separaten Ablauf weiter unten.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
if (!auth || !auth.username || !auth.password) {
|
if (!auth || !auth.username || !auth.password) {
|
||||||
showScoreSaveFeedback(
|
showScoreSaveFeedback(
|
||||||
@ -359,7 +462,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
|
|
||||||
const scoreService = getScoreService();
|
const scoreService = getScoreService();
|
||||||
if (!scoreService) {
|
if (!scoreService) {
|
||||||
showScoreSaveFeedback(
|
showScoreSaveFeedback(
|
||||||
@ -406,9 +508,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt das Endergebnis eines Duells direkt im Browser an (Sieg, Niederlage, Unentschieden).
|
||||||
|
* Wird nach der finalen Runde einer Challenge ausgeführt.
|
||||||
|
* @param {number} score - Die eigene Punktzahl.
|
||||||
|
*/
|
||||||
function renderChallengeResult(score) {
|
function renderChallengeResult(score) {
|
||||||
// Zeigt das lokale Ergebnis der finalen Challenge-Runde sofort an,
|
|
||||||
// unabhaengig davon, ob der Backend-Abschluss erfolgreich ist.
|
|
||||||
if (!scoreSaveFeedback || !isChallengeSecondRound()) {
|
if (!scoreSaveFeedback || !isChallengeSecondRound()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -421,12 +526,15 @@
|
|||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const ownName = auth?.username || "Du";
|
const ownName = auth?.username || "Du";
|
||||||
const opponentName = activeChallenge.opponent;
|
const opponentName = activeChallenge.opponent;
|
||||||
|
|
||||||
|
// Sieger ermitteln
|
||||||
const result = score > opponentScore
|
const result = score > opponentScore
|
||||||
? "winner"
|
? "winner"
|
||||||
: score < opponentScore
|
: score < opponentScore
|
||||||
? "loser"
|
? "loser"
|
||||||
: "draw";
|
: "draw";
|
||||||
|
|
||||||
|
// Texte und Bilder je nach Ausgang des Spiels konfigurieren
|
||||||
const headline = result === "winner"
|
const headline = result === "winner"
|
||||||
? "Du gewinnst die Challenge"
|
? "Du gewinnst die Challenge"
|
||||||
: result === "loser"
|
: result === "loser"
|
||||||
@ -443,6 +551,7 @@
|
|||||||
? "image/verloren.png"
|
? "image/verloren.png"
|
||||||
: "image/unentschieden.png";
|
: "image/unentschieden.png";
|
||||||
|
|
||||||
|
// Ergebnisgrafik zusammenbauen
|
||||||
const graphic = document.createElement("div");
|
const graphic = document.createElement("div");
|
||||||
graphic.className = "play-challenge-result play-challenge-result-" + result;
|
graphic.className = "play-challenge-result play-challenge-result-" + result;
|
||||||
|
|
||||||
@ -479,12 +588,18 @@
|
|||||||
|
|
||||||
scores.append(ownScore, otherScore);
|
scores.append(ownScore, otherScore);
|
||||||
graphic.append(outcomeHeader, title, scores);
|
graphic.append(outcomeHeader, title, scores);
|
||||||
|
|
||||||
|
// Grafik unter dem Speicher-Feedback einfügen
|
||||||
scoreSaveFeedback.insertAdjacentElement("afterend", graphic);
|
scoreSaveFeedback.insertAdjacentElement("afterend", graphic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet den Abschluss einer Challenge an das dafür vorgesehene Challenge-Backend.
|
||||||
|
* Falls dieser Endpoint nicht existiert (altes Backend), liefert die Funktion false.
|
||||||
|
* @param {Object} scoreData - Das Score-Datenobjekt.
|
||||||
|
* @returns {Promise<boolean>} True, falls das Speichern im Challenge-Backend erfolgreich war.
|
||||||
|
*/
|
||||||
async function completeChallenge(scoreData) {
|
async function completeChallenge(scoreData) {
|
||||||
// Primaerer Abschluss ueber das Challenge-Backend.
|
|
||||||
// Wenn dieser Endpoint ablehnt, faellt submitScore auf den Nachrichten-Fallback zurueck.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
if (!auth || !hasActiveChallenge()) {
|
if (!auth || !hasActiveChallenge()) {
|
||||||
return;
|
return;
|
||||||
@ -510,10 +625,11 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
console.warn("Challenge-Abschluss nicht moeglich, nutze Nachrichten-Fallback. Status:", result.status);
|
console.warn("Challenge-Abschluss über API nicht möglich, nutze Nachrichten-Fallback. Status:", result.status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Challenge erfolgreich abgeschlossen
|
||||||
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
||||||
activeChallenge = null;
|
activeChallenge = null;
|
||||||
showScoreSaveFeedback(
|
showScoreSaveFeedback(
|
||||||
@ -528,9 +644,15 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ablauf für die ERSTE Runde einer Challenge (der Geforderte hat gespielt).
|
||||||
|
* Der Score wird normal gespeichert und der Herausforderer wird benachrichtigt.
|
||||||
|
* @param {Object} scoreData - Die Score-Daten des Geforderten.
|
||||||
|
*/
|
||||||
async function finishFirstChallengeRound(scoreData) {
|
async function finishFirstChallengeRound(scoreData) {
|
||||||
// Der Gegner speichert zuerst seinen Score und informiert danach den Herausforderer.
|
// Score im normalen Ranking ablegen
|
||||||
await saveScore(scoreData);
|
await saveScore(scoreData);
|
||||||
|
// Dem Herausforderer eine Nachricht schicken mit den erreichten Punkten
|
||||||
await notifyChallenger(scoreData);
|
await notifyChallenger(scoreData);
|
||||||
|
|
||||||
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
|
||||||
@ -542,14 +664,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benachrichtigt den Herausforderer per Inbox-Nachricht über das eigene Ergebnis.
|
||||||
|
* Da der Challenge-Ablauf über Nachrichten läuft, betten wir die Spieldaten
|
||||||
|
* als JSON-String in die Textnachricht ein.
|
||||||
|
* @param {Object} scoreData - Die erzielten Spieldaten.
|
||||||
|
*/
|
||||||
async function notifyChallenger(scoreData) {
|
async function notifyChallenger(scoreData) {
|
||||||
// Nutzt den bestehenden Nachrichten-Endpunkt, um dem Herausforderer den Gegner-Score zu senden.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
if (!auth || !messageService || !activeChallenge.challenger) {
|
if (!auth || !messageService || !activeChallenge.challenger) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zu übermittelnde Spieldaten strukturieren
|
||||||
const challengeData = {
|
const challengeData = {
|
||||||
id: activeChallenge.id,
|
id: activeChallenge.id,
|
||||||
challenger: activeChallenge.challenger,
|
challenger: activeChallenge.challenger,
|
||||||
@ -558,6 +686,7 @@
|
|||||||
challengeText: scoreData.text,
|
challengeText: scoreData.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Betten das JSON verschlüsselt in das Nachrichtenfeld ein
|
||||||
const messageText =
|
const messageText =
|
||||||
CHALLENGE_DATA_PREFIX +
|
CHALLENGE_DATA_PREFIX +
|
||||||
JSON.stringify(challengeData) +
|
JSON.stringify(challengeData) +
|
||||||
@ -584,6 +713,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den Namen des Siegers auf Basis der Punktzahlen.
|
||||||
|
* @param {number} challengerScore - Punkte des Herausforderers.
|
||||||
|
* @param {number} opponentScore - Punkte des Gegners.
|
||||||
|
* @returns {string} Der Gewinnername oder "draw" bei Gleichstand.
|
||||||
|
*/
|
||||||
function getChallengeWinner(challengerScore, opponentScore) {
|
function getChallengeWinner(challengerScore, opponentScore) {
|
||||||
if (challengerScore > opponentScore) {
|
if (challengerScore > opponentScore) {
|
||||||
return activeChallenge.challenger || getAuth()?.username || "Herausforderer";
|
return activeChallenge.challenger || getAuth()?.username || "Herausforderer";
|
||||||
@ -596,6 +731,13 @@
|
|||||||
return "draw";
|
return "draw";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion zum Senden der Abschlussnachricht (Challenge-Resultat) an den Gegner.
|
||||||
|
* @param {string} recipient - Der Empfänger der Nachricht.
|
||||||
|
* @param {Object} resultData - Das Resultatsobjekt.
|
||||||
|
* @param {string} text - Der Nachrichtentext.
|
||||||
|
* @returns {Promise<boolean>} True bei Erfolg.
|
||||||
|
*/
|
||||||
async function sendChallengeResultMessage(recipient, resultData, text) {
|
async function sendChallengeResultMessage(recipient, resultData, text) {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
const messageService = getMessageService();
|
const messageService = getMessageService();
|
||||||
@ -621,18 +763,25 @@
|
|||||||
return result.ok;
|
return result.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback-Ablauf für die ZWEITE Runde (Herausforderer beendet das Spiel),
|
||||||
|
* falls die direkte Complete-API des Backends fehlschlägt.
|
||||||
|
* Das Ergebnis wird berechnet und per Nachricht an den Gegner gesendet.
|
||||||
|
* @param {Object} scoreData - Die Spieldaten des Herausforderers.
|
||||||
|
*/
|
||||||
async function finishFinalChallengeWithMessages(scoreData) {
|
async function finishFinalChallengeWithMessages(scoreData) {
|
||||||
// Fallback fuer den finalen Challenge-Abschluss:
|
|
||||||
// Score speichern, Gewinner lokal berechnen und Ergebnis per Nachricht an den Gegner senden.
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
if (!auth || !activeChallenge) {
|
if (!auth || !activeChallenge) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eigenen Score speichern
|
||||||
await saveScore(scoreData);
|
await saveScore(scoreData);
|
||||||
|
|
||||||
const opponentScore = Number(activeChallenge.opponentScore);
|
const opponentScore = Number(activeChallenge.opponentScore);
|
||||||
const winner = getChallengeWinner(scoreData.score, opponentScore);
|
const winner = getChallengeWinner(scoreData.score, opponentScore);
|
||||||
|
|
||||||
|
// Resultat-JSON zusammenbauen
|
||||||
const resultData = {
|
const resultData = {
|
||||||
id: activeChallenge.id,
|
id: activeChallenge.id,
|
||||||
challenger: activeChallenge.challenger || auth.username,
|
challenger: activeChallenge.challenger || auth.username,
|
||||||
@ -659,6 +808,7 @@
|
|||||||
winnerText +
|
winnerText +
|
||||||
".";
|
".";
|
||||||
|
|
||||||
|
// Das Ergebnis per Message an den Gegner übermitteln
|
||||||
const opponentMessageSent = await sendChallengeResultMessage(
|
const opponentMessageSent = await sendChallengeResultMessage(
|
||||||
activeChallenge.opponent,
|
activeChallenge.opponent,
|
||||||
resultData,
|
resultData,
|
||||||
@ -686,9 +836,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beendet die Spielrunde, wertet die Eingabe aus, rendert die Fehler
|
||||||
|
* und leitet die Speicherung je nach Spielmodus ein.
|
||||||
|
*/
|
||||||
async function submitScore() {
|
async function submitScore() {
|
||||||
// Gemeinsamer Abschluss fuer normale Spiele und Challenge-Runden.
|
|
||||||
// Der aktive Challenge-Kontext entscheidet, welcher Speicher-/Nachrichtenfluss verwendet wird.
|
|
||||||
if (!userTextInput) return;
|
if (!userTextInput) return;
|
||||||
|
|
||||||
const userInput = userTextInput.value.trim();
|
const userInput = userTextInput.value.trim();
|
||||||
@ -697,14 +849,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button sperren gegen Doppelklicks
|
||||||
if (btnSubmitScore) {
|
if (btnSubmitScore) {
|
||||||
btnSubmitScore.disabled = true;
|
btnSubmitScore.disabled = true;
|
||||||
btnSubmitScore.textContent = "Wird ausgewertet...";
|
btnSubmitScore.textContent = "Wird ausgewertet...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Punktzahl berechnen
|
||||||
const score = calculateScore(currentGameText, userInput);
|
const score = calculateScore(currentGameText, userInput);
|
||||||
|
|
||||||
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
|
// Eingabebereich ausblenden und Ergebnisbereich einblenden
|
||||||
if (phaseInput) phaseInput.classList.add("d-none");
|
if (phaseInput) phaseInput.classList.add("d-none");
|
||||||
if (phaseResult) phaseResult.classList.remove("d-none");
|
if (phaseResult) phaseResult.classList.remove("d-none");
|
||||||
|
|
||||||
@ -715,15 +869,16 @@
|
|||||||
gameStatus.style.color = "#fff";
|
gameStatus.style.color = "#fff";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ergebnis anzeigen
|
||||||
if (resultScore) resultScore.textContent = score;
|
if (resultScore) resultScore.textContent = score;
|
||||||
renderWordComparison(currentGameText, userInput);
|
renderWordComparison(currentGameText, userInput);
|
||||||
renderChallengeResult(score);
|
renderChallengeResult(score);
|
||||||
|
|
||||||
|
// Dauer der Eingabe ermitteln (mindestens 1 Sekunde)
|
||||||
const inputDurationSeconds = inputStartMs
|
const inputDurationSeconds = inputStartMs
|
||||||
? Math.max(1, Math.round((Date.now() - inputStartMs) / 1000))
|
? Math.max(1, Math.round((Date.now() - inputStartMs) / 1000))
|
||||||
: MEMORIZE_TIME_SECONDS;
|
: MEMORIZE_TIME_SECONDS;
|
||||||
|
|
||||||
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
|
|
||||||
const scoreData = {
|
const scoreData = {
|
||||||
score: score,
|
score: score,
|
||||||
time: inputDurationSeconds,
|
time: inputDurationSeconds,
|
||||||
@ -733,21 +888,26 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isChallengeFirstRound()) {
|
if (isChallengeFirstRound()) {
|
||||||
|
// Erste Runde des Duells
|
||||||
showScoreSaveFeedback("Resultat wird an den Herausforderer gesendet...", "info");
|
showScoreSaveFeedback("Resultat wird an den Herausforderer gesendet...", "info");
|
||||||
await finishFirstChallengeRound(scoreData);
|
await finishFirstChallengeRound(scoreData);
|
||||||
} else if (hasActiveChallenge()) {
|
} else if (hasActiveChallenge()) {
|
||||||
|
// Finale Runde des Duells
|
||||||
showScoreSaveFeedback("Challenge-Resultat wird gesendet...", "info");
|
showScoreSaveFeedback("Challenge-Resultat wird gesendet...", "info");
|
||||||
let completed = false;
|
let completed = false;
|
||||||
try {
|
try {
|
||||||
|
// Versuchen über die offizielle API abzuschließen
|
||||||
completed = await completeChallenge(scoreData);
|
completed = await completeChallenge(scoreData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Challenge-Abschluss fehlgeschlagen, nutze Nachrichten-Fallback.", error);
|
console.warn("Challenge-Abschluss fehlgeschlagen, nutze Nachrichten-Fallback.", error);
|
||||||
}
|
}
|
||||||
if (!completed) {
|
if (!completed) {
|
||||||
|
// Falls API fehlschlägt, auf den Nachrichten-Ablauf ausweichen
|
||||||
showScoreSaveFeedback("Backend-Abschluss nicht moeglich. Ergebnis wird ueber Nachrichten gesendet...", "info");
|
showScoreSaveFeedback("Backend-Abschluss nicht moeglich. Ergebnis wird ueber Nachrichten gesendet...", "info");
|
||||||
await finishFinalChallengeWithMessages(scoreData);
|
await finishFinalChallengeWithMessages(scoreData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Normales Spiel ohne Duell
|
||||||
await saveScore(scoreData);
|
await saveScore(scoreData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -764,12 +924,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Initialisierungsfunktion, aufgerufen von navigation.js
|
||||||
|
* nach dem Laden von play.html.
|
||||||
|
*/
|
||||||
window.initPlayPage = function initPlayPage() {
|
window.initPlayPage = function initPlayPage() {
|
||||||
// Die Spielseite wird dynamisch geladen; daher werden DOM-Elemente und Events hier initialisiert.
|
clearInterval(timerInterval); // Bestehende Timer vorsichtshalber stoppen
|
||||||
clearInterval(timerInterval);
|
activeChallenge = readActiveChallenge(); // Prüfen, ob wir aus einer Challenge gestartet wurden
|
||||||
activeChallenge = readActiveChallenge();
|
|
||||||
|
|
||||||
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
|
// DOM-Referenzen holen
|
||||||
phaseStart = document.getElementById("phaseStart");
|
phaseStart = document.getElementById("phaseStart");
|
||||||
phaseMemorize = document.getElementById("phaseMemorize");
|
phaseMemorize = document.getElementById("phaseMemorize");
|
||||||
phaseInput = document.getElementById("phaseInput");
|
phaseInput = document.getElementById("phaseInput");
|
||||||
@ -789,6 +952,7 @@
|
|||||||
const btnRestart = document.getElementById("btnRestart");
|
const btnRestart = document.getElementById("btnRestart");
|
||||||
const btnLeaderboard = document.getElementById("btnLeaderboard");
|
const btnLeaderboard = document.getElementById("btnLeaderboard");
|
||||||
|
|
||||||
|
// Falls eine Challenge aktiv ist, Hinweistext über den Gegner und dessen Score einblenden
|
||||||
const challengeHint = document.getElementById("challengeHint");
|
const challengeHint = document.getElementById("challengeHint");
|
||||||
if (challengeHint && activeChallenge) {
|
if (challengeHint && activeChallenge) {
|
||||||
const opponentScore = activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined
|
const opponentScore = activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined
|
||||||
@ -801,6 +965,7 @@
|
|||||||
challengeHint.classList.remove("d-none");
|
challengeHint.classList.remove("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event-Listener binden
|
||||||
if (btnStart) btnStart.addEventListener("click", startGame);
|
if (btnStart) btnStart.addEventListener("click", startGame);
|
||||||
if (btnSubmitScore) btnSubmitScore.addEventListener("click", submitScore);
|
if (btnSubmitScore) btnSubmitScore.addEventListener("click", submitScore);
|
||||||
if (btnRestart)
|
if (btnRestart)
|
||||||
@ -817,7 +982,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Anti-Cheating Maßnahmen im Eingabebereich einrichten
|
||||||
if (userTextInput) {
|
if (userTextInput) {
|
||||||
|
// Kopieren, Einfügen, Ausschneiden und Tastenkombinationen sperren
|
||||||
userTextInput.addEventListener("keydown", (e) => {
|
userTextInput.addEventListener("keydown", (e) => {
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
if (
|
if (
|
||||||
@ -832,6 +999,8 @@
|
|||||||
userTextInput.addEventListener("paste", (e) => e.preventDefault());
|
userTextInput.addEventListener("paste", (e) => e.preventDefault());
|
||||||
userTextInput.addEventListener("copy", (e) => e.preventDefault());
|
userTextInput.addEventListener("copy", (e) => e.preventDefault());
|
||||||
userTextInput.addEventListener("cut", (e) => e.preventDefault());
|
userTextInput.addEventListener("cut", (e) => e.preventDefault());
|
||||||
|
|
||||||
|
// Sobald getippt wird, startet die Zeitmessung (falls nicht schon geschehen)
|
||||||
userTextInput.addEventListener("input", () => {
|
userTextInput.addEventListener("input", () => {
|
||||||
if (!inputStartMs) {
|
if (!inputStartMs) {
|
||||||
inputStartMs = Date.now();
|
inputStartMs = Date.now();
|
||||||
@ -839,6 +1008,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Textauswahl auf dem zu merkenden Text verhindern, um Kopieren via Maus zu blockieren
|
||||||
if (targetTextDisplay) {
|
if (targetTextDisplay) {
|
||||||
targetTextDisplay.addEventListener("copy", (e) => e.preventDefault());
|
targetTextDisplay.addEventListener("copy", (e) => e.preventDefault());
|
||||||
targetTextDisplay.style.userSelect = "none";
|
targetTextDisplay.style.userSelect = "none";
|
||||||
|
|||||||
361
js/scores.js
361
js/scores.js
@ -1,151 +1,210 @@
|
|||||||
(function () {
|
/**
|
||||||
function formatTime(seconds) {
|
* Eigene Ergebnisse (Scores) verwalten und anzeigen.
|
||||||
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
|
* Dieses Modul lädt nach der Initialisierung die bisherigen Spielergebnisse des
|
||||||
return "-";
|
* eingeloggten Benutzers und stellt diese sortiert in einer Tabelle dar.
|
||||||
}
|
*/
|
||||||
|
(function () {
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
/**
|
||||||
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
* Formatiert Sekunden in ein lesbares MM:SS Format (z.B. 75 Sekunden -> "1:15").
|
||||||
}
|
* @param {number} seconds - Anzahl Sekunden.
|
||||||
|
* @returns {string} Formatiertes Zeit-String oder "-" bei ungültigem Wert.
|
||||||
function normalizeUsername(username) {
|
*/
|
||||||
return String(username ?? "").trim().toLowerCase();
|
function formatTime(seconds) {
|
||||||
}
|
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
|
||||||
|
return "-";
|
||||||
function getLoggedInAuth() {
|
}
|
||||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
|
||||||
return null;
|
const minutes = Math.floor(seconds / 60);
|
||||||
}
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
// padStart stellt sicher, dass Sekunden immer zweistellig ausgegeben werden (z.B. "05" statt "5")
|
||||||
const auth = window.AppAuth.getAuth();
|
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
||||||
if (!auth || !auth.username || !auth.password) {
|
}
|
||||||
return null;
|
|
||||||
}
|
/**
|
||||||
|
* Normalisiert einen Benutzernamen (Trimming und Kleinschreibung) für robuste Vergleiche.
|
||||||
return auth;
|
* @param {string} username - Der Benutzername.
|
||||||
}
|
* @returns {string} Der bereinigte Benutzername.
|
||||||
|
*/
|
||||||
function getScoreService() {
|
function normalizeUsername(username) {
|
||||||
if (!window.config || !window.ScoreService) {
|
return String(username ?? "").trim().toLowerCase();
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
return new window.ScoreService(window.config);
|
* Holt die aktuellen Authentifizierungsdaten aus dem globalen Auth-Modul.
|
||||||
}
|
* @returns {Object|null} Auth-Objekt (mit username/password) oder null, falls nicht angemeldet.
|
||||||
|
*/
|
||||||
function getDisplayedRank(entry, index) {
|
function getLoggedInAuth() {
|
||||||
const place = Number(entry?.place);
|
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||||
if (!Number.isNaN(place) && place > 0) {
|
return null;
|
||||||
return place;
|
}
|
||||||
}
|
|
||||||
|
const auth = window.AppAuth.getAuth();
|
||||||
return index + 1;
|
if (!auth || !auth.username || !auth.password) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
function setFeedback(message, type) {
|
|
||||||
const feedback = document.getElementById("scores-feedback");
|
return auth;
|
||||||
if (!feedback) {
|
}
|
||||||
return;
|
|
||||||
}
|
/**
|
||||||
|
* Initialisiert und liefert eine Instanz des ScoreService.
|
||||||
feedback.className = "alert alert-" + type + " mb-4";
|
* @returns {ScoreService|null} Eine ScoreService-Instanz oder null, falls Konfiguration fehlt.
|
||||||
feedback.textContent = message;
|
*/
|
||||||
feedback.classList.remove("d-none");
|
function getScoreService() {
|
||||||
}
|
if (!window.config || !window.ScoreService) {
|
||||||
|
return null;
|
||||||
function renderScores(entries) {
|
}
|
||||||
const tableBody = document.getElementById("scores-body");
|
|
||||||
if (!tableBody) {
|
return new window.ScoreService(window.config);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
tableBody.innerHTML = "";
|
* Ermittelt den anzuzeigenden Rang eines Score-Eintrags.
|
||||||
|
* Falls das Backend keinen Platz/Rang liefert, wird die Position in der Liste (+1) als Fallback genutzt.
|
||||||
if (!Array.isArray(entries) || entries.length === 0) {
|
* @param {Object} entry - Der Score-Eintrag.
|
||||||
tableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4">Keine eigenen Scores gefunden.</td></tr>';
|
* @param {number} index - Der Listenindex des Eintrags.
|
||||||
return;
|
* @returns {number} Der anzuzeigende Rang (1-basiert).
|
||||||
}
|
*/
|
||||||
|
function getDisplayedRank(entry, index) {
|
||||||
const sortedEntries = entries.slice().sort((a, b) => {
|
const place = Number(entry?.place);
|
||||||
const placeA = Number(a?.place);
|
if (!Number.isNaN(place) && place > 0) {
|
||||||
const placeB = Number(b?.place);
|
return place;
|
||||||
if (!Number.isNaN(placeA) && !Number.isNaN(placeB) && placeA !== placeB) {
|
}
|
||||||
return placeA - placeB;
|
|
||||||
}
|
return index + 1;
|
||||||
|
}
|
||||||
const scoreA = Number(a?.score ?? 0);
|
|
||||||
const scoreB = Number(b?.score ?? 0);
|
/**
|
||||||
if (scoreB !== scoreA) {
|
* Blendet eine Feedback-Nachmeldung (Erfolg/Fehler/Info) auf der Oberfläche ein.
|
||||||
return scoreB - scoreA;
|
* @param {string} message - Der anzuzeigende Text.
|
||||||
}
|
* @param {string} type - Der Bootstrap-Alert-Typ (danger, warning, info, success).
|
||||||
|
*/
|
||||||
const timeA = Number(a?.time ?? Number.MAX_SAFE_INTEGER);
|
function setFeedback(message, type) {
|
||||||
const timeB = Number(b?.time ?? Number.MAX_SAFE_INTEGER);
|
const feedback = document.getElementById("scores-feedback");
|
||||||
return timeA - timeB;
|
if (!feedback) {
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
sortedEntries.forEach((entry, index) => {
|
|
||||||
const row = document.createElement("tr");
|
feedback.className = "alert alert-" + type + " mb-4";
|
||||||
row.innerHTML = `
|
feedback.textContent = message;
|
||||||
<td>${getDisplayedRank(entry, index)}</td>
|
feedback.classList.remove("d-none");
|
||||||
<td>${entry.username ?? "-"}</td>
|
}
|
||||||
<td>${formatTime(entry.time)} min</td>
|
|
||||||
<td>${entry.score ?? "-"}</td>
|
/**
|
||||||
`;
|
* Generiert die HTML-Zeilen für die Score-Tabelle aus den geladenen Einträgen.
|
||||||
tableBody.appendChild(row);
|
* Sortiert die Einträge: 1. nach Platzierung (falls vorhanden), 2. nach Score (absteigend), 3. nach Zeit (aufsteigend).
|
||||||
});
|
* @param {Array<Object>} entries - Liste von Score-Einträgen vom Backend.
|
||||||
}
|
*/
|
||||||
|
function renderScores(entries) {
|
||||||
async function loadMyScores() {
|
const tableBody = document.getElementById("scores-body");
|
||||||
const auth = getLoggedInAuth();
|
if (!tableBody) {
|
||||||
if (!auth || !auth.username) {
|
return;
|
||||||
setFeedback("Bitte logge dich ein, um deine Scores zu sehen.", "warning");
|
}
|
||||||
renderScores([]);
|
|
||||||
return;
|
tableBody.innerHTML = "";
|
||||||
}
|
|
||||||
|
// Falls keine Einträge vorhanden sind, Hinweistext ausgeben
|
||||||
const scoreService = getScoreService();
|
if (!Array.isArray(entries) || entries.length === 0) {
|
||||||
if (!scoreService) {
|
tableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4">Keine eigenen Scores gefunden.</td></tr>';
|
||||||
setFeedback("Score-Service konnte nicht geladen werden.", "danger");
|
return;
|
||||||
renderScores([]);
|
}
|
||||||
return;
|
|
||||||
}
|
// Kopie des Arrays anlegen und sortieren
|
||||||
|
const sortedEntries = entries.slice().sort((a, b) => {
|
||||||
try {
|
// 1. Priorität: Offizielle Platzierung (falls vorhanden)
|
||||||
const result = await scoreService.getScoreByName(auth.username);
|
const placeA = Number(a?.place);
|
||||||
|
const placeB = Number(b?.place);
|
||||||
if (!result.ok || !Array.isArray(result.body)) {
|
if (!Number.isNaN(placeA) && !Number.isNaN(placeB) && placeA !== placeB) {
|
||||||
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
return placeA - placeB;
|
||||||
renderScores([]);
|
}
|
||||||
return;
|
|
||||||
}
|
// 2. Priorität: Höhere Punktzahl gewinnt
|
||||||
|
const scoreA = Number(a?.score ?? 0);
|
||||||
const ownScores = result.body.filter((entry) => {
|
const scoreB = Number(b?.score ?? 0);
|
||||||
if (!entry || !entry.username) {
|
if (scoreB !== scoreA) {
|
||||||
return false;
|
return scoreB - scoreA;
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeUsername(entry.username) === normalizeUsername(auth.username);
|
// 3. Priorität: Bei Punktgleichheit gewinnt die schnellere Zeit
|
||||||
});
|
const timeA = Number(a?.time ?? Number.MAX_SAFE_INTEGER);
|
||||||
|
const timeB = Number(b?.time ?? Number.MAX_SAFE_INTEGER);
|
||||||
if (ownScores.length === 0) {
|
return timeA - timeB;
|
||||||
setFeedback("Für deinen Account wurden noch keine Scores gefunden.", "info");
|
});
|
||||||
}
|
|
||||||
|
// Für jeden sortierten Eintrag eine Tabellenzeile erstellen und anhängen
|
||||||
renderScores(ownScores);
|
sortedEntries.forEach((entry, index) => {
|
||||||
} catch (error) {
|
const row = document.createElement("tr");
|
||||||
console.error("Fehler beim Laden der eigenen Scores:", error);
|
row.innerHTML = `
|
||||||
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
<td>${getDisplayedRank(entry, index)}</td>
|
||||||
renderScores([]);
|
<td>${entry.username ?? "-"}</td>
|
||||||
}
|
<td>${formatTime(entry.time)} min</td>
|
||||||
}
|
<td>${entry.score ?? "-"}</td>
|
||||||
|
`;
|
||||||
window.initScoresPage = function initScoresPage() {
|
tableBody.appendChild(row);
|
||||||
loadMyScores().catch((error) => {
|
});
|
||||||
console.error("Fehler beim Initialisieren der Scores-Seite:", error);
|
}
|
||||||
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
|
||||||
renderScores([]);
|
/**
|
||||||
});
|
* Fordert die Scores des Benutzers vom Backend an und filtert sie.
|
||||||
};
|
*/
|
||||||
})();
|
async function loadMyScores() {
|
||||||
|
const auth = getLoggedInAuth();
|
||||||
|
// Prüfen, ob der Benutzer überhaupt eingeloggt ist
|
||||||
|
if (!auth || !auth.username) {
|
||||||
|
setFeedback("Bitte logge dich ein, um deine Scores zu sehen.", "warning");
|
||||||
|
renderScores([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoreService = getScoreService();
|
||||||
|
if (!scoreService) {
|
||||||
|
setFeedback("Score-Service konnte nicht geladen werden.", "danger");
|
||||||
|
renderScores([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Scores für diesen Namen vom Backend abrufen
|
||||||
|
const result = await scoreService.getScoreByName(auth.username);
|
||||||
|
|
||||||
|
if (!result.ok || !Array.isArray(result.body)) {
|
||||||
|
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
||||||
|
renderScores([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicherheits-Filterung: Nur Scores behalten, die exakt zum aktuell eingeloggten User passen
|
||||||
|
const ownScores = result.body.filter((entry) => {
|
||||||
|
if (!entry || !entry.username) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeUsername(entry.username) === normalizeUsername(auth.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ownScores.length === 0) {
|
||||||
|
setFeedback("Für deinen Account wurden noch keine Scores gefunden.", "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scores rendern
|
||||||
|
renderScores(ownScores);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Laden der eigenen Scores:", error);
|
||||||
|
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
||||||
|
renderScores([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Initialisierungsfunktion, die von navigation.js aufgerufen wird,
|
||||||
|
* sobald die scores.html-Teilseite erfolgreich geladen wurde.
|
||||||
|
*/
|
||||||
|
window.initScoresPage = function initScoresPage() {
|
||||||
|
loadMyScores().catch((error) => {
|
||||||
|
console.error("Fehler beim Initialisieren der Scores-Seite:", error);
|
||||||
|
setFeedback("Deine Scores konnten nicht geladen werden.", "danger");
|
||||||
|
renderScores([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user