diff --git a/client/src/App.jsx b/client/src/App.jsx
index f48ebba..ec5208b 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -5,6 +5,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
import Home from "@/pages/Home";
import WaitingRoom from "@/pages/WaitingRoom";
+import Ending from "@/pages/Ending";
const App = () => {
const {currentState} = useContext(StateContext);
@@ -63,6 +64,7 @@ const App = () => {
{currentState === "WaitingRoom" && }
{currentState === "Home" && }
{currentState === "Game" && }
+ {currentState === "Ending" && }
>
)
}
diff --git a/client/src/pages/Ending/Ending.jsx b/client/src/pages/Ending/Ending.jsx
new file mode 100644
index 0000000..096b7cb
--- /dev/null
+++ b/client/src/pages/Ending/Ending.jsx
@@ -0,0 +1,72 @@
+import { useEffect, useState, useContext } from "react";
+import { StateContext } from "@/common/contexts/StateContext";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faTrophy, faMedal, faAward, faHome, faCrown } from "@fortawesome/free-solid-svg-icons";
+import "./styles.sass";
+
+export const Ending = () => {
+ const { setCurrentState } = useContext(StateContext);
+ const [finalScores, setFinalScores] = useState([]);
+
+ useEffect(() => {
+ const savedData = JSON.parse(localStorage.getItem('finalScores') || '{"scores":{}}');
+ const sortedScores = Object.entries(savedData.scores)
+ .map(([userId, data]) => ({
+ id: userId,
+ name: data.name,
+ score: data.score
+ }))
+ .sort((a, b) => b.score - a.score);
+ setFinalScores(sortedScores);
+ }, []);
+
+ const getPlayerIcon = (index) => {
+ switch(index) {
+ case 0: return ;
+ case 1: return ;
+ case 2: return ;
+ default: return ;
+ }
+ };
+
+ const handleReturnHome = () => {
+ localStorage.removeItem('finalScores');
+ setCurrentState("Home");
+ };
+
+ return (
+
+
+
+
+
Spiel beendet!
+
+
Endstand
+
+ {finalScores.map((player, index) => (
+
+
+ {getPlayerIcon(index)}
+
+
+ {player.name}
+ {player.score} Punkte
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/client/src/pages/Ending/index.js b/client/src/pages/Ending/index.js
new file mode 100644
index 0000000..5f64b6d
--- /dev/null
+++ b/client/src/pages/Ending/index.js
@@ -0,0 +1 @@
+export {Ending as default} from "./Ending.jsx";
\ No newline at end of file
diff --git a/client/src/pages/Ending/styles.sass b/client/src/pages/Ending/styles.sass
new file mode 100644
index 0000000..42ad057
--- /dev/null
+++ b/client/src/pages/Ending/styles.sass
@@ -0,0 +1,128 @@
+@import "@/common/styles/colors"
+
+.ending-page
+ height: 100vh
+ width: 100vw
+ display: flex
+ justify-content: center
+ align-items: center
+ position: relative
+
+ .ending-content
+ text-align: center
+ z-index: 2
+ animation: float-up 0.8s ease-out
+
+ h1
+ font-size: 48pt
+ color: $white
+ margin-bottom: 40px
+ background: linear-gradient(135deg, $yellow, $pink)
+ -webkit-background-clip: text
+ background-clip: text
+ -webkit-text-fill-color: transparent
+ animation: title-shimmer 3s infinite alternate ease-in-out
+
+ .final-scores
+ background: rgba(255, 255, 255, 0.1)
+ backdrop-filter: blur(10px)
+ border-radius: 20px
+ padding: 30px
+ margin-bottom: 30px
+ border: 1px solid rgba(255, 255, 255, 0.2)
+
+ h2
+ color: $white
+ margin-bottom: 20px
+ font-size: 24pt
+
+ .leaderboard
+ display: flex
+ flex-direction: column
+ gap: 15px
+
+ .leaderboard-entry
+ display: flex
+ align-items: center
+ padding: 15px 20px
+ background: rgba(255, 255, 255, 0.05)
+ border-radius: 15px
+ transition: all 0.3s ease
+ border: 1px solid rgba(255, 255, 255, 0.1)
+
+ &:hover
+ transform: translateY(-2px)
+ background: rgba(255, 255, 255, 0.1)
+
+ &.top-1
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1))
+ border-color: rgba(255, 215, 0, 0.3)
+ transform: scale(1.05)
+
+ .rank-icon
+ color: #FFD700
+ filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5))
+
+ &.top-2
+ background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(192, 192, 192, 0.1))
+ border-color: rgba(192, 192, 192, 0.3)
+
+ .rank-icon
+ color: #C0C0C0
+ filter: drop-shadow(0 0 10px rgba(192, 192, 192, 0.5))
+
+ &.top-3
+ background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(205, 127, 50, 0.1))
+ border-color: rgba(205, 127, 50, 0.3)
+
+ .rank-icon
+ color: #CD7F32
+ filter: drop-shadow(0 0 10px rgba(205, 127, 50, 0.5))
+
+ .rank-icon
+ font-size: 24pt
+ margin-right: 20px
+
+ .player-info
+ flex: 1
+ display: flex
+ justify-content: space-between
+ align-items: center
+
+ .player-name
+ color: $white
+ font-size: 18pt
+
+ .player-score
+ color: $yellow
+ font-size: 16pt
+ font-weight: bold
+
+ .return-home
+ padding: 15px 30px
+ background: linear-gradient(135deg, $purple, $blue)
+ border: none
+ border-radius: 12px
+ color: $white
+ font-size: 16pt
+ cursor: pointer
+ transition: all 0.3s ease
+ display: flex
+ align-items: center
+ gap: 10px
+ margin: 0 auto
+
+ &:hover
+ transform: translateY(-3px)
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4)
+
+ svg
+ font-size: 18pt
+
+@keyframes float-up
+ 0%
+ opacity: 0
+ transform: translateY(30px)
+ 100%
+ opacity: 1
+ transform: translateY(0)
diff --git a/client/src/pages/Game/Game.jsx b/client/src/pages/Game/Game.jsx
index 5418721..89817bb 100644
--- a/client/src/pages/Game/Game.jsx
+++ b/client/src/pages/Game/Game.jsx
@@ -1,5 +1,6 @@
import "./styles.sass";
import {SocketContext} from "@/common/contexts/SocketContext";
+import {StateContext} from "@/common/contexts/StateContext";
import {useContext, useState, useEffect, useRef, useCallback} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@@ -10,15 +11,14 @@ import {
faClock,
faCrown,
faPaperPlane,
- faCheckCircle,
- faVolumeUp,
- faStepForward
+ faCheckCircle
} from "@fortawesome/free-solid-svg-icons";
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
export const Game = () => {
const {send, on, socket, connected, connect} = useContext(SocketContext);
+ const {setCurrentState} = useContext(StateContext);
useEffect(() => {
if (!connected) {
@@ -69,12 +69,14 @@ export const Game = () => {
},
"song-selected": setCurrentSong,
"round-started": (data) => {
- setRound(data.round);
- setPhase("composing");
- setTimeLeft(data.timeRemaining);
- setSelectedSong(null);
- setHasGuessed(false);
- setGuessResult(null);
+ requestAnimationFrame(() => {
+ setPhase("composing");
+ setRound(data.round);
+ setTimeLeft(data.timeRemaining);
+ setSelectedSong(null);
+ setHasGuessed(false);
+ setGuessResult(null);
+ });
},
"guessing-phase-started": (data) => {
console.log("Guessing phase started:", data);
@@ -133,7 +135,7 @@ export const Game = () => {
if (phase === "composing") {
console.log("Received frequency update:", data.frequency);
setFrequency(data.frequency);
- setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
+ setComposerIsPlaying(data.isPlaying);
}
},
"phase-changed": (data) => {
@@ -156,6 +158,10 @@ export const Game = () => {
console.log("Received song options early:", data);
setSongOptions(data.songOptions || []);
},
+ "game-ended": (finalData) => {
+ setCurrentState("Ending");
+ localStorage.setItem('finalScores', JSON.stringify(finalData));
+ },
};
const cleanupFunctions = Object.entries(eventHandlers).map(
@@ -168,7 +174,7 @@ export const Game = () => {
}
return () => cleanupFunctions.forEach(cleanup => cleanup());
- }, [socket, on, send, role, currentSong, phase, connected, connectedUsers]);
+ }, [socket, on, send, role, currentSong, phase, connected, connectedUsers, setCurrentState, round]);
useEffect(() => {
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
@@ -279,10 +285,8 @@ export const Game = () => {
}, [selectedSong, send, phase]);
const handleNextRound = useCallback(() => {
- send("next-round");
- setSelectedSong(null);
- setGuessResult(null);
setTimeLeft(0);
+ send("next-round");
}, [send]);
const handlePlayerReady = useCallback(() => {
diff --git a/server/controller/game.js b/server/controller/game.js
index fa655b8..7e908db 100644
--- a/server/controller/game.js
+++ b/server/controller/game.js
@@ -44,6 +44,8 @@ const initializeGameState = (roomId) => {
return gameStates[roomId];
};
+const MAX_ROUNDS = 5;
+
const startNewRound = async (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return false;
@@ -52,6 +54,11 @@ const startNewRound = async (roomId) => {
if (users.length < 2) return false;
gameState.round += 1;
+
+ if (gameState.round > MAX_ROUNDS) {
+ return { gameEnd: true, finalScores: gameState.scores };
+ }
+
gameState.phase = 'composing';
gameState.guessResults = {};
gameState.roundStartTime = Date.now();
@@ -83,7 +90,6 @@ const determineNextComposer = (roomId, gameState, users) => {
return users[(currentIndex + 1) % users.length].id;
};
-// Then modify the selectSongAndOptions function to use the shuffle:
const selectSongAndOptions = async (gameState) => {
try {
console.log(`Fetching songs from playlist: ${gameState.selectedPlaylist}`);
@@ -107,7 +113,6 @@ const selectSongAndOptions = async (gameState) => {
count++;
}
- // Shuffle the song options to randomize the position of the correct answer
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
return true;
@@ -219,6 +224,27 @@ const getGameState = (roomId) => {
return gameStates[roomId] || null;
};
+const getFinalScores = (roomId) => {
+ const gameState = gameStates[roomId];
+ if (!gameState) return null;
+
+ const users = roomController.getRoomUsers(roomId);
+ const finalScores = {};
+
+ Object.entries(gameState.scores).forEach(([userId, score]) => {
+ const user = users.find(u => u.id === userId);
+ finalScores[userId] = {
+ score: score,
+ name: user?.name || "Player"
+ };
+ });
+
+ return {
+ scores: finalScores,
+ lastRound: gameState.round
+ };
+};
+
module.exports = {
initializeGameState,
startNewRound,
@@ -234,5 +260,7 @@ module.exports = {
getSelectedSong,
cleanupGameState,
getCurrentComposer,
- getGameState
+ getGameState,
+ getFinalScores,
+ MAX_ROUNDS
};
diff --git a/server/handler/connection.js b/server/handler/connection.js
index e73b855..d880f1f 100644
--- a/server/handler/connection.js
+++ b/server/handler/connection.js
@@ -109,6 +109,23 @@ module.exports = (io) => (socket) => {
io.to(roomId).emit('round-results', results);
};
+ const handleNextRound = async (roomId) => {
+ try {
+ const result = await gameController.startNewRound(roomId);
+
+ if (result.gameEnd) {
+ const finalData = gameController.getFinalScores(roomId);
+ io.to(roomId).emit('game-ended', finalData);
+ return;
+ }
+
+ handleRoundStart(roomId);
+ } catch (error) {
+ console.error("Error starting next round:", error);
+ socket.emit("error", { message: "Failed to start next round due to an error" });
+ }
+ };
+
socket.on("disconnect", () => {
const roomId = roomController.getUserRoom(socket.id);
if (roomId) {
@@ -298,17 +315,7 @@ module.exports = (io) => (socket) => {
return socket.emit("error", { message: "At least 2 players are required" });
}
- try {
- const success = await gameController.startNewRound(roomId);
- if (success) {
- handleRoundStart(roomId);
- } else {
- socket.emit("error", { message: "Failed to start next round" });
- }
- } catch (error) {
- console.error("Error starting next round:", error);
- socket.emit("error", { message: "Failed to start next round due to an error" });
- }
+ await handleNextRound(roomId);
});
socket.on("get-current-frequency", () => {