Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,690 changes: 2,840 additions & 2,850 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.3",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
Expand All @@ -26,5 +26,10 @@
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1",
"ws": "^8.20.0"
}
}
294 changes: 294 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket Chat</title>
<style>
body {
font-family: sans-serif;
display: flex;
height: 100vh;
margin: 0;
}
#auth {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
#chat-app {
display: none;
width: 100%;
}
.sidebar {
width: 300px;
border-right: 1px solid #ccc;
padding: 20px;
background: #f9f9f9;
overflow-y: auto;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
}
.messages {
flex: 1;
overflow-y: auto;
border: 1px solid #ccc;
margin-bottom: 20px;
padding: 10px;
}
.message {
margin-bottom: 10px;
padding: 10px;
background: #eef;
border-radius: 5px;
}
.message-header {
font-size: 0.8em;
color: #555;
margin-bottom: 5px;
}
.room-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 5px;
background: #fff;
border: 1px solid #ddd;
cursor: pointer;
}
.room-item.active {
background: #d0e8ff;
border-color: #007bff;
}
.controls {
display: flex;
gap: 10px;
}
input {
padding: 8px;
flex: 1;
}
button {
padding: 8px 15px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="auth">
<h2>Введіть ім'я користувача</h2>
<div class="controls">
<input type="text" id="usernameInput" placeholder="Username" />
<button onclick="saveUsername()">Увійти</button>
</div>
</div>

<div id="chat-app">
<div class="sidebar">
<h3>Кімнати</h3>
<div class="controls" style="margin-bottom: 15px">
<input type="text" id="newRoomInput" placeholder="Нова кімната" />
<button onclick="createRoom()">Створити</button>
</div>
<div id="roomsList"></div>
</div>
<div class="main">
<h2 id="currentRoomName">General</h2>
<div class="messages" id="messagesContainer"></div>
<div class="controls">
<input
type="text"
id="messageInput"
placeholder="Введіть повідомлення..."
/>
<button onclick="sendMessage()">Відправити</button>
</div>
</div>
</div>

<script>
let ws;
let currentRoomId = 'general';
let username = localStorage.getItem('username');

if (username) {
showChat();
connectWebSocket();
}

function saveUsername() {
const input = document.getElementById('usernameInput').value.trim();
if (input) {
username = input;
localStorage.setItem('username', username);
showChat();
connectWebSocket();
}
}

function showChat() {
document.getElementById('auth').style.display = 'none';
document.getElementById('chat-app').style.display = 'flex';
}

function connectWebSocket() {
ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
joinRoom(currentRoomId);
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);

switch (data.type) {
case 'ROOMS_LIST':
renderRooms(data.rooms);
break;
case 'ROOM_HISTORY':
if (data.roomId === currentRoomId) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
data.messages.forEach(appendMessage);
}
break;
case 'MESSAGE':
if (data.roomId === currentRoomId) {
appendMessage(data.message);
}
break;
case 'ROOM_DELETED':
joinRoom('general');
break;
}
};
}

function joinRoom(roomId) {
currentRoomId = roomId;
document
.querySelectorAll('.room-item')
.forEach((el) => el.classList.remove('active'));
const activeRoom = document.getElementById(`room-${roomId}`);
if (activeRoom) activeRoom.classList.add('active');

ws.send(
JSON.stringify({
type: 'JOIN_ROOM',
roomId: roomId,
username: username,
}),
);
}

function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (text && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: 'NEW_MESSAGE',
text: text,
}),
);
input.value = '';
}
}

function createRoom() {
const input = document.getElementById('newRoomInput');
const name = input.value.trim();
if (name) {
ws.send(JSON.stringify({ type: 'CREATE_ROOM', roomName: name }));
input.value = '';
}
}

function renameRoom(roomId, currentName) {
const newName = prompt('Введіть нову назву кімнати:', currentName);
if (newName && newName.trim() !== '') {
ws.send(
JSON.stringify({
type: 'RENAME_ROOM',
roomId,
newName: newName.trim(),
}),
);
}
}

function deleteRoom(roomId) {
if (confirm('Ви впевнені, що хочете видалити цю кімнату?')) {
ws.send(JSON.stringify({ type: 'DELETE_ROOM', roomId }));
}
}

function renderRooms(rooms) {
const list = document.getElementById('roomsList');
list.innerHTML = '';
rooms.forEach((room) => {
const div = document.createElement('div');
div.className = `room-item ${room.id === currentRoomId ? 'active' : ''}`;
div.id = `room-${room.id}`;
div.onclick = (e) => {
if (e.target.tagName !== 'BUTTON') joinRoom(room.id);
};

const nameSpan = document.createElement('span');
nameSpan.textContent = room.name;
div.appendChild(nameSpan);

if (room.id !== 'general') {
const actions = document.createElement('div');
const renameBtn = document.createElement('button');
renameBtn.textContent = '✎';
renameBtn.onclick = () => renameRoom(room.id, room.name);

const deleteBtn = document.createElement('button');
deleteBtn.textContent = '✖';
deleteBtn.onclick = () => deleteRoom(room.id);

actions.appendChild(renameBtn);
actions.appendChild(deleteBtn);
div.appendChild(actions);
}

list.appendChild(div);

if (room.id === currentRoomId) {
document.getElementById('currentRoomName').textContent = room.name;
}
});
}

function appendMessage(msg) {
const container = document.getElementById('messagesContainer');
const div = document.createElement('div');
div.className = 'message';

const date = new Date(msg.time);
const timeString = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;

div.innerHTML = `
<div class="message-header"><strong>${msg.author}</strong> о ${timeString}</div>
<div class="message-body">${msg.text}</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}

document
.getElementById('messageInput')
.addEventListener('keypress', function (e) {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>
Loading
Loading