Add Message/Challenge-Logik

This commit is contained in:
DST81 2026-05-23 16:17:57 +02:00
parent 57ae0ccda5
commit c533198c38
12 changed files with 1308 additions and 7 deletions

View File

@ -32,6 +32,24 @@ body {
padding: 5px 10px;
}
.navbar .nav-link.has-unread-messages {
background-color: #dc3545;
color: #ffffff;
font-weight: bold;
border-radius: 6px;
padding: 5px 10px;
}
.navbar .nav-link.has-unread-messages::after {
content: "";
display: inline-block;
width: 8px;
height: 8px;
margin-left: 8px;
border-radius: 50%;
background: #ffffff;
}
/* Main content styles */
.container-dashboard {
flex-wrap: wrap;
@ -276,4 +294,175 @@ p {
.leaderboard-table tbody tr:last-child td {
border-bottom: none;
}
}
/* Messages */
.container > .messages-card:not(.modal),
.messages-card {
text-align: left;
font-size: 1rem;
}
.container > .messages-card:not(.modal):hover,
.messages-card:hover {
transform: none;
}
.messages-header,
.messages-inbox-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.messages-panel {
background: #f8f9fa;
border: 1px solid #dbe3ef;
border-radius: 8px;
padding: 18px;
}
.messages-panel h3 {
font-size: 1.1rem;
margin: 0 0 14px;
}
.messages-user-list {
display: grid;
gap: 10px;
}
.messages-user-button {
width: 100%;
border: 1px solid #c9d6e8;
border-radius: 6px;
background: #ffffff;
color: #1b1b2f;
padding: 10px 12px;
text-align: left;
transition: background-color 0.2s, border-color 0.2s;
}
.messages-user-button:hover {
background: #e8f0fb;
border-color: #4a6fa5;
}
.message-form textarea {
resize: vertical;
}
.message-list {
display: grid;
gap: 12px;
}
.message-item {
border-left: 4px solid #4a6fa5;
border-radius: 8px;
background: #ffffff;
padding: 12px 14px;
}
.message-item-unread {
border-left-color: #dc3545;
background: #fff4f4;
}
.message-meta {
display: flex;
justify-content: space-between;
gap: 1rem;
color: #1b1b2f;
font-size: 0.9rem;
margin-bottom: 8px;
}
.message-meta span,
.messages-empty {
color: #667085;
}
.message-item p,
.messages-empty {
margin: 0;
}
.challenge-result-graphic,
.play-challenge-result {
margin-top: 12px;
border-radius: 8px;
border: 1px solid #dbe3ef;
background: #ffffff;
overflow: hidden;
}
.challenge-result-headline,
.play-challenge-result-title {
padding: 10px 14px;
font-weight: 700;
color: #ffffff;
}
.challenge-result-win,
.play-challenge-result-winner .play-challenge-result-title {
background: #198754;
}
.challenge-result-draw,
.play-challenge-result-draw .play-challenge-result-title {
background: #4a6fa5;
}
.play-challenge-result-loser .play-challenge-result-title {
background: #a84848;
}
.challenge-result-scores,
.play-challenge-result-scores {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
background: #dbe3ef;
}
.challenge-result-score,
.play-challenge-result-score {
display: grid;
gap: 4px;
background: #ffffff;
padding: 12px;
}
.challenge-result-score span,
.play-challenge-result-score span {
color: #4a6fa5;
font-size: 1.25rem;
font-weight: 700;
}
.challenge-hint {
display: inline-block;
margin-top: 8px;
border-radius: 6px;
background: #fff4cc;
color: #1b1b2f;
padding: 6px 10px;
font-size: 0.95rem;
font-weight: 600;
}
@media (max-width: 768px) {
.messages-header,
.messages-inbox-title,
.message-meta {
align-items: flex-start;
flex-direction: column;
}
.challenge-result-scores,
.play-challenge-result-scores {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,66 @@
class ChallengeService {
constructor(config) {
this.baseUrl = config.API_BASE_URL;
this.urlTail = "challenges";
}
async postChallenge(username, password, opponent, text) {
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Username": username,
"X-Password": password,
},
body: JSON.stringify({
opponent: opponent,
text: text,
}),
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in postChallenge");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
async completeChallenge(username, password, challengeId, score, time, text, userWrittenText) {
const response = await fetch(`${this.baseUrl}${this.urlTail}/${challengeId}/complete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Username": username,
"X-Password": password,
},
body: JSON.stringify({
score: score,
time: time,
text: text,
userWrittenText: userWrittenText,
}),
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in completeChallenge");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
}
window.ChallengeService = ChallengeService;

View File

@ -0,0 +1,106 @@
class MessageService {
constructor(config) {
this.baseUrl = config.API_BASE_URL;
this.urlTail = "messages";
}
async getMessages(username, password) {
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
method: "GET",
headers: {
"X-Username": username,
"X-Password": password,
},
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in getMessages");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
async postMessage(username, password, recipient, type, text) {
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Username": username,
"X-Password": password,
},
body: JSON.stringify({
recipient: recipient,
type: type,
text: text,
}),
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in postMessage");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
async markMessageAsRead(username, password, messageId) {
const response = await fetch(`${this.baseUrl}${this.urlTail}/${messageId}/read`, {
method: "PATCH",
headers: {
"X-Username": username,
"X-Password": password,
},
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in markMessageAsRead");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
async markAllMessagesAsRead(username, password) {
const response = await fetch(`${this.baseUrl}${this.urlTail}/read`, {
method: "PATCH",
headers: {
"X-Username": username,
"X-Password": password,
},
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in markAllMessagesAsRead");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
}
window.MessageService = MessageService;

View File

@ -26,6 +26,28 @@ class UserService {
};
}
async getUsers(username, password) {
const response = await fetch(`${this.baseUrl}users`, {
method: "GET",
headers: {
"X-Username": username,
"X-Password": password,
},
});
let body = null;
try {
body = await response.json();
} catch {
console.error("Error in getUsers");
}
return {
status: response.status,
ok: response.ok,
body: body,
};
}
async deleteUser(username, password) {
const response = await fetch(`${this.baseUrl}${this.urlTail}`, {
method: "DELETE",

View File

@ -86,6 +86,42 @@ If a user with that name already exists, it should return a 400.
}
```
### GET all users
**Endpoint**
GET /users
**Description** Returns all existing users so the frontend can show a list of possible challenge recipients. Requires authentication.
**Authentication Headers (required)**
- X-Username
- X-Password
**Response (List\<UserListItemDTO\>)**
``` json
[
{
"username": "string"
}
]
```
Alternative response shape accepted by the frontend:
``` json
[
{
"name": "string"
}
]
```
**Status Codes**
- 200 if users were loaded successfully
- 401 if authentication failed
------------------------------------------------------------------------
## 2. Score
@ -193,5 +229,266 @@ The response should return the created object. It should give the place, the sco
]
```
------------------------------------------------------------------------
## 4. Messages / Challenges
Messages are used for user-to-user challenges. A challenge is just a message with `type: "challenge"`.
All message endpoints require authentication via the existing headers.
**Authentication Headers (required)**
- X-Username
- X-Password
### GET messages
**Endpoint**
GET /messages
**Description** Returns all messages relevant for the authenticated user. The response should include received messages and may also include sent messages, so the frontend can show a complete conversation history. The frontend uses this endpoint on page load and polls it regularly to turn the navbar item "Nachrichten" red when unread messages exist.
**Response (List\<MessageDTO\>)**
``` json
[
{
"id": 0,
"sender": "string",
"recipient": "string",
"type": "challenge",
"text": "string",
"read": false,
"createdAt": "2026-05-23T15:42:00",
"challenge": null
},
{
"id": 1,
"sender": "System",
"recipient": "string",
"type": "challenge-result",
"text": "Daniela gewinnt gegen Florin mit 18 zu 14 Punkten.",
"read": false,
"createdAt": "2026-05-23T15:48:00",
"challenge": {
"id": 5,
"challenger": "Florin",
"opponent": "Daniela",
"challengerScore": 14,
"opponentScore": 18,
"winner": "Daniela"
}
}
]
```
**Status Codes**
- 200 if messages were loaded successfully
- 401 if authentication failed
### POST message
**Endpoint**
POST /messages
**Description** Creates a new message from the authenticated user to another existing user. The sender must be taken from the authentication headers, not from the request body.
**Request Body (CreateMessageDTO)**
``` json
{
"recipient": "string",
"type": "challenge",
"text": "string"
}
```
**Response (MessageDTO)**
``` json
{
"id": 0,
"sender": "string",
"recipient": "string",
"type": "challenge",
"text": "string",
"read": false,
"createdAt": "2026-05-23T15:42:00"
}
```
**Status Codes**
- 201 if the message was created successfully
- 400 if recipient, type, or text is missing or invalid
- 401 if authentication failed
- 404 if the recipient user does not exist
### PATCH single message as read
**Endpoint**
PATCH /messages/{id}/read
**Description** Marks one received message as read. Only the recipient of the message is allowed to mark it as read.
**Response (MessageDTO)**
``` json
{
"id": 0,
"sender": "string",
"recipient": "string",
"type": "challenge",
"text": "string",
"read": true,
"createdAt": "2026-05-23T15:42:00"
}
```
**Status Codes**
- 200 if the message was marked as read
- 401 if authentication failed
- 403 if the authenticated user is not the recipient
- 404 if the message does not exist
### PATCH all messages as read
**Endpoint**
PATCH /messages/read
**Description** Marks all received messages of the authenticated user as read. This endpoint is optional, but recommended. If it is not implemented, the frontend can call `PATCH /messages/{id}/read` for each unread message.
**Response**
``` json
{
"message": "Alle Nachrichten wurden als gelesen markiert."
}
```
**Status Codes**
- 200 if all received messages were marked as read
- 401 if authentication failed
------------------------------------------------------------------------
## 5. Challenges
Challenges are competitive rounds between two users. The challenged user's current reference result is sent back when the challenge is created. The challenger then plays a round. When the round is completed, the backend compares both scores and creates result messages for both users.
All challenge endpoints require authentication via the existing headers.
**Authentication Headers (required)**
- X-Username
- X-Password
### POST challenge
**Endpoint**
POST /challenges
**Description** Creates a challenge from the authenticated user against another user. The backend should determine the opponent's reference result, ideally the opponent's best score from `/score/{opponent}`. The response is used by the frontend to start the challenger into the game with the opponent score visible.
**Request Body (CreateChallengeDTO)**
``` json
{
"opponent": "string",
"text": "Ich fordere dich zu einer Lorem-Ipsum-Challenge heraus!"
}
```
**Response (ChallengeDTO)**
``` json
{
"id": 0,
"challenger": "string",
"opponent": "string",
"opponentScore": 18,
"opponentScoreId": 42,
"status": "open",
"createdAt": "2026-05-23T15:42:00"
}
```
**Backend behavior**
- The sender/challenger must be taken from the authentication headers.
- `opponentScore` should be the opponent's best available score.
- If the opponent has no score yet, return `opponentScore: 0` or reject with 409. Returning 0 is easier for the frontend.
- The backend may also create an informational message for the opponent that they were challenged.
**Status Codes**
- 201 if the challenge was created successfully
- 400 if opponent is missing or invalid
- 401 if authentication failed
- 404 if the opponent user does not exist
- 409 optional, if the opponent has no score and the backend does not want to use 0
### POST complete challenge
**Endpoint**
POST /challenges/{id}/complete
**Description** Completes an open challenge with the authenticated user's played result. The backend compares challenger and opponent scores and creates a `challenge-result` message for both users.
**Request Body (CompleteChallengeDTO)**
``` json
{
"score": 14,
"time": 15,
"text": "string",
"userWrittenText": "string"
}
```
**Response (ChallengeResultDTO)**
``` json
{
"id": 0,
"challenger": "Florin",
"opponent": "Daniela",
"challengerScore": 14,
"opponentScore": 18,
"winner": "Daniela",
"status": "completed",
"completedAt": "2026-05-23T15:48:00",
"messages": [
{
"id": 10,
"recipient": "Florin",
"type": "challenge-result"
},
{
"id": 11,
"recipient": "Daniela",
"type": "challenge-result"
}
]
}
```
For a draw, `winner` can be `null` or `"draw"`.
**Backend behavior**
- Only the original challenger should be allowed to complete the challenge.
- The backend should persist the challenger's new result or link to the already persisted score if `/score` is called separately.
- After comparing the scores, the backend must create one unread result message for the challenger and one unread result message for the opponent.
- The result message should have `type: "challenge-result"` and include the nested `challenge` result object shown in `GET /messages`, so the frontend can render the winner/loser graphic.
**Status Codes**
- 200 if the challenge was completed successfully
- 400 if score data is missing or invalid
- 401 if authentication failed
- 403 if the authenticated user is not allowed to complete the challenge
- 404 if the challenge does not exist
- 409 if the challenge was already completed

View File

@ -131,10 +131,13 @@
<script src="assets/src/service/user-service.js"></script>
<script src="assets/src/service/score-service.js"></script>
<script src="assets/src/service/leaderboard-service.js"></script>
<script src="assets/src/service/message-service.js"></script>
<script src="assets/src/service/challenge-service.js"></script>
<script src="js/login.js"></script>
<script src="js/leaderboard.js"></script>
<script src="js/messages.js"></script>
<!--Navigation Script -->
<script src="js/play.js"></script>
<script src="js/navigation.js"></script>
</body>
</html>
</html>

View File

@ -137,6 +137,9 @@
setFeedback("Login erfolgreich.", "success");
updateSessionBox();
updateHeaderUsername();
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
setTimeout(function() {
if (typeof window.loadPage === "function") {
window.loadPage("home", "nav-home");
@ -178,6 +181,9 @@
saveAuth(createdName, createdPassword);
updateSessionBox();
updateHeaderUsername();
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
// Modal mit Daten füllen und anzeigen
const modalUsername = document.getElementById("modal-username");
@ -240,6 +246,9 @@
setFeedback("Du wurdest ausgeloggt.", "info");
updateSessionBox();
updateHeaderUsername();
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
}
async function handleDeleteAccount() {
@ -267,6 +276,9 @@
clearAuth();
updateSessionBox();
updateHeaderUsername();
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
setFeedback("Account wurde gelöscht.", "success");
return;
}

433
js/messages.js Normal file
View File

@ -0,0 +1,433 @@
(function () {
const MESSAGE_TYPE_CHALLENGE = "challenge";
const MESSAGE_TYPE_CHALLENGE_RESULT = "challenge-result";
const MESSAGE_POLL_INTERVAL_MS = 30000;
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
let currentMessages = [];
let currentUsers = [];
let messagePollingInterval = null;
function getAuth() {
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
return null;
}
const auth = window.AppAuth.getAuth();
if (!auth || !auth.username || !auth.password) {
return null;
}
return auth;
}
function normalizeUsername(username) {
return String(username || "").trim().toLowerCase();
}
function getMessageService() {
if (!window.config || !window.MessageService) {
return null;
}
return new window.MessageService(window.config);
}
function getUserService() {
if (!window.config || !window.UserService) {
return null;
}
return new window.UserService(window.config);
}
function getChallengeService() {
if (!window.config || !window.ChallengeService) {
return null;
}
return new window.ChallengeService(window.config);
}
function normalizeMessage(message) {
return {
id: message.id,
sender: message.sender ?? message.from ?? "",
recipient: message.recipient ?? message.to ?? "",
type: message.type ?? MESSAGE_TYPE_CHALLENGE,
text: message.text ?? "",
read: Boolean(message.read),
createdAt: message.createdAt ?? message.time ?? message.date ?? "",
challenge: message.challenge ?? message.result ?? null,
};
}
function normalizeUser(user) {
if (typeof user === "string") {
return user;
}
return user?.name ?? user?.username ?? "";
}
function formatMessageTime(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "";
}
return date.toLocaleString("de-CH", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function setFeedback(message, type) {
const feedback = document.getElementById("messages-feedback");
if (!feedback) {
return;
}
feedback.className = "alert alert-" + type + " mt-3 mb-0";
feedback.textContent = message;
feedback.classList.remove("d-none");
}
function setFormEnabled(enabled) {
const formElements = document.querySelectorAll(
"#challenge-form button, #challenge-form select, #challenge-form textarea, #mark-read-button, #refresh-messages-button",
);
formElements.forEach((element) => {
element.disabled = !enabled;
});
}
function updateMessagesNavState(messages = currentMessages) {
const navLink = document.getElementById("navbar-messages");
if (!navLink) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
const hasUnreadMessages = messages.some((message) => {
const isIncoming = normalizeUsername(message.recipient) === ownName
|| normalizeUsername(message.sender) !== ownName;
return isIncoming && !message.read;
});
navLink.classList.toggle("has-unread-messages", hasUnreadMessages);
}
function renderUserList(users) {
const userList = document.getElementById("messages-user-list");
const recipientSelect = document.getElementById("challenge-recipient");
if (!userList || !recipientSelect) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
const uniqueUsers = Array.from(new Set(users.map(normalizeUser)))
.filter(Boolean)
.filter((username) => normalizeUsername(username) !== ownName)
.sort((a, b) => a.localeCompare(b, "de-CH"));
currentUsers = uniqueUsers;
userList.innerHTML = "";
recipientSelect.innerHTML = "";
if (uniqueUsers.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "messages-empty";
emptyMessage.textContent = "Keine anderen User gefunden.";
userList.appendChild(emptyMessage);
return;
}
uniqueUsers.forEach((username) => {
const userButton = document.createElement("button");
userButton.type = "button";
userButton.className = "messages-user-button";
userButton.textContent = username;
userButton.addEventListener("click", () => {
recipientSelect.value = username;
document.getElementById("challenge-text")?.focus();
});
userList.appendChild(userButton);
const option = document.createElement("option");
option.value = username;
option.textContent = username;
recipientSelect.appendChild(option);
});
}
function renderMessages(messages = currentMessages) {
const messageList = document.getElementById("message-list");
if (!messageList) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
messageList.innerHTML = "";
if (messages.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "messages-empty";
emptyMessage.textContent = "Noch keine Nachrichten vorhanden.";
messageList.appendChild(emptyMessage);
return;
}
messages
.slice()
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.forEach((message) => {
const item = document.createElement("article");
const isOutgoing = normalizeUsername(message.sender) === ownName;
item.className = "message-item";
if (!message.read && !isOutgoing) {
item.classList.add("message-item-unread");
}
const fromToText = isOutgoing
? "An " + message.recipient
: "Von " + message.sender;
const meta = document.createElement("div");
meta.className = "message-meta";
const sender = document.createElement("strong");
sender.textContent = fromToText;
const time = document.createElement("span");
time.textContent = formatMessageTime(message.createdAt);
const text = document.createElement("p");
text.textContent = message.text;
meta.append(sender, time);
item.append(meta, text);
if (message.type === MESSAGE_TYPE_CHALLENGE_RESULT && message.challenge) {
item.appendChild(createChallengeResultGraphic(message.challenge));
}
messageList.appendChild(item);
});
}
function createChallengeResultGraphic(result) {
const graphic = document.createElement("div");
graphic.className = "challenge-result-graphic";
const winnerName = result.winner ?? "Unentschieden";
const isDraw = result.winner === null || result.winner === "draw";
const headline = document.createElement("div");
headline.className = "challenge-result-headline " + (isDraw ? "challenge-result-draw" : "challenge-result-win");
headline.textContent = isDraw ? "Unentschieden" : "Sieger: " + winnerName;
const scores = document.createElement("div");
scores.className = "challenge-result-scores";
const challenger = document.createElement("div");
challenger.className = "challenge-result-score";
challenger.innerHTML = "<strong></strong><span></span>";
challenger.querySelector("strong").textContent = result.challenger ?? "Herausforderer";
challenger.querySelector("span").textContent = String(result.challengerScore ?? "-") + " Punkte";
const opponent = document.createElement("div");
opponent.className = "challenge-result-score";
opponent.innerHTML = "<strong></strong><span></span>";
opponent.querySelector("strong").textContent = result.opponent ?? "Gegner";
opponent.querySelector("span").textContent = String(result.opponentScore ?? "-") + " Punkte";
scores.append(challenger, opponent);
graphic.append(headline, scores);
return graphic;
}
async function loadUsers() {
const auth = getAuth();
const userService = getUserService();
if (!auth || !userService || typeof userService.getUsers !== "function") {
renderUserList([]);
return;
}
const result = await userService.getUsers(auth.username, auth.password);
if (!result.ok || !Array.isArray(result.body)) {
renderUserList([]);
setFeedback("User konnten nicht geladen werden. Backend-Endpunkt GET /users fehlt eventuell noch.", "warning");
return;
}
renderUserList(result.body);
}
async function loadMessages(options = {}) {
const auth = getAuth();
const messageService = getMessageService();
if (!auth) {
currentMessages = [];
renderMessages();
updateMessagesNavState();
setFormEnabled(false);
if (options.showFeedback !== false) {
setFeedback("Bitte zuerst einloggen, um Nachrichten zu nutzen.", "warning");
}
return;
}
if (!messageService) {
setFormEnabled(false);
setFeedback("Message-Service konnte nicht geladen werden.", "danger");
return;
}
const result = await messageService.getMessages(auth.username, auth.password);
if (!result.ok || !Array.isArray(result.body)) {
currentMessages = [];
renderMessages();
updateMessagesNavState();
setFormEnabled(false);
setFeedback("Nachrichten konnten nicht geladen werden. Backend-Endpunkt GET /messages fehlt eventuell noch.", "warning");
return;
}
currentMessages = result.body.map(normalizeMessage);
renderMessages();
updateMessagesNavState();
setFormEnabled(true);
if (options.showFeedback) {
setFeedback("Nachrichten wurden aktualisiert.", "success");
}
}
async function handleChallengeSubmit(event) {
event.preventDefault();
const auth = getAuth();
const challengeService = getChallengeService();
const recipientSelect = document.getElementById("challenge-recipient");
const textInput = document.getElementById("challenge-text");
if (!auth || !challengeService || !recipientSelect || !textInput) {
setFeedback("Bitte zuerst einloggen, um Challenges zu senden.", "warning");
return;
}
const recipient = recipientSelect.value;
const text = textInput.value.trim();
if (!recipient || !text) {
setFeedback("Bitte Empfaenger und Nachricht eingeben.", "warning");
return;
}
const result = await challengeService.postChallenge(
auth.username,
auth.password,
recipient,
text,
);
if (!result.ok) {
setFeedback("Challenge konnte nicht gesendet werden (Status " + result.status + ").", "danger");
return;
}
const challenge = result.body || {};
sessionStorage.setItem(
ACTIVE_CHALLENGE_STORAGE_KEY,
JSON.stringify({
id: challenge.id,
challenger: challenge.challenger || auth.username,
opponent: challenge.opponent || recipient,
opponentScore: challenge.opponentScore ?? null,
}),
);
const scoreText = challenge.opponentScore !== undefined && challenge.opponentScore !== null
? " Gegner-Score: " + challenge.opponentScore + "."
: "";
setFeedback("Challenge an " + recipient + " wurde erstellt." + scoreText + " Du wirst zum Spiel weitergeleitet.", "success");
window.setTimeout(() => {
if (typeof window.loadPage === "function") {
window.loadPage("play", "nav-play");
}
}, 900);
await loadMessages({ showFeedback: false });
}
async function handleMarkRead() {
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService) {
setFeedback("Bitte zuerst einloggen, um Nachrichten zu markieren.", "warning");
return;
}
const result = await messageService.markAllMessagesAsRead(auth.username, auth.password);
if (!result.ok) {
const unreadMessages = currentMessages.filter((message) => !message.read);
const readResults = await Promise.all(
unreadMessages.map((message) =>
messageService.markMessageAsRead(auth.username, auth.password, message.id),
),
);
if (readResults.some((readResult) => !readResult.ok)) {
setFeedback("Nachrichten konnten nicht als gelesen markiert werden.", "danger");
return;
}
}
setFeedback("Alle Nachrichten wurden als gelesen markiert.", "info");
await loadMessages({ showFeedback: false });
}
async function initMessagesPage() {
const challengeForm = document.getElementById("challenge-form");
const markReadButton = document.getElementById("mark-read-button");
const refreshMessagesButton = document.getElementById("refresh-messages-button");
if (challengeForm) {
challengeForm.addEventListener("submit", handleChallengeSubmit);
}
if (markReadButton) {
markReadButton.addEventListener("click", handleMarkRead);
}
if (refreshMessagesButton) {
refreshMessagesButton.addEventListener("click", () => loadMessages({ showFeedback: true }));
}
setFormEnabled(Boolean(getAuth()));
await loadUsers();
await loadMessages({ showFeedback: false });
}
window.initMessagesPage = initMessagesPage;
window.updateMessagesNavState = function () {
loadMessages({ showFeedback: false }).catch((error) => {
console.error("Nachrichtenstatus konnte nicht geladen werden:", error);
updateMessagesNavState([]);
});
};
document.addEventListener("DOMContentLoaded", () => {
window.updateMessagesNavState();
if (!messagePollingInterval) {
messagePollingInterval = window.setInterval(
window.updateMessagesNavState,
MESSAGE_POLL_INTERVAL_MS,
);
}
});
})();

View File

@ -29,6 +29,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (page === "play" && typeof window.initPlayPage === "function") {
window.initPlayPage();
}
if (page === "messages" && typeof window.initMessagesPage === "function") {
window.initMessagesPage();
}
})
.catch(error => {
console.error("Fehler beim Laden von " + page + ":", error);
@ -49,6 +52,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (navLeaderboard) navLeaderboard.onclick = (e) => { e.preventDefault(); loadPage("leaderboard", "nav-leaderboard"); };
if (navbarLogin) navbarLogin.onclick = (e) => { e.preventDefault(); loadPage("login", "navbar-login"); };
if (navbarMessages) navbarMessages.onclick = (e) => { e.preventDefault(); loadPage("messages", "navbar-messages"); };
if (typeof window.updateMessagesNavState === "function") window.updateMessagesNavState();
//Startseite laden
loadPage("home", "nav-home");

View File

@ -1,6 +1,7 @@
(function () {
// --- Konfiguration ---
const MEMORIZE_TIME_SECONDS = 15;
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
// Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann.
const TEXT_PARTS = {
@ -75,6 +76,7 @@
let gameStatus;
let scoreSaveFeedback;
let btnSubmitScore;
let activeChallenge = null;
// --- Funktionen ---
@ -251,6 +253,32 @@
return new window.ScoreService(window.config);
}
function getChallengeService() {
if (!window.config || !window.ChallengeService) {
return null;
}
return new window.ChallengeService(window.config);
}
function readActiveChallenge() {
const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY);
if (!raw) {
return null;
}
try {
const challenge = JSON.parse(raw);
if (!challenge || !challenge.id || !challenge.opponent) {
return null;
}
return challenge;
} catch {
return null;
}
}
function showScoreSaveFeedback(message, type) {
if (!scoreSaveFeedback) return;
@ -316,6 +344,97 @@
);
}
function renderChallengeResult(score) {
if (!scoreSaveFeedback || !activeChallenge) {
return;
}
const opponentScore = Number(activeChallenge.opponentScore);
if (Number.isNaN(opponentScore)) {
return;
}
const auth = getAuth();
const ownName = auth?.username || "Du";
const opponentName = activeChallenge.opponent;
const result = score > opponentScore
? "winner"
: score < opponentScore
? "loser"
: "draw";
const headline = result === "winner"
? "Du gewinnst die Challenge"
: result === "loser"
? opponentName + " gewinnt die Challenge"
: "Unentschieden";
const graphic = document.createElement("div");
graphic.className = "play-challenge-result play-challenge-result-" + result;
const title = document.createElement("div");
title.className = "play-challenge-result-title";
title.textContent = headline;
const scores = document.createElement("div");
scores.className = "play-challenge-result-scores";
const ownScore = document.createElement("div");
ownScore.className = "play-challenge-result-score";
ownScore.innerHTML = "<strong></strong><span></span>";
ownScore.querySelector("strong").textContent = ownName;
ownScore.querySelector("span").textContent = score + " Punkte";
const otherScore = document.createElement("div");
otherScore.className = "play-challenge-result-score";
otherScore.innerHTML = "<strong></strong><span></span>";
otherScore.querySelector("strong").textContent = opponentName;
otherScore.querySelector("span").textContent = opponentScore + " Punkte";
scores.append(ownScore, otherScore);
graphic.append(title, scores);
scoreSaveFeedback.insertAdjacentElement("afterend", graphic);
}
async function completeChallenge(scoreData) {
const auth = getAuth();
if (!auth || !activeChallenge) {
return;
}
const challengeService = getChallengeService();
if (!challengeService) {
showScoreSaveFeedback(
"Score gespeichert, aber der Challenge-Service konnte nicht geladen werden.",
"warning",
);
return;
}
const result = await challengeService.completeChallenge(
auth.username,
auth.password,
activeChallenge.id,
scoreData.score,
scoreData.time,
scoreData.text,
scoreData.userWrittenText,
);
if (!result.ok) {
showScoreSaveFeedback(
"Score gespeichert, aber die Challenge konnte nicht abgeschlossen werden (Status " + result.status + ").",
"warning",
);
return;
}
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
}
async function submitScore() {
if (!userTextInput) return;
@ -345,6 +464,7 @@
if (resultScore) resultScore.textContent = score;
renderWordComparison(currentGameText, userInput);
renderChallengeResult(score);
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
const scoreData = {
@ -358,6 +478,7 @@
try {
await saveScore(scoreData);
await completeChallenge(scoreData);
} catch (error) {
console.error("Fehler beim Speichern des Scores:", error);
showScoreSaveFeedback(
@ -374,6 +495,7 @@
window.initPlayPage = function initPlayPage() {
clearInterval(timerInterval);
activeChallenge = readActiveChallenge();
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
phaseStart = document.getElementById("phaseStart");
@ -395,6 +517,15 @@
const btnRestart = document.getElementById("btnRestart");
const btnLeaderboard = document.getElementById("btnLeaderboard");
const challengeHint = document.getElementById("challengeHint");
if (challengeHint && activeChallenge) {
const opponentScore = activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined
? " Score: " + activeChallenge.opponentScore + "."
: "";
challengeHint.textContent = "Challenge gegen " + activeChallenge.opponent + "." + opponentScore;
challengeHint.classList.remove("d-none");
}
if (btnStart) btnStart.addEventListener("click", startGame);
if (btnSubmitScore) btnSubmitScore.addEventListener("click", submitScore);
if (btnRestart)

View File

@ -1,6 +1,43 @@
<div class="card">
<h2 class="mb-3">Nachrichten</h2>
<div class="card messages-card">
<div class="messages-header">
<div>
<h2 class="mb-1">Nachrichten</h2>
<p class="text-muted mb-0">Fordere andere User heraus und lies eingehende Antworten.</p>
</div>
<button class="btn btn-sm" id="refresh-messages-button" type="button">Aktualisieren</button>
</div>
<p class="mt-2">Hier kannst du deine Nachrichten ansehen und verwalten.</p>
<!-- Hier könnte eine Nachrichtenliste oder ähnliches eingebunden werden -->
</div>
<div id="messages-feedback" class="alert d-none mt-3 mb-0" role="alert"></div>
<div class="row g-4 mt-2">
<div class="col-lg-4">
<section class="messages-panel">
<h3>User</h3>
<div id="messages-user-list" class="messages-user-list"></div>
</section>
</div>
<div class="col-lg-8">
<section class="messages-panel mb-4">
<h3>Challenge senden</h3>
<form id="challenge-form" class="message-form">
<label for="challenge-recipient" class="form-label">Empfaenger</label>
<select id="challenge-recipient" class="form-select mb-3"></select>
<label for="challenge-text" class="form-label">Nachricht</label>
<textarea id="challenge-text" class="form-control mb-3" rows="3">Ich fordere dich zu einer Lorem-Ipsum-Challenge heraus!</textarea>
<button class="btn" type="submit">Challenge senden</button>
</form>
</section>
<section class="messages-panel">
<div class="messages-inbox-title">
<h3>Posteingang</h3>
<button class="btn btn-sm" id="mark-read-button" type="button">Als gelesen markieren</button>
</div>
<div id="message-list" class="message-list"></div>
</section>
</div>
</div>
</div>

View File

@ -5,6 +5,7 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">Lorem Ipsum - Challenge you brain</h2>
<div id="challengeHint" class="challenge-hint d-none"></div>
<p class="text-muted mb-0">Merken Sie sich den Text so gut wie möglich.</p>
</div>
<div id="gameStatus" class="badge bg-secondary fs-6 px-3 py-2">