diff --git a/index.html b/index.html
new file mode 100644
index 000000000..ac24d7997
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Document
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 4f64337fe..8cf87fa65 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 000000000..e0a39cfa7
--- /dev/null
+++ b/src/App.jsx
@@ -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 (
+
+ {!name && (
+
+ setName(e.target.value)}
+ />
+
+
+ )}
+
+ {name && (
+
+
Rooms
+ {rooms.map((r) => (
+
+ ))}
+
+
+ setRoom(e.target.value)}
+ />
+
+
+
+
+ {room && (
+
+ setNewRoomName(e.target.value)}
+ />
+
+
+ )}
+
+ )}
+
+ {room && (
+
+ setMessage(e.target.value)}
+ />
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/MessagesList.jsx b/src/components/MessagesList.jsx
new file mode 100644
index 000000000..d2acd6032
--- /dev/null
+++ b/src/components/MessagesList.jsx
@@ -0,0 +1,12 @@
+export const MessagesList = ({ list }) => {
+ return (
+
+ {list.map((m) => (
+
+ {m.author}: {m.text}
+ ({new Date(m.time).toLocaleTimeString()})
+
+ ))}
+
+ );
+};
diff --git a/src/db.js b/src/db.js
new file mode 100644
index 000000000..9dea12c10
--- /dev/null
+++ b/src/db.js
@@ -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 };
diff --git a/src/index.js b/src/index.js
index ad9a93a7c..e12005d55 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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}`);
+});
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 000000000..dcda26a1c
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { App } from './App';
+
+ReactDOM.createRoot(document.getElementById("root")).render(
+
+
+
+);
diff --git a/src/websocket.js b/src/websocket.js
new file mode 100644
index 000000000..8211e8bf5
--- /dev/null
+++ b/src/websocket.js
@@ -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;
+}