From 11b3b48caa74a8641a2a2c2c118b5749861c5b19 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Fri, 28 Feb 2025 22:36:11 +0100 Subject: [PATCH] Initial server state --- .../contexts/SocketContext/SocketContext.jsx | 70 +++++++++++++----- client/src/pages/Game/Game.jsx | 71 +++++++++++++++++-- client/src/pages/Game/styles.sass | 10 +++ client/src/pages/Home/Home.jsx | 48 ++++++++++--- .../Home/components/JoinForm/JoinForm.jsx | 10 ++- server/controller/room.js | 28 ++++++-- server/handler/connection.js | 31 ++++++-- 7 files changed, 225 insertions(+), 43 deletions(-) diff --git a/client/src/common/contexts/SocketContext/SocketContext.jsx b/client/src/common/contexts/SocketContext/SocketContext.jsx index 8a9c285..4c8b00d 100644 --- a/client/src/common/contexts/SocketContext/SocketContext.jsx +++ b/client/src/common/contexts/SocketContext/SocketContext.jsx @@ -1,30 +1,64 @@ -import {createContext} from "react"; -import {io} from "socket.io-client"; +import { createContext, useState, useEffect, useCallback } from 'react'; +import { io } from 'socket.io-client'; -export const SocketContext = createContext({}); +export const SocketContext = createContext(); export const SocketProvider = ({ children }) => { - const socket = io("/", {autoConnect: false}); + const [socket, setSocket] = useState(null); + const [connected, setConnected] = useState(false); - const connect = () => { - socket.connect(); + const connect = useCallback(() => { + if (!socket) { + const newSocket = io(window.location.hostname === 'localhost' ? 'http://localhost:5287' : '/'); + + newSocket.on('connect', () => { + setConnected(true); + console.log('Socket connected'); + }); + + newSocket.on('disconnect', () => { + setConnected(false); + console.log('Socket disconnected'); + }); + + setSocket(newSocket); } + }, [socket]); - const send = (event, data) => { - socket.emit(event, data); + const disconnect = useCallback(() => { + if (socket) { + socket.disconnect(); + setSocket(null); + setConnected(false); } + }, [socket]); - const disconnect = () => { - socket.disconnect(); + const send = useCallback((event, data) => { + if (socket) { + socket.emit(event, data); + } else { + console.error('Cannot send event, socket not connected'); } + }, [socket]); - const addListener = (event, callback) => { - socket.on(event, callback); + const on = useCallback((event, callback) => { + if (socket) { + socket.on(event, callback); } + return () => { + if (socket) socket.off(event, callback); + }; + }, [socket]); - return ( - - {children} - - ) -} \ No newline at end of file + useEffect(() => { + return () => { + if (socket) socket.disconnect(); + }; + }, [socket]); + + return ( + + {children} + + ); +}; diff --git a/client/src/pages/Game/Game.jsx b/client/src/pages/Game/Game.jsx index a652052..53ff219 100644 --- a/client/src/pages/Game/Game.jsx +++ b/client/src/pages/Game/Game.jsx @@ -1,5 +1,6 @@ import "./styles.sass"; import {StateContext} from "@/common/contexts/StateContext"; +import {SocketContext} from "@/common/contexts/SocketContext"; import {useContext, useState, useEffect, useRef} from "react"; import MusicSlider from "@/pages/Game/components/MusicSlider"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -7,10 +8,59 @@ import {faMessage} from "@fortawesome/free-solid-svg-icons"; export const Game = () => { const {setCurrentState} = useContext(StateContext); - const [messages, setMessages] = useState([{sender: "Marco", text: "Hallo!"}]); + const {send, on, socket} = useContext(SocketContext); + const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); + const [connectedUsers, setConnectedUsers] = useState([]); + const [username, setUsername] = useState(""); const messageEndRef = useRef(null); + useEffect(() => { + const handleChatMessage = (messageData) => { + setMessages(prev => [...prev, messageData]); + }; + + const handleUserConnected = (userData) => { + setMessages(prev => [...prev, { + system: true, + text: `${userData.name} ist beigetreten` + }]); + setConnectedUsers(prev => [...prev, userData]); + }; + + const handleUserDisconnected = (userId) => { + setConnectedUsers(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); + }); + }; + + if (socket && socket.id) { + send("get-user-info"); + } + + const handleUserInfo = (userInfo) => { + setUsername(userInfo.name); + }; + + const cleanupChatMessage = on("chat-message", handleChatMessage); + const cleanupUserConnected = on("user-connected", handleUserConnected); + const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected); + const cleanupUserInfo = on("user-info", handleUserInfo); + + return () => { + cleanupChatMessage(); + cleanupUserConnected(); + cleanupUserDisconnected(); + cleanupUserInfo(); + }; + }, [on, send, socket]); useEffect(() => { messageEndRef.current?.scrollIntoView({behavior: "smooth"}); @@ -18,7 +68,12 @@ export const Game = () => { const handleSendMessage = () => { if (inputValue.trim()) { - setMessages([...messages, {sender: "User", text: inputValue}]); + const messageData = { + text: inputValue, + sender: username + }; + send("send-message", messageData); + setMessages(prev => [...prev, messageData]); setInputValue(""); } }; @@ -47,9 +102,15 @@ export const Game = () => {
{messages.map((message, index) => ( -
- {message.sender}: - {message.text} +
+ {message.system ? ( + {message.text} + ) : ( + <> + {message.sender}: + {message.text} + + )}
))}
diff --git a/client/src/pages/Game/styles.sass b/client/src/pages/Game/styles.sass index 8925e12..de5fe3d 100644 --- a/client/src/pages/Game/styles.sass +++ b/client/src/pages/Game/styles.sass @@ -210,6 +210,16 @@ .message-text margin-left: 5px + .message-text.system + font-style: italic + color: #888 + + .system-message + text-align: center + font-style: italic + color: #888 + padding: 5px 0 + .chat-input display: flex padding: 15px diff --git a/client/src/pages/Home/Home.jsx b/client/src/pages/Home/Home.jsx index d9583e7..2d5ed5e 100644 --- a/client/src/pages/Home/Home.jsx +++ b/client/src/pages/Home/Home.jsx @@ -5,35 +5,63 @@ import CreateForm from "@/pages/Home/components/CreateForm"; import {faArrowRightToBracket, faPlusSquare} from "@fortawesome/free-solid-svg-icons"; import {StateContext} from "@/common/contexts/StateContext"; import {SocketContext} from "@/common/contexts/SocketContext"; -import {useContext, useState} from "react"; +import {useContext, useState, useEffect} from "react"; export const Home = () => { const {setCurrentState} = useContext(StateContext); - const {connect, send} = useContext(SocketContext); + const {connect, send, on} = useContext(SocketContext); const [homeState, setHomeState] = useState("initial"); // initial, joining, creating + const [error, setError] = useState(""); + + useEffect(() => { + connect(); + + const handleRoomCreated = (roomId) => { + console.log("Room created", roomId); + setCurrentState("Game"); + }; + + const handleRoomJoined = (roomId) => { + console.log("Room joined", roomId); + setCurrentState("Game"); + }; + + const handleRoomNotFound = (roomId) => { + setError(`Room ${roomId} not found`); + }; + + const cleanupRoomCreated = on("room-created", handleRoomCreated); + const cleanupRoomJoined = on("room-joined", handleRoomJoined); + const cleanupRoomNotFound = on("room-not-found", handleRoomNotFound); + + return () => { + cleanupRoomCreated(); + cleanupRoomJoined(); + cleanupRoomNotFound(); + }; + }, [connect, setCurrentState, on]); const handleJoinClick = () => { setHomeState("joining"); + setError(""); }; const handleCreateClick = () => { setHomeState("creating"); + setError(""); }; const handleBack = () => { setHomeState("initial"); + setError(""); }; const handleJoinSubmit = (name, roomCode) => { - connect(); - send("joinRoom", {name, roomCode}); - setCurrentState("Game"); + send("join-room", {roomId: roomCode, name}); }; const handleCreateSubmit = (name) => { - connect(); - send("createRoom", {name}); - setCurrentState("Game"); + send("create-room", {name}); }; return ( @@ -55,13 +83,13 @@ export const Home = () => {
{homeState === 'joining' && ( - + )}
{homeState === 'creating' && ( - + )}
diff --git a/client/src/pages/Home/components/JoinForm/JoinForm.jsx b/client/src/pages/Home/components/JoinForm/JoinForm.jsx index 583d44b..7f3c570 100644 --- a/client/src/pages/Home/components/JoinForm/JoinForm.jsx +++ b/client/src/pages/Home/components/JoinForm/JoinForm.jsx @@ -1,13 +1,19 @@ import "@/common/styles/forms.sass"; -import {useState} from "react"; +import {useState, useEffect} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faArrowLeft, faArrowRightToBracket} from "@fortawesome/free-solid-svg-icons"; import FormInput from "@/common/components/FormInput"; -export const JoinForm = ({onSubmit, onBack}) => { +export const JoinForm = ({onSubmit, onBack, error}) => { const [name, setName] = useState(""); const [roomCode, setRoomCode] = useState(""); const [errors, setErrors] = useState({}); + + useEffect(() => { + if (error) { + setErrors(prev => ({...prev, roomCode: error})); + } + }, [error]); const handleSubmit = (e) => { e.preventDefault(); diff --git a/server/controller/room.js b/server/controller/room.js index 01279d7..be452c7 100644 --- a/server/controller/room.js +++ b/server/controller/room.js @@ -8,7 +8,7 @@ module.exports.connectUserToRoom = (roomId, user) => { } else { rooms[roomId] = {members: [{...user, creator: true}], settings: {}}; } - console.log(JSON.stringify(rooms)); + console.log(`User ${user.name} connected to room ${roomId}`); } module.exports.getUserRoom = (userId) => { @@ -18,6 +18,21 @@ module.exports.getUserRoom = (userId) => { if (memberIndex !== -1) return roomId; } + return null; +} + +module.exports.getRoomUsers = (roomId) => { + if (rooms[roomId]) { + return rooms[roomId].members; + } + return []; +} + +module.exports.getRoomCreator = (roomId) => { + if (rooms[roomId]) { + return rooms[roomId].members.find(member => member.creator); + } + return null; } module.exports.disconnectUser = (userId) => { @@ -26,12 +41,17 @@ module.exports.disconnectUser = (userId) => { const memberIndex = room.members.findIndex(member => member.id === userId); if (memberIndex !== -1) { - if (room.members[memberIndex].creator) { - if (room.members.length > 1) room.members[1].creator = true; + if (room.members[memberIndex].creator && room.members.length > 1) { + room.members[1].creator = true; } room.members.splice(memberIndex, 1); - if (room.members.length === 0) delete rooms[roomId]; + console.log(`User ${userId} disconnected from room ${roomId}`); + + if (room.members.length === 0) { + delete rooms[roomId]; + console.log(`Room ${roomId} deleted because it's empty`); + } break; } diff --git a/server/handler/connection.js b/server/handler/connection.js index 08b10e6..17c600d 100644 --- a/server/handler/connection.js +++ b/server/handler/connection.js @@ -1,7 +1,8 @@ -const {connectUserToRoom, roomExists, disconnectUser, getUserRoom} = require("../controller/room"); +const {connectUserToRoom, roomExists, disconnectUser, getUserRoom, getRoomUsers} = require("../controller/room"); module.exports = (socket) => { let currentRoomId = null; + let currentUser = null; socket.on("disconnect", () => { const roomId = getUserRoom(socket.id); @@ -14,9 +15,10 @@ module.exports = (socket) => { if (currentRoomId) return socket.emit("already-in-room", currentRoomId); if (roomExists(roomId.toString())) { - connectUserToRoom(roomId, {id: socket.id, name: name.toString()}); + currentUser = {id: socket.id, name: name.toString()}; + connectUserToRoom(roomId, currentUser); socket.join(roomId); - socket.to(roomId).emit("user-connected", {name: name.toString(), id: socket.id}); + socket.to(roomId).emit("user-connected", currentUser); socket.emit("room-joined", roomId); currentRoomId = roomId; } else { @@ -27,10 +29,31 @@ module.exports = (socket) => { socket.on("create-room", ({name}) => { if (!name) return socket.emit("room-name-required"); const roomId = Math.random().toString(36).substring(7); - connectUserToRoom(roomId, {id: socket.id, name: name?.toString()}); + currentUser = {id: socket.id, name: name?.toString()}; + connectUserToRoom(roomId, currentUser); socket.join(roomId); socket.emit("room-created", roomId); currentRoomId = roomId; }); + socket.on("send-message", (messageData) => { + const roomId = getUserRoom(socket.id); + if (roomId) { + socket.to(roomId).emit("chat-message", messageData); + } + }); + + socket.on("get-user-info", () => { + if (currentUser) { + socket.emit("user-info", currentUser); + } + }); + + socket.on("get-room-users", () => { + const roomId = getUserRoom(socket.id); + if (roomId) { + const users = getRoomUsers(roomId); + socket.emit("room-users", users); + } + }); } \ No newline at end of file