commit d45c96feb16bec578dca6a088ff372a5da48a12f
parent c915d1b4f87de6a18b5a9e28317760e04ff27b35
Author: AMIT DUTTA <amitdutta4255@gmail.com>
Date: Tue, 17 Feb 2026 16:19:22 +0530
Change greeting from 'Hello' to 'Goodbye'
Diffstat:
| M | index.html | | | 1169 | ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- |
1 file changed, 711 insertions(+), 458 deletions(-)
diff --git a/index.html b/index.html
@@ -3,10 +3,14 @@
<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>
+ <title>WhatsApp Logger</title>
+
+ <!-- PWA Manifest -->
+ <link rel="manifest" href="manifest.json">
+ <meta name="theme-color" content="#006C4C">
<!-- 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">
+ <link rel="icon" href="icon.svg">
<!-- Fonts & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -20,7 +24,7 @@
<style>
:root {
- /* M3 Light Theme Tokens - Nature/Mint Palette */
+ /* Light Theme Tokens */
--md-sys-color-primary: #006C4C;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #89F8C7;
@@ -46,17 +50,47 @@
/* WhatsApp Specifics */
--chat-bubble-me: #E2FFEC;
--chat-bubble-them: #FFFFFF;
+ --chat-bg-color: #EFE7DE;
+ --chat-bg-image-opacity: 0.08;
+ --search-highlight: #ffeb3b;
/* 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;
+
+ /* Settings */
+ --base-font-size: 15px;
+ }
+
+ /* Dark Theme Tokens */
+ [data-theme="dark"] {
+ --md-sys-color-primary: #89F8C7;
+ --md-sys-color-on-primary: #003828;
+ --md-sys-color-primary-container: #005138;
+ --md-sys-color-on-primary-container: #89F8C7;
+
+ --md-sys-color-secondary: #BACCB3;
+ --md-sys-color-on-secondary: #25342D;
+ --md-sys-color-secondary-container: #3B4B43;
+ --md-sys-color-on-secondary-container: #D6E8D8;
+
+ --md-sys-color-surface: #111412;
+ --md-sys-color-surface-dim: #111412;
+ --md-sys-color-surface-container: #1E211F;
+ --md-sys-color-surface-container-high: #282B29;
+
+ --md-sys-color-on-surface: #E2E3DD;
+ --md-sys-color-on-surface-variant: #C2C8C5;
+ --md-sys-color-outline: #8C938F;
+ --md-sys-color-outline-variant: #424945;
- /* Motion */
- --motion-easing-emphasized: cubic-bezier(0.2, 0.0, 0.0, 1.0);
- --motion-duration-medium: 400ms;
+ --md-sys-color-error: #FFB4AB;
+
+ --chat-bubble-me: #005C4B;
+ --chat-bubble-them: #202C33;
+ --chat-bg-color: #0B141A;
+ --chat-bg-image-opacity: 0.05;
+ --search-highlight: #6b6200;
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
@@ -69,78 +103,52 @@
overflow: hidden;
display: flex;
justify-content: center;
+ transition: background-color 0.3s, color 0.3s;
}
/* --- ANIMATIONS --- */
.hidden { display: none !important; }
- .fade-in { animation: fadeIn 0.4s var(--motion-easing-emphasized); }
+ .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); }
@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 { 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; } }
+
+ @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
+ .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.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%);
+ background: radial-gradient(circle at 50% 10%, var(--md-sys-color-surface-container) 0%, var(--md-sys-color-surface) 60%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
- padding: 24px;
+ padding: 24px; height: 100dvh; padding-bottom: 15vh;
+ /* Initially handled by inline style to prevent flash */
+ transition: opacity 0.3s;
}
- .auth-card {
- width: 100%; max-width: 400px;
- text-align: center;
- }
+ .auth-card { width: 100%; max-width: 400px; text-align: center; }
+ .brand-icon { width: 96px; height: 96px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 24px; }
+ .brand-icon img { width: 100%; height: 100%; filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2)); }
- .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); } }
+ .auth-card h1 { font-family: 'Google Sans', sans-serif; }
+ /* --- UI ELEMENTS --- */
.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);
+ transition: 0.3s;
}
.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;
+ width: 100%; padding: 24px 16px 8px 16px;
border: none; background: transparent; outline: none;
- font-size: 16px; color: var(--md-sys-color-on-surface);
- font-family: inherit;
+ font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit;
}
.m3-label {
position: absolute; left: 16px; top: 18px;
@@ -150,14 +158,10 @@
.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;
+ 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); }
@@ -166,300 +170,508 @@
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;
+ transition: 0.2s; position: relative; overflow: hidden;
}
.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; }
+ .m3-btn.filled:hover { box-shadow: var(--elevation-2); opacity: 0.9; }
+ .m3-btn:disabled { opacity: 0.7; cursor: not-allowed; }
+
+ /* Spinner */
+ .spinner {
+ width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3);
+ border-radius: 50%; border-top-color: white;
+ animation: spin 0.8s linear infinite; display: none;
+ }
+ .m3-btn.loading .btn-text { visibility: hidden; }
+ .m3-btn.loading .btn-icon { display: none; }
+ .m3-btn.loading .spinner { display: block; position: absolute; }
+ @keyframes spin { to { transform: rotate(360deg); } }
/* --- 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;
+ background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out;
+ visibility: hidden;
}
+ #app-layout.visible { opacity: 1; visibility: visible; }
- /* --- SIDEBAR --- */
+ /* 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;
+ background: var(--md-sys-color-surface); z-index: 10;
}
-
- .sidebar-header {
- padding: 16px 20px;
- display: flex; justify-content: space-between; align-items: center;
+ .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; }
+ .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(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
+
.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;
+ border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px;
}
- .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;
+ border: none; background: transparent; outline: none; flex: 1; margin-left: 12px;
+ font-size: 16px; 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;
+ display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px;
+ animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
}
- .contact-item:hover { background: rgba(0,0,0,0.03); }
+ .contact-item:hover { background: rgba(128,128,128,0.05); }
.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;
+ font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px;
}
- .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 */
.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");
+ flex: 1; display: flex; flex-direction: column; position: relative;
+ background-color: var(--chat-bg-color);
+ transition: background-color 0.3s;
+ }
+ .chat-bg-pattern {
+ position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none;
+ 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 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-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 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-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);
+ height: 64px; background: var(--md-sys-color-surface);
display: flex; align-items: center; padding: 0 16px;
- border-bottom: 1px solid rgba(0,0,0,0.05);
- z-index: 5;
+ border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5;
}
-
.chat-content {
- flex: 1; overflow-y: auto; padding: 16px;
+ flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
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;
+ max-width: 80%; padding: 8px 12px; position: relative;
+ font-size: var(--base-font-size); 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;
+ animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
}
-
.msg-in {
- align-self: flex-start;
- background: var(--chat-bubble-them);
- border-radius: 0 16px 16px 16px;
- color: var(--md-sys-color-on-surface);
+ 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);
+ 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; }
+ .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
- @keyframes popIn { from { opacity: 0; transform: translateY(10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
+ .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
+
+ /* Search In Chat */
+ .in-chat-search {
+ background: var(--md-sys-color-surface-container);
+ padding: 8px 16px; display: flex; align-items: center; gap: 10px;
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
+ animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0);
+ }
+ .in-chat-search input {
+ border: none; background: transparent; outline: none; flex: 1;
+ color: var(--md-sys-color-on-surface);
+ }
- /* 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);
+ /* Menu */
+ .menu-container { position: relative; }
+ .dropdown-menu {
+ position: absolute; top: 100%; right: 0;
+ background: var(--md-sys-color-surface-container-high);
+ border-radius: 12px; padding: 8px 0; min-width: 200px;
+ display: none; flex-direction: column; z-index: 100;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2);
+ animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right;
+ }
+ .dropdown-menu.show { display: flex; }
+ .menu-item {
+ padding: 12px 16px; display: flex; align-items: center; gap: 12px;
+ color: var(--md-sys-color-on-surface); background: none; border: none;
+ font-family: inherit; font-size: 14px; cursor: pointer; text-align: left;
+ }
+ .menu-item:hover { background: rgba(128,128,128,0.1); }
+
+ /* Settings Modal */
+ .modal-overlay {
+ position: fixed; inset: 0; z-index: 2000;
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center;
- transition: 0.2s;
+ opacity: 0; visibility: hidden; transition: 0.3s;
+ }
+ .modal-overlay.active { opacity: 1; visibility: visible; }
+ .settings-card {
+ background: var(--md-sys-color-surface);
+ width: 90%; max-width: 600px; height: 80vh;
+ border-radius: 28px; display: flex; flex-direction: column;
+ overflow: hidden; animation: scaleIn 0.3s var(--anim-enter);
+ }
+ .settings-header {
+ padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant);
+ display: flex; justify-content: space-between; align-items: center;
+ }
+ .settings-content { flex: 1; overflow-y: auto; padding: 24px; }
+ .setting-section { margin-bottom: 32px; }
+ .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
+ .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
+ .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; }
+ .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
+
+ /* Toggle Switch */
+ .switch { position: relative; display: inline-block; width: 50px; height: 28px; }
+ .switch input { opacity: 0; width: 0; height: 0; }
+ .slider {
+ position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
+ background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s;
+ border: 1px solid var(--md-sys-color-outline);
+ }
+ .slider:before {
+ position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px;
+ background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s;
+ }
+ input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
+ input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); }
+
+ /* Accordion */
+ .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); }
+ .faq-question {
+ width: 100%; padding: 16px 0; text-align: left; background: none; border: none;
+ font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer;
+ display: flex; justify-content: space-between;
+ }
+ .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; }
+ .faq-item.open .faq-answer { display: block; }
+
+ /* Background Selector */
+ .bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
+ .bg-option {
+ height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent;
+ background-size: cover; position: relative; overflow: hidden;
+ }
+ .bg-option.selected { border-color: var(--md-sys-color-primary); }
+ .bg-1 { background-color: #EFE7DE; }
+ .bg-2 { background-color: #d1d7db; }
+ .bg-3 { background-color: #e5ddd5; }
+ .bg-4 { background-color: #ffffff; }
+
+ /* Install Sheet - Hidden by default on PC */
+ .install-sheet { display: none; }
+
+ /* Settings Install Card */
+ .install-card {
+ background: var(--md-sys-color-primary-container);
+ color: var(--md-sys-color-on-primary-container);
+ padding: 16px; border-radius: 12px; margin-bottom: 32px;
+ display: flex; align-items: center; justify-content: space-between;
+ }
+ .install-card h4 { margin-bottom: 4px; }
+ .install-card p { font-size: 12px; }
+ .install-card .m3-btn {
+ width: auto; height: 36px; padding: 0 16px;
+ font-size: 14px; white-space: nowrap; flex-shrink: 0;
+ box-shadow: none;
+ }
+
+ @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.3s; background-color: var(--chat-bg-color); }
+ .chat-area.active { transform: translateX(0); }
+ .back-btn { display: flex !important; margin-right: 8px; }
+
+ /* Bottom Sheet Install Prompt - Only visible on Mobile */
+ .install-sheet {
+ display: block; /* Enable it for mobile logic */
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000;
+ background: var(--md-sys-color-surface);
+ border-top-left-radius: 28px; border-top-right-radius: 28px;
+ padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
+ transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
+ }
+ .install-sheet.visible { transform: translateY(0); }
+ .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
+ .sheet-content { text-align: center; }
+ .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; }
}
- .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 */
+ /* UTILITY MODALS STYLING FIX */
+ .modal-card {
+ background-color: var(--md-sys-color-surface);
+ padding: 24px;
+ border-radius: 28px;
+ width: 90%;
+ max-width: 320px;
+ transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1);
+ animation: scaleIn 0.3s var(--anim-enter);
+ }
+ .modal-overlay.active .modal-card { transform: scale(1); }
+
+ .modal-card.utility {
+ width: 320px;
+ padding: 24px;
+ border-radius: 28px;
+ }
+ .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 24px;
+ }
+ .modal-actions .m3-btn {
+ width: auto;
+ min-width: 80px;
+ height: 40px;
+ padding: 0 16px;
+ }
+
+ /* Empty State Centering */
.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;
+ height: 100%; width: 100%; color: var(--md-sys-color-outline);
+ text-align: center; animation: fadeIn 0.5s;
}
- /* --- SKELETON LOADER --- */
- .skeleton-wrapper { width: 100%; }
+ /* Skeleton */
.skeleton {
- background: linear-gradient(90deg, #f0f2f0 25%, #f7f9f7 50%, #f0f2f0 75%);
- background-size: 200% 100%;
- animation: shimmer 1.5s infinite linear;
- border-radius: 4px;
+ background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 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-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; }
.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-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; }
+ .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;}
.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 -->
+ <!-- AUTH SCREEN -->
<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 class="brand-icon animate-enter">
+ <img src="icon.svg" alt="App Logo" style="width: 100%; height: 100%;">
</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">
+ <h1 style="font-family:'Google Sans'; margin-bottom: 8px;" class="animate-enter">Welcome Back</h1>
+ <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom: 32px;" class="animate-enter">Enter your credentials</p>
+ <div class="m3-field animate-enter"><input type="text" id="username" class="m3-input" placeholder=" "><label class="m3-label">Username</label></div>
+ <div class="m3-field animate-enter">
<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 style="display:flex; align-items:center; gap:12px; margin: 16px 0 24px;" class="animate-enter">
+ <input type="checkbox" id="rememberMe" style="width:18px; height:18px;"><label for="rememberMe">Remember me</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 class="m3-btn filled animate-enter" id="loginBtn" onclick="attemptLogin()">
+ <span class="btn-text">Access Dashboard</span>
+ <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
+ <div class="spinner"></div>
</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>
+ <p id="authError" style="color: var(--md-sys-color-error); margin-top: 16px; opacity: 0;">Invalid Credentials</p>
</div>
</div>
- <!-- MAIN APPLICATION -->
+ <!-- MAIN APP -->
<div id="app-layout">
- <!-- Sidebar: Contact List -->
- <aside class="sidebar">
+ <aside class="sidebar" id="sidebar">
<div class="sidebar-header">
- <h2 style="font-family:'Google Sans'; font-size: 22px;">Chats</h2>
+ <div style="display:flex; align-items:center; gap:12px;">
+ <h2 style="font-family:'Google Sans'; font-size: 20px;">Chat log</h2>
+ </div>
<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>
+ <button class="icon-btn ripple-surface" onclick="promptAddChat()"><span class="material-symbols-rounded">add_circle</span></button>
+ <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</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>
+ <div class="contact-list" id="contactList"></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>
+ <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;">
+ <div class="avatar" id="headerAvatar">A</div>
+ <div style="flex:1;">
<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 class="menu-container">
+ <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button>
+ <div class="dropdown-menu" id="chatMenu">
+ <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button>
+ <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button>
+ <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button>
+ </div>
+ </div>
</div>
</header>
+
+ <div id="inChatSearch" class="in-chat-search hidden">
+ <input type="text" id="msgSearchInput" placeholder="Find message...">
+ <div id="searchCount" style="font-size:12px; margin-right:8px;"></div>
+ <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button>
+ </div>
+ <div class="chat-bg-pattern"></div>
<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>
+ <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span>
+ <p>Select a conversation</p>
</div>
</div>
</main>
</div>
+ <!-- SETTINGS MODAL -->
+ <div id="settingsModal" class="modal-overlay">
+ <div class="settings-card">
+ <div class="settings-header">
+ <h2 style="font-family:'Google Sans'">Settings</h2>
+ <button class="icon-btn" onclick="closeModal('settingsModal')"><span class="material-symbols-rounded">close</span></button>
+ </div>
+ <div class="settings-content">
+
+ <!-- Install App -->
+ <div id="installAppSection" class="install-card hidden">
+ <div>
+ <h4>Install App</h4>
+ <p>Get the app for better experience</p>
+ </div>
+ <button class="m3-btn filled" onclick="installPWA()">Install</button>
+ </div>
+
+ <!-- Profile -->
+ <div class="setting-section">
+ <div class="setting-title">Profile</div>
+ <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
+ <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
+ </div>
+
+ <!-- Appearance -->
+ <div class="setting-section">
+ <div class="setting-title">Appearance</div>
+ <div class="setting-row">
+ <div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div>
+ <label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label>
+ </div>
+ <div class="setting-row">
+ <div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div>
+ <input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)">
+ </div>
+
+ <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div>
+ <div class="bg-selector">
+ <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div>
+ <div class="bg-option bg-2" onclick="changeBg(2)"></div>
+ <div class="bg-option bg-3" onclick="changeBg(3)"></div>
+ <div class="bg-option bg-4" onclick="changeBg(4)"></div>
+ </div>
+ </div>
+
+ <!-- Help & Legal -->
+ <div class="setting-section">
+ <div class="setting-title">Help & Support</div>
+ <div id="faqContainer">
+ <!-- FAQs injected via JS -->
+ </div>
+ <div style="margin-top:24px; display:flex; gap:12px;">
+ <button class="m3-btn filled"
+ style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;"
+ onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">
+ View FAQ
+ </button>
+ </div>
+ <div style="margin-top:24px; display:flex; gap:12px;">
+ <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container)" onclick="window.open('mailto:mail@amit.is-a.dev')">Email Support</button>
+ </div>
+ </div>
+
+ <div class="setting-section">
+ <div class="setting-title">Legal</div>
+ <p style="font-size:12px; color:var(--md-sys-color-on-surface-variant); line-height:1.5;">
+ <strong>MIT License</strong><br>
+ <strong>Security:</strong> Logs are stored strictly in your private Firebase. No external access.
+ <br>Check out this project on <a href="https://amit.is-a.dev/logger">amit.is-a.dev</a> or <a href="https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-">Github</a>
+ <br>Developed by <a href="https://github.com/notamitgamer">@notamitgamer</a>
+ </p>
+ </div>
+
+ <button class="m3-btn filled" style="background:var(--md-sys-color-error);" onclick="showLogoutModal()">Log Out</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UTILITY MODALS -->
+ <div id="logoutModal" class="modal-overlay">
+ <div class="modal-card utility">
+ <h3 class="modal-title">Log Out?</h3>
+ <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p>
+ <div class="modal-actions">
+ <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button>
+ <button class="m3-btn filled" onclick="handleLogout()">Logout</button>
+ </div>
+ </div>
+ </div>
+
+ <div id="renameModal" class="modal-overlay">
+ <div class="modal-card utility">
+ <h3 class="modal-title">Rename Chat</h3>
+ <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div>
+ <div class="modal-actions">
+ <button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button>
+ <button class="m3-btn filled" onclick="saveRename()">Save</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- PWA INSTALL SHEET (Mobile Only) -->
+ <div id="installSheet" class="install-sheet">
+ <div class="sheet-header">
+ <h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3>
+ <button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button>
+ </div>
+ <div class="sheet-content">
+ <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;">
+ <img src="icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;">
+ </div>
+ <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, and full screen view.</p>
+ <div class="sheet-actions">
+ <button class="m3-btn filled" onclick="installPWA()">Install App</button>
+ <button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button>
+ </div>
+ </div>
+ </div>
+
<script>
- // CONFIGURATION
const RENDER_BACKEND_URL = "";
const firebaseConfig = {
apiKey: "",
@@ -470,34 +682,68 @@
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);
+ let deferredPrompt;
+ let currentMessages = [];
+
+ // --- PWA INSTALLATION ---
+ if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
+ navigator.serviceWorker.register('sw.js').catch(err => {
+ console.warn('SW Register Fail:', err);
+ });
+ }
+ window.addEventListener('beforeinstallprompt', (e) => {
+ e.preventDefault();
+ deferredPrompt = e;
+ // Also show settings button for manual install
+ document.getElementById('installAppSection').classList.remove('hidden');
+
+ // Show bottom sheet on mobile, simple banner on desktop if preferred
+ if (window.innerWidth <= 768) {
+ setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
}
});
+ async function installPWA() {
+ if (!deferredPrompt) return;
+ deferredPrompt.prompt();
+ const { outcome } = await deferredPrompt.userChoice;
+ if (outcome === 'accepted') {
+ closeInstallSheet();
+ document.getElementById('installAppSection').classList.add('hidden');
+ }
+ deferredPrompt = null;
+ }
+
+ function closeInstallSheet() {
+ document.getElementById('installSheet').classList.remove('visible');
+ }
// --- AUTH & INIT ---
+ // Immediate check to prevent flash
+ if(localStorage.getItem('wp_auth_expiry') > Date.now()) {
+ document.write('<style>#auth-screen{display:none} #app-layout{opacity:1; visibility:visible}</style>');
+ window.addEventListener('DOMContentLoaded', loadChats);
+ } else {
+ document.write('<style>#auth-screen{opacity:1; visibility:visible}</style>');
+ }
+
window.onload = () => {
- if(localStorage.getItem('wp_auth_expiry') > Date.now()) {
- revealApp();
+ // Load theme
+ if (localStorage.getItem('theme') === 'dark') {
+ document.body.setAttribute('data-theme', 'dark');
+ document.getElementById('darkModeToggle').checked = true;
}
+ // Load Profile
+ document.getElementById('profileName').value = localStorage.getItem('profileName') || '';
+ document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || '';
+
+ // Load BG
+ const bg = localStorage.getItem('chatBg') || 1;
+ changeBg(bg, false);
+
+ populateFAQ();
};
function togglePasswordVisibility() {
@@ -514,14 +760,11 @@
async function attemptLogin() {
const btn = document.getElementById('loginBtn');
- const btnText = document.getElementById('loginText');
- const btnIcon = document.getElementById('loginIcon');
const err = document.getElementById('authError');
+ btn.classList.add('loading');
btn.disabled = true;
- btnText.innerText = 'Verifying...';
- btnIcon.innerText = 'refresh';
- btnIcon.style.animation = 'spin 1s linear infinite';
+ err.style.opacity = '0';
try {
const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
@@ -532,97 +775,145 @@
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');
- }
+ revealApp();
+ } else throw new Error();
} catch (e) {
err.style.opacity = '1';
- err.innerText = e.message === 'Invalid' ? 'Incorrect username or password' : 'Server unavailable';
-
+ btn.classList.remove('loading');
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';
+ document.getElementById('auth-screen').style.display = 'none';
+ document.getElementById('app-layout').classList.add('visible');
loadChats();
}
- function handleLogout() {
- localStorage.removeItem('wp_auth_expiry');
- location.reload();
+ // --- SETTINGS LOGIC ---
+ function openSettings() {
+ document.getElementById('settingsModal').classList.add('active');
+ }
+ function closeModal(id) {
+ document.getElementById(id).classList.remove('active');
+ }
+ function toggleTheme() {
+ const isDark = document.getElementById('darkModeToggle').checked;
+ document.body.setAttribute('data-theme', isDark ? 'dark' : 'light');
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
+ }
+ function changeFontSize(val) {
+ document.documentElement.style.setProperty('--base-font-size', `${val}px`);
+ }
+ function changeBg(id, save=true) {
+ document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected'));
+ const colors = ['#EFE7DE', '#d1d7db', '#e5ddd5', '#ffffff'];
+ // Apply to variable so it works everywhere
+ document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]);
+
+ const option = document.querySelector(`.bg-${id}`);
+ if(option) option.classList.add('selected');
+ if(save) localStorage.setItem('chatBg', id);
+ }
+ function saveProfile() {
+ localStorage.setItem('profileName', document.getElementById('profileName').value);
+ localStorage.setItem('profilePhone', document.getElementById('profilePhone').value);
+ }
+ function populateFAQ() {
+ const faqs = [
+ {q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."},
+ {q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."},
+ {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."},
+ {q: "Can I reply from here?", a: "No, this is a read-only archive designed for logging and privacy, not active chatting."},
+ {q: "What is the 'Pinger' for?", a: "It keeps your free Render server awake so it never misses a message due to inactivity."}
+ ];
+ const container = document.getElementById('faqContainer');
+ faqs.forEach(item => {
+ const div = document.createElement('div');
+ div.className = 'faq-item';
+ div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`;
+ container.appendChild(div);
+ });
}
- // --- 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;
+ // --- CHAT FUNCTIONS ---
+ function toggleChatMenu(event) {
+ if(event) event.stopPropagation();
+ document.getElementById('chatMenu').classList.toggle('show');
}
+
+ document.addEventListener('click', function(e) {
+ const menu = document.getElementById('chatMenu');
+ if(menu.classList.contains('show') && !e.target.closest('.menu-container')) {
+ menu.classList.remove('show');
+ }
+ });
- 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>
- `;
+ function toggleChatSearch() {
+ document.getElementById('inChatSearch').classList.toggle('hidden');
+ document.getElementById('chatMenu').classList.remove('show');
+ document.getElementById('msgSearchInput').focus();
}
+
+ document.getElementById('msgSearchInput').addEventListener('input', (e) => {
+ const term = e.target.value.toLowerCase();
+ const bubbles = document.querySelectorAll('.msg-bubble');
+ let count = 0;
+
+ // Unwrap highlights
+ document.querySelectorAll('.highlight').forEach(span => {
+ const parent = span.parentNode;
+ parent.innerHTML = parent.textContent;
+ });
+
+ bubbles.forEach(b => {
+ const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)');
+ if(textDiv) {
+ const originalText = textDiv.textContent;
+ if(term && originalText.toLowerCase().includes(term)) {
+ b.style.display = 'flex';
+ const regex = new RegExp(`(${term})`, 'gi');
+ textDiv.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>');
+ count++;
+ } else {
+ b.style.display = term ? 'none' : 'flex';
+ textDiv.textContent = originalText;
+ }
+ }
+ });
+ document.getElementById('searchCount').innerText = term ? `${count} found` : '';
+ });
- // --- CHAT LOGIC ---
async function loadChats() {
- renderContactSkeleton();
-
+ // Skeleton
+ const list = document.getElementById('contactList');
+ list.innerHTML = '';
+ for(let i=0; i<5; i++) {
+ list.innerHTML += `<div class="skel-contact fade-in"><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>`;
+ }
+
try {
- const list = document.getElementById('contactList');
const snap = await db.collection('Chats').get();
-
- let chats = [];
- const excludedIds = [];
+ const groups = {};
+ const excluded = [];
snap.forEach(doc => {
- const data = doc.data();
- if (data.lastActive && !excludedIds.includes(doc.id)) {
- chats.push({ id: doc.id, ...data });
+ const d = doc.data();
+ if (!d.lastActive || excluded.includes(doc.id)) return;
+ const phone = d.phoneNumber || doc.id.split('@')[0];
+ if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] };
+ else {
+ groups[phone].ids.push(doc.id);
+ if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d};
}
});
- chats.sort((a, b) => b.lastActive - a.lastActive);
+ const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
list.innerHTML = '';
if(chats.length === 0) {
@@ -630,182 +921,145 @@
return;
}
- chats.forEach((chat, index) => {
- const isGroup = chat.id.includes('@g.us');
- let name, subText;
+ chats.forEach(c => {
+ const el = document.createElement('div');
+ el.className = 'contact-item';
+ const name = c.customName || c.displayName || c.phoneNumber || c.id;
+ const isGroup = c.id.includes('@g.us');
+ const avatarContent = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
- 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);
+ el.innerHTML = `<div class="avatar">${avatarContent}</div><div class="contact-info"><div class="contact-name">${name}</div><div class="contact-sub">${c.phoneNumber || c.id}</div></div>`;
+ el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id);
+ list.appendChild(el);
});
-
} catch (err) {
- console.error("Error loading chats:", err);
+ console.error(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;
+ function openChat(ids, name, el, isGroup, displayId) {
+ activeChatId = ids[0];
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';
+
+ // Update Header ID with phone/group ID
+ document.getElementById('headerId').innerText = displayId;
+
+ const avatarEl = document.getElementById('headerAvatar');
+ avatarEl.innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
- renderChatSkeleton();
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
+ if (isMobile) {
+ window.history.pushState({chatOpen: true}, "");
+ document.getElementById('sidebar').classList.add('inactive');
+ document.getElementById('chatArea').classList.add('active');
+ }
+
+ if(window.unsub) window.unsub.forEach(u => u());
+ window.unsub = [];
+
+ const container = document.getElementById('messagesContainer');
+ container.innerHTML = `<div class="skel-msg-group"><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>`;
- if(messageUnsubscribe) messageUnsubscribe();
+ const msgs = new Map();
- messageUnsubscribe = db.collection('Chats').doc(id).collection('Messages')
- .orderBy('timestamp', 'asc')
- .onSnapshot(snap => {
- renderMessages(snap);
- });
+ ids.forEach(id => {
+ window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => {
+ snap.forEach(d => msgs.set(d.id, d.data()));
+ renderMsgs(msgs);
+ }));
+ });
}
- function renderMessages(snap) {
+ function renderMsgs(map) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
+ currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp);
- if(snap.empty) {
+ if(currentMessages.length === 0) {
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'}`;
+ currentMessages.forEach(m => {
+ const div = document.createElement('div');
+ div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
+ const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown";
- 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>
+ div.innerHTML = `
+ ${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}
+ <div>${m.text}</div>
+ <div class="msg-meta">${new Date(m.timestamp*1000).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>
`;
-
- container.appendChild(bubble);
+ container.appendChild(div);
});
-
- container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
+ container.scrollTop = container.scrollHeight;
}
+ function handleLogout() {
+ localStorage.removeItem('wp_auth_expiry');
+ location.reload();
+ }
+
function closeChatMobile() {
+ window.history.back();
+ }
+
+ window.addEventListener('popstate', () => {
+ document.getElementById('chatArea').classList.remove('remove');
document.getElementById('chatArea').classList.remove('active');
- setTimeout(() => {
- if(!document.querySelector('.chat-area.active')) {
- activeChatId = null;
- }
- }, 300);
+ });
+
+ // --- DOWNLOAD ---
+ function downloadChat() {
+ if(!currentMessages.length) return;
+ const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
+ const blob = new Blob([text], {type:'text/plain'});
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = 'chat.txt';
+ a.click();
}
-
- 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);
+
+ // --- RENAME CHAT ---
+ function showRenameModal() {
+ if(!activeChatId) return;
+ document.getElementById('renameModal').classList.add('active');
+ document.getElementById('renameInput').value = document.getElementById('headerName').innerText;
+ document.getElementById('chatMenu').classList.remove('show');
+ }
+
+ async function saveRename() {
+ const name = document.getElementById('renameInput').value;
+ if(name) {
+ await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true});
+ document.getElementById('headerName').innerText = name;
+ closeModal('renameModal');
+ loadChats();
}
}
+
+ function showLogoutModal() {
+ document.getElementById('logoutModal').classList.add('active');
+ }
+ // Contact search
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';
+ el.style.display = name.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 promptAddChat() {
+ const id = prompt("Enter WhatsApp ID (e.g. 12345@s.whatsapp.net)");
+ if(id) {
+ db.collection('Chats').doc(id).set({displayName:'Manual Add', id:id, lastActive: Math.floor(Date.now()/1000)}, {merge:true});
+ setTimeout(loadChats, 1000);
}
}
-
- 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
+</html>