diff --git a/src/index.js b/src/index.js
index ad9a93a7c..4e2ffef06 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1 +1,156 @@
'use strict';
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { Server } = require('socket.io');
+
+const PORT = process.env.PORT || 3001;
+const PUBLIC_DIR = path.join(__dirname, 'public');
+
+const rooms = new Map();
+const DEFAULT_ROOM = 'general';
+
+rooms.set(DEFAULT_ROOM, {
+ id: DEFAULT_ROOM,
+ name: 'General',
+ messages: [],
+});
+
+function createRoom(id, roomName) {
+ const room = { id, name: roomName, messages: [] };
+
+ rooms.set(id, room);
+
+ return room;
+}
+
+function getRoomsData() {
+ return Array.from(rooms.values()).map((room) => ({
+ id: room.id,
+ name: room.name,
+ }));
+}
+
+const server = http.createServer((req, res) => {
+ const filePath = path.join(
+ PUBLIC_DIR,
+ req.url === '/' ? 'index.html' : req.url,
+ );
+
+ const ext = path.extname(filePath);
+ const contentTypes = {
+ '.html': 'text/html',
+ '.js': 'application/javascript',
+ '.jsx': 'application/javascript',
+ '.css': 'text/css',
+ };
+
+ const contentType = contentTypes[ext] || 'text/plain';
+
+ fs.readFile(filePath, (err, data) => {
+ if (err) {
+ res.writeHead(404);
+ res.end('Not found');
+
+ return;
+ }
+ res.writeHead(200, { 'Content-Type': contentType });
+ res.end(data);
+ });
+});
+
+const io = new Server(server);
+
+io.on('connection', (socket) => {
+ let currentRoom = null;
+ let username = null;
+
+ socket.emit('rooms:list', getRoomsData());
+
+ socket.on('user:set', (userName) => {
+ username = userName;
+ });
+
+ socket.on('room:join', (roomId) => {
+ if (!rooms.has(roomId)) {
+ socket.emit('error', `Room "${roomId}" does not exist`);
+
+ return;
+ }
+
+ if (currentRoom) {
+ socket.leave(currentRoom);
+ }
+
+ currentRoom = roomId;
+ socket.join(roomId);
+
+ const room = rooms.get(roomId);
+
+ socket.emit('room:history', { roomId, messages: room.messages });
+ });
+
+ socket.on('room:create', ({ id, name: roomName }) => {
+ if (rooms.has(id)) {
+ socket.emit('error', `Room "${id}" already exists`);
+
+ return;
+ }
+
+ const room = createRoom(id, roomName);
+
+ io.emit('rooms:list', getRoomsData());
+ socket.emit('room:created', { id: room.id, name: room.name });
+ });
+
+ socket.on('room:rename', ({ id, name: newName }) => {
+ if (!rooms.has(id)) {
+ socket.emit('error', `Room "${id}" does not exist`);
+
+ return;
+ }
+
+ rooms.get(id).name = newName;
+ io.emit('rooms:list', getRoomsData());
+ });
+
+ socket.on('room:delete', (id) => {
+ if (id === DEFAULT_ROOM) {
+ socket.emit('error', 'Cannot delete the default room');
+
+ return;
+ }
+
+ if (!rooms.has(id)) {
+ socket.emit('error', `Room "${id}" does not exist`);
+
+ return;
+ }
+
+ rooms.delete(id);
+ io.emit('rooms:list', getRoomsData());
+ io.to(id).emit('room:deleted', id);
+ });
+
+ socket.on('message:send', ({ roomId, text }) => {
+ if (!rooms.has(roomId)) {
+ return;
+ }
+
+ const message = {
+ id: Date.now() + Math.random().toString(36).slice(2),
+ author: username || 'Anonymous',
+ text,
+ time: new Date().toISOString(),
+ };
+
+ rooms.get(roomId).messages.push(message);
+ io.to(roomId).emit('message:new', { roomId, message });
+ });
+});
+
+server.listen(PORT, () => {
+ // eslint-disable-next-line no-console
+ console.log(`Chat server running at http://localhost:${PORT}`);
+});
diff --git a/src/public/app.jsx b/src/public/app.jsx
new file mode 100644
index 000000000..d397eba25
--- /dev/null
+++ b/src/public/app.jsx
@@ -0,0 +1,356 @@
+const { useState, useEffect, useRef, useCallback } = React;
+
+const socket = io();
+
+// ── Helpers ──────────────────────────────────────────────────────────────────
+
+function formatTime(iso) {
+ const d = new Date(iso);
+
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+}
+
+function slugify(str) {
+ return (
+ str
+ .toLowerCase()
+ .replace(/\s+/g, '-')
+ .replace(/[^a-z0-9-]/g, '')
+ .slice(0, 32) || 'room'
+ );
+}
+
+function Modal({
+ title,
+ initialValue = '',
+ onConfirm,
+ onCancel,
+ placeholder,
+ requireInput = false,
+}) {
+ const [value, setValue] = useState(initialValue);
+ const inputRef = useRef(null);
+ const confirmDisabled = requireInput && !value.trim();
+
+ useEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ function handleKeyDown(e) {
+ if (e.key === 'Enter' && !confirmDisabled) onConfirm(value.trim());
+ if (e.key === 'Escape') onCancel();
+ }
+
+ return (
+
+
e.stopPropagation()}>
+
{title}
+
setValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ />
+
+
+
+
+
+
+ );
+}
+
+function LoginScreen({ onLogin }) {
+ const [name, setName] = useState('');
+
+ function handleSubmit(e) {
+ e.preventDefault();
+ const trimmed = name.trim();
+ if (!trimmed) return;
+ onLogin(trimmed);
+ }
+
+ return (
+
+
💬 Node Chat
+
Enter your name to start chatting
+
+
+ );
+}
+
+// ── Room Item ─────────────────────────────────────────────────────────────────
+
+function RoomItem({ room, active, onJoin, onRename, onDelete }) {
+ return (
+ onJoin(room.id)}
+ >
+
# {room.name}
+
e.stopPropagation()}>
+
+
+
+
+ );
+}
+
+// ── Main App ──────────────────────────────────────────────────────────────────
+
+function App() {
+ const [username, setUsername] = useState(
+ () => localStorage.getItem('chat_username') || '',
+ );
+ const [rooms, setRooms] = useState([]);
+ const [currentRoomId, setCurrentRoomId] = useState(null);
+ // messages: { [roomId]: Message[] }
+ const [messages, setMessages] = useState({});
+ const [inputText, setInputText] = useState('');
+ const [modal, setModal] = useState(null); // null | { type, room? }
+ const messagesEndRef = useRef(null);
+
+ // ── Actions ──
+
+ const joinRoom = useCallback((roomId) => {
+ setCurrentRoomId(roomId);
+ socket.emit('room:join', roomId);
+ }, []);
+
+ // ── Socket events ──
+
+ useEffect(() => {
+ socket.on('rooms:list', (list) => {
+ setRooms(list);
+ });
+
+ socket.on('room:history', ({ roomId, messages: msgs }) => {
+ setMessages((prev) => ({ ...prev, [roomId]: msgs }));
+ });
+
+ socket.on('message:new', ({ roomId, message }) => {
+ setMessages((prev) => ({
+ ...prev,
+ [roomId]: [...(prev[roomId] || []), message],
+ }));
+ });
+
+ socket.on('room:deleted', (roomId) => {
+ setCurrentRoomId((prev) => (prev === roomId ? null : prev));
+ setMessages((prev) => {
+ const copy = { ...prev };
+ delete copy[roomId];
+ return copy;
+ });
+ });
+
+ socket.on('room:created', ({ id }) => {
+ joinRoom(id);
+ });
+
+ return () => {
+ socket.off('rooms:list');
+ socket.off('room:history');
+ socket.off('message:new');
+ socket.off('room:deleted');
+ socket.off('room:created');
+ };
+ }, [joinRoom]);
+
+ // Sync username to server on login and after page reload
+ useEffect(() => {
+ if (!username) return;
+
+ socket.emit('user:set', username);
+ joinRoom('general');
+ }, [username, joinRoom]);
+
+ // Auto-scroll to bottom when messages update
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages, currentRoomId]);
+
+ function handleLogin(newUsername) {
+ localStorage.setItem('chat_username', newUsername);
+ setUsername(newUsername);
+ }
+
+ function sendMessage(e) {
+ e.preventDefault();
+ const text = inputText.trim();
+ if (!text || !currentRoomId) return;
+ socket.emit('message:send', { roomId: currentRoomId, text });
+ setInputText('');
+ }
+
+ function handleCreateRoom(name) {
+ if (!name) return;
+ const id = slugify(name) + '-' + Date.now().toString(36);
+ socket.emit('room:create', { id, name });
+ setModal(null);
+ }
+
+ function handleRenameRoom(name) {
+ if (!name || !modal?.room) return;
+ socket.emit('room:rename', { id: modal.room.id, name });
+ setModal(null);
+ }
+
+ function handleDeleteRoom(room) {
+ socket.emit('room:delete', room.id);
+ setModal(null);
+ }
+
+ // ── Render ──
+
+ if (!username) {
+ return ;
+ }
+
+ const currentMessages = messages[currentRoomId] || [];
+ const currentRoom = rooms.find((r) => r.id === currentRoomId);
+
+ return (
+
+ {/* Sidebar */}
+
+
+
💬 Node Chat
+
+ Logged in as {username}
+
+
+
+
+ {rooms.map((room) => (
+ setModal({ type: 'rename', room: r })}
+ onDelete={(r) => setModal({ type: 'delete', room: r })}
+ />
+ ))}
+
+
+
+
+
+
+
+ {/* Chat main */}
+
+
+ {currentRoom ? `# ${currentRoom.name}` : 'Select a room'}
+
+
+ {currentRoomId ? (
+ <>
+
+ {currentMessages.map((msg) => (
+
+
+ {msg.author}
+ {formatTime(msg.time)}
+
+
{msg.text}
+
+ ))}
+
+
+
+
+ >
+ ) : (
+
+ Pick a room from the sidebar to start chatting
+
+ )}
+
+
+ {/* Modals */}
+ {modal?.type === 'create' && (
+
setModal(null)}
+ />
+ )}
+
+ {modal?.type === 'rename' && (
+ setModal(null)}
+ />
+ )}
+
+ {modal?.type === 'delete' && (
+ handleDeleteRoom(modal.room)}
+ onCancel={() => setModal(null)}
+ requireInput
+ />
+ )}
+
+ );
+}
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/src/public/index.html b/src/public/index.html
new file mode 100644
index 000000000..ddafcf8b5
--- /dev/null
+++ b/src/public/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Node Chat
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/public/styles.css b/src/public/styles.css
new file mode 100644
index 000000000..9ccfc412b
--- /dev/null
+++ b/src/public/styles.css
@@ -0,0 +1,420 @@
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ background: #1a1a2e;
+ color: #e0e0e0;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* ── Login Screen ── */
+.login-screen {
+ background: #16213e;
+ border-radius: 12px;
+ padding: 40px;
+ width: 360px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
+ text-align: center;
+}
+
+.login-screen h1 {
+ font-size: 28px;
+ margin-bottom: 8px;
+ color: #a78bfa;
+}
+
+.login-screen p {
+ color: #8888aa;
+ margin-bottom: 28px;
+ font-size: 14px;
+}
+
+.login-screen input {
+ width: 100%;
+ padding: 12px 16px;
+ border-radius: 8px;
+ border: 1px solid #334;
+ background: #0f3460;
+ color: #e0e0e0;
+ font-size: 16px;
+ margin-bottom: 14px;
+ outline: none;
+}
+
+.login-screen input:focus {
+ border-color: #a78bfa;
+}
+
+.login-screen button {
+ width: 100%;
+ padding: 12px;
+ border-radius: 8px;
+ border: none;
+ background: #a78bfa;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.login-screen button:hover {
+ background: #7c3aed;
+}
+
+/* ── Chat Layout ── */
+.chat-layout {
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* ── Sidebar ── */
+.sidebar {
+ width: 260px;
+ background: #16213e;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid #223;
+ flex-shrink: 0;
+}
+
+.sidebar-header {
+ padding: 18px 16px 12px;
+ border-bottom: 1px solid #223;
+}
+
+.sidebar-header h2 {
+ font-size: 18px;
+ color: #a78bfa;
+ margin-bottom: 10px;
+}
+
+.sidebar-header .user-info {
+ font-size: 13px;
+ color: #8888aa;
+}
+
+.sidebar-header .user-info span {
+ color: #c4b5fd;
+ font-weight: 600;
+}
+
+.rooms-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+
+.room-item {
+ display: flex;
+ align-items: center;
+ padding: 10px 16px;
+ cursor: pointer;
+ transition: background 0.15s;
+ gap: 8px;
+}
+
+.room-item:hover {
+ background: #1e2f5a;
+}
+
+.room-item.active {
+ background: #2d1b69;
+ border-left: 3px solid #a78bfa;
+}
+
+.room-item .room-name {
+ flex: 1;
+ font-size: 14px;
+ color: #ccc;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.room-item.active .room-name {
+ color: #e0d0ff;
+ font-weight: 600;
+}
+
+.room-actions {
+ display: flex;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.room-item:hover .room-actions {
+ opacity: 1;
+}
+
+.icon-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1;
+ transition: background 0.15s;
+ color: #aaa;
+}
+
+.icon-btn:hover {
+ background: #3a2e6e;
+ color: #fff;
+}
+
+.icon-btn.danger:hover {
+ background: #7f1d1d;
+ color: #fca5a5;
+}
+
+.sidebar-footer {
+ padding: 12px 16px;
+ border-top: 1px solid #223;
+}
+
+.new-room-btn {
+ width: 100%;
+ padding: 10px;
+ border-radius: 8px;
+ border: 1px dashed #4c1d95;
+ background: transparent;
+ color: #a78bfa;
+ font-size: 14px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.new-room-btn:hover {
+ background: #2d1b69;
+}
+
+/* ── Main Chat Area ── */
+.chat-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: #1a1a2e;
+ overflow: hidden;
+}
+
+.chat-header {
+ padding: 16px 24px;
+ background: #16213e;
+ border-bottom: 1px solid #223;
+ font-size: 17px;
+ font-weight: 600;
+ color: #c4b5fd;
+}
+
+.messages-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.messages-area::-webkit-scrollbar {
+ width: 6px;
+}
+
+.messages-area::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.messages-area::-webkit-scrollbar-thumb {
+ background: #334;
+ border-radius: 3px;
+}
+
+.message {
+ display: flex;
+ flex-direction: column;
+ max-width: 680px;
+}
+
+.message.own {
+ align-self: flex-end;
+ align-items: flex-end;
+}
+
+.message-meta {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 4px;
+}
+
+.message.own .message-meta {
+ color: #8888cc;
+}
+
+.message-meta .author {
+ font-weight: 600;
+ color: #a78bfa;
+ margin-right: 6px;
+}
+
+.message.own .message-meta .author {
+ color: #c4b5fd;
+}
+
+.message-bubble {
+ background: #16213e;
+ border-radius: 12px 12px 12px 2px;
+ padding: 10px 14px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: #ddd;
+ max-width: 100%;
+ word-break: break-word;
+}
+
+.message.own .message-bubble {
+ background: #3b1f7a;
+ border-radius: 12px 12px 2px 12px;
+ color: #e8d5ff;
+}
+
+.no-room {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #555;
+ font-size: 16px;
+}
+
+.message-input-area {
+ padding: 16px 24px;
+ background: #16213e;
+ border-top: 1px solid #223;
+ display: flex;
+ gap: 10px;
+}
+
+.message-input-area input {
+ flex: 1;
+ padding: 12px 16px;
+ border-radius: 8px;
+ border: 1px solid #334;
+ background: #0f3460;
+ color: #e0e0e0;
+ font-size: 14px;
+ outline: none;
+}
+
+.message-input-area input:focus {
+ border-color: #a78bfa;
+}
+
+.message-input-area button {
+ padding: 12px 20px;
+ border-radius: 8px;
+ border: none;
+ background: #a78bfa;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+ white-space: nowrap;
+}
+
+.message-input-area button:hover {
+ background: #7c3aed;
+}
+
+.message-input-area button:disabled {
+ background: #444;
+ cursor: not-allowed;
+}
+
+/* ── Modal ── */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+}
+
+.modal {
+ background: #16213e;
+ border-radius: 12px;
+ padding: 28px;
+ width: 360px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
+}
+
+.modal h3 {
+ font-size: 18px;
+ color: #c4b5fd;
+ margin-bottom: 18px;
+}
+
+.modal input {
+ width: 100%;
+ padding: 10px 14px;
+ border-radius: 8px;
+ border: 1px solid #334;
+ background: #0f3460;
+ color: #e0e0e0;
+ font-size: 14px;
+ outline: none;
+ margin-bottom: 10px;
+}
+
+.modal input:focus {
+ border-color: #a78bfa;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ margin-top: 6px;
+}
+
+.modal-actions button {
+ padding: 9px 18px;
+ border-radius: 8px;
+ border: none;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.btn-primary {
+ background: #a78bfa;
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background: #7c3aed;
+}
+
+.btn-secondary {
+ background: #223;
+ color: #aaa;
+}
+
+.btn-secondary:hover {
+ background: #334;
+ color: #eee;
+}