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", () => {