Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,28 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.2",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"nodemon": "^3.1.11",
"prettier": "^3.3.2"
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"body-parser": "^2.2.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"http": "^0.0.1-security",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"sequelize": "^6.37.7",
"socket.io": "^4.8.1",
"uuid": "^13.0.0",
"ws": "^8.18.3"
}
}
163 changes: 163 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* eslint-disable */
import { useEffect, useState } from "react";
import { MessagesList } from "./components/MessagesList.jsx";

const socket = new WebSocket("ws://localhost:3232");

export const App = () => {
const [name, setName] = useState(localStorage.getItem("Name") || "");
const [room, setRoom] = useState("");
const [newRoomName, setNewRoomName] = useState("");
const [rooms, setRooms] = useState([]);
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);

useEffect(() => {
socket.onmessage = (event) => {
const data = JSON.parse(event.data);

if (data.type === "rooms") {
setRooms(data.list);
}

if (data.type === "history") {
setMessages(data.messages);
}

if (data.type === "message") {
setMessages((prev) => [...prev, data.message]);
}
};
}, []);

function saveName() {
if (!name.trim()) return;
localStorage.setItem("Name", name);
}

function joinRoom(r) {
setRoom(r);
setMessages([]);

socket.send(
JSON.stringify({
type: "join_room",
name: r,
}),
);
}

function createRoom() {
if (!room.trim()) return;

socket.send(
JSON.stringify({
type: "create_room",
name: room,
}),
);
}

function deleteRoom() {
if (!room) return;

socket.send(
JSON.stringify({
type: "delete_room",
name: room,
}),
);

setRoom("");
setMessages([]);
}

function renameRoom() {
if (!room || !newRoomName.trim()) return;

socket.send(
JSON.stringify({
type: "rename_room",
oldName: room,
newName: newRoomName,
}),
);

setRoom(newRoomName);
setNewRoomName("");
}

function sendMessage() {
if (!message.trim()) return;

socket.send(
JSON.stringify({
type: "message",
author: name,
text: message,
}),
);

setMessage("");
}

return (
<div>
{!name && (
<div>
<input
placeholder="Your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={saveName}>Save</button>
</div>
)}

{name && (
<div>
<h3>Rooms</h3>
{rooms.map((r) => (
<button key={r} onClick={() => joinRoom(r)}>
{r}
</button>
))}

<div>
<input
placeholder="Room name"
value={room}
onChange={(e) => setRoom(e.target.value)}
/>
<button onClick={createRoom}>Create</button>
<button onClick={deleteRoom}>Delete</button>
</div>
Comment on lines +126 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block of controls has a state management issue. It uses the room state for the input field's value, which is also meant to track the currently joined room. This leads to incorrect behavior for the Create, Delete, and Rename buttons.

For example:

  1. A user joins 'Room A'. The room state becomes 'Room A'.
  2. The user then types 'Room B' into this input field. The room state is now 'Room B'.
  3. If the user clicks 'Delete', the app will try to delete 'Room B', not 'Room A' which they originally joined.

To fix this, consider using a separate state variable for this input field (e.g., roomNameInput). The createRoom function should use this new state, while deleteRoom and renameRoom should operate on the room state, which should only represent the currently joined room.


{room && (
<div>
<input
placeholder="New room name"
value={newRoomName}
onChange={(e) => setNewRoomName(e.target.value)}
/>
<button onClick={renameRoom}>Rename</button>
</div>
)}
</div>
)}

{room && (
<div>
<input
placeholder="Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={sendMessage}>Send</button>

<MessagesList list={messages} />
</div>
)}
</div>
);
};
12 changes: 12 additions & 0 deletions src/components/MessagesList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const MessagesList = ({ list }) => {
return (
<div>
{list.map((m) => (
<p key={m.id}>
<b>{m.author}</b>: {m.text}
<small> ({new Date(m.time).toLocaleTimeString()})</small>
</p>
))}
</div>
);
};
87 changes: 87 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import express from 'express';
import { WebSocketServer } from 'ws';
import http from 'http';

import {
rooms,
createRoom,
renameRoom,
deleteRoom,
joinRoom,
} from './websocket.js';

const app = express();
const server = http.createServer(app);

app.use(express.static('src'));

const wss = new WebSocketServer({ server });

function broadcastRoomList() {
const list = Object.keys(rooms);
const message = JSON.stringify({ type: 'rooms', list });

wss.clients.forEach((client) => {
client.send(message);
});
}

wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const data = JSON.parse(raw);

if (data.type === 'create_room') {
createRoom(data.name);
broadcastRoomList();
}

if (data.type === 'rename_room') {
renameRoom(data.oldName, data.newName);
broadcastRoomList();
}

if (data.type === 'delete_room') {
deleteRoom(data.name);
broadcastRoomList();
}

if (data.type === 'join_room') {
joinRoom(data.name, ws);

ws.send(
JSON.stringify({
type: 'history',
messages: rooms[data.name]?.messages || [],
}),
);
}

if (data.type === 'message') {
const msg = {
id: Date.now(),
author: data.author,
text: data.text,
time: new Date().toISOString(),
};

Object.values(rooms).forEach((room) => {
if (room.users.has(ws)) {
room.messages.push(msg);

room.users.forEach((client) => {
client.send(
JSON.stringify({
type: 'message',
message: msg,
}),
);
});
}
});
}
});

broadcastRoomList();
});

export { server };
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
'use strict';

import { server } from './db.js';
import dotenv from 'dotenv';

dotenv.config();

const PORT = process.env.PORT || 3232;

server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);

Check failure on line 11 in src/index.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected console statement
});
9 changes: 9 additions & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from './App';

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
50 changes: 50 additions & 0 deletions src/websocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export const rooms = {};

export function createRoom(name) {
if (!name || rooms[name]) {
return false;
}

rooms[name] = {
name,
users: new Set(),
messages: [],
};

return true;
}

export function renameRoom(oldName, newName) {
if (!rooms[oldName] || rooms[newName]) {
return false;
}

rooms[newName] = rooms[oldName];
rooms[newName].name = newName;
delete rooms[oldName];

return true;
}

export function deleteRoom(name) {
if (!rooms[name]) {
return false;
}

delete rooms[name];
return true;
}

export function joinRoom(roomName, client) {
if (!rooms[roomName]) {
return false;
}

// remove client from all rooms
Object.values(rooms).forEach((room) => {
room.users.delete(client);
});

rooms[roomName].users.add(client);
return true;
}
Loading