Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions .github/workflows/test.yml-template
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
43 changes: 43 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="login">
<h2>Enter your name</h2>

<input type="text" id="usernameInput" placeholder="Name" />
<button id="enterNameBtn">Enter</button>
</div>

<div id="app" class="layout hidden">
<div class="sidebar">
<h3>Rooms</h3>
<ul id="roomsList"></ul>

<input id="newRoomInput" type="text" placeholder="New room name" />
<button id="createRoomBtn">Create room</button>
</div>

<div class="chat">
<div id="chatHeader">
<h3 id="roomTitle">Select a room</h3>
</div>

<div id="messageList" class="messages"></div>

<div class="input">
<input id="messageInput" type="text" placeholder="Type a message..." />
<button id="sendMessageBtn">Send</button>
</div>
</div>
</div>

<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
217 changes: 217 additions & 0 deletions client/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { state } from './state.js';

const socket = io('http://localhost:3000');
const app = document.getElementById('app');
const loginBlock = document.getElementById('login');
const usernameInput = document.getElementById('usernameInput');
const enterNameBtn = document.getElementById('enterNameBtn');
const roomsList = document.getElementById('roomsList');
const newRoomInput = document.getElementById('newRoomInput');
const createRoomBtn = document.getElementById('createRoomBtn');
const roomTitle = document.getElementById('roomTitle');
const messageList = document.getElementById('messageList');
const messageInput = document.getElementById('messageInput');
const sendMessageBtn = document.getElementById('sendMessageBtn');

function renderApp() {
if (state.user) {
loginBlock.classList.add('hidden');
app.classList.remove('hidden');
} else {
app.classList.add('hidden');
loginBlock.classList.remove('hidden');
}
}

function renderRooms() {
const rooms = state.rooms;
roomsList.innerHTML = '';

rooms.forEach(room => {
const li = document.createElement('li');
const roomNameEl = document.createElement('span');
roomNameEl.textContent = room.name;

const joinRoomBtn = document.createElement('button');
joinRoomBtn.textContent = 'Join room';
joinRoomBtn.addEventListener('click', () => joinRoom(room.id, room.name));

const renameRoomBtn = document.createElement('button');
renameRoomBtn.textContent = 'Rename room';
renameRoomBtn.addEventListener('click', () => {
const newName = prompt('New room name', room.name);
if (newName) {
renameRoom(room.id, newName);
}
});

const deleteRoomBtn = document.createElement('button');
deleteRoomBtn.textContent = 'Delete room';
deleteRoomBtn.addEventListener('click', () => deleteRoom(room.id));

li.appendChild(roomNameEl);
li.appendChild(joinRoomBtn);
li.appendChild(renameRoomBtn);
li.appendChild(deleteRoomBtn);

roomsList.appendChild(li);
});
}

function renderChatHeader() {
if (!state.activeRoom) {
return;
}

roomTitle.textContent = state.activeRoom.name;
}

function renderMessages() {
const messages = state.messages;
clearChat();

messages.forEach(message => renderOneMessage(message));

messageList.scrollTop = messageList.scrollHeight;
}

function renderOneMessage(message) {
const messageBlock = document.createElement('div');
const authorEl = document.createElement('strong');
const textEl = document.createElement('p');
const dateEl = document.createElement('span');
authorEl.textContent = message.authorName;
textEl.textContent = message.text;
messageBlock.className = 'message';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable messageBlock is used here and in the following lines, but it has not been declared anywhere in this function's scope. You need to create this element, for example: const messageBlock = document.createElement('div');.


dateEl.textContent = new Date(message.createdAt).toLocaleTimeString('uk-UA', {
hour: '2-digit',
minute: '2-digit',
});

messageBlock.appendChild(authorEl);
messageBlock.appendChild(dateEl);
messageBlock.appendChild(textEl);
Comment on lines +92 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dateEl element containing the message timestamp is created but is never appended to the messageBlock. This means the timestamp will not be displayed in the chat, which is a core requirement. You need to append it, similar to how authorEl and textEl are appended.


messageList.appendChild(messageBlock);
}

function saveUsername(name) {
state.user = name;
window.localStorage.setItem('username', name);
}

function joinRoom(roomId, roomName) {
const userName = state.user;

if (!userName) {
throw new Error('Username is required');
}

state.activeRoom = { id: roomId, name: roomName, };
state.messages = [];
renderChatHeader();

socket.emit('room_join', {
roomId,
userName,
});

clearChat();
}

function renameRoom(roomId, newName) {
if (!newName) {
throw new Error('New room name is required');
}

socket.emit('room_rename', {
roomId,
newName,
});
}

function deleteRoom(roomId) {
socket.emit('room_delete', { roomId })
}

function clearChat() {
messageList.innerHTML = '';
}

const enterNameBtnHandler = () => {
const normalizedName = usernameInput.value.trim();

if (!normalizedName) {
return;
}
saveUsername(normalizedName);
renderApp();
}

const createRoomHandler = () => {
const owner = state.user;
const roomName = newRoomInput.value.trim();

if (!owner || !roomName) {
throw new Error('User and room name are required')
}


socket.emit('room_create', {
name: roomName,
owner,
});

newRoomInput.value = '';
};

const sendMessageHandler = () => {
const text = messageInput.value.trim();

console.log('CLICK SEND', {
text,
activeRoom: state.activeRoom,
});

if (!text || !state.activeRoom) {
return;
}

socket.emit('message_send', {
text,
});

messageInput.value = '';
}

socket.on('room_list', (rooms) => {
state.rooms = rooms;

renderRooms();
});

socket.on('message_history', (messages) => {
state.messages = messages;
renderMessages();
});

socket.on('message_new', message => {
if (!state.messages) {
state.messages = [];
}

state.messages.push(message);
renderOneMessage(message);
messageList.scrollTop = messageList.scrollHeight;
});

socket.on('error_message', message => {
console.error('SERVER ERROR:', message);
});

createRoomBtn.addEventListener('click', createRoomHandler);
enterNameBtn.addEventListener('click', enterNameBtnHandler);
sendMessageBtn.addEventListener('click', sendMessageHandler)

renderApp();
6 changes: 6 additions & 0 deletions client/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const state = {
user: window.localStorage.getItem('username') || null,
rooms: [],
activeRoom: null,
messages: [],
};
37 changes: 37 additions & 0 deletions client/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
body {
margin: 0;
font-family: sans-serif;
}

.hidden {
display: none;
}

.layout {
display: flex;
height: 100vh;
}

.sidebar {
width: 450px;
border-right: 1px solid #ccc;
padding: 10px;
box-sizing: border-box;
}

.chat {
flex: 1;
display: flex;
flex-direction: column;
}

.messages {
flex: 1;
overflow-y: auto;
padding: 10px;
}

.input {
border-top: 1px solid #ccc;
padding: 10px;
}
Loading
Loading