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");