-
Notifications
You must be signed in to change notification settings - Fork 266
add solution #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
add solution #198
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <title>JS Chat - WebSocket</title> | ||
| <style> | ||
| body { | ||
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||
| display: flex; | ||
| height: 100vh; | ||
| margin: 0; | ||
| background: #f0f2f5; | ||
| } | ||
| #auth-screen { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 100%; | ||
| } | ||
| #chat-screen { | ||
| display: none; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .sidebar { | ||
| width: 260px; | ||
| background: #ffffff; | ||
| border-right: 1px solid #ddd; | ||
| display: flex; | ||
| flex-direction: column; | ||
| } | ||
| .main-chat { | ||
| flex-grow: 1; | ||
| display: flex; | ||
| flex-direction: column; | ||
| } | ||
| .messages-area { | ||
| flex-grow: 1; | ||
| overflow-y: auto; | ||
| padding: 20px; | ||
| background: #fff; | ||
| } | ||
| .input-area { | ||
| padding: 20px; | ||
| background: #fff; | ||
| border-top: 1px solid #ddd; | ||
| display: flex; | ||
| gap: 10px; | ||
| } | ||
| .room-item { | ||
| padding: 12px; | ||
| border-bottom: 1px solid #eee; | ||
| cursor: pointer; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| } | ||
| .room-item:hover { | ||
| background: #f5f5f5; | ||
| } | ||
| .room-item.active { | ||
| background: #e7f3ff; | ||
| color: #1877f2; | ||
| font-weight: bold; | ||
| } | ||
| .message { | ||
| margin-bottom: 15px; | ||
| padding: 10px; | ||
| border-radius: 8px; | ||
| background: #f1f0f0; | ||
| max-width: 80%; | ||
| } | ||
| .msg-meta { | ||
| font-size: 11px; | ||
| color: #65676b; | ||
| margin-bottom: 4px; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="auth-screen"> | ||
| <h1>Введіть ваш Username</h1> | ||
| <input type="text" id="usernameInput" placeholder="Мій нікнейм..." /> | ||
| <button onclick="login()" style="margin-top: 10px; padding: 10px 20px"> | ||
| Увійти | ||
| </button> | ||
| </div> | ||
|
|
||
| <div id="chat-screen"> | ||
| <div class="sidebar"> | ||
| <div style="padding: 15px; border-bottom: 1px solid #ddd"> | ||
| <button onclick="createNewRoom()">+ Нова кімната</button> | ||
| </div> | ||
| <div id="rooms-list"></div> | ||
| </div> | ||
| <div class="main-chat"> | ||
| <div | ||
| style=" | ||
| padding: 15px; | ||
| background: #fff; | ||
| border-bottom: 1px solid #ddd; | ||
| font-weight: bold; | ||
| " | ||
| id="active-room-title" | ||
| > | ||
| General | ||
| </div> | ||
| <div class="messages-area" id="messages-area"></div> | ||
| <div class="input-area"> | ||
| <input | ||
| type="text" | ||
| id="msgInput" | ||
| style="flex-grow: 1" | ||
| placeholder="Напишіть щось..." | ||
| /> | ||
| <button onclick="sendMsg()">Відправити</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <script> | ||
| let socket; | ||
| let currentRoom = 'general'; | ||
| let myUser = localStorage.getItem('chat_username'); | ||
|
|
||
| if (myUser) { | ||
| initChat(); | ||
| } | ||
|
|
||
| function login() { | ||
| const val = document.getElementById('usernameInput').value.trim(); | ||
| if (val) { | ||
| localStorage.setItem('chat_username', val); | ||
| myUser = val; | ||
| initChat(); | ||
| } | ||
| } | ||
|
|
||
| function initChat() { | ||
| document.getElementById('auth-screen').style.display = 'none'; | ||
| document.getElementById('chat-screen').style.display = 'flex'; | ||
|
|
||
| socket = new WebSocket('ws://localhost:8080'); | ||
|
|
||
| socket.onopen = () => { | ||
| socket.send( | ||
| JSON.stringify({ | ||
| type: 'JOIN_ROOM', | ||
| roomId: currentRoom, | ||
| username: myUser, | ||
| }), | ||
| ); | ||
| }; | ||
|
|
||
| socket.onmessage = (event) => { | ||
| const data = JSON.parse(event.data); | ||
| if (data.type === 'ROOMS_LIST') updateRoomsUI(data.rooms); | ||
| if (data.type === 'ROOM_HISTORY') renderHistory(data.messages); | ||
| if (data.type === 'MESSAGE') appendMessage(data.message); | ||
| if (data.type === 'ROOM_DELETED') switchRoom('general'); | ||
| }; | ||
| } | ||
|
|
||
| function updateRoomsUI(rooms) { | ||
| const list = document.getElementById('rooms-list'); | ||
| list.innerHTML = ''; | ||
| rooms.forEach((r) => { | ||
| const div = document.createElement('div'); | ||
| div.className = `room-item ${r.id === currentRoom ? 'active' : ''}`; | ||
| div.innerHTML = `<span>${r.name}</span>`; | ||
|
|
||
| const btnContainer = document.createElement('div'); | ||
| if (r.id !== 'general') { | ||
| const ren = document.createElement('button'); | ||
| ren.innerText = 'R'; | ||
| ren.onclick = (e) => { | ||
| e.stopPropagation(); | ||
| renameRoom(r.id); | ||
| }; | ||
| const del = document.createElement('button'); | ||
| del.innerText = 'D'; | ||
| del.onclick = (e) => { | ||
| e.stopPropagation(); | ||
| deleteRoom(r.id); | ||
| }; | ||
| btnContainer.append(ren, del); | ||
| } | ||
| div.append(btnContainer); | ||
| div.onclick = () => switchRoom(r.id); | ||
| list.append(div); | ||
| }); | ||
| } | ||
|
|
||
| function switchRoom(id) { | ||
| currentRoom = id; | ||
| socket.send( | ||
| JSON.stringify({ type: 'JOIN_ROOM', roomId: id, username: myUser }), | ||
| ); | ||
| document.getElementById('active-room-title').innerText = id; | ||
| } | ||
|
|
||
| function renderHistory(msgs) { | ||
| const area = document.getElementById('messages-area'); | ||
| area.innerHTML = ''; | ||
| msgs.forEach(appendMessage); | ||
| } | ||
|
|
||
| function appendMessage(m) { | ||
| const area = document.getElementById('messages-area'); | ||
| const div = document.createElement('div'); | ||
| div.className = 'message'; | ||
| const time = new Date(m.time).toLocaleTimeString([], { | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| }); | ||
| div.innerHTML = `<div class="msg-meta">${m.author} • ${time}</div><div>${m.text}</div>`; | ||
| area.append(div); | ||
| area.scrollTop = area.scrollHeight; | ||
| } | ||
|
|
||
| function sendMsg() { | ||
| const input = document.getElementById('msgInput'); | ||
| if (input.value.trim()) { | ||
| socket.send( | ||
| JSON.stringify({ type: 'NEW_MESSAGE', text: input.value }), | ||
| ); | ||
| input.value = ''; | ||
| } | ||
| } | ||
|
|
||
| function createNewRoom() { | ||
| const n = prompt('Назва кімнати:'); | ||
| if (n) | ||
| socket.send(JSON.stringify({ type: 'CREATE_ROOM', roomName: n })); | ||
| } | ||
|
|
||
| function renameRoom(id) { | ||
| const n = prompt('Нова назва:'); | ||
| if (n) | ||
| socket.send( | ||
| JSON.stringify({ type: 'RENAME_ROOM', roomId: id, newName: n }), | ||
| ); | ||
| } | ||
|
|
||
| function deleteRoom(id) { | ||
| if (confirm('Видалити?')) | ||
| socket.send(JSON.stringify({ type: 'DELETE_ROOM', roomId: id })); | ||
| } | ||
| </script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* eslint-disable no-console */ | ||
| const { rooms } = require('./store'); | ||
| const { broadcastRooms, broadcastToRoom } = require('./utils'); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In src/index.js you require './src/websocket' from a file that is already in src; this will resolve to src/src/websocket.js and fail. Change the path to './websocket' (or correct relative path) so Node can load the websocket module. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect require path: index.js is inside src, but this imports './src/websocket' which resolves to src/src/websocket and will likely throw MODULE_NOT_FOUND. Change to require('./websocket') or the correct relative path to the websocket module. |
||
| function handleMessage(ws, wss, messageAsString) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This repository contains server-side code only. The task requires both client and server and specifically that the username is typed by the user and saved in localStorage (checklist items #1, #3, #4 and #15). Add the client implementation that sends the username to the server and stores it in localStorage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The server-side implements room operations and history broadcasting, which is good, but the task requires that the username be saved in localStorage (client-side). Make sure the client saves the username in localStorage and re-sends it when reconnecting/joining a room so the server can set |
||
| let data; | ||
|
|
||
| try { | ||
| data = JSON.parse(messageAsString); | ||
| } catch (e) { | ||
| console.error('Malformed JSON received'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| switch (data.type) { | ||
| case 'JOIN_ROOM': | ||
| if (!data.username) { | ||
| return; | ||
| } | ||
|
|
||
| ws.username = data.username; | ||
| ws.roomId = data.roomId || 'general'; | ||
|
|
||
| let isNewRoom = false; | ||
|
|
||
| if (!rooms[ws.roomId]) { | ||
| rooms[ws.roomId] = { id: ws.roomId, name: ws.roomId, messages: [] }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you auto-create a room here, call broadcastRooms(wss) immediately after creating the room so other connected clients receive the updated room list right away (instead of relying on an isNewRoom flag and broadcasting later). |
||
| isNewRoom = true; | ||
| } | ||
|
|
||
| ws.send( | ||
| JSON.stringify({ | ||
| type: 'ROOM_HISTORY', | ||
| roomId: ws.roomId, | ||
| messages: rooms[ws.roomId].messages, | ||
| }), | ||
| ); | ||
|
|
||
| if (isNewRoom) { | ||
| broadcastRooms(wss); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now you call broadcastRooms after sending ROOM_HISTORY (line 41). That delays notifying other clients. Move the broadcastRooms call into the creation block (immediately after creating rooms[ws.roomId]). |
||
| } | ||
|
|
||
| break; | ||
|
|
||
| case 'NEW_MESSAGE': | ||
| if (ws.username && ws.roomId && rooms[ws.roomId]) { | ||
| const newMessage = { | ||
| id: Date.now().toString(), | ||
| author: ws.username, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message author is set to |
||
| time: new Date().toISOString(), | ||
| text: data.text, | ||
| }; | ||
|
|
||
| rooms[ws.roomId].messages.push(newMessage); | ||
|
|
||
| broadcastToRoom(wss, ws.roomId, { | ||
| type: 'MESSAGE', | ||
| roomId: ws.roomId, | ||
| message: newMessage, | ||
| }); | ||
| } | ||
| break; | ||
|
|
||
| case 'CREATE_ROOM': | ||
| const newRoomId = 'room_' + Date.now(); | ||
|
|
||
| rooms[newRoomId] = { id: newRoomId, name: data.roomName, messages: [] }; | ||
| broadcastRooms(wss); | ||
| break; | ||
|
|
||
| case 'RENAME_ROOM': | ||
| if (rooms[data.roomId] && data.roomId !== 'general') { | ||
| rooms[data.roomId].name = data.newName; | ||
| broadcastRooms(wss); | ||
| } | ||
| break; | ||
|
|
||
| case 'DELETE_ROOM': | ||
| if (rooms[data.roomId] && data.roomId !== 'general') { | ||
| delete rooms[data.roomId]; | ||
|
|
||
| wss.clients.forEach((client) => { | ||
| if (client.roomId === data.roomId) { | ||
| client.roomId = 'general'; | ||
| client.send(JSON.stringify({ type: 'ROOM_DELETED' })); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a room is deleted you set moving clients' |
||
|
|
||
| client.send( | ||
| JSON.stringify({ | ||
| type: 'ROOM_HISTORY', | ||
| roomId: 'general', | ||
| messages: rooms['general'].messages, | ||
| }), | ||
| ); | ||
| } | ||
| }); | ||
|
Comment on lines
+83
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When deleting a room you set affected clients' roomId to 'general' and send a |
||
| broadcastRooms(wss); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| module.exports = { handleMessage }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,13 @@ | ||
| 'use strict'; | ||
| /* eslint-disable no-console */ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The overall repo is missing client-side implementation files (UI and client JS). The task explicitly requires both client and server; without client code you can't meet requirements like saving username to localStorage, letting users create/rename/join/delete rooms from the UI, and showing previous messages to new users. |
||
| const http = require('http'); | ||
| const { setupWebSocket } = require('./websocket'); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The repository contains only server-side code (handlers, websocket, utils, store). The task requires a client as well (UI to type username, send it to server, and save username in localStorage). Without client-side code, checklist items #1, #2, #3 and #15 are not fulfilled — add a client that sends JOIN_ROOM/CREATE_ROOM/etc. and saves the username in localStorage. |
||
| const server = http.createServer(); | ||
|
|
||
| setupWebSocket(server); | ||
|
|
||
| const PORT = 8080; | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The server stores |
||
| server.listen(PORT, () => { | ||
| console.log(`Server is listening on port ${PORT}`); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| const rooms = { | ||
| general: { id: 'general', name: 'General', messages: [] }, | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This require is incorrect given this file is already in src. Requiring './src/websocket' will try to load src/src/websocket and fail with MODULE_NOT_FOUND. Change to require('./websocket') (or adjust path relative to this file). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This require is incorrect given this file is already in src. Requiring './src/websocket' will try to load src/src/websocket and fail with MODULE_NOT_FOUND. Change to require('./websocket') (or adjust path relative to this file). |
||
|
|
||
| module.exports = { rooms }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The project currently lacks the client-side implementation required by the task: UI for typing a username, saving it to localStorage, sending it to the server, and room UI (create/rename/join/delete). These are mandatory according to the description and must be added.