abdullahalioo's picture
Upload 8 files
81923e0 verified
Raw History Blame Contribute Delete
31 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WhatsApp-like Chat</title>
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #111b21;
height: 100vh;
overflow: hidden;
}
.auth-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.auth-box {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
width: 400px;
max-width: 90%;
}
.auth-box h2 {
margin-bottom: 20px;
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.btn:hover {
background: #5568d3;
}
.toggle-auth {
text-align: center;
margin-top: 15px;
color: #666;
}
.toggle-auth a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.error-msg {
background: #fee;
color: #c33;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
}
.success-msg {
background: #efe;
color: #3c3;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
}
.chat-container {
display: none;
height: 100vh;
background: #222e35;
}
.chat-layout {
display: flex;
height: 100%;
}
.sidebar {
width: 400px;
background: #111b21;
border-right: 1px solid #2a3942;
display: flex;
flex-direction: column;
}
.sidebar-header {
background: #202c33;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
color: #e9edef;
font-weight: 500;
}
.user-id-display {
font-size: 12px;
color: #8696a0;
}
.logout-btn {
background: #dc3545;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.add-contact-section {
background: #202c33;
padding: 15px;
border-bottom: 1px solid #2a3942;
}
.add-contact-form {
display: flex;
gap: 10px;
}
.add-contact-input {
flex: 1;
padding: 10px;
border: 1px solid #2a3942;
border-radius: 5px;
background: #2a3942;
color: #e9edef;
font-size: 14px;
}
.add-contact-btn {
background: #00a884;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
}
.contacts-list {
flex: 1;
overflow-y: auto;
}
.contact-item {
padding: 15px;
border-bottom: 1px solid #2a3942;
cursor: pointer;
display: flex;
align-items: center;
transition: background 0.2s;
}
.contact-item:hover {
background: #202c33;
}
.contact-item.active {
background: #2a3942;
}
.contact-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 20px;
margin-right: 15px;
}
.contact-info {
flex: 1;
}
.contact-name {
color: #e9edef;
font-weight: 500;
margin-bottom: 3px;
}
.contact-status {
font-size: 12px;
color: #8696a0;
}
.contact-status.online {
color: #00a884;
}
.unread-badge {
background: #00a884;
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: #0b141a;
}
.chat-header {
background: #202c33;
padding: 15px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #2a3942;
}
.chat-header-info {
flex: 1;
}
.chat-partner-name {
color: #e9edef;
font-weight: 500;
margin-bottom: 3px;
}
.chat-partner-status {
font-size: 13px;
color: #8696a0;
}
.typing-indicator {
font-size: 13px;
color: #00a884;
display: none;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48cGF0dGVybiBpZD0iYSIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMGIxNDFhIi8+PHBhdGggZD0iTTUwIDUwbTAtMzVhMzUgMzUgMCAxIDAgNzAgMGEzNSAzNSAwIDEgMC03MCAwIiBzdHJva2U9IiMxMTFiMjEiIHN0cm9rZS13aWR0aD0iMC41IiBmaWxsPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+');
}
.message {
margin-bottom: 12px;
display: flex;
animation: messageSlideIn 0.3s ease;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.sent {
justify-content: flex-end;
}
.message-bubble {
max-width: 60%;
padding: 8px 12px;
border-radius: 8px;
position: relative;
}
.message.received .message-bubble {
background: #202c33;
color: #e9edef;
}
.message.sent .message-bubble {
background: #005c4b;
color: #e9edef;
}
.message-text {
margin-bottom: 5px;
word-wrap: break-word;
}
.message-meta {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 5px;
font-size: 11px;
color: #8696a0;
}
.message-status {
font-size: 16px;
}
.status-sent {
color: #8696a0;
}
.status-delivered {
color: #8696a0;
}
.status-read {
color: #53bdeb;
}
.message-input-area {
background: #202c33;
padding: 15px;
display: flex;
gap: 10px;
align-items: center;
}
.message-input {
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 8px;
background: #2a3942;
color: #e9edef;
font-size: 15px;
resize: none;
max-height: 100px;
}
.message-input:focus {
outline: none;
}
.send-btn {
background: #00a884;
color: white;
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.send-btn:hover {
background: #06cf9c;
}
.empty-chat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #8696a0;
text-align: center;
padding: 40px;
}
.empty-chat-icon {
font-size: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
.empty-chat-text {
font-size: 18px;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #111b21;
}
::-webkit-scrollbar-thumb {
background: #2a3942;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #374955;
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
}
.chat-area {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
}
.chat-area.mobile-visible {
display: flex;
}
.sidebar.mobile-hidden {
display: none;
}
}
</style>
</head>
<body>
<div id="authContainer" class="auth-container">
<div class="auth-box">
<div id="registerForm">
<h2>Register</h2>
<div id="authMessage"></div>
<div class="form-group">
<label>Name</label>
<input type="text" id="registerName" placeholder="Enter your name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="registerEmail" placeholder="Enter your email">
</div>
<button class="btn" onclick="register()">Register</button>
<div class="toggle-auth">
Already have an account? <a href="#" onclick="toggleAuth('login'); return false;">Login</a>
</div>
</div>
<div id="loginForm" style="display: none;">
<h2>Login</h2>
<div id="loginMessage"></div>
<div class="form-group">
<label>User ID (8 digits)</label>
<input type="text" id="loginUserId" placeholder="Enter your 8-digit ID" maxlength="8">
</div>
<div class="form-group">
<label>Token</label>
<input type="text" id="loginToken" placeholder="Enter your token">
</div>
<button class="btn" onclick="login()">Login</button>
<div class="toggle-auth">
Don't have an account? <a href="#" onclick="toggleAuth('register'); return false;">Register</a>
</div>
</div>
</div>
</div>
<div id="chatContainer" class="chat-container">
<div class="chat-layout">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div>
<div class="user-info" id="currentUserName">User</div>
<div class="user-id-display">ID: <span id="currentUserId"></span></div>
</div>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
<div class="add-contact-section">
<div class="add-contact-form">
<input type="text" class="add-contact-input" id="addContactId"
placeholder="Enter 8-digit User ID" maxlength="8">
<button class="add-contact-btn" onclick="addContact()">Add</button>
</div>
</div>
<div class="contacts-list" id="contactsList"></div>
</div>
<div class="chat-area" id="chatArea">
<div class="empty-chat">
<div class="empty-chat-icon">💬</div>
<div class="empty-chat-text">Select a contact to start chatting</div>
</div>
</div>
</div>
</div>
<script>
let socket;
let currentUser = null;
let currentChat = null;
let contacts = [];
let unreadCounts = {};
let typingTimeout = null;
function toggleAuth(mode) {
if (mode === 'login') {
document.getElementById('registerForm').style.display = 'none';
document.getElementById('loginForm').style.display = 'block';
document.getElementById('authMessage').innerHTML = '';
document.getElementById('loginMessage').innerHTML = '';
} else {
document.getElementById('registerForm').style.display = 'block';
document.getElementById('loginForm').style.display = 'none';
document.getElementById('authMessage').innerHTML = '';
document.getElementById('loginMessage').innerHTML = '';
}
}
async function register() {
const name = document.getElementById('registerName').value.trim();
const email = document.getElementById('registerEmail').value.trim();
const msgDiv = document.getElementById('authMessage');
if (!name || !email) {
msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>';
return;
}
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email })
});
const data = await response.json();
if (response.ok) {
msgDiv.innerHTML = `<div class="success-msg">
Registration successful!<br>
<strong>Your ID: ${data.user_id}</strong><br>
<strong>Your Token: ${data.token}</strong><br>
<small>Save these credentials to login</small>
</div>`;
localStorage.setItem('user_id', data.user_id);
localStorage.setItem('token', data.token);
setTimeout(() => {
currentUser = data;
initChat();
}, 2000);
} else {
msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`;
}
} catch (error) {
msgDiv.innerHTML = `<div class="error-msg">Registration failed: ${error.message}</div>`;
}
}
async function login() {
const user_id = document.getElementById('loginUserId').value.trim();
const token = document.getElementById('loginToken').value.trim();
const msgDiv = document.getElementById('loginMessage');
if (!user_id || !token) {
msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>';
return;
}
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id, token })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('user_id', user_id);
localStorage.setItem('token', token);
currentUser = { user_id, token, ...data };
initChat();
} else {
msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`;
}
} catch (error) {
msgDiv.innerHTML = `<div class="error-msg">Login failed: ${error.message}</div>`;
}
}
function initChat() {
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'block';
document.getElementById('currentUserName').textContent = currentUser.name;
document.getElementById('currentUserId').textContent = currentUser.user_id;
initWebSocket();
loadContacts();
}
function initWebSocket() {
socket = io();
socket.on('connect', () => {
console.log('Connected to server');
socket.emit('authenticate', { token: currentUser.token });
});
socket.on('authenticated', (data) => {
if (data.success) {
console.log('Authenticated successfully');
} else {
alert('Authentication failed: ' + data.error);
logout();
}
});
socket.on('user_status', (data) => {
updateContactStatus(data.user_id, data.online);
});
socket.on('new_message', (message) => {
if (currentChat && message.from === currentChat.user_id) {
appendMessage(message, false);
socket.emit('mark_conversation_read', {
token: currentUser.token,
contact_id: currentChat.user_id
});
} else {
unreadCounts[message.from] = (unreadCounts[message.from] || 0) + 1;
updateContactsList();
}
});
socket.on('message_sent', (message) => {
appendMessage(message, true);
});
socket.on('message_status_update', (data) => {
updateMessageStatus(data.message_id, data.status);
});
socket.on('user_typing', (data) => {
if (currentChat && data.user_id === currentChat.user_id) {
const typingIndicator = document.querySelector('.typing-indicator');
if (typingIndicator) {
typingIndicator.style.display = data.typing ? 'block' : 'none';
}
}
});
}
async function loadContacts() {
try {
const response = await fetch('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: currentUser.token })
});
const data = await response.json();
contacts = data.contacts || [];
updateContactsList();
} catch (error) {
console.error('Failed to load contacts:', error);
}
}
function updateContactsList() {
const list = document.getElementById('contactsList');
list.innerHTML = '';
contacts.forEach(contact => {
const item = document.createElement('div');
item.className = 'contact-item';
if (currentChat && currentChat.user_id === contact.user_id) {
item.className += ' active';
}
const initial = contact.name.charAt(0).toUpperCase();
const unreadCount = unreadCounts[contact.user_id] || 0;
const onlineStatus = contact.online ? 'online' : 'offline';
item.innerHTML = `
<div class="contact-avatar">${initial}</div>
<div class="contact-info">
<div class="contact-name">${contact.name}</div>
<div class="contact-status ${onlineStatus}">
${contact.online ? 'Online' : 'Offline'}
</div>
</div>
${unreadCount > 0 ? `<div class="unread-badge">${unreadCount}</div>` : ''}
`;
item.onclick = () => openChat(contact);
list.appendChild(item);
});
}
function updateContactStatus(userId, online) {
const contact = contacts.find(c => c.user_id === userId);
if (contact) {
contact.online = online;
updateContactsList();
if (currentChat && currentChat.user_id === userId) {
const statusEl = document.querySelector('.chat-partner-status');
if (statusEl && !statusEl.classList.contains('typing-indicator')) {
statusEl.textContent = online ? 'Online' : 'Offline';
}
}
}
}
async function addContact() {
const contactId = document.getElementById('addContactId').value.trim();
if (!contactId || contactId.length !== 8) {
alert('Please enter a valid 8-digit User ID');
return;
}
try {
const response = await fetch('/api/contacts/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: currentUser.token,
contact_id: contactId
})
});
const data = await response.json();
if (response.ok) {
document.getElementById('addContactId').value = '';
loadContacts();
alert(`Contact added: ${data.name}`);
} else {
alert(data.error);
}
} catch (error) {
alert('Failed to add contact: ' + error.message);
}
}
async function openChat(contact) {
currentChat = contact;
unreadCounts[contact.user_id] = 0;
updateContactsList();
const chatArea = document.getElementById('chatArea');
chatArea.innerHTML = `
<div class="chat-header">
<div class="chat-header-info">
<div class="chat-partner-name">${contact.name}</div>
<div class="chat-partner-status">${contact.online ? 'Online' : 'Offline'}</div>
<div class="typing-indicator">typing...</div>
</div>
</div>
<div class="messages-container" id="messagesContainer"></div>
<div class="message-input-area">
<textarea class="message-input" id="messageInput"
placeholder="Type a message" rows="1"></textarea>
<button class="send-btn" onclick="sendMessage()">â–¶</button>
</div>
`;
chatArea.classList.add('mobile-visible');
document.getElementById('sidebar').classList.add('mobile-hidden');
const messageInput = document.getElementById('messageInput');
messageInput.addEventListener('input', handleTyping);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
await loadMessages(contact.user_id);
socket.emit('mark_conversation_read', {
token: currentUser.token,
contact_id: contact.user_id
});
}
async function loadMessages(contactId) {
try {
const response = await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: currentUser.token,
contact_id: contactId
})
});
const data = await response.json();
const messages = data.messages || [];
messages.forEach(msg => {
const isSent = msg.from === currentUser.user_id;
appendMessage(msg, isSent);
});
scrollToBottom();
} catch (error) {
console.error('Failed to load messages:', error);
}
}
function appendMessage(message, isSent) {
const container = document.getElementById('messagesContainer');
if (!container) return;
const msgDiv = document.createElement('div');
msgDiv.className = `message ${isSent ? 'sent' : 'received'}`;
msgDiv.dataset.messageId = message.message_id;
const time = new Date(message.timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
const statusIcon = getStatusIcon(message.status);
msgDiv.innerHTML = `
<div class="message-bubble">
<div class="message-text">${escapeHtml(message.message)}</div>
<div class="message-meta">
<span>${time}</span>
${isSent ? `<span class="message-status">${statusIcon}</span>` : ''}
</div>
</div>
`;
container.appendChild(msgDiv);
scrollToBottom();
}
function getStatusIcon(status) {
if (status === 'sent') return '<span class="status-sent">✓</span>';
if (status === 'delivered') return '<span class="status-delivered">✓✓</span>';
if (status === 'read') return '<span class="status-read">✓✓</span>';
return '';
}
function updateMessageStatus(messageId, status) {
const msgDiv = document.querySelector(`[data-message-id="${messageId}"]`);
if (msgDiv) {
const statusSpan = msgDiv.querySelector('.message-status');
if (statusSpan) {
statusSpan.innerHTML = getStatusIcon(status);
}
}
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message || !currentChat) return;
socket.emit('send_message', {
token: currentUser.token,
to: currentChat.user_id,
message: message
});
input.value = '';
input.style.height = 'auto';
socket.emit('typing', {
token: currentUser.token,
to: currentChat.user_id,
typing: false
});
}
function handleTyping() {
if (!currentChat) return;
socket.emit('typing', {
token: currentUser.token,
to: currentChat.user_id,
typing: true
});
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('typing', {
token: currentUser.token,
to: currentChat.user_id,
typing: false
});
}, 1000);
}
function scrollToBottom() {
const container = document.getElementById('messagesContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function logout() {
localStorage.removeItem('user_id');
localStorage.removeItem('token');
if (socket) socket.disconnect();
location.reload();
}
window.onload = () => {
const savedUserId = localStorage.getItem('user_id');
const savedToken = localStorage.getItem('token');
if (savedUserId && savedToken) {
document.getElementById('loginUserId').value = savedUserId;
document.getElementById('loginToken').value = savedToken;
}
};
</script>
</body>
</html>