Implement waiting room

This commit is contained in:
Mathias Wagner 2025-02-28 22:50:00 +01:00
parent 11b3b48caa
commit 76580841af
9 changed files with 705 additions and 11 deletions

View File

@ -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 = () => {
</div>
))}
</div>
{currentState === "WaitingRoom" && <WaitingRoom />}
{currentState === "Home" && <Home />}
{currentState === "Game" && <Game />}
</>

View File

@ -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) => {

View File

@ -27,7 +27,7 @@ export const JoinForm = ({onSubmit, onBack, error}) => {
return;
}
onSubmit(name, roomCode);
onSubmit(name, roomCode.toUpperCase());
};
return (

View File

@ -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 (
<div className="waiting-room-page">
<div className="background-overlay">
<div className="rotating-gradient"></div>
</div>
<div className="waiting-room-header">
<button className="back-button" onClick={handleLeaveRoom}>
<FontAwesomeIcon icon={faArrowLeft} />
<span>Zurück zur Startseite</span>
</button>
<h1>Warteraum</h1>
<div className="room-code-container">
<div className="room-code" onClick={copyRoomCode}>
Code: <span className="code">{roomCode}</span>
<FontAwesomeIcon icon={copied ? faCheck : faCopy} className={copied ? "copied" : ""} />
</div>
<div className="copy-hint">{copied ? "Kopiert!" : "Klicken zum Kopieren"}</div>
</div>
</div>
<div className="waiting-room-content">
<div className="users-panel">
<div className="panel-header">
<FontAwesomeIcon icon={faUsers} />
<h2>Spieler ({users.length})</h2>
</div>
<div className="users-list">
{users.length === 0 ? (
<div className="no-users">Keine Spieler im Raum</div>
) : (
users.map((user) => (
<div key={user.id} className={`user-item ${user.creator ? 'host' : ''}`}>
{user.name} {user.creator && <span className="host-badge">Host</span>}
</div>
))
)}
</div>
{isHost && (
<div className="game-controls">
<button
className={`start-game-button ${canStartGame ? '' : 'disabled'}`}
onClick={handleStartGame}
disabled={!canStartGame}
>
<FontAwesomeIcon icon={faPlayCircle} />
Spiel starten
</button>
{!canStartGame && users.length < minPlayersToStart && (
<div className="start-hint">Mindestens {minPlayersToStart} Spieler benötigt</div>
)}
</div>
)}
</div>
<div className="chat-panel">
<div className="panel-header">
<FontAwesomeIcon icon={faMessage} />
<h2>Chat</h2>
</div>
<div className="chat-messages">
{messages.map((message, index) => (
<div key={index} className={`message ${message.system ? 'system-message' : ''}`}>
{message.system ? (
<span className="message-text system">{message.text}</span>
) : (
<>
<span className="message-sender">{message.sender}:</span>
<span className="message-text">{message.text}</span>
</>
)}
</div>
))}
<div ref={messageEndRef}></div>
</div>
<div className="chat-input">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Gib eine Nachricht ein..."
/>
<button onClick={handleSendMessage}>Senden</button>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { WaitingRoom as default } from './WaitingRoom';

View File

@ -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

View File

@ -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];

View File

@ -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;
@ -14,21 +23,32 @@ module.exports = (socket) => {
socket.on("join-room", ({roomId, name}) => {
if (currentRoomId) return socket.emit("already-in-room", currentRoomId);
if (roomExists(roomId.toString())) {
roomId = roomId.toString().toUpperCase();
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);
}
});
}

View File

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