WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted...
Log | Files | Refs | README | LICENSE

commit a468b13f9a5208f847f6f56266325e90f8faeff2
parent b8bd6b4d89793ac3ea46dfdd9c30c58fef4b536d
Author: AMIT DUTTA <amitdutta4255@gmail.com>
Date:   Mon, 16 Feb 2026 21:47:26 +0530

Add files via upload
Diffstat:
Aindex.html | 812+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 812 insertions(+), 0 deletions(-)

diff --git a/index.html b/index.html @@ -0,0 +1,811 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <title>WhatsApp Logger M3</title> + + <!-- SVG Favicon --> + <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='30' fill='%23006C4C'/%3E%3Cpath d='M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z' fill='white'/%3E%3Ccircle cx='42' cy='45' r='3' fill='%23006C4C'/%3E%3Ccircle cx='50' cy='45' r='3' fill='%23006C4C'/%3E%3Ccircle cx='58' cy='45' r='3' fill='%23006C4C'/%3E%3C/svg%3E"> + + <!-- Fonts & Icons --> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> + + <!-- Firebase SDKs --> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script> + + <style> + :root { + /* M3 Light Theme Tokens - Nature/Mint Palette */ + --md-sys-color-primary: #006C4C; + --md-sys-color-on-primary: #FFFFFF; + --md-sys-color-primary-container: #89F8C7; + --md-sys-color-on-primary-container: #002114; + + --md-sys-color-secondary: #4D6357; + --md-sys-color-on-secondary: #FFFFFF; + --md-sys-color-secondary-container: #CFE9D9; + --md-sys-color-on-secondary-container: #092016; + + --md-sys-color-surface: #FBFDF9; + --md-sys-color-surface-dim: #D9DBD9; + --md-sys-color-surface-container: #ECEEEB; + --md-sys-color-surface-container-high: #E6E8E5; + + --md-sys-color-on-surface: #191C1A; + --md-sys-color-on-surface-variant: #404944; + --md-sys-color-outline: #707974; + --md-sys-color-outline-variant: #BFC9C2; + + --md-sys-color-error: #BA1A1A; + + /* WhatsApp Specifics */ + --chat-bubble-me: #E2FFEC; + --chat-bubble-them: #FFFFFF; + + /* Elevations & Shapes */ + --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + --shape-corner-extra-large: 28px; + --shape-corner-large: 16px; + --shape-corner-medium: 12px; + + /* Motion */ + --motion-easing-emphasized: cubic-bezier(0.2, 0.0, 0.0, 1.0); + --motion-duration-medium: 400ms; + } + + * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; } + + body { + font-family: 'Roboto', sans-serif; + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + height: 100vh; + overflow: hidden; + display: flex; + justify-content: center; + } + + /* --- ANIMATIONS --- */ + .hidden { display: none !important; } + .fade-in { animation: fadeIn 0.4s var(--motion-easing-emphasized); } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + + .stagger-enter { opacity: 0; transform: translateY(15px); animation: staggerSlideUp 0.5s var(--motion-easing-emphasized) forwards; } + @keyframes staggerSlideUp { to { opacity: 1; transform: translateY(0); } } + + .delay-1 { animation-delay: 0.1s; } + .delay-2 { animation-delay: 0.2s; } + .delay-3 { animation-delay: 0.3s; } + .delay-4 { animation-delay: 0.4s; } + + /* --- RIPPLE EFFECT --- */ + .ripple-surface { + position: relative; + overflow: hidden; + transform: translate3d(0, 0, 0); + transition: transform 0.1s; + } + .ripple-surface:active { transform: scale(0.98); } + .ripple { + position: absolute; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.1); + transform: scale(0); + animation: ripple-anim 0.6s linear; + pointer-events: none; + } + @keyframes ripple-anim { to { transform: scale(4); opacity: 0; } } + + /* --- AUTH SCREEN --- */ + #auth-screen { + position: fixed; inset: 0; z-index: 999; + background: radial-gradient(circle at 50% 10%, #E6F7EF 0%, var(--md-sys-color-surface) 60%); + display: flex; flex-direction: column; align-items: center; justify-content: center; + padding: 24px; + } + + .auth-card { + width: 100%; max-width: 400px; + text-align: center; + } + + .brand-icon { + width: 80px; height: 80px; + display: inline-flex; align-items: center; justify-content: center; + margin-bottom: 24px; + animation: float 6s ease-in-out infinite; + } + .brand-icon svg { width: 100%; height: 100%; filter: drop-shadow(0 4px 12px rgba(0, 108, 76, 0.2)); } + @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } + + .m3-field { + position: relative; margin-bottom: 16px; + background: var(--md-sys-color-surface-container-high); + border-radius: 4px 4px 0 0; + border-bottom: 1px solid var(--md-sys-color-outline); + transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + .m3-field:focus-within { + border-bottom: 2px solid var(--md-sys-color-primary); + background: var(--md-sys-color-surface-container); + } + + .m3-input { + width: 100%; padding: 24px 48px 8px 16px; + border: none; background: transparent; outline: none; + font-size: 16px; color: var(--md-sys-color-on-surface); + font-family: inherit; + } + .m3-label { + position: absolute; left: 16px; top: 18px; + color: var(--md-sys-color-on-surface-variant); + font-size: 16px; pointer-events: none; transition: 0.2s; + } + .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { + top: 6px; font-size: 12px; color: var(--md-sys-color-primary); + } + + .eye-toggle { + position: absolute; right: 12px; top: 50%; transform: translateY(-50%); + border: none; background: none; cursor: pointer; + color: var(--md-sys-color-on-surface-variant); + padding: 8px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + transition: background 0.2s; + } + .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } + + .m3-btn { + width: 100%; height: 56px; border-radius: 28px; + border: none; outline: none; cursor: pointer; + font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 16px; + display: flex; align-items: center; justify-content: center; gap: 8px; + transition: 0.2s; + } + .m3-btn.filled { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + box-shadow: var(--elevation-1); + } + .m3-btn.filled:hover { box-shadow: var(--elevation-2); background: #005A3F; transform: translateY(-1px); } + .m3-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } + + /* --- APP LAYOUT --- */ + #app-layout { + display: flex; width: 100%; height: 100%; max-width: 1600px; + background: var(--md-sys-color-surface); + opacity: 0; transition: opacity 0.6s ease-in-out; + } + + /* --- SIDEBAR --- */ + .sidebar { + width: 400px; display: flex; flex-direction: column; + border-right: 1px solid var(--md-sys-color-outline-variant); + background: var(--md-sys-color-surface); + z-index: 10; + } + + .sidebar-header { + padding: 16px 20px; + display: flex; justify-content: space-between; align-items: center; + } + + .search-container { padding: 0 16px 12px; } + .search-bar { + background: var(--md-sys-color-surface-container-high); + border-radius: 28px; height: 48px; + display: flex; align-items: center; padding: 0 16px; + transition: 0.3s; + } + .search-bar:focus-within { background: var(--md-sys-color-surface-container); box-shadow: 0 1px 4px rgba(0,0,0,0.1); } + .search-bar input { + border: none; background: transparent; outline: none; + flex: 1; font-size: 16px; margin-left: 12px; + color: var(--md-sys-color-on-surface); + } + + .contact-list { + flex: 1; overflow-y: auto; overflow-x: hidden; + padding: 8px; + } + + .contact-item { + display: flex; align-items: center; padding: 12px 16px; + border-radius: 16px; cursor: pointer; + transition: background 0.2s, transform 0.1s; margin-bottom: 2px; + } + .contact-item:hover { background: rgba(0,0,0,0.03); } + .contact-item.active { background: var(--md-sys-color-secondary-container); } + .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; } + + .avatar { + width: 48px; height: 48px; border-radius: 50%; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + display: flex; align-items: center; justify-content: center; + font-size: 18px; font-weight: 500; flex-shrink: 0; + margin-right: 16px; + transition: transform 0.3s; + } + .contact-item:hover .avatar { transform: scale(1.05); } + + .contact-info { flex: 1; min-width: 0; } + .contact-top { display: flex; justify-content: space-between; margin-bottom: 4px; } + .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .contact-time { font-size: 12px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; margin-left: 8px;} + .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + /* --- CHAT AREA --- */ + .chat-area { + flex: 1; display: flex; flex-direction: column; + position: relative; + background-color: #EFE7DE; + background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); + } + + .chat-header { + height: 64px; + background: rgba(251, 253, 249, 0.95); + backdrop-filter: blur(10px); + display: flex; align-items: center; padding: 0 16px; + border-bottom: 1px solid rgba(0,0,0,0.05); + z-index: 5; + } + + .chat-content { + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 4px; + } + + /* Messages */ + .date-divider { + align-self: center; + background: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface-variant); + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; font-weight: 500; + margin: 16px 0; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + } + + .msg-bubble { + max-width: 80%; + padding: 8px 12px; + position: relative; + font-size: 15px; line-height: 1.4; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + animation: popIn 0.3s var(--motion-easing-emphasized); + display: flex; flex-direction: column; + } + + .msg-in { + align-self: flex-start; + background: var(--chat-bubble-them); + border-radius: 0 16px 16px 16px; + color: var(--md-sys-color-on-surface); + } + .msg-out { + align-self: flex-end; + background: var(--chat-bubble-me); + border-radius: 16px 0 16px 16px; + color: var(--md-sys-color-on-surface); + } + + .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; } + .msg-meta { font-size: 10px; opacity: 0.6; align-self: flex-end; margin-top: 2px; margin-left: 8px; display: inline-flex; align-items: center; gap: 2px; } + + @keyframes popIn { from { opacity: 0; transform: translateY(10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } + + /* Icon Buttons */ + .icon-btn { + width: 40px; height: 40px; border-radius: 50%; + border: none; background: transparent; cursor: pointer; + color: var(--md-sys-color-on-surface-variant); + display: flex; align-items: center; justify-content: center; + transition: 0.2s; + } + .icon-btn:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } + .back-btn { display: none; margin-right: 8px; } + + /* Empty State */ + .empty-state { + display: flex; flex-direction: column; align-items: center; justify-content: center; + height: 100%; color: var(--md-sys-color-outline); + text-align: center; + animation: fadeIn 0.5s; + } + + /* --- SKELETON LOADER --- */ + .skeleton-wrapper { width: 100%; } + .skeleton { + background: linear-gradient(90deg, #f0f2f0 25%, #f7f9f7 50%, #f0f2f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite linear; + border-radius: 4px; + } + @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } + + /* Specific Skeleton Items */ + .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; } + .skel-avatar { width: 48px; height: 48px; border-radius: 50%; } + .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; } + + .skel-chat { display: flex; flex-direction: column; gap: 16px; padding: 20px; } + .skel-bubble { height: 40px; border-radius: 12px; width: 40%; } + .skel-left { align-self: flex-start; border-bottom-left-radius: 0; } + .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; } + + /* --- MOBILE ADAPTATION --- */ + @media (max-width: 768px) { + .sidebar { width: 100%; flex: 1; border-right: none; } + .chat-area { + position: fixed; inset: 0; z-index: 20; + transform: translateX(100%); transition: transform 0.35s cubic-bezier(0.2, 0, 0, 1); + } + .chat-area.active { transform: translateX(0); } + .back-btn { display: flex; } + } + </style> +</head> +<body> + + <!-- AUTHENTICATION OVERLAY --> + <div id="auth-screen"> + <div class="auth-card"> + <div class="brand-icon stagger-enter"> + <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> + <rect width="100" height="100" rx="30" fill="#006C4C"/> + <path d="M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z" fill="white"/> + <circle cx="42" cy="45" r="3" fill="#006C4C"/> + <circle cx="50" cy="45" r="3" fill="#006C4C"/> + <circle cx="58" cy="45" r="3" fill="#006C4C"/> + </svg> + </div> + <h1 class="stagger-enter delay-1" style="font-family: 'Google Sans'; margin-bottom: 8px; color: var(--md-sys-color-on-surface);">Welcome Back</h1> + <p class="stagger-enter delay-1" style="color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px;">Enter your credentials to access logs</p> + + <div class="m3-field stagger-enter delay-2"> + <input type="text" id="username" class="m3-input" placeholder=" " autocomplete="off"> + <label class="m3-label">Username</label> + </div> + + <div class="m3-field stagger-enter delay-3"> + <input type="password" id="password" class="m3-input" placeholder=" "> + <label class="m3-label">Password</label> + <button class="eye-toggle" onclick="togglePasswordVisibility()"> + <span class="material-symbols-rounded" id="passIcon">visibility</span> + </button> + </div> + + <div class="stagger-enter delay-3" style="display:flex; align-items:center; gap:12px; margin: 16px 0 24px;"> + <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary); cursor: pointer;"> + <label for="rememberMe" style="font-size:14px; color:var(--md-sys-color-on-surface); cursor: pointer;">Keep me logged in</label> + </div> + + <button class="m3-btn filled ripple-surface stagger-enter delay-4" onclick="attemptLogin()" id="loginBtn"> + <span id="loginText">Access Dashboard</span> + <span class="material-symbols-rounded" id="loginIcon">arrow_forward</span> + </button> + <p id="authError" style="color: var(--md-sys-color-error); margin-top: 16px; font-size: 14px; opacity: 0; transition: 0.3s;">Invalid Credentials</p> + </div> + </div> + + <!-- MAIN APPLICATION --> + <div id="app-layout"> + <!-- Sidebar: Contact List --> + <aside class="sidebar"> + <div class="sidebar-header"> + <h2 style="font-family:'Google Sans'; font-size: 22px;">Chats</h2> + <div style="display:flex; gap: 8px;"> + <button class="icon-btn ripple-surface" onclick="promptAddChat()" title="Add Chat"> + <span class="material-symbols-rounded">add_circle</span> + </button> + <button class="icon-btn ripple-surface" onclick="handleLogout()" title="Logout"> + <span class="material-symbols-rounded">logout</span> + </button> + </div> + </div> + + <div class="search-container"> + <div class="search-bar"> + <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span> + <input type="text" id="searchInput" placeholder="Search..."> + </div> + </div> + + <div class="contact-list" id="contactList"> + <!-- Content injected via JS --> + </div> + </aside> + + <!-- Main Chat Area --> + <main class="chat-area" id="chatArea"> + <header class="chat-header"> + <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"> + <span class="material-symbols-rounded">arrow_back</span> + </button> + <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0; transition:0.3s;"> + <div class="avatar" id="headerAvatar" style="width:40px; height:40px; font-size:16px;">A</div> + <div> + <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div> + <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div> + </div> + </div> + </header> + + <div class="chat-content" id="messagesContainer"> + <div class="empty-state"> + <div style="width:120px; height:120px; margin-bottom:24px;"> + <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" style="width: 100%; height: 100%;"> + <rect width="100" height="100" rx="30" fill="#E6F7EF"/> + <path d="M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z" fill="#006C4C"/> + <circle cx="42" cy="45" r="3" fill="white"/> + <circle cx="50" cy="45" r="3" fill="white"/> + <circle cx="58" cy="45" r="3" fill="white"/> + </svg> + </div> + <h3 style="margin-bottom:8px; color:var(--md-sys-color-on-surface);">WhatsApp Logger</h3> + <p>Select a conversation to view history</p> + </div> + </div> + </main> + </div> + + <script> + // CONFIGURATION + const RENDER_BACKEND_URL = ""; + const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" + }; + + // INIT FIREBASE + firebase.initializeApp(firebaseConfig); + const db = firebase.firestore(); + let messageUnsubscribe = null; + let activeChatId = null; + let isCurrentChatGroup = false; + + // --- RIPPLE EFFECT --- + document.addEventListener('mousedown', function(e) { + const target = e.target.closest('.ripple-surface'); + if (target) { + const rect = target.getBoundingClientRect(); + const ripple = document.createElement('span'); + ripple.className = 'ripple'; + const size = Math.max(rect.width, rect.height); + ripple.style.width = ripple.style.height = `${size}px`; + ripple.style.left = `${e.clientX - rect.left - size/2}px`; + ripple.style.top = `${e.clientY - rect.top - size/2}px`; + target.appendChild(ripple); + setTimeout(() => ripple.remove(), 600); + } + }); + + // --- AUTH & INIT --- + window.onload = () => { + if(localStorage.getItem('wp_auth_expiry') > Date.now()) { + revealApp(); + } + }; + + function togglePasswordVisibility() { + const input = document.getElementById('password'); + const icon = document.getElementById('passIcon'); + if (input.type === 'password') { + input.type = 'text'; + icon.innerText = 'visibility_off'; + } else { + input.type = 'password'; + icon.innerText = 'visibility'; + } + } + + async function attemptLogin() { + const btn = document.getElementById('loginBtn'); + const btnText = document.getElementById('loginText'); + const btnIcon = document.getElementById('loginIcon'); + const err = document.getElementById('authError'); + + btn.disabled = true; + btnText.innerText = 'Verifying...'; + btnIcon.innerText = 'refresh'; + btnIcon.style.animation = 'spin 1s linear infinite'; + + try { + const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + username: document.getElementById('username').value, + password: document.getElementById('password').value + }) + }); + + const data = await response.json(); + + if(data.success) { + if(document.getElementById('rememberMe').checked) { + localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000)); + } + setTimeout(revealApp, 600); + } else { + throw new Error('Invalid'); + } + } catch (e) { + err.style.opacity = '1'; + err.innerText = e.message === 'Invalid' ? 'Incorrect username or password' : 'Server unavailable'; + + btn.disabled = false; + btnText.innerText = 'Access Dashboard'; + btnIcon.innerText = 'arrow_forward'; + btnIcon.style.animation = 'none'; + + btn.style.animation = 'shake 0.4s ease-in-out'; + setTimeout(() => btn.style.animation = '', 400); + } + } + + function revealApp() { + const auth = document.getElementById('auth-screen'); + const app = document.getElementById('app-layout'); + + auth.style.opacity = '0'; + auth.style.pointerEvents = 'none'; + setTimeout(() => auth.style.display = 'none', 600); + + app.style.opacity = '1'; + loadChats(); + } + + function handleLogout() { + localStorage.removeItem('wp_auth_expiry'); + location.reload(); + } + + // --- SKELETON GENERATORS --- + function renderContactSkeleton() { + const list = document.getElementById('contactList'); + let html = ''; + for(let i=0; i<6; i++) { + html += ` + <div class="skel-contact fade-in" style="animation-delay: ${i*0.05}s"> + <div class="skeleton skel-avatar"></div> + <div class="skel-text-col"> + <div class="skeleton" style="width:60%; height:16px;"></div> + <div class="skeleton" style="width:40%; height:12px;"></div> + </div> + </div>`; + } + list.innerHTML = html; + } + + function renderChatSkeleton() { + const container = document.getElementById('messagesContainer'); + container.innerHTML = ` + <div class="skel-chat"> + <div class="skeleton skel-bubble skel-left"></div> + <div class="skeleton skel-bubble skel-right" style="width:60%"></div> + <div class="skeleton skel-bubble skel-left" style="width:30%"></div> + <div class="skeleton skel-bubble skel-left" style="width:50%"></div> + <div class="skeleton skel-bubble skel-right"></div> + </div> + `; + } + + // --- CHAT LOGIC --- + async function loadChats() { + renderContactSkeleton(); + + try { + const list = document.getElementById('contactList'); + const snap = await db.collection('Chats').get(); + + let chats = []; + const excludedIds = []; + + snap.forEach(doc => { + const data = doc.data(); + if (data.lastActive && !excludedIds.includes(doc.id)) { + chats.push({ id: doc.id, ...data }); + } + }); + + chats.sort((a, b) => b.lastActive - a.lastActive); + + list.innerHTML = ''; + if(chats.length === 0) { + list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`; + return; + } + + chats.forEach((chat, index) => { + const isGroup = chat.id.includes('@g.us'); + let name, subText; + + if (isGroup) { + name = (chat.displayName && chat.displayName !== 'Unknown') ? chat.displayName : "Group Chat"; + subText = "Group Chat"; + } else { + const phone = chat.phoneNumber || chat.id.split('@')[0]; + name = (chat.displayName && chat.displayName !== 'Unknown') ? chat.displayName : phone; + subText = (name === phone) ? chat.id.split('@')[0] : phone; + } + + const time = formatTime(chat.lastActive); + const colorIndex = (chat.id.charCodeAt(0) + chat.id.charCodeAt(chat.id.length-1)) % 5; + const colors = ['#FFD8E4', '#D7F6E3', '#EADDFF', '#FDE2C4', '#DCECF9']; + const textColors = ['#31111D', '#04210F', '#1D192B', '#2B1700', '#001E30']; + + const div = document.createElement('div'); + div.className = 'contact-item ripple-surface fade-in'; + div.style.animationDelay = `${index * 0.05}s`; + div.dataset.id = chat.id; + div.innerHTML = ` + <div class="avatar" style="background:${colors[colorIndex]}; color:${textColors[colorIndex]}">${isGroup ? '<span class="material-symbols-rounded">group</span>' : name.slice(0,1).toUpperCase()}</div> + <div class="contact-info"> + <div class="contact-top"> + <span class="contact-name">${name}</span> + <span class="contact-time">${time}</span> + </div> + <div class="contact-sub">${subText}</div> + </div> + `; + div.onclick = () => openChat(chat.id, name, div, colors[colorIndex], textColors[colorIndex], isGroup); + list.appendChild(div); + }); + + } catch (err) { + console.error("Error loading chats:", err); + } + } + + function openChat(id, name, element, avatarBg, avatarColor, isGroup) { + document.querySelectorAll('.contact-item').forEach(el => el.classList.remove('active')); + if(element) element.classList.add('active'); + activeChatId = id; + isCurrentChatGroup = isGroup; + + const chatArea = document.getElementById('chatArea'); + chatArea.classList.add('active'); + + document.getElementById('headerAvatar').style.background = avatarBg || '#e0e0e0'; + document.getElementById('headerAvatar').style.color = avatarColor || '#000'; + document.getElementById('headerAvatar').innerHTML = isGroup ? '<span class="material-symbols-rounded">group</span>' : name.slice(0,1).toUpperCase(); + document.getElementById('headerName').innerText = name; + document.getElementById('headerId').innerText = isGroup ? "Group Info" : id.split('@')[0]; + document.getElementById('chatHeaderContent').style.opacity = '1'; + + renderChatSkeleton(); + + if(messageUnsubscribe) messageUnsubscribe(); + + messageUnsubscribe = db.collection('Chats').doc(id).collection('Messages') + .orderBy('timestamp', 'asc') + .onSnapshot(snap => { + renderMessages(snap); + }); + } + + function renderMessages(snap) { + const container = document.getElementById('messagesContainer'); + container.innerHTML = ''; + + if(snap.empty) { + container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>'; + return; + } + + let lastDateStr = ''; + + snap.forEach(doc => { + const msg = doc.data(); + const date = new Date(msg.timestamp * 1000); + const dateStr = getDateString(date); + + if(dateStr !== lastDateStr) { + const div = document.createElement('div'); + div.className = 'date-divider'; + div.innerText = dateStr; + container.appendChild(div); + lastDateStr = dateStr; + } + + const bubble = document.createElement('div'); + bubble.className = `msg-bubble ${msg.fromMe ? 'msg-out' : 'msg-in'}`; + + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + const showSenderName = !msg.fromMe && isCurrentChatGroup; + + bubble.innerHTML = ` + ${showSenderName ? `<div class="msg-sender">${msg.senderName || 'Sender'}</div>` : ''} + <div>${msg.text}</div> + <div class="msg-meta"> + ${timeStr} + ${msg.fromMe ? '<span class="material-symbols-rounded" style="font-size:14px; color:var(--md-sys-color-primary)">done_all</span>' : ''} + </div> + `; + + container.appendChild(bubble); + }); + + container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); + } + + function closeChatMobile() { + document.getElementById('chatArea').classList.remove('active'); + setTimeout(() => { + if(!document.querySelector('.chat-area.active')) { + activeChatId = null; + } + }, 300); + } + + function promptAddChat() { + const id = prompt("Enter WhatsApp ID (e.g. 123456789@s.whatsapp.net):"); + if(id && id.includes('@')) { + db.collection('Chats').doc(id).set({ + displayName: 'Manual Add', + id: id, + lastActive: Math.floor(Date.now()/1000) + }, {merge:true}); + setTimeout(loadChats, 1000); + } + } + + document.getElementById('searchInput').addEventListener('input', (e) => { + const val = e.target.value.toLowerCase(); + document.querySelectorAll('.contact-item').forEach(el => { + const name = el.innerText.toLowerCase(); + const id = el.dataset.id.toLowerCase(); + el.style.display = (name.includes(val) || id.includes(val)) ? 'flex' : 'none'; + }); + }); + + function formatTime(ts) { + if (!ts) return ''; + const date = new Date(ts * 1000); + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === now.toDateString()) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + } else if (date.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } else { + return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); + } + } + + function getDateString(date) { + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === now.toDateString()) return 'Today'; + if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'; + return date.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' }); + } + + const style = document.createElement('style'); + style.innerHTML = ` + @keyframes spin { to { transform: rotate(360deg); } } + @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } + `; + document.head.appendChild(style); + + </script> +</body> +</html>+ \ No newline at end of file
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror