diff --git a/src/client/index.html b/src/client/index.html
new file mode 100644
index 000000000..e49457f7d
--- /dev/null
+++ b/src/client/index.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Node Chat
+
+
+
+
+
Node Chat
+
+
+
+
+
+
+
+
Connecting...
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/main.js b/src/client/main.js
new file mode 100644
index 000000000..196954b55
--- /dev/null
+++ b/src/client/main.js
@@ -0,0 +1,234 @@
+/* eslint-env browser */
+
+import { io } from 'socket.io-client';
+import './style.css';
+
+const connectionStatusEl = document.querySelector('#status');
+const usernameInputEl = document.querySelector('#username-input');
+const saveBtnEl = document.querySelector('#save-btn');
+const currentUserEl = document.querySelector('#current-user');
+
+const roomInputEl = document.querySelector('#room-input');
+const createRoomBtnEl = document.querySelector('#create-room-btn');
+const roomsListEl = document.querySelector('#rooms-list');
+const currentRoomEl = document.querySelector('#current-room');
+
+const messagesListEl = document.querySelector('#messages-list');
+const messageInputEl = document.querySelector('#message-input');
+const sendBtnEl = document.querySelector('#send-btn');
+
+const socket = io('http://localhost:3000');
+
+let currentUsername = localStorage.getItem('username') || '';
+let currentRoomId = null;
+let rooms = [];
+
+function renderRooms() {
+ roomsListEl.innerHTML = '';
+
+ rooms.forEach((room) => {
+ const li = document.createElement('li');
+
+ li.className = 'room-item';
+
+ const joinButton = document.createElement('button');
+
+ joinButton.textContent = room.name;
+ joinButton.type = 'button';
+ joinButton.className = 'room-join-btn';
+
+ if (room.id === currentRoomId) {
+ joinButton.disabled = true;
+ }
+
+ joinButton.addEventListener('click', () => {
+ socket.emit('room:join', { roomId: room.id });
+ });
+
+ const renameButton = document.createElement('button');
+
+ renameButton.textContent = 'Rename';
+ renameButton.type = 'button';
+ renameButton.className = 'room-action-btn';
+
+ renameButton.addEventListener('click', () => {
+ const newName = prompt('Enter new room name', room.name);
+
+ if (!newName) {
+ return;
+ }
+
+ socket.emit('room:rename', {
+ roomId: room.id,
+ name: newName.trim(),
+ });
+ });
+
+ li.append(joinButton, renameButton);
+
+ if (room.id !== 'general') {
+ const deleteButton = document.createElement('button');
+
+ deleteButton.textContent = 'Delete';
+ deleteButton.type = 'button';
+ deleteButton.className = 'room-action-btn';
+
+ deleteButton.addEventListener('click', () => {
+ const shouldDelete = confirm(`Delete room "${room.name}"?`);
+
+ if (!shouldDelete) {
+ return;
+ }
+
+ socket.emit('room:delete', { roomId: room.id });
+ });
+
+ li.append(deleteButton);
+ }
+
+ roomsListEl.append(li);
+ });
+}
+
+function updateCurrentRoomText() {
+ const activeRoom = rooms.find((room) => room.id === currentRoomId);
+
+ currentRoomEl.textContent = activeRoom
+ ? `Current room: ${activeRoom.name}`
+ : 'Current room: none';
+}
+
+function renderMessage(message) {
+ const li = document.createElement('li');
+
+ li.className = 'message-item';
+ li.textContent = `[${message.time}] ${message.author}: ${message.text}`;
+
+ messagesListEl.append(li);
+}
+
+function renderMessages(messages) {
+ messagesListEl.innerHTML = '';
+ messages.forEach(renderMessage);
+}
+
+function setUser(username) {
+ currentUsername = username;
+ localStorage.setItem('username', username);
+
+ currentUserEl.textContent = `You are: ${username}`;
+
+ usernameInputEl.style.display = 'none';
+ saveBtnEl.style.display = 'none';
+
+ socket.emit('user:set', { username });
+}
+
+const savedUsername = localStorage.getItem('username');
+
+if (savedUsername) {
+ setUser(savedUsername);
+}
+
+socket.on('connect', () => {
+ connectionStatusEl.textContent = `Connected: ${socket.id}`;
+
+ if (currentUsername) {
+ socket.emit('user:set', { username: currentUsername });
+ }
+
+ socket.emit('rooms:get');
+});
+
+socket.on('disconnect', () => {
+ connectionStatusEl.textContent = 'Disconnected';
+});
+
+socket.on('rooms:list', (serverRooms) => {
+ rooms = serverRooms;
+
+ const roomStillExists = rooms.some((room) => room.id === currentRoomId);
+
+ if (!roomStillExists) {
+ currentRoomId = null;
+ messagesListEl.innerHTML = '';
+ }
+
+ renderRooms();
+ updateCurrentRoomText();
+});
+
+socket.on('room:joined', ({ roomId }) => {
+ currentRoomId = roomId;
+ renderRooms();
+ updateCurrentRoomText();
+});
+
+socket.on('room:history', ({ roomId, messages }) => {
+ if (roomId !== currentRoomId) {
+ return;
+ }
+
+ renderMessages(messages);
+});
+
+socket.on('message:new', ({ roomId, message }) => {
+ if (roomId !== currentRoomId) {
+ return;
+ }
+
+ renderMessage(message);
+});
+
+socket.on('error:message', ({ message }) => {
+ alert(message);
+});
+
+saveBtnEl.addEventListener('click', () => {
+ const username = usernameInputEl.value.trim();
+
+ if (!username) {
+ return;
+ }
+
+ setUser(username);
+});
+
+createRoomBtnEl.addEventListener('click', () => {
+ const roomName = roomInputEl.value.trim();
+
+ if (!roomName) {
+ return;
+ }
+
+ socket.emit('room:create', { name: roomName });
+ roomInputEl.value = '';
+});
+
+roomInputEl.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ createRoomBtnEl.click();
+ }
+});
+
+sendBtnEl.addEventListener('click', () => {
+ const text = messageInputEl.value.trim();
+
+ if (!text || !currentUsername || !currentRoomId) {
+ return;
+ }
+
+ socket.emit('message:send', {
+ roomId: currentRoomId,
+ author: currentUsername,
+ text,
+ });
+
+ messageInputEl.value = '';
+});
+
+messageInputEl.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ sendBtnEl.click();
+ }
+});
diff --git a/src/client/style.css b/src/client/style.css
new file mode 100644
index 000000000..cd3fef5a9
--- /dev/null
+++ b/src/client/style.css
@@ -0,0 +1,64 @@
+body {
+ margin: 0;
+ font-family: Arial, sans-serif;
+ background: #f4f4f4;
+}
+
+#app {
+ max-width: 700px;
+ margin: 40px auto;
+ padding: 24px;
+ background: #fff;
+ border-radius: 12px;
+}
+
+#user-block,
+#room-form,
+#message-form {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+input,
+button {
+ padding: 8px 12px;
+ font-size: 16px;
+}
+
+#rooms-list,
+#messages-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#messages-list li {
+ margin-bottom: 8px;
+}
+
+#room-input,
+#message-input {
+ flex: 1;
+}
+
+.room-item {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.room-join-btn {
+ flex: 1;
+ text-align: left;
+}
+
+.room-action-btn {
+ white-space: nowrap;
+}
+
+.message-item {
+ padding: 10px 12px;
+ background: #f1f1f1;
+ border-radius: 8px;
+}
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index ad9a93a7c..000000000
--- a/src/index.js
+++ /dev/null
@@ -1 +0,0 @@
-'use strict';
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 000000000..c28436036
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,212 @@
+const http = require('http');
+const { Server } = require('socket.io');
+
+const server = http.createServer();
+
+const io = new Server(server, {
+ cors: {
+ origin: 'http://localhost:5173',
+ methods: ['GET', 'POST'],
+ },
+});
+
+const PORT = 3000;
+
+const rooms = [
+ {
+ id: 'general',
+ name: 'General',
+ },
+];
+
+const messages = [];
+
+function getCurrentTime() {
+ return new Date().toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+}
+
+function broadcastRooms() {
+ io.emit('rooms:list', rooms);
+}
+
+function getRoomMessages(roomId) {
+ return messages.filter((message) => message.roomId === roomId);
+}
+
+io.on('connection', (socket) => {
+ socket.on('user:set', ({ username }) => {
+ socket.username = username;
+ });
+
+ socket.on('rooms:get', () => {
+ socket.emit('rooms:list', rooms);
+ });
+
+ socket.on('room:create', ({ name }) => {
+ const trimmedName = name.trim();
+
+ if (!trimmedName) {
+ return;
+ }
+
+ const existingRoom = rooms.find(
+ (room) => room.name.toLowerCase() === trimmedName.toLowerCase(),
+ );
+
+ if (existingRoom) {
+ socket.emit('error:message', {
+ message: 'Room with this name already exists',
+ });
+
+ return;
+ }
+
+ const newRoom = {
+ id: Date.now().toString(),
+ name: trimmedName,
+ };
+
+ rooms.push(newRoom);
+ broadcastRooms();
+ });
+
+ socket.on('room:rename', ({ roomId, name }) => {
+ const trimmedName = name.trim();
+ const room = rooms.find((item) => item.id === roomId);
+
+ if (!room) {
+ socket.emit('error:message', { message: 'Room not found' });
+
+ return;
+ }
+
+ if (!trimmedName) {
+ socket.emit('error:message', { message: 'Room name cannot be empty' });
+
+ return;
+ }
+
+ const existingRoom = rooms.find(
+ (item) =>
+ item.id !== roomId &&
+ item.name.toLowerCase() === trimmedName.toLowerCase(),
+ );
+
+ if (existingRoom) {
+ socket.emit('error:message', {
+ message: 'Room with this name already exists',
+ });
+
+ return;
+ }
+
+ room.name = trimmedName;
+ broadcastRooms();
+ });
+
+ socket.on('room:delete', async ({ roomId }) => {
+ if (roomId === 'general') {
+ socket.emit('error:message', {
+ message: 'General room cannot be deleted',
+ });
+
+ return;
+ }
+
+ const roomIndex = rooms.findIndex((room) => room.id === roomId);
+
+ if (roomIndex === -1) {
+ socket.emit('error:message', { message: 'Room not found' });
+
+ return;
+ }
+
+ rooms.splice(roomIndex, 1);
+
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
+ if (messages[index].roomId === roomId) {
+ messages.splice(index, 1);
+ }
+ }
+
+ const deletedRoomSockets = await io.in(roomId).fetchSockets();
+
+ for (const currentSocket of deletedRoomSockets) {
+ currentSocket.leave(roomId);
+ currentSocket.join('general');
+ currentSocket.currentRoomId = 'general';
+
+ currentSocket.emit('room:joined', { roomId: 'general' });
+
+ currentSocket.emit('room:history', {
+ roomId: 'general',
+ messages: getRoomMessages('general'),
+ });
+ }
+
+ broadcastRooms();
+ });
+
+ socket.on('room:join', ({ roomId }) => {
+ const room = rooms.find((item) => item.id === roomId);
+
+ if (!room) {
+ socket.emit('error:message', { message: 'Room not found' });
+
+ return;
+ }
+
+ if (socket.currentRoomId) {
+ socket.leave(socket.currentRoomId);
+ }
+
+ socket.join(roomId);
+ socket.currentRoomId = roomId;
+
+ socket.emit('room:joined', { roomId });
+
+ socket.emit('room:history', {
+ roomId,
+ messages: getRoomMessages(roomId),
+ });
+ });
+
+ socket.on('message:send', ({ roomId, author, text }) => {
+ const room = rooms.find((item) => item.id === roomId);
+
+ if (!room) {
+ socket.emit('error:message', { message: 'Room not found' });
+
+ return;
+ }
+
+ if (!author || !text) {
+ return;
+ }
+
+ const message = {
+ id: Date.now().toString(),
+ roomId,
+ author,
+ text,
+ time: getCurrentTime(),
+ };
+
+ messages.push(message);
+
+ io.to(roomId).emit('message:new', {
+ roomId,
+ message,
+ });
+ });
+
+ socket.on('disconnect', () => {});
+});
+
+server.listen(PORT, () => {
+ // eslint-disable-next-line no-console
+ console.log(`Server is running on http://localhost:${PORT}`);
+});