<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat Client</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
width: 90%;
max-width: 800px;
height: 600px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.status {
padding: 10px 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
font-size: 14px;
color: #6c757d;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 200px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
}
.sidebar h3 {
padding: 15px;
background: #e9ecef;
margin: 0;
font-size: 14px;
color: #495057;
}
.online-users {
flex: 1;
overflow-y: auto;
}
.user-item {
padding: 10px 15px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid #e9ecef;
}
.user-item:hover {
background: #e9ecef;
}
.user-item.active {
background: #007bff;
color: white;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 15px 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
font-weight: 600;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #ffffff;
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 15px;
max-width: 70%;
word-wrap: break-word;
}
.message.sent {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin-left: auto;
text-align: right;
}
.message.received {
background: #f1f3f4;
color: #333;
}
.message-info {
font-size: 12px;
opacity: 0.7;
margin-bottom: 5px;
}
.message-input {
display: flex;
padding: 20px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
gap: 10px;
}
.message-input input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ced4da;
border-radius: 25px;
outline: none;
font-size: 14px;
}
.message-input input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.message-input button {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s;
}
.message-input button:hover {
transform: translateY(-2px);
}
.message-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.login-form {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-card {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
width: 90%;
max-width: 400px;
}
.login-card h2 {
margin-bottom: 20px;
text-align: center;
color: #333;
}
.login-card input {
width: 100%;
padding: 15px;
margin-bottom: 15px;
border: 1px solid #ced4da;
border-radius: 10px;
font-size: 16px;
outline: none;
}
.login-card input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.login-card button {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.login-card button:hover {
transform: translateY(-2px);
}
.empty-chat {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #6c757d;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>WebSocket Chat</h1>
</div>
<div class="status" id="connectionStatus">
Disconnected
</div>
<div class="main-content">
<div class="sidebar">
<h3>Online Users</h3>
<div class="online-users" id="onlineUsers">
<div style="padding: 15px; text-align: center; color: #6c757d; font-style: italic;">
Loading users...
</div>
</div>
</div>
<div class="chat-area">
<div class="chat-header" id="chatHeader">
Select a user to start chatting
</div>
<div class="messages" id="messages">
<div class="empty-chat">
Select a user from the sidebar to start a conversation
</div>
</div>
<div class="message-input">
<input type="text" id="messageInput" placeholder="Type your message..." disabled>
<button id="sendButton" disabled>Send</button>
</div>
</div>
</div>
</div>
<div class="login-form" id="loginForm">
<div class="login-card">
<h2>Join Chat</h2>
<input type="number" id="usernameInput" placeholder="Enter your user_id" maxlength="50">
<input type="text" id="serverInput" placeholder="WebSocket Server URL" value="ws://localhost:3000">
<button id="connectButton">Connect</button>
</div>
</div>
<script>
class ChatClient {
constructor() {
this.ws = null;
this.user_id = null;
this.serverUrl = '';
this.currentChatUser = null;
this.onlineUsers = [];
this.messages = {};
this.initEventListeners();
this.fetchOnlineUsers();
}
initEventListeners() {
document.getElementById('connectButton').addEventListener('click', () => this.connect());
document.getElementById('usernameInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.connect();
});
document.getElementById('serverInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.connect();
});
document.getElementById('sendButton').addEventListener('click', () => this.sendMessage());
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendMessage();
});
}
connect() {
const userIdInput = document.getElementById('usernameInput').value.trim();
const serverUrl = document.getElementById('serverInput').value.trim();
if (!userIdInput) {
alert('Please enter a user_id');
return;
}
if (!serverUrl) {
alert('Please enter server URL');
return;
}
const user_id = parseInt(userIdInput, 10);
if (isNaN(user_id)) {
alert('Please enter a valid numeric user_id');
return;
}
this.user_id = user_id;
this.serverUrl = serverUrl;
const wsUrl = `${serverUrl}/ws?token=${encodeURIComponent(user_id)}`;
try {
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => this.onConnect();
this.ws.onmessage = (event) => this.onMessage(event);
this.ws.onclose = () => this.onDisconnect();
this.ws.onerror = (error) => this.onError(error);
this.updateStatus('Connecting...', 'connecting');
} catch (error) {
this.onError(error);
}
}
onConnect() {
document.getElementById('loginForm').style.display = 'none';
this.updateStatus(`Connected as ${this.user_id}`, 'connected');
document.getElementById('messageInput').disabled = false;
document.getElementById('sendButton').disabled = false;
this.fetchOnlineUsers();
this.onlineUsersInterval = setInterval(() => this.fetchOnlineUsers(), 5000);
}
onMessage(event) {
try {
const message = JSON.parse(event.data);
if (message.DirectMessage) {
const { from, to, content } = message.DirectMessage;
const chatUser = from === this.user_id ? to : from;
if (!this.messages[chatUser]) {
this.messages[chatUser] = [];
}
this.messages[chatUser].push({
from,
to,
content,
timestamp: new Date()
});
if (this.currentChatUser === chatUser) {
this.renderMessages();
}
}
} catch (error) {
console.error('Error parsing message:', error);
}
}
onDisconnect() {
this.updateStatus('Disconnected', 'error');
document.getElementById('messageInput').disabled = true;
document.getElementById('sendButton').disabled = true;
if (this.onlineUsersInterval) {
clearInterval(this.onlineUsersInterval);
}
setTimeout(() => {
if (this.user_id && this.serverUrl) {
this.connect();
}
}, 3000);
}
onError(error) {
console.error('WebSocket error:', error);
this.updateStatus('Connection error', 'error');
}
sendMessage() {
const messageInput = document.getElementById('messageInput');
const content = messageInput.value.trim();
if (!content || !this.currentChatUser || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
const message = {
DirectMessage: {
from: this.user_id, to: this.currentChatUser, content: content
}
};
try {
this.ws.send(JSON.stringify(message));
if (!this.messages[this.currentChatUser]) {
this.messages[this.currentChatUser] = [];
}
this.messages[this.currentChatUser].push({
from: this.user_id,
to: this.currentChatUser,
content: content,
timestamp: new Date()
});
this.renderMessages();
messageInput.value = '';
} catch (error) {
console.error('Error sending message:', error);
}
}
async fetchOnlineUsers() {
try {
const baseUrl = this.serverUrl.replace('ws://', 'http://').replace('wss://', 'https://');
const response = await fetch(`${baseUrl}/users/online`);
if (response.ok) {
const users = await response.json();
this.onlineUsers = users.filter(user => user !== this.user_id).map(user => parseInt(user, 10));
this.renderOnlineUsers();
}
} catch (error) {
console.error('Error fetching online users:', error);
}
}
renderOnlineUsers() {
const container = document.getElementById('onlineUsers');
if (this.onlineUsers.length === 0) {
container.innerHTML = '<div style="padding: 15px; text-align: center; color: #6c757d; font-style: italic;">No other users online</div>';
return;
}
container.innerHTML = this.onlineUsers.map(user => `
<div class="user-item ${this.currentChatUser === user ? 'active' : ''}" data-user="${user}">
User ${user}
</div>
`).join('');
container.querySelectorAll('.user-item').forEach(item => {
item.addEventListener('click', () => {
const user = parseInt(item.dataset.user, 10); this.selectChatUser(user);
});
});
}
selectChatUser(user) {
this.currentChatUser = user;
document.getElementById('chatHeader').textContent = `Chat with User ${user}`;
document.querySelectorAll('.user-item').forEach(item => {
item.classList.toggle('active', parseInt(item.dataset.user, 10) === user);
});
this.renderMessages();
}
renderMessages() {
const container = document.getElementById('messages');
const userMessages = this.messages[this.currentChatUser] || [];
if (userMessages.length === 0) {
container.innerHTML = '<div class="empty-chat">No messages yet. Start the conversation!</div>';
return;
}
container.innerHTML = userMessages.map(msg => `
<div class="message ${msg.from === this.user_id ? 'sent' : 'received'}">
<div class="message-info">
User ${msg.from} • ${msg.timestamp.toLocaleTimeString()}
</div>
<div>${this.escapeHtml(msg.content)}</div>
</div>
`).join('');
container.scrollTop = container.scrollHeight;
}
updateStatus(text, type = '') {
const statusEl = document.getElementById('connectionStatus');
statusEl.textContent = text;
statusEl.className = `status ${type}`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
document.addEventListener('DOMContentLoaded', () => {
new ChatClient();
});
</script>
</body>
</html>