Real-Time Chat Application (Novaxjs2, nodejs, was)
A simple, responsive real-time chat application built with Node.js, WebSockets, and vanilla JavaScript.


Features
- Real-time messaging using WebSockets
- User authentication with profile pictures
- Responsive design that works on mobile and desktop
- System messages for user join/leave notifications
- Message timestamps
- Toast notifications
- Persistent user sessions using localStorage
- Image upload and display (converted to base64)
Technologies Used
- Backend:
- Node.js
- novaxjs2 (web framework)
- ws (WebSocket library)
- Frontend:
- Vanilla JavaScript
- HTML5
- CSS3 (with responsive design)
Available in GitHub
git clone https://github.com/WhoappRoom/real-time-chat-app.git
cd real-time-chat-app
index.jsconst novax = require('novaxjs2');
const { WebSocketServer } = require('ws');
const app = new novax();
const server = app.server;
app.serveStatic(); // default public folder
const wss = new WebSocketServer({ server });
const activeUsers = new Set();
wss.on('connection', (ws) => {
console.log('Client Connected');
ws.on('message', (message) => {
try {
const parsedMessage = JSON.parse(message);
// Broadcast message to all clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(parsedMessage));
}
});
// Track active users for join/leave messages
if (parsedMessage.type === 'system' && parsedMessage.content.includes('joined')) {
activeUsers.add(parsedMessage.userId);
} else if (parsedMessage.type === 'system' && parsedMessage.content.includes('left')) {
activeUsers.delete(parsedMessage.userId);
}
} catch (error) {
console.error('Error processing message:', error);
}
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
app.get('/', (req, res) => app.sendFile('./public/index.html', res));
app.at(3000, '0.0.0.0', () => console.log('Server is running on port 3000'));
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="main.css">
<title>Real Time Chat App</title>
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
</head>
<body style="touch-action: manipulation;">
<div id="auth">
<div class="modal">
<h2>Create Your Profile</h2>
<input type="text" id="fullName" placeholder="Full Name">
<input type="file" hidden accept="image/*" id="file">
<div class="avatar-container">
<img src="img_avatar.png" alt="User pic" id="preview-pic">
<button class="upload-btn" onclick="document.getElementById('file').click()">Choose Image</button>
</div>
<button id="save">Join Chat</button>
</div>
</div>
<div id="app">
<div id="header">
<h1>Real Time Chat App</h1>
<button id="logout"><i class='bx bx-log-out'></i></button>
</div>
<main>
<div id="messages"></div>
<div class="footer">
<input type="text" placeholder="Type your message..." id="input-message">
<button id="send"><i class="bx bx-send"></i></button>
</div>
</main>
</div>
<div id="toast"></div>
<script src="main.js"></script>
</body>
</html>
main.css/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
touch-action: manipulation;
}
/* Authentication modal styles */
#auth {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
#auth .modal {
background-color: white;
padding: 2rem;
border-radius: 10px;
width: 90%;
max-width: 400px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
#auth .modal h2 {
margin-bottom: 1.5rem;
color: #333;
}
#auth input {
width: 100%;
padding: 12px;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
outline: none;
}
.avatar-container {
margin: 1rem 0;
}
#preview-pic {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto;
border: 3px solid #4CAF50;
}
.upload-btn {
background-color: #f0f0f0;
color: #333;
border: none;
padding: 8px 16px;
border-radius: 5px;
margin-top: 10px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.3s;
min-height: 44px;
min-width: 44px;
}
.upload-btn:hover {
background-color: #e0e0e0;
}
#auth button#save {
background-color: #4CAF50;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
width: 100%;
transition: background-color 0.3s;
min-height: 44px;
}
#auth button#save:hover {
background-color: #45a049;
}
/* Main app styles */
#app {
display: none;
width: 100%;
max-width: 800px;
height: 93vh;
background-color: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
#header {
background-color: #4CAF50;
color: white;
padding: 1rem;
text-align: center;
display: flex;
justify-content: space-between;
align-items: center;
}
#header h1 {
font-size: 1.5rem;
margin: 0;
}
#logout {
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 5px;
min-height: 44px;
min-width: 44px;
}
main {
display: flex;
flex-direction: column;
height: calc(100% - 60px);
}
#messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
background-color: #f9f9f9;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.footer {
display: flex;
padding: 1rem;
background-color: #f5f5f5;
border-top: 1px solid #ddd;
position: relative;
min-height: 60px;
}
#input-message {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
outline: none;
min-width: 0;
max-width: 100%;
}
#send {
background-color: #4CAF50;
color: white;
border: none;
padding: 0 1.5rem;
margin-left: 0.5rem;
border-radius: 5px;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
flex-shrink: 0;
}
#send:hover {
background-color: #45a049;
}
/* Message styles */
.message {
margin-bottom: 1rem;
padding: 0.8rem 1rem;
border-radius: 8px;
max-width: 80%;
word-wrap: break-word;
}
.message.sent {
background-color: #4CAF50;
color: white;
margin-left: auto;
border-bottom-right-radius: 0;
}
.message.received {
background-color: #e9e9e9;
margin-right: auto;
border-bottom-left-radius: 0;
}
.message.system {
background-color: #f0f0f0;
color: #666;
text-align: center;
margin: 0.5rem auto;
padding: 0.5rem;
font-size: 0.9rem;
max-width: 100%;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 5px;
flex-wrap: wrap;
}
.message-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 8px;
object-fit: cover;
}
.message-sender {
font-weight: bold;
margin-right: 8px;
}
.message-time {
font-size: 0.8rem;
opacity: 0.8;
}
.message-content {
overflow-wrap: break-word;
word-break: break-word;
}
/* Toast notification */
#toast {
visibility: hidden;
min-width: 250px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 4px;
padding: 12px;
position: fixed;
z-index: 1;
left: 0;
right: 30px;
top: 0;
font-size: 0.9rem;
}
#toast.show {
visibility: visible;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
#toast.success {
background-color: #4CAF50;
}
#toast.error {
background-color: #f44336;
}
@keyframes fadein {
from {right: 0; opacity: 0;}
to {right: 30px; opacity: 1;}
}
@keyframes fadeout {
from {right: 30px; opacity: 1;}
to {right: 0; opacity: 0;}
}
/* Responsive styles */
@media (max-width: 600px) {
#app {
height: 100vh;
width: 100vw;
border-radius: 0;
max-width: 100%;
}
#header {
padding: 0.8rem;
}
#header h1 {
font-size: 1.2rem;
}
.message {
max-width: 90%;
padding: 0.6rem 0.8rem;
}
.footer {
padding: 0.8rem;
}
#input-message {
padding: 10px;
}
#send {
padding: 0 1rem;
}
/* Auth modal adjustments */
#auth .modal {
width: 95%;
padding: 1.5rem;
}
#auth input {
padding: 10px;
}
#auth button#save {
padding: 10px;
}
}
/* Very small screens (e.g., iPhone 5/SE) */
@media (max-width: 480px) {
input, textarea {
font-size: 16px !important;
}
.footer {
padding: 0.6rem;
}
#input-message {
padding: 8px 10px;
font-size: 0.95rem;
}
#send {
padding: 0 0.8rem;
margin-left: 0.3rem;
}
.message.system {
font-size: 0.8rem;
padding: 0.4rem;
}
}
@media (max-width: 320px) {
.footer {
padding: 0.5rem;
}
#input-message {
width: 100%;
margin-right: 0;
}
#send {
margin-left: 0;
padding: 8px;
}
}
/* Keyboard avoidance for mobile */
@media (max-height: 600px) {
#messages {
max-height: 60vh;
}
}
/* Portrait mode specific adjustments */
@media (max-width: 600px) and (orientation: portrait) {
#app {
height: calc(var(--vh, 1vh) * 100);
}
}
/* Landscape mode specific adjustments */
@media (max-width: 900px) and (orientation: landscape) {
#app {
height: 100vh;
}
#messages {
max-height: 70vh;
}
}
main.js// Generate unique ID for users
function generateUserId() {
return 'user-' + Math.random().toString(36).substr(2, 9);
}
// Convert image file to base64
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
// WebSocket connection
const socket = new WebSocket(`${location.hostname === 'localhost' ? `ws://${location.host}` : `wss://${location.host}`}`);
// DOM Elements
const authSection = document.getElementById('auth');
const appSection = document.getElementById('app');
const fullNameInput = document.getElementById('fullName');
const fileInput = document.getElementById('file');
const previewPic = document.getElementById('preview-pic');
const saveBtn = document.getElementById('save');
const messagesDiv = document.getElementById('messages');
const inputMessage = document.getElementById('input-message');
const sendBtn = document.getElementById('send');
const logoutBtn = document.getElementById('logout');
const toast = document.getElementById('toast');
// Current user data
let currentUser = JSON.parse(localStorage.getItem('currentUser')) || null;
// Initialize app
function initApp() {
if (currentUser) {
// Convert stored base64 to object URL for local display
if (currentUser.picture && currentUser.picture.startsWith('data:')) {
currentUser.localPicture = URL.createObjectURL(dataURItoBlob(currentUser.picture));
}
authSection.style.display = 'none';
appSection.style.display = 'inline-block';
connectToChat();
} else {
authSection.style.display = 'flex';
appSection.style.display = 'none';
}
document.querySelector('meta[name="viewport"]').content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
}
// Convert data URI to Blob
function dataURItoBlob(dataURI) {
const byteString = atob(dataURI.split(',')[1]);
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
// Connect to chat
function connectToChat() {
socket.onopen = () => {
showToast('Connected to chat', 'success');
// Send user join notification
const joinMessage = {
type: 'system',
content: `${currentUser.name} has joined the chat`,
userId: currentUser.id,
userName: currentUser.name,
userPic: currentUser.picture, // Send base64 version
timestamp: new Date().toISOString()
};
socket.send(JSON.stringify(joinMessage));
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
displayMessage(message);
};
socket.onclose = () => {
showToast('Disconnected from chat', 'error');
};
socket.onerror = (error) => {
showToast('Connection error', 'error');
console.error('WebSocket error:', error);
};
}
// Display message
function displayMessage(message) {
const messageElement = document.createElement('div');
if (message.type === 'system') {
messageElement.className = 'message system';
messageElement.textContent = message.content;
} else {
messageElement.className = `message ${message.userId === currentUser?.id ? 'sent' : 'received'}`;
// Use base64 image directly
const displayPic = message.userPic || 'default-avatar.png';
messageElement.innerHTML = `
`;
}
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Format time
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// Show toast notification
function showToast(message, type) {
toast.textContent = message;
toast.className = `show ${type}`;
setTimeout(() => {
toast.className = toast.className.replace('show', '');
}, 3000);
}
// Event Listeners
fileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
previewPic.src = URL.createObjectURL(this.files[0]);
}
});
saveBtn.addEventListener('click', async function() {
const fullName = fullNameInput.value.trim();
if (!fullName) {
showToast('Please enter your name', 'error');
return;
}
const userId = generateUserId();
let userPic = 'default-avatar.png';
if (fileInput.files[0]) {
try {
userPic = await fileToBase64(fileInput.files[0]);
} catch (error) {
console.error('Error converting image:', error);
showToast('Error processing image', 'error');
return;
}
}
currentUser = {
id: userId,
name: fullName,
picture: userPic
};
localStorage.setItem('currentUser', JSON.stringify(currentUser));
initApp();
});
sendBtn.addEventListener('click', sendMessage);
inputMessage.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
function sendMessage() {
const content = inputMessage.value.trim();
if (content && currentUser) {
const message = {
type: 'chat',
content: content,
userId: currentUser.id,
userName: currentUser.name,
userPic: currentUser.picture, // Send base64 version
timestamp: new Date().toISOString()
};
socket.send(JSON.stringify(message));
inputMessage.value = '';
}
}
logoutBtn.addEventListener('click', function() {
// Send leave notification
const leaveMessage = {
type: 'system',
content: `${currentUser.name} has left the chat`,
userId: currentUser.id,
userName: currentUser.name,
userPic: currentUser.picture,
timestamp: new Date().toISOString()
};
socket.send(JSON.stringify(leaveMessage));
// Clean up object URLs
if (currentUser.localPicture) {
URL.revokeObjectURL(currentUser.localPicture);
}
localStorage.removeItem('currentUser');
currentUser = null;
initApp();
window.location.reload();
});
// Initialize the app
initApp();
function handleViewport() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', handleViewport);
window.addEventListener('orientationchange', handleViewport);
handleViewport(); // Initial call


Comments(0)
Please login to leave a comment