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 = `
      
${message.userName} ${message.userName} ${formatTime(message.timestamp)}
${message.content}
`; } 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