From 76580841afdb47ea9d90a7f80de398cde9b0715e Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Fri, 28 Feb 2025 22:50:00 +0100 Subject: [PATCH] Implement waiting room --- client/src/App.jsx | 2 + client/src/pages/Home/Home.jsx | 12 +- .../Home/components/JoinForm/JoinForm.jsx | 2 +- client/src/pages/WaitingRoom/WaitingRoom.jsx | 233 +++++++++++ client/src/pages/WaitingRoom/index.js | 1 + client/src/pages/WaitingRoom/styles.sass | 375 ++++++++++++++++++ server/controller/room.js | 36 +- server/handler/connection.js | 52 ++- server/index.js | 3 +- 9 files changed, 705 insertions(+), 11 deletions(-) create mode 100644 client/src/pages/WaitingRoom/WaitingRoom.jsx create mode 100644 client/src/pages/WaitingRoom/index.js create mode 100644 client/src/pages/WaitingRoom/styles.sass diff --git a/client/src/App.jsx b/client/src/App.jsx index fcf746c..f48ebba 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,6 +4,7 @@ import {StateContext} from "@/common/contexts/StateContext"; 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"; const App = () => { const {currentState} = useContext(StateContext); @@ -59,6 +60,7 @@ const App = () => { ))} + {currentState === "WaitingRoom" && } {currentState === "Home" && } {currentState === "Game" && } diff --git a/client/src/pages/Home/Home.jsx b/client/src/pages/Home/Home.jsx index 2d5ed5e..c792592 100644 --- a/client/src/pages/Home/Home.jsx +++ b/client/src/pages/Home/Home.jsx @@ -18,26 +18,32 @@ export const Home = () => { const handleRoomCreated = (roomId) => { console.log("Room created", roomId); - setCurrentState("Game"); + setCurrentState("WaitingRoom"); }; const handleRoomJoined = (roomId) => { console.log("Room joined", roomId); - setCurrentState("Game"); + setCurrentState("WaitingRoom"); }; const handleRoomNotFound = (roomId) => { setError(`Room ${roomId} not found`); }; + const handleRoomClosed = (roomId) => { + setError(`Room ${roomId} is already in game`); + }; + const cleanupRoomCreated = on("room-created", handleRoomCreated); const cleanupRoomJoined = on("room-joined", handleRoomJoined); const cleanupRoomNotFound = on("room-not-found", handleRoomNotFound); + const cleanupRoomClosed = on("room-closed", handleRoomClosed); return () => { cleanupRoomCreated(); cleanupRoomJoined(); cleanupRoomNotFound(); + cleanupRoomClosed(); }; }, [connect, setCurrentState, on]); @@ -57,7 +63,7 @@ export const Home = () => { }; const handleJoinSubmit = (name, roomCode) => { - send("join-room", {roomId: roomCode, name}); + send("join-room", {roomId: roomCode.toUpperCase(), name}); }; const handleCreateSubmit = (name) => { diff --git a/client/src/pages/Home/components/JoinForm/JoinForm.jsx b/client/src/pages/Home/components/JoinForm/JoinForm.jsx index 7f3c570..75035b2 100644 --- a/client/src/pages/Home/components/JoinForm/JoinForm.jsx +++ b/client/src/pages/Home/components/JoinForm/JoinForm.jsx @@ -27,7 +27,7 @@ export const JoinForm = ({onSubmit, onBack, error}) => { return; } - onSubmit(name, roomCode); + onSubmit(name, roomCode.toUpperCase()); }; return ( diff --git a/client/src/pages/WaitingRoom/WaitingRoom.jsx b/client/src/pages/WaitingRoom/WaitingRoom.jsx new file mode 100644 index 0000000..62cbc97 --- /dev/null +++ b/client/src/pages/WaitingRoom/WaitingRoom.jsx @@ -0,0 +1,233 @@ +import { useContext, useEffect, useState, useRef } from "react"; +import { SocketContext } from "@/common/contexts/SocketContext"; +import { StateContext } from "@/common/contexts/StateContext"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlayCircle, faUsers, faMessage, faArrowLeft, faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; +import "./styles.sass"; + +export const WaitingRoom = () => { + const { socket, on, send } = useContext(SocketContext); + const { setCurrentState } = useContext(StateContext); + const [users, setUsers] = useState([]); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [roomCode, setRoomCode] = useState(""); + const [isHost, setIsHost] = useState(false); + const [username, setUsername] = useState(""); + const [copied, setCopied] = useState(false); + const messageEndRef = useRef(null); + + useEffect(() => { + // Check if the user is a host and get other initial data + send("check-host-status"); + send("get-user-info"); + send("get-room-users"); + send("get-room-code"); + + const handleHostStatus = (status) => { + setIsHost(status.isHost); + }; + + const handleUserInfo = (userInfo) => { + setUsername(userInfo.name); + }; + + const handleRoomUsers = (users) => { + setUsers(users); + }; + + const handleRoomUsersUpdate = (updatedUsers) => { + setUsers(updatedUsers); + }; + + const handleRoomCode = (code) => { + setRoomCode(code); + }; + + const handleUserConnected = (userData) => { + setMessages(prev => [...prev, { + system: true, + text: `${userData.name} ist beigetreten` + }]); + }; + + const handleUserDisconnected = (userId) => { + setUsers(prev => { + const user = prev.find(u => u.id === userId); + if (user) { + setMessages(prevMsgs => [...prevMsgs, { + system: true, + text: `${user.name} hat den Raum verlassen` + }]); + } + return prev.filter(u => u.id !== userId); + }); + }; + + const handleChatMessage = (messageData) => { + setMessages(prev => [...prev, messageData]); + }; + + const handleGameStarted = () => { + setCurrentState("Game"); + }; + + // Register event listeners + const cleanupHostStatus = on("host-status", handleHostStatus); + const cleanupUserInfo = on("user-info", handleUserInfo); + const cleanupRoomUsers = on("room-users", handleRoomUsers); + const cleanupRoomCode = on("room-code", handleRoomCode); + const cleanupRoomUsersUpdate = on("room-users-update", handleRoomUsersUpdate); + const cleanupUserConnected = on("user-connected", handleUserConnected); + const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected); + const cleanupChatMessage = on("chat-message", handleChatMessage); + const cleanupGameStarted = on("game-started", handleGameStarted); + + // Add welcome message + setMessages([{ + system: true, + text: "Welcome to the waiting room! Waiting for others to join..." + }]); + + return () => { + cleanupHostStatus(); + cleanupUserInfo(); + cleanupRoomUsers(); + cleanupRoomCode(); + cleanupRoomUsersUpdate(); + cleanupUserConnected(); + cleanupUserDisconnected(); + cleanupChatMessage(); + cleanupGameStarted(); + }; + }, [on, send, socket, setCurrentState]); + + useEffect(() => { + messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const handleSendMessage = () => { + if (inputValue.trim()) { + const messageData = { + text: inputValue, + sender: username + }; + send("send-message", messageData); + setMessages(prev => [...prev, messageData]); + setInputValue(""); + } + }; + + const handleStartGame = () => { + if (isHost) { + send("start-game"); + } + }; + + const handleLeaveRoom = () => { + // Disconnect from the current room + if (socket) { + socket.disconnect(); + } + setCurrentState("Home"); + }; + + const copyRoomCode = () => { + navigator.clipboard.writeText(roomCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const minPlayersToStart = 1; // Set your minimum players requirement here + const canStartGame = isHost && users.length >= minPlayersToStart; + + return ( +
+
+
+
+ +
+ +

Warteraum

+
+
+ Code: {roomCode} + +
+
{copied ? "Kopiert!" : "Klicken zum Kopieren"}
+
+
+ +
+
+
+ +

Spieler ({users.length})

+
+
+ {users.length === 0 ? ( +
Keine Spieler im Raum
+ ) : ( + users.map((user) => ( +
+ {user.name} {user.creator && Host} +
+ )) + )} +
+ {isHost && ( +
+ + {!canStartGame && users.length < minPlayersToStart && ( +
Mindestens {minPlayersToStart} Spieler benötigt
+ )} +
+ )} +
+ +
+
+ +

Chat

+
+
+ {messages.map((message, index) => ( +
+ {message.system ? ( + {message.text} + ) : ( + <> + {message.sender}: + {message.text} + + )} +
+ ))} +
+
+
+ setInputValue(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} + placeholder="Gib eine Nachricht ein..." + /> + +
+
+
+
+ ); +}; diff --git a/client/src/pages/WaitingRoom/index.js b/client/src/pages/WaitingRoom/index.js new file mode 100644 index 0000000..babc67b --- /dev/null +++ b/client/src/pages/WaitingRoom/index.js @@ -0,0 +1 @@ +export { WaitingRoom as default } from './WaitingRoom'; \ No newline at end of file diff --git a/client/src/pages/WaitingRoom/styles.sass b/client/src/pages/WaitingRoom/styles.sass new file mode 100644 index 0000000..3bae174 --- /dev/null +++ b/client/src/pages/WaitingRoom/styles.sass @@ -0,0 +1,375 @@ +@import "@/common/styles/colors" + +.waiting-room-page + height: 100vh + width: 100vw + display: flex + flex-direction: column + align-items: center + position: relative + padding: 20px + animation: page-fade-in 0.8s ease-in-out + + .waiting-room-header + display: flex + flex-direction: column + align-items: center + justify-content: center + width: 100% + margin-bottom: 30px + position: relative + z-index: 2 + animation: float-up 0.8s ease-out + + .back-button + position: absolute + left: 20px + top: 0 + background: none + border: none + color: $white + font-size: 1rem + display: flex + align-items: center + gap: 10px + cursor: pointer + transition: all 0.2s ease + padding: 8px 16px + border-radius: 5px + + &:hover + background: rgba(255, 255, 255, 0.1) + + h1 + font-size: 42pt + color: $white + margin-bottom: 15px + text-shadow: 0 0 15px rgba(255, 255, 255, 0.7) + background: linear-gradient(135deg, $pink, $blue 45%, $mint-green 65%, $yellow 85%, $pink) + background-size: 300% 100% + animation: title-shimmer 10s infinite alternate ease-in-out + -webkit-background-clip: text + background-clip: text + -webkit-text-fill-color: transparent + + .room-code-container + display: flex + flex-direction: column + align-items: center + + .room-code + font-size: 1.2rem + color: $white + background: rgba(255, 255, 255, 0.1) + padding: 10px 25px + border-radius: 10px + cursor: pointer + transition: all 0.3s ease + border: 1px solid rgba(255, 255, 255, 0.2) + backdrop-filter: blur(5px) + display: flex + align-items: center + gap: 10px + + &:hover + background: rgba(255, 255, 255, 0.2) + border: 1px solid rgba(255, 255, 255, 0.4) + + .code + font-weight: bold + font-size: 1.3rem + color: $yellow + letter-spacing: 2px + margin-left: 5px + margin-right: 5px + + svg + font-size: 1rem + transition: all 0.3s ease + + &.copied + color: $mint-green + animation: pop 0.3s ease + + .copy-hint + margin-top: 5px + font-size: 0.8rem + color: $border + opacity: 0.7 + + .waiting-room-content + display: flex + width: 100% + max-width: 1200px + gap: 30px + height: calc(100vh - 180px) + z-index: 2 + position: relative + animation: float-up 1.2s ease-out + + .users-panel, + .chat-panel + background: rgba(255, 255, 255, 0.08) + backdrop-filter: blur(10px) + border-radius: 20px + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 255, 255, 0.1) + border: 1px solid rgba(255, 255, 255, 0.2) + display: flex + flex-direction: column + animation: card-pulse 6s infinite alternate ease-in-out + transition: all 0.3s ease + + &:hover + border: 1px solid rgba(255, 255, 255, 0.3) + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.3), 0 0 30px rgba(255, 255, 255, 0.15) + + .users-panel + width: 30% + + .users-list + flex-grow: 1 + overflow-y: auto + padding: 15px + + &::-webkit-scrollbar + width: 6px + background: rgba(0, 0, 0, 0.2) + border-radius: 3px + + &::-webkit-scrollbar-thumb + background: rgba(255, 255, 255, 0.2) + border-radius: 3px + + &:hover + background: rgba(255, 255, 255, 0.3) + + .no-users + color: $border + text-align: center + padding: 20px 0 + font-style: italic + + .user-item + margin-bottom: 10px + padding: 12px 15px + background: rgba(255, 255, 255, 0.05) + border-radius: 10px + color: $white + display: flex + align-items: center + justify-content: space-between + transition: all 0.2s ease + border: 1px solid rgba(255, 255, 255, 0.1) + + &:hover + background: rgba(255, 255, 255, 0.1) + transform: translateY(-2px) + + &.host + border: 1px solid $yellow + box-shadow: 0 0 10px rgba($yellow, 0.3) + + .host-badge + background: $yellow + color: #000 + font-size: 0.75rem + padding: 3px 8px + border-radius: 10px + font-weight: bold + text-transform: uppercase + + .game-controls + margin: 15px + display: flex + flex-direction: column + gap: 10px + + .start-game-button + padding: 15px + background: linear-gradient(135deg, $purple, $blue) + border: none + border-radius: 12px + color: $white + font-size: 1.1rem + display: flex + align-items: center + justify-content: center + gap: 10px + cursor: pointer + transition: all 0.3s ease + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3) + + svg + font-size: 1.2rem + + &:hover:not(.disabled) + transform: translateY(-3px) + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 15px rgba($purple, 0.6) + background: linear-gradient(135deg, lighten($purple, 5%), lighten($blue, 5%)) + + &.disabled + background: linear-gradient(135deg, desaturate($purple, 40%), desaturate($blue, 40%)) + opacity: 0.7 + cursor: not-allowed + + .start-hint + color: $border + text-align: center + font-size: 0.8rem + font-style: italic + + .chat-panel + width: 70% + display: flex + flex-direction: column + + .chat-messages + flex-grow: 1 + overflow-y: auto + padding: 15px + display: flex + flex-direction: column + + &::-webkit-scrollbar + width: 8px + background: rgba(0, 0, 0, 0.2) + border-radius: 4px + + &::-webkit-scrollbar-thumb + background: rgba(255, 255, 255, 0.2) + border-radius: 4px + + &:hover + background: rgba(255, 255, 255, 0.3) + + .message + margin-bottom: 10px + padding: 10px 15px + border-radius: 12px + background: rgba(255, 255, 255, 0.05) + border: 1px solid rgba(255, 255, 255, 0.1) + color: $white + animation: message-fade-in 0.3s ease-out + width: fit-content + max-width: 80% + align-self: flex-start + + &:hover + background: rgba(255, 255, 255, 0.1) + + .message-sender + font-weight: bold + color: $yellow + margin-right: 5px + + .message-text + color: $white + + &.system-message + background: rgba(0, 0, 0, 0.2) + border: 1px solid rgba(255, 255, 255, 0.05) + color: $border + padding: 8px 12px + font-style: italic + align-self: center + + .message-text.system + color: $border + font-style: italic + + .chat-input + display: flex + padding: 15px + gap: 10px + background: rgba(30, 30, 30, 0.5) + border-top: 1px solid rgba(255, 255, 255, 0.1) + border-radius: 0 0 20px 20px + + input + flex: 1 + padding: 12px 15px + background: rgba(0, 0, 0, 0.3) + border: 1px solid rgba(255, 255, 255, 0.1) + border-radius: 8px + color: $white + outline: none + transition: all 0.2s ease + + &:focus + border: 1px solid rgba(255, 255, 255, 0.3) + box-shadow: 0 0 15px rgba(255, 255, 255, 0.1) + + button + padding: 12px 20px + background: linear-gradient(135deg, $purple, $blue) + border: none + border-radius: 8px + color: $white + cursor: pointer + transition: all 0.3s ease + + &:hover + transform: translateY(-2px) + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4), 0 0 10px rgba($purple, 0.5) + + .panel-header + display: flex + align-items: center + gap: 10px + padding: 15px + background: rgba(30, 30, 30, 0.5) + border-bottom: 1px solid rgba(255, 255, 255, 0.1) + border-radius: 20px 20px 0 0 + + svg + color: $white + font-size: 1.4rem + + h2 + color: $white + font-size: 1.4rem + margin: 0 + +@keyframes pop + 0% + transform: scale(1) + 50% + transform: scale(1.3) + 100% + transform: scale(1) + +@keyframes title-shimmer + 0% + background-position: 0% 50% + 50% + background-position: 100% 50% + 100% + background-position: 0% 50% + +@keyframes message-fade-in + 0% + opacity: 0 + transform: translateY(5px) + 100% + opacity: 1 + transform: translateY(0) + +@keyframes card-pulse + 0%, 100% + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 255, 255, 0.1) + 50% + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.3), 0 0 30px rgba(255, 255, 255, 0.2) + +@keyframes float-up + 0% + opacity: 0 + transform: translateY(20px) + 100% + opacity: 1 + transform: translateY(0) + +@keyframes page-fade-in + 0% + opacity: 0 + 100% + opacity: 1 diff --git a/server/controller/room.js b/server/controller/room.js index be452c7..6d18fc6 100644 --- a/server/controller/room.js +++ b/server/controller/room.js @@ -2,11 +2,18 @@ let rooms = {}; module.exports.roomExists = (roomId) => rooms[roomId] !== undefined; +module.exports.isRoomOpen = (roomId) => rooms[roomId] && rooms[roomId].state === 'waiting'; + module.exports.connectUserToRoom = (roomId, user) => { + roomId = roomId.toUpperCase(); if (rooms[roomId]) { rooms[roomId].members.push({...user, creator: false}); } else { - rooms[roomId] = {members: [{...user, creator: true}], settings: {}}; + rooms[roomId] = { + members: [{...user, creator: true}], + settings: {}, + state: 'waiting' + }; } console.log(`User ${user.name} connected to room ${roomId}`); } @@ -35,6 +42,33 @@ module.exports.getRoomCreator = (roomId) => { return null; } +module.exports.isUserHost = (userId) => { + for (const roomId in rooms) { + const room = rooms[roomId]; + const member = room.members.find(m => m.id === userId); + if (member && member.creator) { + return true; + } + } + return false; +} + +module.exports.startGame = (roomId) => { + if (rooms[roomId]) { + rooms[roomId].state = 'playing'; + console.log(`Game started in room ${roomId}`); + return true; + } + return false; +} + +module.exports.getRoomState = (roomId) => { + if (rooms[roomId]) { + return rooms[roomId].state; + } + return null; +} + module.exports.disconnectUser = (userId) => { for (const roomId in rooms) { const room = rooms[roomId]; diff --git a/server/handler/connection.js b/server/handler/connection.js index 17c600d..1af6a1e 100644 --- a/server/handler/connection.js +++ b/server/handler/connection.js @@ -1,6 +1,15 @@ -const {connectUserToRoom, roomExists, disconnectUser, getUserRoom, getRoomUsers} = require("../controller/room"); +const { + connectUserToRoom, + roomExists, + disconnectUser, + getUserRoom, + getRoomUsers, + isRoomOpen, + startGame, + isUserHost +} = require("../controller/room"); -module.exports = (socket) => { +module.exports = (io) => (socket) => { let currentRoomId = null; let currentUser = null; @@ -13,22 +22,33 @@ module.exports = (socket) => { socket.on("join-room", ({roomId, name}) => { if (currentRoomId) return socket.emit("already-in-room", currentRoomId); + + roomId = roomId.toString().toUpperCase(); - if (roomExists(roomId.toString())) { + if (roomExists(roomId)) { + if (!isRoomOpen(roomId)) { + return socket.emit("room-closed", roomId); + } + currentUser = {id: socket.id, name: name.toString()}; connectUserToRoom(roomId, currentUser); socket.join(roomId); + + const users = getRoomUsers(roomId); + io.to(roomId).emit("room-users-update", users); + socket.to(roomId).emit("user-connected", currentUser); + socket.emit("room-joined", roomId); currentRoomId = roomId; } else { - socket.emit("room-not-found", roomId.toString()); + socket.emit("room-not-found", roomId); } }); socket.on("create-room", ({name}) => { if (!name) return socket.emit("room-name-required"); - const roomId = Math.random().toString(36).substring(7); + const roomId = Math.random().toString(36).substring(7).toUpperCase(); currentUser = {id: socket.id, name: name?.toString()}; connectUserToRoom(roomId, currentUser); socket.join(roomId); @@ -36,6 +56,17 @@ module.exports = (socket) => { currentRoomId = roomId; }); + socket.on("start-game", () => { + const roomId = getUserRoom(socket.id); + if (roomId && isUserHost(socket.id)) { + if (startGame(roomId)) { + io.to(roomId).emit("game-started"); + } + } else { + socket.emit("not-authorized"); + } + }); + socket.on("send-message", (messageData) => { const roomId = getUserRoom(socket.id); if (roomId) { @@ -56,4 +87,15 @@ module.exports = (socket) => { socket.emit("room-users", users); } }); + + socket.on("check-host-status", () => { + const isHost = isUserHost(socket.id); + socket.emit("host-status", { isHost }); + }); + + socket.on("get-room-code", () => { + if (currentRoomId) { + socket.emit("room-code", currentRoomId); + } + }); } \ No newline at end of file diff --git a/server/index.js b/server/index.js index fe485fa..e0b267c 100644 --- a/server/index.js +++ b/server/index.js @@ -13,7 +13,8 @@ app.disable("x-powered-by"); const server = http.createServer(app); const io = new Server(server, {cors: {origin: "*"}}); -io.on("connection", require("./handler/connection")); +// Pass io to the connection handler +io.on("connection", require("./handler/connection")(io)); server.listen(5287, () => { console.log("Server running on port 5287");