Add Message/Challenge-Logik
This commit is contained in:
parent
57ae0ccda5
commit
c533198c38
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
66
assets/src/service/challenge-service.js
Normal file
66
assets/src/service/challenge-service.js
Normal 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;
|
||||
106
assets/src/service/message-service.js
Normal file
106
assets/src/service/message-service.js
Normal 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;
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
12
js/login.js
12
js/login.js
@ -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
433
js/messages.js
Normal 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@ -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");
|
||||
|
||||
131
js/play.js
131
js/play.js
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user