From d50683fb3104db2e5bc496566ad43ed657995676 Mon Sep 17 00:00:00 2001 From: Oleg Date: Thu, 4 Sep 2025 18:17:29 +0300 Subject: [PATCH 1/2] solution --- public/index.html | 85 ++++++++++ public/script.js | 395 ++++++++++++++++++++++++++++++++++++++++++++++ public/style.css | 394 +++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 125 +++++++++++++++ 4 files changed, 999 insertions(+) create mode 100644 public/index.html create mode 100644 public/script.js create mode 100644 public/style.css diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..ffa34e7dc --- /dev/null +++ b/public/index.html @@ -0,0 +1,85 @@ + + + + + + Node Chat + + + +
+ + + +
+ + + + + + + + + + diff --git a/public/script.js b/public/script.js new file mode 100644 index 000000000..35be25697 --- /dev/null +++ b/public/script.js @@ -0,0 +1,395 @@ +/* eslint-disable function-paren-newline */ +/* eslint-env browser */ +/* global io */ + +class ChatApp { + constructor() { + this.socket = io(); + this.username = localStorage.getItem('chat-username') || ''; + this.currentRoom = null; + + this.initializeElements(); + this.setupEventListeners(); + this.setupSocketListeners(); + + if (this.username) { + this.showChatApp(); + } + } + + initializeElements() { + this.loginScreen = document.getElementById('loginScreen'); + this.chatApp = document.getElementById('chatApp'); + this.usernameInput = document.getElementById('usernameInput'); + this.setUsernameBtn = document.getElementById('setUsernameBtn'); + this.currentUsernameSpan = document.getElementById('currentUsername'); + this.logoutBtn = document.getElementById('logoutBtn'); + this.roomsList = document.getElementById('roomsList'); + this.currentRoomName = document.getElementById('currentRoomName'); + this.messagesContainer = document.getElementById('messagesContainer'); + this.messageInput = document.getElementById('messageInput'); + this.sendMessageBtn = document.getElementById('sendMessageBtn'); + + this.messageInputContainer = document.getElementById( + 'messageInputContainer', + ); + this.createRoomBtn = document.getElementById('createRoomBtn'); + this.renameRoomBtn = document.getElementById('renameRoomBtn'); + this.deleteRoomBtn = document.getElementById('deleteRoomBtn'); + + this.createRoomModal = document.getElementById('createRoomModal'); + this.newRoomNameInput = document.getElementById('newRoomNameInput'); + this.confirmCreateRoomBtn = document.getElementById('confirmCreateRoomBtn'); + this.cancelCreateRoomBtn = document.getElementById('cancelCreateRoomBtn'); + + this.renameRoomModal = document.getElementById('renameRoomModal'); + this.renameRoomInput = document.getElementById('renameRoomInput'); + this.confirmRenameRoomBtn = document.getElementById('confirmRenameRoomBtn'); + this.cancelRenameRoomBtn = document.getElementById('cancelRenameRoomBtn'); + } + + setupEventListeners() { + this.setUsernameBtn.addEventListener('click', () => this.setUsername()); + + this.usernameInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.setUsername(); + } + }); + + this.logoutBtn.addEventListener('click', () => this.logout()); + + this.createRoomBtn.addEventListener('click', () => + this.showCreateRoomModal(), + ); + + this.renameRoomBtn.addEventListener('click', () => + this.showRenameRoomModal(), + ); + this.deleteRoomBtn.addEventListener('click', () => this.deleteRoom()); + + this.sendMessageBtn.addEventListener('click', () => this.sendMessage()); + + this.messageInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.sendMessage(); + } + }); + + this.confirmCreateRoomBtn.addEventListener('click', () => + this.createRoom(), + ); + + this.cancelCreateRoomBtn.addEventListener('click', () => + this.hideCreateRoomModal(), + ); + + this.newRoomNameInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.createRoom(); + } + }); + + this.confirmRenameRoomBtn.addEventListener('click', () => + this.renameRoom(), + ); + + this.cancelRenameRoomBtn.addEventListener('click', () => + this.hideRenameRoomModal(), + ); + + this.renameRoomInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.renameRoom(); + } + }); + + this.createRoomModal.addEventListener('click', (e) => { + if (e.target === this.createRoomModal) { + this.hideCreateRoomModal(); + } + }); + + this.renameRoomModal.addEventListener('click', (e) => { + if (e.target === this.renameRoomModal) { + this.hideRenameRoomModal(); + } + }); + } + + setupSocketListeners() { + this.socket.on('connect', () => { + if (this.username && this.currentRoom) { + this.socket.emit('join-room', { + username: this.username, + roomName: this.currentRoom, + }); + } + }); + + this.socket.on('rooms-list', (rooms) => { + this.updateRoomsList(rooms); + }); + + this.socket.on('room-created', (data) => { + this.hideCreateRoomModal(); + this.socket.emit('get-rooms'); + this.joinRoom(data.roomName); + }); + + this.socket.on('room-history', (messages) => { + this.displayMessages(messages); + }); + + this.socket.on('new-message', (message) => { + this.displayMessage(message); + }); + + this.socket.on('user-joined', (data) => { + this.displaySystemMessage(`${data.username} joined the room`); + }); + + this.socket.on('user-left', (data) => { + this.displaySystemMessage(`${data.username} left the room`); + }); + + this.socket.on('room-deleted', (data) => { + if (this.currentRoom === data.roomName) { + this.currentRoom = null; + this.currentRoomName.textContent = 'Select a room'; + + this.messagesContainer.innerHTML = ` +
+

Please select or create a room to start chatting

+
+ `; + this.messageInputContainer.style.display = 'none'; + this.renameRoomBtn.style.display = 'none'; + this.deleteRoomBtn.style.display = 'none'; + } + this.socket.emit('get-rooms'); + }); + + this.socket.on('room-renamed', (data) => { + if (this.currentRoom === data.oldName) { + this.currentRoom = data.newName; + this.currentRoomName.textContent = data.newName; + } + this.hideRenameRoomModal(); + this.socket.emit('get-rooms'); + }); + + this.socket.on('room-error', (data) => { + alert(data.message); + }); + } + + setUsername() { + const username = this.usernameInput.value.trim(); + + if (username) { + this.username = username; + localStorage.setItem('chat-username', username); + this.showChatApp(); + } + } + + showChatApp() { + this.currentUsernameSpan.textContent = this.username; + this.loginScreen.style.display = 'none'; + this.chatApp.style.display = 'flex'; + this.socket.emit('get-rooms'); + } + + logout() { + this.username = ''; + this.currentRoom = null; + localStorage.removeItem('chat-username'); + this.loginScreen.style.display = 'block'; + this.chatApp.style.display = 'none'; + this.usernameInput.value = ''; + this.socket.disconnect(); + this.socket.connect(); + } + + updateRoomsList(rooms) { + this.roomsList.innerHTML = ''; + + rooms.forEach((room) => { + const roomElement = document.createElement('div'); + + roomElement.className = 'room-item'; + roomElement.textContent = room; + roomElement.addEventListener('click', () => this.joinRoom(room)); + + if (room === this.currentRoom) { + roomElement.classList.add('active'); + } + + this.roomsList.appendChild(roomElement); + }); + } + + joinRoom(roomName) { + if (this.currentRoom) { + document.querySelector('.room-item.active')?.classList.remove('active'); + } + + this.currentRoom = roomName; + this.currentRoomName.textContent = roomName; + this.messageInputContainer.style.display = 'flex'; + this.renameRoomBtn.style.display = 'inline-block'; + this.deleteRoomBtn.style.display = 'inline-block'; + + const roomElement = Array.from(this.roomsList.children).find( + (el) => el.textContent === roomName, + ); + + if (roomElement) { + roomElement.classList.add('active'); + } + + this.socket.emit('join-room', { + username: this.username, + roomName: roomName, + }); + } + + showCreateRoomModal() { + this.createRoomModal.style.display = 'block'; + this.newRoomNameInput.focus(); + } + + hideCreateRoomModal() { + this.createRoomModal.style.display = 'none'; + this.newRoomNameInput.value = ''; + } + + createRoom() { + const roomName = this.newRoomNameInput.value.trim(); + + if (roomName) { + this.socket.emit('create-room', { + roomName: roomName, + username: this.username, + }); + } + } + + showRenameRoomModal() { + if (this.currentRoom) { + this.renameRoomInput.value = this.currentRoom; + this.renameRoomModal.style.display = 'block'; + this.renameRoomInput.focus(); + } + } + + hideRenameRoomModal() { + this.renameRoomModal.style.display = 'none'; + this.renameRoomInput.value = ''; + } + + renameRoom() { + const newName = this.renameRoomInput.value.trim(); + + if (newName && newName !== this.currentRoom) { + this.socket.emit('rename-room', { + oldName: this.currentRoom, + newName: newName, + }); + } + } + + deleteRoom() { + if ( + this.currentRoom && + confirm(`Are you sure you want to delete the room "${this.currentRoom}"?`) + ) { + this.socket.emit('delete-room', { roomName: this.currentRoom }); + } + } + + sendMessage() { + const message = this.messageInput.value.trim(); + + if (message && this.currentRoom) { + this.socket.emit('send-message', { + message: message, + roomName: this.currentRoom, + username: this.username, + }); + this.messageInput.value = ''; + } + } + + displayMessages(messages) { + this.messagesContainer.innerHTML = ''; + messages.forEach((message) => this.displayMessage(message)); + this.scrollToBottom(); + } + + displayMessage(message) { + const messageElement = document.createElement('div'); + + messageElement.className = 'message'; + + const formattedTime = this.formatTimestamp(message.timestamp); + + messageElement.innerHTML = ` +
+ ${this.escapeHtml(message.username)} + ${formattedTime} +
+
${this.escapeHtml(message.message)}
+ `; + + this.messagesContainer.appendChild(messageElement); + this.scrollToBottom(); + } + + displaySystemMessage(text) { + const messageElement = document.createElement('div'); + + messageElement.className = 'system-message'; + messageElement.textContent = text; + this.messagesContainer.appendChild(messageElement); + this.scrollToBottom(); + } + + formatTimestamp(timestamp) { + const date = new Date(timestamp); + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + + if (isToday) { + return date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + } else { + return date.toLocaleDateString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } + } + + escapeHtml(text) { + const div = document.createElement('div'); + + div.textContent = text; + + return div.innerHTML; + } + + scrollToBottom() { + this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; + } +} + +document.addEventListener('DOMContentLoaded', () => { + // eslint-disable-next-line no-new + new ChatApp(); +}); diff --git a/public/style.css b/public/style.css new file mode 100644 index 000000000..ced98d85d --- /dev/null +++ b/public/style.css @@ -0,0 +1,394 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + height: 100vh; + background: linear-gradient(135deg, #ff9800 0%, #ff5722 100%); +} + +.container { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.login-screen { + background: white; + padding: 2rem; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0,0,0,0.15); + text-align: center; + min-width: 300px; +} + +.login-screen h1 { + color: #333; + margin-bottom: 1.5rem; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.login-form input { + padding: 0.75rem; + border: 2px solid #e1e1e1; + border-radius: 5px; + font-size: 1rem; +} + +.login-form input:focus { + outline: none; + border-color: #ff9800; +} + +.login-form button { + padding: 0.75rem; + background: #ff9800; + color: white; + border: none; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + transition: background 0.3s; +} + +.login-form button:hover { + background: #e68900; +} + +.chat-app { + width: 100%; + height: 100vh; + display: flex; + background: white; +} + +.sidebar { + width: 300px; + background: #333; + color: white; + display: flex; + flex-direction: column; +} + +.user-info { + padding: 1rem; + background: #444; + display: flex; + justify-content: space-between; + align-items: center; +} + +.user-info button { + background: #e64a19; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.8rem; +} + +.user-info button:hover { + background: #bf360c; +} + +.rooms-section { + flex: 1; + padding: 1rem; + overflow-y: auto; +} + +.rooms-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.rooms-header h3 { + margin: 0; +} + +.rooms-header button { + background: #ff9800; + color: white; + border: none; + padding: 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.8rem; +} + +.rooms-header button:hover { + background: #e68900; +} + +.rooms-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.room-item { + padding: 0.75rem; + background: #555; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s; + word-break: break-word; +} + +.room-item:hover { + background: #ff7043; +} + +.room-item.active { + background: #ff9800; +} + +.chat-section { + flex: 1; + display: flex; + flex-direction: column; + height: 100vh; +} + +.chat-header { + padding: 1rem; + background: #f5f5f5; + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + align-items: center; +} + +.chat-header h2 { + margin: 0; + color: #333; +} + +.room-actions { + display: flex; + gap: 0.5rem; +} + +.room-actions button { + background: #ff9800; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.8rem; +} + +.room-actions button:hover { + background: #e68900; +} + +.room-actions button:nth-child(2) { + background: #e64a19; +} + +.room-actions button:nth-child(2):hover { + background: #bf360c; +} + +.messages-container { + flex: 1; + padding: 1rem; + overflow-y: auto; + background: #fff7f0; +} + +.no-room-message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #999; + font-size: 1.1rem; +} + +.message { + margin-bottom: 1rem; + padding: 0.75rem; + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.message-author { + font-weight: bold; + color: #ff9800; +} + +.message-time { + font-size: 0.8rem; + color: #999; +} + +.message-text { + color: #333; + line-height: 1.4; +} + +.system-message { + background: #ff5722; + color: white; + text-align: center; + padding: 0.5rem; + border-radius: 15px; + margin: 0.5rem 0; + font-size: 0.9rem; +} + +.message-input-container { + padding: 1rem; + background: #f5f5f5; + border-top: 1px solid #ddd; + display: flex; + gap: 0.5rem; +} + +.message-input-container input { + flex: 1; + padding: 0.75rem; + border: 2px solid #ddd; + border-radius: 25px; + font-size: 1rem; +} + +.message-input-container input:focus { + outline: none; + border-color: #ff9800; +} + +.message-input-container button { + background: #ff9800; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 25px; + cursor: pointer; + font-size: 1rem; + transition: background 0.3s; +} + +.message-input-container button:hover { + background: #e68900; +} + +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 1000; +} + +.modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 2rem; + border-radius: 10px; + min-width: 300px; +} + +.modal-content h3 { + margin-bottom: 1rem; + color: #333; +} + +.modal-content input { + width: 100%; + padding: 0.75rem; + border: 2px solid #e1e1e1; + border-radius: 5px; + font-size: 1rem; + margin-bottom: 1rem; +} + +.modal-content input:focus { + outline: none; + border-color: #ff9800; +} + +.modal-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +.modal-actions button { + padding: 0.5rem 1rem; + border: none; + border-radius: 3px; + cursor: pointer; + font-size: 0.9rem; +} + +.modal-actions button:first-child { + background: #ff9800; + color: white; +} + +.modal-actions button:first-child:hover { + background: #e68900; +} + +.modal-actions button:last-child { + background: #9e9e9e; + color: white; +} + +.modal-actions button:last-child:hover { + background: #757575; +} + +@media (max-width: 768px) { + .chat-app { + flex-direction: column; + } + + .sidebar { + width: 100%; + height: 200px; + } + + .rooms-section { + overflow-x: auto; + } + + .rooms-list { + flex-direction: row; + padding-bottom: 0.5rem; + } + + .room-item { + min-width: 120px; + text-align: center; + } +} diff --git a/src/index.js b/src/index.js index ad9a93a7c..05f88029c 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,126 @@ 'use strict'; + +const express = require('express'); +const http = require('http'); +const socketIo = require('socket.io'); +const path = require('path'); + +const app = express(); +const server = http.createServer(app); +const io = socketIo(server); + +const PORT = process.env.PORT || 3000; + +app.use(express.static(path.join(__dirname, '../public'))); + +const rooms = new Map(); + +io.on('connection', (socket) => { + socket.on('join-room', (data) => { + const { username, roomName } = data; + + if (!rooms.has(roomName)) { + rooms.set(roomName, { + name: roomName, + messages: [], + users: new Set(), + }); + } + + const room = rooms.get(roomName); + + room.users.add(username); + + socket.join(roomName); + socket.username = username; + socket.currentRoom = roomName; + + socket.emit('room-history', room.messages); + socket.to(roomName).emit('user-joined', { username, roomName }); + }); + + socket.on('send-message', (data) => { + const { message, roomName, username } = data; + const timestamp = new Date().toISOString(); + + const messageData = { + id: Date.now(), + username, + message, + timestamp, + roomName, + }; + + if (rooms.has(roomName)) { + rooms.get(roomName).messages.push(messageData); + } + + io.to(roomName).emit('new-message', messageData); + }); + + socket.on('create-room', (data) => { + const { roomName } = data; + + if (!rooms.has(roomName)) { + rooms.set(roomName, { + name: roomName, + messages: [], + users: new Set(), + }); + socket.emit('room-created', { roomName }); + } else { + socket.emit('room-error', { message: 'Room already exists' }); + } + }); + + socket.on('get-rooms', () => { + const roomList = Array.from(rooms.keys()); + + socket.emit('rooms-list', roomList); + }); + + socket.on('delete-room', (data) => { + const { roomName } = data; + + if (rooms.has(roomName)) { + io.to(roomName).emit('room-deleted', { roomName }); + rooms.delete(roomName); + } + }); + + socket.on('rename-room', (data) => { + const { oldName, newName } = data; + + if (rooms.has(oldName) && !rooms.has(newName)) { + const roomData = rooms.get(oldName); + + roomData.name = newName; + rooms.set(newName, roomData); + rooms.delete(oldName); + + io.to(oldName).emit('room-renamed', { oldName, newName }); + } else { + socket.emit('room-error', { message: 'Room rename failed' }); + } + }); + + socket.on('disconnect', () => { + if (socket.username && socket.currentRoom) { + const room = rooms.get(socket.currentRoom); + + if (room) { + room.users.delete(socket.username); + + socket.to(socket.currentRoom).emit('user-left', { + username: socket.username, + roomName: socket.currentRoom, + }); + } + } + }); +}); + +server.listen(PORT, () => { + // eslint-disable-next-line no-console + console.log(`Server running on port ${PORT}`); +}); From 041472507f9a8595debde518a4227956e4b5923a Mon Sep 17 00:00:00 2001 From: Oleg Date: Thu, 4 Sep 2025 18:30:16 +0300 Subject: [PATCH 2/2] solution --- src/index.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 05f88029c..b1912d1f5 100644 --- a/src/index.js +++ b/src/index.js @@ -19,12 +19,16 @@ io.on('connection', (socket) => { socket.on('join-room', (data) => { const { username, roomName } = data; + // if (!rooms.has(roomName)) { + // rooms.set(roomName, { + // name: roomName, + // messages: [], + // users: new Set(), + // }); + // } + if (!rooms.has(roomName)) { - rooms.set(roomName, { - name: roomName, - messages: [], - users: new Set(), - }); + return socket.emit('room-error', { message: 'Room does not exist' }); } const room = rooms.get(roomName); @@ -43,11 +47,17 @@ io.on('connection', (socket) => { const { message, roomName, username } = data; const timestamp = new Date().toISOString(); + if (!username || !roomName || !message || message.trim() === '') { + return socket.emit('error', { + error: 'Message cannot be empty and all fields are required', + }); + } + const messageData = { id: Date.now(), - username, - message, - timestamp, + author: username, + text: message, + time: timestamp, roomName, };