Initial server state

This commit is contained in:
Mathias Wagner 2025-02-28 22:36:11 +01:00
parent 5ab0b61f80
commit 11b3b48caa
7 changed files with 225 additions and 43 deletions

View File

@ -1,30 +1,64 @@
import {createContext} from "react"; import { createContext, useState, useEffect, useCallback } from 'react';
import {io} from "socket.io-client"; import { io } from 'socket.io-client';
export const SocketContext = createContext({}); export const SocketContext = createContext();
export const SocketProvider = ({ children }) => { export const SocketProvider = ({ children }) => {
const socket = io("/", {autoConnect: false}); const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
const connect = () => { const connect = useCallback(() => {
socket.connect(); 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) => { const disconnect = useCallback(() => {
socket.emit(event, data); if (socket) {
socket.disconnect();
setSocket(null);
setConnected(false);
} }
}, [socket]);
const disconnect = () => { const send = useCallback((event, data) => {
socket.disconnect(); if (socket) {
socket.emit(event, data);
} else {
console.error('Cannot send event, socket not connected');
} }
}, [socket]);
const addListener = (event, callback) => { const on = useCallback((event, callback) => {
socket.on(event, callback); if (socket) {
socket.on(event, callback);
} }
return () => {
if (socket) socket.off(event, callback);
};
}, [socket]);
return ( useEffect(() => {
<SocketContext.Provider value={{connect, disconnect, send, addListener}}> return () => {
{children} if (socket) socket.disconnect();
</SocketContext.Provider> };
) }, [socket]);
}
return (
<SocketContext.Provider value={{ connected, connect, disconnect, send, on, socket }}>
{children}
</SocketContext.Provider>
);
};

View File

@ -1,5 +1,6 @@
import "./styles.sass"; import "./styles.sass";
import {StateContext} from "@/common/contexts/StateContext"; import {StateContext} from "@/common/contexts/StateContext";
import {SocketContext} from "@/common/contexts/SocketContext";
import {useContext, useState, useEffect, useRef} from "react"; import {useContext, useState, useEffect, useRef} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider"; import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@ -7,10 +8,59 @@ import {faMessage} from "@fortawesome/free-solid-svg-icons";
export const Game = () => { export const Game = () => {
const {setCurrentState} = useContext(StateContext); 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 [inputValue, setInputValue] = useState("");
const [connectedUsers, setConnectedUsers] = useState([]);
const [username, setUsername] = useState("");
const messageEndRef = useRef(null); 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(() => { useEffect(() => {
messageEndRef.current?.scrollIntoView({behavior: "smooth"}); messageEndRef.current?.scrollIntoView({behavior: "smooth"});
@ -18,7 +68,12 @@ export const Game = () => {
const handleSendMessage = () => { const handleSendMessage = () => {
if (inputValue.trim()) { if (inputValue.trim()) {
setMessages([...messages, {sender: "User", text: inputValue}]); const messageData = {
text: inputValue,
sender: username
};
send("send-message", messageData);
setMessages(prev => [...prev, messageData]);
setInputValue(""); setInputValue("");
} }
}; };
@ -47,9 +102,15 @@ export const Game = () => {
</div> </div>
<div className="chat-messages"> <div className="chat-messages">
{messages.map((message, index) => ( {messages.map((message, index) => (
<div key={index} className="message"> <div key={index} className={`message ${message.system ? 'system-message' : ''}`}>
<span className="message-sender">{message.sender}:</span> {message.system ? (
<span className="message-text">{message.text}</span> <span className="message-text system">{message.text}</span>
) : (
<>
<span className="message-sender">{message.sender}:</span>
<span className="message-text">{message.text}</span>
</>
)}
</div> </div>
))} ))}
<div ref={messageEndRef}></div> <div ref={messageEndRef}></div>

View File

@ -210,6 +210,16 @@
.message-text .message-text
margin-left: 5px 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 .chat-input
display: flex display: flex
padding: 15px padding: 15px

View File

@ -5,35 +5,63 @@ import CreateForm from "@/pages/Home/components/CreateForm";
import {faArrowRightToBracket, faPlusSquare} from "@fortawesome/free-solid-svg-icons"; import {faArrowRightToBracket, faPlusSquare} from "@fortawesome/free-solid-svg-icons";
import {StateContext} from "@/common/contexts/StateContext"; import {StateContext} from "@/common/contexts/StateContext";
import {SocketContext} from "@/common/contexts/SocketContext"; import {SocketContext} from "@/common/contexts/SocketContext";
import {useContext, useState} from "react"; import {useContext, useState, useEffect} from "react";
export const Home = () => { export const Home = () => {
const {setCurrentState} = useContext(StateContext); const {setCurrentState} = useContext(StateContext);
const {connect, send} = useContext(SocketContext); const {connect, send, on} = useContext(SocketContext);
const [homeState, setHomeState] = useState("initial"); // initial, joining, creating 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 = () => { const handleJoinClick = () => {
setHomeState("joining"); setHomeState("joining");
setError("");
}; };
const handleCreateClick = () => { const handleCreateClick = () => {
setHomeState("creating"); setHomeState("creating");
setError("");
}; };
const handleBack = () => { const handleBack = () => {
setHomeState("initial"); setHomeState("initial");
setError("");
}; };
const handleJoinSubmit = (name, roomCode) => { const handleJoinSubmit = (name, roomCode) => {
connect(); send("join-room", {roomId: roomCode, name});
send("joinRoom", {name, roomCode});
setCurrentState("Game");
}; };
const handleCreateSubmit = (name) => { const handleCreateSubmit = (name) => {
connect(); send("create-room", {name});
send("createRoom", {name});
setCurrentState("Game");
}; };
return ( return (
@ -55,13 +83,13 @@ export const Home = () => {
<div className={`form-container ${homeState === 'joining' ? 'active' : ''}`}> <div className={`form-container ${homeState === 'joining' ? 'active' : ''}`}>
{homeState === 'joining' && ( {homeState === 'joining' && (
<JoinForm onSubmit={handleJoinSubmit} onBack={handleBack} /> <JoinForm onSubmit={handleJoinSubmit} onBack={handleBack} error={error} />
)} )}
</div> </div>
<div className={`form-container ${homeState === 'creating' ? 'active' : ''}`}> <div className={`form-container ${homeState === 'creating' ? 'active' : ''}`}>
{homeState === 'creating' && ( {homeState === 'creating' && (
<CreateForm onSubmit={handleCreateSubmit} onBack={handleBack} /> <CreateForm onSubmit={handleCreateSubmit} onBack={handleBack} error={error} />
)} )}
</div> </div>
</div> </div>

View File

@ -1,13 +1,19 @@
import "@/common/styles/forms.sass"; import "@/common/styles/forms.sass";
import {useState} from "react"; import {useState, useEffect} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faArrowLeft, faArrowRightToBracket} from "@fortawesome/free-solid-svg-icons"; import {faArrowLeft, faArrowRightToBracket} from "@fortawesome/free-solid-svg-icons";
import FormInput from "@/common/components/FormInput"; import FormInput from "@/common/components/FormInput";
export const JoinForm = ({onSubmit, onBack}) => { export const JoinForm = ({onSubmit, onBack, error}) => {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [roomCode, setRoomCode] = useState(""); const [roomCode, setRoomCode] = useState("");
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
useEffect(() => {
if (error) {
setErrors(prev => ({...prev, roomCode: error}));
}
}, [error]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -8,7 +8,7 @@ module.exports.connectUserToRoom = (roomId, user) => {
} else { } else {
rooms[roomId] = {members: [{...user, creator: true}], settings: {}}; 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) => { module.exports.getUserRoom = (userId) => {
@ -18,6 +18,21 @@ module.exports.getUserRoom = (userId) => {
if (memberIndex !== -1) return roomId; 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) => { module.exports.disconnectUser = (userId) => {
@ -26,12 +41,17 @@ module.exports.disconnectUser = (userId) => {
const memberIndex = room.members.findIndex(member => member.id === userId); const memberIndex = room.members.findIndex(member => member.id === userId);
if (memberIndex !== -1) { if (memberIndex !== -1) {
if (room.members[memberIndex].creator) { if (room.members[memberIndex].creator && room.members.length > 1) {
if (room.members.length > 1) room.members[1].creator = true; room.members[1].creator = true;
} }
room.members.splice(memberIndex, 1); 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; break;
} }

View File

@ -1,7 +1,8 @@
const {connectUserToRoom, roomExists, disconnectUser, getUserRoom} = require("../controller/room"); const {connectUserToRoom, roomExists, disconnectUser, getUserRoom, getRoomUsers} = require("../controller/room");
module.exports = (socket) => { module.exports = (socket) => {
let currentRoomId = null; let currentRoomId = null;
let currentUser = null;
socket.on("disconnect", () => { socket.on("disconnect", () => {
const roomId = getUserRoom(socket.id); const roomId = getUserRoom(socket.id);
@ -14,9 +15,10 @@ module.exports = (socket) => {
if (currentRoomId) return socket.emit("already-in-room", currentRoomId); if (currentRoomId) return socket.emit("already-in-room", currentRoomId);
if (roomExists(roomId.toString())) { 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.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); socket.emit("room-joined", roomId);
currentRoomId = roomId; currentRoomId = roomId;
} else { } else {
@ -27,10 +29,31 @@ module.exports = (socket) => {
socket.on("create-room", ({name}) => { socket.on("create-room", ({name}) => {
if (!name) return socket.emit("room-name-required"); if (!name) return socket.emit("room-name-required");
const roomId = Math.random().toString(36).substring(7); 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.join(roomId);
socket.emit("room-created", roomId); socket.emit("room-created", roomId);
currentRoomId = 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);
}
});
} }