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; +}