ALDI WAHYUDIN
bagian chat list di kiri bisa di sembnyikan dan di rampilkan dengan tombol icon berger di kiri atas - Initial Deployment
dd20ee6
verified
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WhatsApp Web Clone</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| .chat-container { | |
| height: calc(100vh - 120px); | |
| } | |
| .message-input { | |
| resize: none; | |
| } | |
| .scrollbar-hide::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .scrollbar-hide { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| .typing-indicator::after { | |
| content: '...'; | |
| animation: typing 1.5s infinite; | |
| display: inline-block; | |
| width: 0; | |
| } | |
| @keyframes typing { | |
| 0% { width: 0; } | |
| 50% { width: 0.5em; } | |
| 100% { width: 1em; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <div class="flex flex-col h-screen"> | |
| <!-- Header --> | |
| <div class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white p-4 shadow-md"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-4"> | |
| <button id="toggle-sidebar" class="p-1 rounded-full hover:bg-indigo-700"> | |
| <i data-feather="menu" class="w-6 h-6"></i> | |
| </button> | |
| <i data-feather="message-circle" class="w-6 h-6"></i> | |
| <h1 class="text-xl font-semibold">WhatsApp Web</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="refresh-btn" class="p-2 rounded-full hover:bg-emerald-600"> | |
| <i data-feather="refresh-cw" class="w-5 h-5"></i> | |
| </button> | |
| <button id="settings-btn" class="p-2 rounded-full hover:bg-emerald-600"> | |
| <i data-feather="settings" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex flex-1 overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="w-1/3 border-r border-gray-300 bg-white flex flex-col transition-all duration-300 ease-in-out"> | |
| <!-- Search --> | |
| <div class="p-3 bg-gray-50"> | |
| <div class="relative"> | |
| <input type="text" placeholder="Search or start new chat" | |
| class="w-full py-2 pl-10 pr-4 bg-white rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500"> | |
| <i data-feather="search" class="absolute left-3 top-2.5 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <!-- Chat List --> | |
| <div class="flex-1 overflow-y-auto scrollbar-hide"> | |
| <div id="chat-list" class="divide-y divide-gray-200"> | |
| <!-- Chat items will be added here dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Area --> | |
| <div class="flex-1 flex flex-col bg-gray-100"> | |
| <!-- Chat Header --> | |
| <div id="chat-header" class="p-3 bg-white border-b border-gray-300 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center"> | |
| <i data-feather="user" class="text-gray-500"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold">Select a chat</h3> | |
| <p class="text-xs text-gray-500">Last seen</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="search" class="w-5 h-5"></i> | |
| </button> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="more-vertical" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div id="messages-container" class="flex-1 overflow-y-auto p-4 space-y-3 scrollbar-hide"> | |
| <div class="text-center py-10 text-gray-500"> | |
| <i data-feather="message-circle" class="w-12 h-12 mx-auto mb-2"></i> | |
| <p>Select a chat to start messaging</p> | |
| </div> | |
| </div> | |
| <!-- Message Input --> | |
| <div id="message-input-container" class="p-3 bg-white border-t border-gray-300 hidden"> | |
| <div class="flex items-center space-x-2"> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="smile" class="w-5 h-5"></i> | |
| </button> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="paperclip" class="w-5 h-5"></i> | |
| </button> | |
| <div class="flex-1"> | |
| <textarea id="message-input" rows="1" placeholder="Type a message" | |
| class="w-full py-2 px-4 bg-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 message-input"></textarea> | |
| </div> | |
| <button id="send-btn" class="p-2 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 text-white hover:from-indigo-600 hover:to-purple-600 shadow-lg"> | |
| <i data-feather="send" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div id="settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> | |
| <div class="bg-white rounded-lg w-full max-w-md"> | |
| <div class="p-4 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold">Settings</h3> | |
| <button id="close-settings" class="p-1 rounded-full hover:bg-gray-200"> | |
| <i data-feather="x" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="p-4 space-y-4"> | |
| <div class="flex items-center space-x-4"> | |
| <div class="w-12 h-12 rounded-full bg-gray-300 flex items-center justify-center"> | |
| <i data-feather="user" class="text-gray-500"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-medium">User Profile</h4> | |
| <p class="text-sm text-gray-500">Update your profile information</p> | |
| </div> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex items-center justify-between p-3 hover:bg-gray-100 rounded-lg cursor-pointer"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="user" class="w-5 h-5 text-gray-500"></i> | |
| <span>Profile</span> | |
| </div> | |
| <i data-feather="chevron-right" class="w-5 h-5 text-gray-400"></i> | |
| </div> | |
| <div class="flex items-center justify-between p-3 hover:bg-gray-100 rounded-lg cursor-pointer"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="lock" class="w-5 h-5 text-purple-500"></i> | |
| <span class="text-purple-600">Privacy</span> | |
| </div> | |
| <i data-feather="chevron-right" class="w-5 h-5 text-purple-400"></i> | |
| </div> | |
| <div class="flex items-center justify-between p-3 hover:bg-gray-100 rounded-lg cursor-pointer"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="bell" class="w-5 h-5 text-indigo-500"></i> | |
| <span class="text-indigo-600">Notifications</span> | |
| </div> | |
| <i data-feather="chevron-right" class="w-5 h-5 text-indigo-400"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t border-gray-200"> | |
| <button id="logout-btn" class="w-full py-2 px-4 bg-red-600 text-white rounded-lg hover:bg-red-700"> | |
| Logout | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize AOS and Feather Icons | |
| AOS.init(); | |
| feather.replace(); | |
| // Sample data for demonstration | |
| let chats = []; | |
| let currentChat = null; | |
| let messages = {}; | |
| // DOM Elements | |
| const chatList = document.getElementById('chat-list'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const toggleSidebarBtn = document.getElementById('toggle-sidebar'); | |
| const messagesContainer = document.getElementById('messages-container'); | |
| const messageInputContainer = document.getElementById('message-input-container'); | |
| const messageInput = document.getElementById('message-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const chatHeader = document.getElementById('chat-header'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const closeSettings = document.getElementById('close-settings'); | |
| const refreshBtn = document.getElementById('refresh-btn'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| // API base URL | |
| const API_BASE = 'http://localhost:3000'; | |
| // Fetch chats from API | |
| async function fetchChats() { | |
| try { | |
| const response = await fetch(`${API_BASE}/chats`); | |
| const data = await response.json(); | |
| if (data.code === 'SUCCESS') { | |
| chats = data.results.data; | |
| renderChatList(); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching chats:', error); | |
| } | |
| } | |
| // Render chat list | |
| function renderChatList() { | |
| chatList.innerHTML = ''; | |
| chats.forEach(chat => { | |
| const chatItem = document.createElement('div'); | |
| chatItem.className = `p-3 flex items-center space-x-3 hover:bg-gray-100 cursor-pointer ${chat.unread > 0 ? 'bg-gray-50' : ''}`; | |
| chatItem.dataset.chatId = chat.id; | |
| chatItem.innerHTML = ` | |
| <div class="relative"> | |
| <div class="w-12 h-12 rounded-full bg-gray-300 flex items-center justify-center"> | |
| <i data-feather="user" class="text-gray-500"></i> | |
| </div> | |
| ${chat.isOnline ? `<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>` : ''} | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex justify-between items-center"> | |
| <h4 class="font-medium truncate">${chat.name}</h4> | |
| <span class="text-xs text-gray-500 whitespace-nowrap">${chat.time}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <p class="text-sm text-gray-500 truncate">${chat.lastMessage}</p> | |
| ${chat.unread > 0 ? `<span class="bg-purple-600 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">${chat.unread}</span>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| chatItem.addEventListener('click', () => loadChat(chat.id)); | |
| chatList.appendChild(chatItem); | |
| }); | |
| feather.replace(); | |
| } | |
| // Load chat messages | |
| async function loadChat(chatJid) { | |
| currentChat = chatJid; | |
| try { | |
| // Fetch chat info | |
| const infoResponse = await fetch(`${API_BASE}/user/info?phone=${encodeURIComponent(chatJid)}`); | |
| const infoData = await infoResponse.json(); | |
| // Fetch messages | |
| const messagesResponse = await fetch(`${API_BASE}/chat/${encodeURIComponent(chatJid)}/messages`); | |
| const messagesData = await messagesResponse.json(); | |
| if (infoData.code === 'SUCCESS' && messagesData.code === 'SUCCESS') { | |
| const chatInfo = infoData.results; | |
| messages[chatJid] = messagesData.results.data; | |
| // Update chat header | |
| chatHeader.innerHTML = ` | |
| <div class="flex items-center space-x-3"> | |
| <div class="relative"> | |
| <div class="w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center"> | |
| <i data-feather="user" class="text-gray-500"></i> | |
| </div> | |
| ${chatInfo.isOnline ? `<div class="absolute bottom-0 right-0 w-2 h-2 bg-green-500 rounded-full border border-white"></div>` : ''} | |
| </div> | |
| <div> | |
| <h3 class="font-semibold">${chatInfo.verified_name || chatJid}</h3> | |
| <p class="text-xs text-gray-500">${chatInfo.isOnline ? 'online' : 'last seen recently'}</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="search" class="w-5 h-5"></i> | |
| </button> | |
| <button class="p-2 rounded-full hover:bg-gray-200"> | |
| <i data-feather="more-vertical" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Render messages | |
| messagesContainer.innerHTML = ''; | |
| messages.forEach(msg => { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex ${msg.isMe ? 'justify-end' : 'justify-start'}`; | |
| messageDiv.innerHTML = ` | |
| <div class="max-w-xs md:max-w-md lg:max-w-lg rounded-lg p-3 ${msg.isMe ? 'bg-gradient-to-r from-indigo-100 to-purple-100' : 'bg-white'} shadow"> | |
| <p>${msg.text}</p> | |
| <p class="text-xs text-gray-500 text-right mt-1">${msg.time}</p> | |
| </div> | |
| `; | |
| messagesContainer.appendChild(messageDiv); | |
| }); | |
| // Scroll to bottom | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| // Show message input | |
| messageInputContainer.classList.remove('hidden'); | |
| messageInput.focus(); | |
| feather.replace(); | |
| } | |
| // Send message | |
| async function sendMessage() { | |
| const text = messageInput.value.trim(); | |
| if (!text || !currentChat) return; | |
| try { | |
| // Show typing indicator | |
| await fetch(`${API_BASE}/send/chat-presence`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| phone: currentChat, | |
| action: 'start' | |
| }) | |
| }); | |
| // Send message | |
| const response = await fetch(`${API_BASE}/send/message`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| phone: currentChat, | |
| message: text | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.code === 'SUCCESS') { | |
| // Add message to UI | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'flex justify-end'; | |
| messageDiv.innerHTML = ` | |
| <div class="max-w-xs md:max-w-md lg:max-w-lg rounded-lg p-3 bg-gradient-to-r from-emerald-100 to-teal-100 shadow"> | |
| <p>${text}</p> | |
| <p class="text-xs text-gray-500 text-right mt-1">Just now</p> | |
| </div> | |
| `; | |
| messagesContainer.appendChild(messageDiv); | |
| messageInput.value = ''; | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| // Stop typing indicator | |
| await fetch(`${API_BASE}/send/chat-presence`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| phone: currentChat, | |
| action: 'stop' | |
| }) | |
| }); | |
| // Refresh messages | |
| loadChat(currentChat); | |
| } | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| } | |
| } | |
| // Event Listeners | |
| sendBtn.addEventListener('click', sendMessage); | |
| messageInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| settingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.remove('hidden'); | |
| }); | |
| closeSettings.addEventListener('click', () => { | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| refreshBtn.addEventListener('click', () => { | |
| fetchChats(); | |
| if (currentChat) { | |
| loadChat(currentChat); | |
| } | |
| }); | |
| logoutBtn.addEventListener('click', async () => { | |
| try { | |
| const response = await fetch(`${API_BASE}/app/logout`); | |
| const data = await response.json(); | |
| if (data.code === 'SUCCESS') { | |
| window.location.reload(); | |
| } | |
| } catch (error) { | |
| console.error('Error logging out:', error); | |
| } | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| // Toggle sidebar | |
| toggleSidebarBtn.addEventListener('click', () => { | |
| sidebar.classList.toggle('hidden'); | |
| feather.replace(); | |
| }); | |
| // Initialize | |
| fetchChats(); | |
| </script> | |
| </body> | |
| </html> | |