1/* ========================================== 2 4. ATVEO-STYLE ADMIN SIDEBAR & WORKSPACE 3 ========================================== */ 4 5function injectAdminSidebar() { 6 if (document.getElementById('admin-gear-btn')) return; 7 8 // ð¢ NEW: Dynamically load post-creator.js ONLY for verified Admins 9 if (!document.getElementById('post-creator-script')) { 10 const postScript = document.createElement('script'); 11 postScript.id = 'post-creator-script'; 12 postScript.src = 'js/post-creator.js'; 13 postScript.defer = true; 14 document.body.appendChild(postScript); 15 } 16 17 const sidebarHTML = ` 18 <!-- Floating Admin Gear Button --> 19 <button id="admin-gear-btn" title="Admin Control Center" class="fixed right-6 bottom-16 z-[100] bg-amber-500 hover:bg-amber-600 text-white w-14 h-14 rounded-full shadow-2xl flex items-center justify-center text-2xl transition-all transform hover:scale-110 border-2 border-white"> 20 <i class="fa-solid fa-gear"></i> 21 </button> 22 23 <!-- Admin Slide-over Menu Drawer --> 24 <div id="admin-sidebar-drawer" class="fixed inset-y-0 right-0 w-80 bg-slate-900 shadow-2xl z-[110] transform translate-x-full transition-transform duration-300 flex flex-col border-l border-slate-800"> 25 <div class="bg-slate-950 text-white p-5 flex items-center justify-between border-b border-slate-800"> 26 <div class="flex items-center gap-3"> 27 <i class="fa-solid fa-shield-halved text-amber-400 text-xl"></i> 28 <h3 class="font-black text-base text-white">Platform Admin</h3> 29 </div> 30 <button id="close-admin-sidebar" class="text-gray-400 hover:text-white text-xl w-8 h-8 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark"></i></button> 31 </div> 32 33 <div class="p-6 overflow-y-auto flex-1 space-y-4"> 34 <p class="text-[11px] font-extrabold text-gray-400 uppercase tracking-wider mb-2">Platform Content & Pages</p> 35 36 <div id="admin-sidebar-nav-buttons" class="space-y-3"> 37 <!-- Navigation buttons injected dynamically --> 38 </div> 39 40 <div class="pt-6 border-t border-slate-800 space-y-3"> 41 42 <!-- ð¢ PAGE CREATOR STUDIO BUTTON --> 43 <button onclick="openPageCreatorStudio()" class="admin-menu-btn" style="background: #d97706;"> 44 <i class="fa-solid fa-wand-magic-sparkles w-5 text-center"></i> Page Creator Studio 45 </button> 46 47 <!-- ð¢ POST CREATOR STUDIO BUTTON --> 48 <button onclick="openPostCreatorStudio()" class="admin-menu-btn" style="background: #8b5cf6;"> 49 <i class="fa-solid fa-feather-pointed w-5 text-center"></i> Post Creator Studio 50 </button> 51 52 <!-- ð¢ MENU MANAGEMENT BUTTON --> 53 <button id="sidebar-btn-menu" class="admin-menu-btn" style="background: #e11d48;"> 54 <i class="fa-solid fa-bars-staggered w-5 text-center"></i> Menu Management 55 </button> 56 57 <!-- ð¢ USER MANAGEMENT BUTTON --> 58 <button id="sidebar-btn-users" class="admin-menu-btn" style="background: #1d4ed8;"> 59 <i class="fa-solid fa-users-gear w-5 text-center"></i> User Management 60 </button> 61 62 <!-- ð¢ PAGE MANAGEMENT PANEL BUTTON --> 63 <button onclick="openFullscreenPageManager()" class="admin-menu-btn" style="background: #10b981;"> 64 <i class="fa-solid fa-file-lines w-5 text-center"></i> Page Management Panel 65 </button> 66 67 <!-- ð¢ POST MANAGEMENT BUTTON --> 68 <button id="sidebar-btn-posts" class="admin-menu-btn" style="background: #9333ea;"> 69 <i class="fa-solid fa-newspaper w-5 text-center"></i> Post Management Panel 70 </button> 71 72 <!-- ð¢ COMMENT MODERATION BUTTON --> 73 <button id="sidebar-btn-comments" class="admin-menu-btn" style="background: #eab308; color: #0f172a;"> 74 <i class="fa-solid fa-comments w-5 text-center"></i> Comment Moderation Panel 75 </button> 76 77 <!-- ð¢ Student Registrations BUTTON --> 78 <button id="sidebar-btn-regs" class="admin-menu-btn" style="background: #047857;"> 79 <i class="fa-solid fa-users-rectangle w-5 text-center"></i> Student Registrations 80 </button> 81 82 <!-- ð¢ NEW LIBRARY MANAGEMENT BUTTON --> 83 <button id="sidebar-btn-library" class="admin-menu-btn" style="background: #0284c7;"> 84 <i class="fa-solid fa-book-bookmark w-5 text-center"></i> Library Management Panel 85 </button> 86 87 <!-- ð¢ CENTRALIZED PAYMENT METHOD PANEL BUTTON --> 88 <button id="sidebar-btn-payments" class="admin-menu-btn" style="background: #0ea5e9;"> 89 <i class="fa-solid fa-credit-card w-5 text-center"></i> Payment Method Panel 90 </button> 91 92 <!-- ð¢ NEW SITEMAP GENERATOR BUTTON --> 93 <button id="sidebar-btn-sitemap" class="admin-menu-btn" style="background: #b45309;"> 94 <i class="fa-solid fa-sitemap w-5 text-center"></i> Sitemap Generator 95 </button> 96 97 <!-- ð¢ UNIVERSAL BACKUP & RESTORE PANEL BUTTON --> 98 <button onclick="openFullscreenBackupManager()" class="admin-menu-btn" style="background: #0d9488;"> 99 <i class="fa-solid fa-database w-5 text-center"></i> Database Backup & Restore 100 </button> 101 </div> 102 </div> 103 </div> 104 `; 105 106 document.body.insertAdjacentHTML('beforeend', sidebarHTML); 107 108 const gearBtn = document.getElementById('admin-gear-btn'); 109 const drawer = document.getElementById('admin-sidebar-drawer'); 110 const closeBtn = document.getElementById('close-admin-sidebar'); 111 112 gearBtn.addEventListener('click', () => { 113 drawer.classList.remove('translate-x-full'); 114 renderAdminSidebarNavList(); 115 });
116 117 closeBtn.addEventListener('click', () => { 118 drawer.classList.add('translate-x-full'); 119 }); 120 121 // ð¢ Menu Manager Button Event 122 document.getElementById('sidebar-btn-menu')?.addEventListener('click', () => { 123 drawer.classList.add('translate-x-full'); 124 openFullscreenMenuManager(); 125 }); 126 127 // Post Panel Button 128 document.getElementById('sidebar-btn-posts').addEventListener('click', () => { 129 drawer.classList.add('translate-x-full'); 130 openFullscreenPostManager(); 131 }); 132 133 // Comment Panel Button 134 document.getElementById('sidebar-btn-comments').addEventListener('click', () => { 135 drawer.classList.add('translate-x-full'); 136 openFullscreenCommentModeration(); 137 }); 138 139 // Registrations Button 140 document.getElementById('sidebar-btn-regs').addEventListener('click', () => { 141 drawer.classList.add('translate-x-full'); 142 openFullscreenRegistrationsManager(); 143 }); 144 145 // Users Button 146 document.getElementById('sidebar-btn-users').addEventListener('click', () => { 147 drawer.classList.add('translate-x-full'); 148 openFullscreenUsersManager(); 149 }); 150 151 // Library Panel Button Event 152 document.getElementById('sidebar-btn-library')?.addEventListener('click', () => { 153 drawer.classList.add('translate-x-full'); 154 openFullscreenLibraryManager(); 155 }); 156 157 // Centralized Payment Panel Button Event 158 document.getElementById('sidebar-btn-payments')?.addEventListener('click', () => { 159 drawer.classList.add('translate-x-full'); 160 openFullscreenPaymentManager(); 161 }); 162 163 // ð¢ Sitemap Generator Button Event 164 document.getElementById('sidebar-btn-sitemap').addEventListener('click', () => { 165 drawer.classList.add('translate-x-full'); 166 openFullscreenSitemapGenerator(); 167 }); 168} 169 170// Render Admin Menu 171function renderAdminSidebarNavList() { 172 const container = document.getElementById('admin-sidebar-nav-buttons'); 173 if (!container) return; 174 175 container.innerHTML = ` 176 <div class="p-4 bg-slate-900 border border-slate-800 rounded-xl text-center shadow-md"> 177 <i class="fa-solid fa-pen-nib text-3xl text-amber-500 mb-3 block"></i> 178 <h4 class="text-white font-bold mb-1">Live Page Editor</h4> 179 <p class="text-[11px] text-gray-400 mb-4 leading-relaxed">Edit text and images directly on the live website.</p> 180 181 <button onclick="enableLiveEditMode()" class="w-full inline-flex items-center justify-center gap-2 bg-amber-500 hover:bg-amber-400 text-slate-950 font-black py-2.5 px-4 rounded-lg transition shadow-lg text-sm"> 182 Enable Live Editing 183 </button> 184 </div> 185 `; 186} 187 188// ð¢ Full-Screen Page Editor Workspace (Like your screenshot 2) 189window.openFullscreenPageEditor = async function(pageId, pageName, pageUrl) { 190 // Close sidebar drawer 191 const drawer = document.getElementById('admin-sidebar-drawer'); 192 if (drawer) drawer.classList.add('translate-x-full'); 193 194 // Remove existing workspace if any 195 const existingWs = document.getElementById('admin-fullscreen-workspace'); 196 if (existingWs) existingWs.remove(); 197 198 let savedTitle = ""; 199 let savedVideo = ""; 200 let savedDesc = ""; 201 202 try { 203 const doc = await db.collection("site_pages").doc(pageId).get(); 204 if (doc.exists) { 205 const data = doc.data(); 206 savedTitle = data.title || ""; 207 savedVideo = data.video || ""; 208 savedDesc = data.desc || ""; 209 } 210 } catch(e) {} 211 212 const workspaceHTML = ` 213 <div id="admin-fullscreen-workspace"> 214 <div class="bg-slate-950 text-white px-8 py-5 flex items-center justify-between border-b border-slate-800 shadow-xl"> 215 <div class="flex items-center gap-4"> 216 <div class="w-10 h-10 bg-amber-500 text-slate-950 rounded-xl flex items-center justify-center font-black text-xl"> 217 <i class="fa-solid fa-pen-to-square"></i> 218 </div> 219 <div> 220 <h2 class="font-black text-xl text-white">Edit Page: ${pageName}</h2> 221 <p class="text-xs text-gray-400">Modify title, video links, and content for /${pageUrl}</p> 222 </div> 223 </div> 224 225 <div class="flex items-center gap-4"> 226 <a href="/${pageUrl}" target="_blank" class="bg-blue-600/30 text-blue-300 border border-blue-500/30 px-4 py-2.5 rounded-xl text-xs font-bold hover:bg-blue-600/50 transition flex items-center gap-2"> 227 <i class="fa-solid fa-external-link"></i> Preview Page 228 </a> 229 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 230 â Close Editor 231 </button> 232 </div> 233 </div> 234 235 <div class="p-8 md:p-12 max-w-5xl mx-auto w-full flex-1 space-y-6"> 236 <div class="bg-slate-900 p-8 rounded-3xl border border-slate-800 shadow-2xl space-y-6"> 237 <div> 238 <label class="block text-xs font-extrabold text-gray-400 uppercase tracking-wider mb-2">Page Title / Heading</label> 239 <input type="text" id="fs-edit-title" value="${savedTitle}" class="w-full p-4 bg-slate-950 border border-slate-800 rounded-2xl text-base text-white focus:ring-2 focus:ring-amber-500 focus:outline-none"> 240 </div> 241 242 <div> 243 <label class="block text-xs font-extrabold text-gray-400 uppercase tracking-wider mb-2">YouTube Video / Media URL</label> 244 <input type="text" id="fs-edit-video" value="${savedVideo}" placeholder="https://www.youtube.com/embed/..." class="w-full p-4 bg-slate-950 border border-slate-800 rounded-2xl text-base text-white focus:ring-2 focus:ring-amber-500 focus:outline-none"> 245 </div> 246 247 <div> 248 <label class="block text-xs font-extrabold text-gray-400 uppercase tracking-wider mb-2">Page Description / Main Content</label> 249 <textarea id="fs-edit-desc" rows="10" class="w-full p-4 bg-slate-950 border border-slate-800 rounded-2xl text-base text-white focus:ring-2 focus:ring-amber-500 focus:outline-none leading-relaxed">${savedDesc}</textarea> 250 </div> 251 252 <button id="fs-save-btn" class="w-full bg-amber-500 hover:bg-amber-600 text-slate-950 font-black py-4 rounded-2xl transition shadow-xl text-base flex items-center justify-center gap-3"> 253 <i class="fa-solid fa-cloud-arrow-up text-lg"></i> Save Changes to Firebase 254 </button> 255 </div> 256 </div> 257 </div> 258 `; 259 260 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 261 262 document.getElementById('fs-save-btn').addEventListener('click', async () => { 263 const title = document.getElementById('fs-edit-title').value; 264 const video = document.getElementById('fs-edit-video').value; 265 const desc = document.getElementById('fs-edit-desc').value; 266 267 try { 268 await db.collection("site_pages").doc(pageId).set({ 269 title, video, desc, updatedAt: new Date() 270 }, { merge: true });
271 272 customToast(`Successfully saved changes for ${pageName}!`, "success"); 273 } catch(e) { 274 customToast(e.message, "error"); 275 } 276 }); 277}; 278 279// Full-Screen Registrations Manager 280function openFullscreenRegistrationsManager() { 281 const existingWs = document.getElementById('admin-fullscreen-workspace'); 282 if (existingWs) existingWs.remove(); 283 284 const workspaceHTML = ` 285 <div id="admin-fullscreen-workspace"> 286 <div class="bg-slate-950 text-white px-8 py-5 flex items-center justify-between border-b border-slate-800 shadow-xl"> 287 <div class="flex items-center gap-4"> 288 <div class="w-10 h-10 bg-emerald-500 text-slate-950 rounded-xl flex items-center justify-center font-black text-xl"> 289 <i class="fa-solid fa-users-rectangle"></i> 290 </div> 291 <div> 292 <h2 class="font-black text-xl text-white">Student Course Registrations</h2> 293 <p class="text-xs text-gray-400">Review and approve incoming student enrolments</p> 294 </div> 295 </div> 296 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 297 â Close Dashboard 298 </button> 299 </div> 300 301 <div class="p-8 md:p-12 max-w-6xl mx-auto w-full flex-1 space-y-6"> 302 <div id="fs-registrations-list" class="grid grid-cols-1 md:grid-cols-2 gap-4"> 303 <p class="italic text-gray-400">Loading registrations...</p> 304 </div> 305 </div> 306 </div> 307 `; 308 309 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 310 loadFullscreenRegistrations(); 311} 312 313async function loadFullscreenRegistrations() { 314 const container = document.getElementById('fs-registrations-list'); 315 if (!container) return; 316 317 try { 318 const snapshot = await db.collection("course_registrations").orderBy("createdAt", "desc").get(); 319 if (snapshot.empty) { 320 container.innerHTML = `<p class="italic text-gray-400">No course registrations found.</p>`; 321 return; 322 } 323 324 let html = ''; 325 snapshot.forEach(doc => { 326 const reg = doc.data(); 327 const docId = doc.id; 328 329 html += ` 330 <div class="bg-slate-900 p-5 rounded-2xl border border-slate-800 space-y-3"> 331 <div class="flex justify-between items-start"> 332 <div> 333 <h4 class="font-black text-white text-base">${reg.courseName}</h4> 334 <p class="text-gray-300 text-sm font-semibold">${reg.studentName} (${reg.studentEmail})</p> 335 </div> 336 <span class="text-xs font-bold px-3 py-1 rounded-full ${reg.status === 'Approved' ? 'bg-green-500/20 text-green-400 border border-green-500/30' : reg.status === 'Rejected' ? 'bg-red-500/20 text-red-400 border border-red-500/30' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30'}"> 337 ${reg.status} 338 </span> 339 </div> 340 <p class="text-gray-400 text-xs">Phone: ${reg.studentPhone} | Schedule: ${reg.batch} | Payment: ${reg.paymentMethod}</p> 341 <div class="flex gap-3 pt-2 border-t border-slate-800"> 342 <button onclick="updateFullscreenRegStatus('${docId}', 'Approved')" class="bg-green-600 hover:bg-green-500 text-white text-xs font-bold px-4 py-2 rounded-xl transition"> 343 â Approve Enrolment 344 </button> 345 <button onclick="updateFullscreenRegStatus('${docId}', 'Rejected')" class="bg-red-600 hover:bg-red-500 text-white text-xs font-bold px-4 py-2 rounded-xl transition"> 346 â Reject 347 </button> 348 </div> 349 </div>`; 350 }); 351 container.innerHTML = html; 352 } catch(e) { 353 container.innerHTML = `<p class="text-red-400 text-xs">Error: ${e.message}</p>`; 354 } 355} 356 357window.updateFullscreenRegStatus = async function(docId, newStatus) { 358 try { 359 await db.collection("course_registrations").doc(docId).update({ status: newStatus, reviewedAt: new Date() }); 360 customToast(`Registration set to '${newStatus}'!`, "success"); 361 loadFullscreenRegistrations(); 362 } catch(e) { 363 customToast(e.message, "error"); 364 } 365}; 366 367/* ========================================== 368 ATVEO-STYLE 1280px USER MANAGEMENT PANEL 369 (With Stats, Search, Full ID & Pagination) 370 ========================================== */ 371 372window.allUsersData = []; 373window.currentUsersStatusFilter = 'all'; 374window.currentUsersPage = 1; 375window.usersPerPage = 10; 376 377window.openFullscreenUsersManager = function() { 378 const existingWs = document.getElementById('admin-fullscreen-workspace'); 379 if (existingWs) existingWs.remove(); 380 381 const workspaceHTML = ` 382 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 383 384 <!-- Top Bar --> 385 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl"> 386 <div class="flex items-center gap-3"> 387 <div class="w-9 h-9 bg-blue-600 text-white rounded-xl flex items-center justify-center font-black text-lg"> 388 <i class="fa-solid fa-users-gear"></i> 389 </div> 390 <div> 391 <h2 class="font-black text-lg text-white leading-none">User Management</h2> 392 <p class="text-[11px] text-gray-400">Manage user accounts, roles, registration approvals, and permissions.</p> 393 </div> 394 </div> 395 396 <button onclick="document.getElementById('admin-fullscreen-workspace').remove(); document.body.classList.remove('overflow-hidden');" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 397 â Close Dashboard 398 </button> 399 </div> 400 401 <!-- Main Panel Container (1280px Max Width) --> 402 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center"> 403 <div class="w-full max-w-[1280px] space-y-6"> 404 405 <!-- Table Card --> 406 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl"> 407 <div class="mb-6 flex flex-col md:flex-row md:items-center justify-between gap-4"> 408 <div> 409 <h3 class="text-2xl font-black text-white mb-1">User Management</h3> 410 <p class="text-xs text-gray-400">Review new account signups, assign roles, approve accounts, or ban/delete users.</p> 411 </div> 412 </div> 413 414 <!-- ð¢ 1. TOP STATISTICS COUNT PILLS --> 415 <div class="flex flex-wrap items-center gap-2 text-xs font-semibold text-gray-400 mb-6 pb-4 border-b border-slate-800/80"> 416 <button onclick="filterUsersByStatus('all')" id="usr-pill-all" class="text-blue-400 hover:text-white transition font-bold">Total Users - <span id="usr-count-total">0</span></button>
417 <span class="text-slate-700">|</span> 418 <button onclick="filterUsersByStatus('Approved')" id="usr-pill-approved" class="text-gray-400 hover:text-white transition">Approved - <span id="usr-count-approved">0</span></button> 419 <span class="text-slate-700">|</span> 420 <button onclick="filterUsersByStatus('Pending')" id="usr-pill-pending" class="text-gray-400 hover:text-white transition">Pending - <span id="usr-count-pending">0</span></button> 421 <span class="text-slate-700">|</span> 422 <button onclick="filterUsersByStatus('Banned')" id="usr-pill-banned" class="text-gray-400 hover:text-white transition">Banned Users - <span id="usr-count-banned">0</span></button> 423 <span class="text-slate-700">|</span> 424 <button onclick="filterUsersByStatus('Deleted')" id="usr-pill-deleted" class="text-gray-400 hover:text-white transition">Deleted Users - <span id="usr-count-deleted">0</span></button> 425 </div> 426 427 <!-- ð¢ 2. FILTER & REAL-TIME SEARCH BAR --> 428 <div class="flex flex-wrap justify-between items-center mb-6 gap-4 text-xs"> 429 <div class="flex items-center gap-2"> 430 <label class="text-gray-400 font-bold uppercase text-[10px]">Role Filter:</label> 431 <select id="usr-filter-role" onchange="renderUsersTable()" class="bg-slate-950 border border-slate-700 text-amber-400 font-bold rounded-xl px-3 py-2 focus:outline-none focus:border-amber-400 text-xs cursor-pointer"> 432 <option value="all">All Roles</option> 433 <option value="Subscriber">Subscriber</option> 434 <option value="Student">Student</option> 435 <option value="Teacher">Teacher</option> 436 <option value="Admin">Admin</option> 437 <option value="Others">Others</option> 438 </select> 439 </div> 440 441 <div class="flex items-center gap-2"> 442 <input type="text" id="usr-search-input" placeholder="Search by Name, Email, or Full ID..." onkeyup="renderUsersTable()" class="bg-slate-950 border border-slate-700 text-white rounded-xl px-4 py-2 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none w-64 sm:w-80"> 443 <button onclick="renderUsersTable()" class="bg-blue-600 hover:bg-blue-500 text-white font-bold px-4 py-2 rounded-xl transition text-xs shadow-md"> 444 <i class="fa-solid fa-search mr-1"></i> Search 445 </button> 446 </div> 447 </div> 448 449 <!-- 1280px Table --> 450 <div class="overflow-x-auto"> 451 <table class="w-full text-left text-xs border-collapse"> 452 <thead> 453 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 pb-3"> 454 <th class="py-3 px-4">Status</th> 455 <th class="py-3 px-4">Name / Full ID</th> 456 <th class="py-3 px-4">Email Address</th> 457 <th class="py-3 px-4">Joined Date</th> 458 <th class="py-3 px-4">Role</th> 459 <th class="py-3 px-4 text-center">Action</th> 460 </tr> 461 </thead> 462 <tbody id="admin-users-table-body" class="divide-y divide-slate-800/80"> 463 <tr> 464 <td colspan="6" class="py-8 text-center text-gray-400 italic">Loading users...</td> 465 </tr> 466 </tbody> 467 </table> 468 </div> 469 470 <!-- ð¢ 3. BOTTOM PAGE NAVIGATION --> 471 <div class="flex flex-wrap justify-between items-center pt-6 border-t border-slate-800/80 text-xs text-gray-400 gap-4 mt-6"> 472 <div class="flex items-center gap-2">
473 <span>Showing <span id="usr-showing-start" class="font-bold text-white">0</span> - <span id="usr-showing-end" class="font-bold text-white">0</span> of <span id="usr-showing-total" class="font-bold text-white">0</span> Users</span> 474 </div> 475 <div class="flex items-center gap-3"> 476 <button onclick="changeUsersPage(-1)" id="usr-prev-btn" class="bg-slate-800 hover:bg-slate-700 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold px-4 py-2 rounded-xl transition shadow"> 477 â¹ Previous 478 </button> 479 <span class="font-bold text-white px-2">Page <span id="usr-current-page" class="text-amber-400">1</span> of <span id="usr-total-pages">1</span></span> 480 <button onclick="changeUsersPage(1)" id="usr-next-btn" class="bg-slate-800 hover:bg-slate-700 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold px-4 py-2 rounded-xl transition shadow"> 481 Next ⺠482 </button> 483 </div> 484 </div> 485 486 </div> 487 488 </div> 489 </div> 490 </div> 491 `; 492 493 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 494 window.currentUsersStatusFilter = 'all'; 495 window.currentUsersPage = 1; 496 loadFullscreenUsers(); 497}; 498 499// ð¢ FIXED: Removed 'async' and added .onSnapshot() for real-time live updates 500function loadFullscreenUsers() { 501 const tableBody = document.getElementById('admin-users-table-body'); 502 if (!tableBody) return; 503 504 // Listen to Firestore in real-time 505 db.collection("users").onSnapshot((snapshot) => { 506 window.allUsersData = []; 507 508 let totalCount = 0; 509 let approvedCount = 0; 510 let pendingCount = 0; 511 let bannedCount = 0; 512 let deletedCount = 0; 513 514 snapshot.forEach(doc => { 515 const u = doc.data(); 516 u.uid = doc.id; 517 518 // ð¢ Safe condition (Strict email/UID check only): 519 const uEmailLower = (u.email || u.userEmail || '').toLowerCase(); 520 u.isSuperAdmin = (uEmailLower === '[email protected]') || (u.uid === 'qpJ1ojwul2YL6LO1udqJOkQzUI23'); 521 522 // Email Fallback 523 let userEmail = u.email || u.userEmail || ''; 524 if (!userEmail && u.isSuperAdmin) userEmail = '[email protected]'; 525 u.displayEmail = userEmail || 'N/A'; 526 527 // Status Calculation 528 if (u.isSuperAdmin) u.status = 'Approved'; 529 else if (!u.status) u.status = 'Pending'; 530 531 // Counts 532 totalCount++; 533 if (u.status === 'Approved') approvedCount++; 534 else if (u.status === 'Pending') pendingCount++; 535 else if (u.status === 'Banned') bannedCount++; 536 else if (u.status === 'Deleted') deletedCount++; 537 538 window.allUsersData.push(u); 539 }); 540 541 // Ensure the panel is still open before trying to update text 542 if (document.getElementById('usr-count-total')) { 543 document.getElementById('usr-count-total').innerText = totalCount; 544 document.getElementById('usr-count-approved').innerText = approvedCount; 545 document.getElementById('usr-count-pending').innerText = pendingCount; 546 document.getElementById('usr-count-banned').innerText = bannedCount; 547 document.getElementById('usr-count-deleted').innerText = deletedCount; 548 549 renderUsersTable(); 550 } 551 552 }, (error) => { 553 if (tableBody) { 554 tableBody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-red-400 italic">Error loading users: ${error.message}</td></tr>`; 555 } 556 }); 557} 558 559window.filterUsersByStatus = function(status) { 560 window.currentUsersStatusFilter = status; 561 window.currentUsersPage = 1; 562 563 const pills = ['all', 'approved', 'pending', 'banned', 'deleted'];
564 pills.forEach(p => { 565 const btn = document.getElementById(`usr-pill-${p}`); 566 if (btn) { 567 btn.className = (p.toLowerCase() === status.toLowerCase()) 568 ? 'text-blue-400 hover:text-white transition font-black underline' 569 : 'text-gray-400 hover:text-white transition font-medium'; 570 } 571 }); 572 573 renderUsersTable(); 574}; 575 576window.renderUsersTable = function() { 577 const tableBody = document.getElementById('admin-users-table-body'); 578 if (!tableBody) return; 579 580 const roleFilter = document.getElementById('usr-filter-role').value; 581 const searchQuery = (document.getElementById('usr-search-input').value || '').trim().toLowerCase(); 582 583 // ð¢ FILTER DATA 584 const filteredUsers = window.allUsersData.filter(u => { 585 let match = true; 586 587 // Status Filter 588 if (window.currentUsersStatusFilter !== 'all') { 589 if (u.status.toLowerCase() !== window.currentUsersStatusFilter.toLowerCase()) match = false; 590 } 591 592 // Role Filter 593 if (roleFilter !== 'all') { 594 const uRole = u.isSuperAdmin ? 'Admin' : (u.role || 'Student'); 595 if (uRole.toLowerCase() !== roleFilter.toLowerCase()) match = false; 596 } 597 598 // Search Query (Name, Email, or Full User ID) 599 if (searchQuery) { 600 const nameMatch = (u.fullName || '').toLowerCase().includes(searchQuery); 601 const emailMatch = (u.displayEmail || '').toLowerCase().includes(searchQuery); 602 const idMatch = (u.uid || '').toLowerCase().includes(searchQuery); 603 if (!nameMatch && !emailMatch && !idMatch) match = false; 604 } 605 606 return match; 607 }); 608 609 if (filteredUsers.length === 0) { 610 tableBody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-gray-500 italic">No users match your filters or search query.</td></tr>`; 611 updatePaginationControls(0, 0, 0); 612 return; 613 } 614 615 // ð¢ PAGINATION CALCULATION 616 const totalUsers = filteredUsers.length; 617 const totalPages = Math.ceil(totalUsers / window.usersPerPage); 618 if (window.currentUsersPage > totalPages) window.currentUsersPage = totalPages; 619 if (window.currentUsersPage < 1) window.currentUsersPage = 1; 620 621 const startIndex = (window.currentUsersPage - 1) * window.usersPerPage; 622 const endIndex = Math.min(startIndex + window.usersPerPage, totalUsers); 623 const paginatedUsers = filteredUsers.slice(startIndex, endIndex); 624 625 let html = ''; 626 paginatedUsers.forEach(u => { 627 const uid = u.uid; 628 629 // ð¢ Dynamic fallback: 630 let joinedDateStr = "Recently"; 631 if (u.createdAt && typeof u.createdAt.toDate === 'function') { 632 const d = u.createdAt.toDate(); 633 const day = String(d.getDate()).padStart(2, '0');
634 const month = String(d.getMonth() + 1).padStart(2, '0'); 635 const year = d.getFullYear(); 636 joinedDateStr = `${day}/${month}/${year}`; 637 } 638 639 // Status Badge 640 let statusBadge = ''; 641 if (u.isSuperAdmin) { 642 statusBadge = `<span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Super Admin</span>`; 643 } else if (u.status === 'Approved') { 644 statusBadge = `<span class="bg-green-500/20 text-green-400 border border-green-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Approved</span>`; 645 } else if (u.status === 'Banned') { 646 statusBadge = `<span class="bg-red-500/20 text-red-400 border border-red-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Banned</span>`; 647 } else if (u.status === 'Deleted') { 648 statusBadge = `<span class="bg-gray-500/20 text-gray-400 border border-gray-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Deleted</span>`; 649 } else { 650 statusBadge = `<span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase animate-pulse">Pending</span>`; 651 } 652 653 // Role Dropdown 654 const currentRole = u.isSuperAdmin ? 'Admin' : (u.role || 'Student'); 655 const roles = ['Subscriber', 'Student', 'Teacher', 'Admin', 'Others']; 656 let roleHtml = ''; 657 if (u.isSuperAdmin) { 658 roleHtml = `<span class="text-amber-400 font-extrabold text-xs px-3 py-1.5 bg-slate-950 border border-amber-500/30 rounded-xl inline-block">Admin (Owner)</span>`; 659 } else { 660 let roleOptions = ''; 661 roles.forEach(r => { 662 roleOptions += `<option value="${r}" ${currentRole.toLowerCase() === r.toLowerCase() ? 'selected' : ''}>${r}</option>`; 663 }); 664 roleHtml = ` 665 <select onchange="updateUserRole('${uid}', this.value)" class="bg-slate-950 border border-slate-700 text-amber-400 font-bold text-xs rounded-xl px-2.5 py-1.5 focus:outline-none focus:border-amber-400 cursor-pointer"> 666 ${roleOptions} 667 </select>`; 668 } 669 670 // Actions 671 let actionHtml = ''; 672 if (u.isSuperAdmin) { 673 actionHtml = `<span class="text-gray-500 text-xs italic font-bold">ð System Protected</span>`; 674 } else { 675 actionHtml = `<div class="flex items-center justify-center gap-1.5">`; 676 677 if (u.status === 'Banned') { 678 // ð¢ UNBAN BUTTON: Restores user to Approved status 679 actionHtml += `<button onclick="updateUserStatus('${uid}', 'Approved')" class="bg-blue-600 hover:bg-blue-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Unban</button>`; 680 } else if (u.status === 'Approved') { 681 // ð¢ DISAPPROVE BUTTON: Reverts user status back to Pending 682 actionHtml += `<button onclick="updateUserStatus('${uid}', 'Pending')" class="bg-slate-700 hover:bg-slate-600 text-amber-300 font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]" title="Revert user to Pending status">â Disapprove</button>`; 683 actionHtml += `<button onclick="updateUserStatus('${uid}', 'Banned')" class="bg-orange-600 hover:bg-orange-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Ban</button>`; 684 } else { 685 // PENDING USERS: Show Approve and Ban 686 actionHtml += `<button onclick="updateUserStatus('${uid}', 'Approved')" class="bg-green-600 hover:bg-green-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">â Approve</button>`; 687 actionHtml += `<button onclick="updateUserStatus('${uid}', 'Banned')" class="bg-orange-600 hover:bg-orange-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Ban</button>`; 688 } 689 690 // Always show Warn and Delete buttons 691 actionHtml += `<button onclick="openAdminWarningModal('${uid}', '${(u.fullName || 'User').replace(/'/g, "\\'")}', '${uid}')" class="bg-amber-400 hover:bg-amber-300 text-slate-950 font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Warn</button>`; 692 actionHtml += `<button onclick="deleteUserAccount('${uid}', '${u.displayEmail}')" class="bg-red-600 hover:bg-red-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Delete</button>`; 693 694 actionHtml += `</div>`; 695 } 696 697 const studentIdBadge = u.studentId ? `<p class="text-[10px] text-amber-400 font-bold">Student ID: ${u.studentId}</p>` : ''; 698 699 html += ` 700 <tr class="hover:bg-slate-900/50 transition border-b border-slate-800/50"> 701 <td class="py-4 px-4 align-middle">${statusBadge}</td> 702 703 <td class="py-4 px-4 align-middle"> 704 <div class="flex items-center gap-3"> 705 <div class="w-8 h-8 rounded-full ${u.isSuperAdmin ? 'bg-amber-500 text-slate-950' : 'bg-blue-600 text-white'} font-bold flex items-center justify-center text-xs flex-shrink-0"> 706 ${(u.fullName || u.displayEmail || 'U')[0].toUpperCase()} 707 </div> 708 <div> 709 <p class="font-bold text-white text-xs">${u.fullName || 'Alin Tagar User'}</p> 710 ${studentIdBadge} 711 <p class="text-[10px] text-gray-400 font-mono select-all hover:text-amber-400 transition cursor-pointer" title="Click to select Full ID">ID: ${uid}</p> 712 </div> 713 </div> 714 </td> 715 716 <td class="py-4 px-4 align-middle text-gray-300 font-medium">${u.displayEmail}</td> 717 <td class="py-4 px-4 align-middle text-gray-400 font-mono">${joinedDateStr}</td> 718 <td class="py-4 px-4 align-middle">${roleHtml}</td> 719 <td class="py-4 px-4 align-middle text-center">${actionHtml}</td> 720 </tr>`; 721 });
722 723 tableBody.innerHTML = html; 724 updatePaginationControls(startIndex + 1, endIndex, totalUsers, totalPages); 725}; 726 727function updatePaginationControls(start, end, total, totalPages = 1) { 728 document.getElementById('usr-showing-start').innerText = start; 729 document.getElementById('usr-showing-end').innerText = end; 730 document.getElementById('usr-showing-total').innerText = total; 731 732 document.getElementById('usr-current-page').innerText = window.currentUsersPage; 733 document.getElementById('usr-total-pages').innerText = totalPages || 1; 734 735 const prevBtn = document.getElementById('usr-prev-btn'); 736 const nextBtn = document.getElementById('usr-next-btn'); 737 738 if (prevBtn) prevBtn.disabled = window.currentUsersPage <= 1; 739 if (nextBtn) nextBtn.disabled = window.currentUsersPage >= totalPages || totalPages === 0; 740} 741 742window.changeUsersPage = function(delta) { 743 window.currentUsersPage += delta; 744 renderUsersTable(); 745}; 746 747// ð¢ UPDATED: UPDATE USER STATUS & DISPATCH TAILORED EMAILS 748window.updateUserStatus = async function(uid, newStatus) { 749 try { 750 const uDoc = await db.collection("users").doc(uid).get(); 751 if (!uDoc.exists) return; 752 753 const userData = uDoc.data(); 754 const currentRole = userData.role || 'Subscriber'; 755 const userEmail = userData.email || userData.displayEmail || ''; 756 const userName = userData.fullName || userData.firstName || 'Student'; 757 758 await db.collection("users").doc(uid).set({ 759 status: newStatus, 760 role: currentRole, 761 updatedAt: new Date() 762 }, { merge: true }); 763 764 // ð¢ DISPATCH STATUS-SPECIFIC EMAIL NOTIFICATION 765 if (userEmail) { 766 await sendAccountStatusEmail(userEmail, userName, newStatus); 767 } 768 769 customToast(`User set to ${newStatus} successfully!`, "success"); 770 loadFullscreenUsers(); 771 } catch(e) { 772 customToast(e.message, "error"); 773 } 774}; 775 776// ð¢ COMPREHENSIVE ACCOUNT STATUS EMAIL DISPATCHER 777async function sendAccountStatusEmail(email, name, status) { 778 const logoUrl = "https://firebasestorage.googleapis.com/v0/b/alintagar-official-website.firebasestorage.app/o/photos%2FAlintagarlogosmall.png?alt=media&token=68c0cef0-b24c-4e40-9deb-17ebe6cfc10b"; 779 780 let subject = ""; 781 let statusHeading = ""; 782 let statusBodyText = ""; 783 let highlightColor = "#05396b"; 784 785 if (status === 'Approved') { 786 subject = "Your Alin Tagar Account is Approved & Ready! ð"; 787 statusHeading = "Account Approved!"; 788 statusBodyText = "Great news! Your student account registration has been reviewed and <strong>APPROVED</strong> by our Administration team. You now have full student access to log in, enrol in courses, and access learning materials."; 789 highlightColor = "#16a34a"; // Green 790 } else if (status === 'Pending') { 791 subject = "Alin Tagar Account Status Update: Pending Review"; 792 statusHeading = "Account Under Review"; 793 statusBodyText = "Your account status has been updated to <strong>Pending Review</strong> by the Administrator. Please allow some time for verification."; 794 highlightColor = "#d97706"; // Amber 795 } else if (status === 'Banned') { 796 subject = "Important Notice Regarding Your Alin Tagar Account â ï¸"; 797 statusHeading = "Account Access Restricted"; 798 statusBodyText = "Your account access has been restricted by the Administrator due to a violation of our terms and conditions. If you believe this is a mistake, please contact our support desk."; 799 highlightColor = "#dc2626"; // Red 800 } else { 801 return; // Exit if status is unhandled 802 } 803 804 try { 805 await db.collection("email_queue").add({ 806 to: email, 807 message: { 808 subject: subject, 809 text: `Hello ${name},\n\nYour Alin Tagar account status has been updated to: ${status}.\n\nVisit your profile: https://alintagar-official-website.web.app/profile`, 810 html: ` 811 <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 650px; margin: 0 auto; background-color: #ffffff; overflow: hidden;"> 812 813 <!-- Header --> 814 <div style="padding: 20px 10px; border-bottom: 2px solid #05396b;"> 815 <table style="width: 100%; border-collapse: collapse;"> 816 <tr> 817 <td style="vertical-align: middle; width: 70px;"> 818 <img src="${logoUrl}" alt="Alin Tagar Logo" style="height: 50px;"> 819 </td> 820 <td style="vertical-align: middle; text-align: right;"> 821 <h2 style="color: #05396b; margin: 0; font-size: 18px; font-weight: 900; letter-spacing: 1px;">ALIN TAGAR</h2> 822 <p style="color: #64748b; margin: 2px 0 0 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; font-weight: bold;">
822Alin Tagar English Language Training School</p> 823 </td> 824 </tr> 825 </table> 826 </div> 827 828 <!-- Body --> 829 <div style="padding: 30px 10px;"> 830 <h3 style="color: ${highlightColor}; margin-top: 0; font-size: 20px; font-weight: 800;">${statusHeading}</h3> 831 <p style="color: #0f172a; font-size: 16px;">Hello <strong>${name}</strong>,</p> 832 <p style="color: #334155; font-size: 14px; line-height: 1.6;">${statusBodyText}</p> 833 834 ${status === 'Approved' ? ` 835 <div style="margin-top: 25px;"> 836 <a href="https://alintagar-official-website.web.app/profile" style="background: #05396b; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: bold; display: inline-block; font-size: 14px;">Log In to Your Profile →</a> 837 </div>` : ''} 838 </div> 839 840 <!-- Signature --> 841 <div style="padding: 0 10px 30px 10px;"> 842 <p style="color: #333333; font-size: 14px; margin-bottom: 15px;">Best regards,</p> 843 <table style="width: 100%; border-collapse: collapse;"> 844 <tr> 845 <td style="width: 90px; vertical-align: top; padding-right: 15px;"> 846 <img src="${logoUrl}" alt="Alin Tagar Logo" style="width: 90px; height: auto;"> 847 </td> 848 <td style="vertical-align: top; font-size: 13px; color: #333333; line-height: 1.5;"> 849 <strong style="font-size: 15px; color: #000000;">Administrator</strong><br> 850 CEO, Admin, Customer Relations Manager (CRM)<br> 851 <strong>âï¸</strong> +95 (0) 9 781 010 356<br> 852 <strong>ð§</strong> <a href="mailto:[email protected]" style="color: #05396b; text-decoration: none;">[email protected]</a><br> 853 <strong>ð</strong> <a href="https://alintagar.com" style="color: #05396b; text-decoration: none;">alintagar.com</a><br> 854 855 <div style="margin-top: 8px;"> 856 <span style="background-color: #00b67a; color: white; font-size: 10px; font-weight: bold; padding: 3px 8px; border-radius: 4px; display: inline-block;">â Trustindex</span><br> 857 <span style="color: #00b67a; font-size: 18px; letter-spacing: 2px;">â â â â â </span><br> 858 <span style="font-size: 12px; font-weight: bold; color: #000000;">40 reviews</span> 859 </div> 860 </td> 861 </tr> 862 </table> 863 864 <div style="margin-top: 30px; font-size: 10px; color: #777777; line-height: 1.5; text-align: justify; border-top: 1px solid #e2e8f0; padding-top: 15px;"> 865 <p style="margin-bottom: 8px;">Please do not print this e-mail unless it is necessary. Every unprinted e-mail helps the environment.</p> 866 <p style="margin: 0;">This e-mail and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. Please notify the sender immediately by e-mail if you have received this e-mail by mistake and delete this e-mail from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing or taking any action in reliance on the contents of this information is strictly prohibited.</p> 867 </div> 868 </div> 869 </div>` 870 }, 871 status: "queued", 872 createdAt: new Date() 873 }); 874 console.log("Status update email queued successfully for:", email); 875 } catch(e) { 876 console.error("Email queue error:", e); 877 } 878} 879 880// ð¢ ACCOUNT APPROVAL EMAIL DISPATCHER 881async function sendAccountApprovalEmail(email, name) { 882 try { 883 // Queue approval email in Firestore for Cloud Functions / Extension 884 await db.collection("email_queue").add({ 885 to: email, 886 message: { 887 subject: "Your Alin Tagar Account is Approved & Ready!", 888 text: `Hello ${name},\n\nGreat news! Your account registration at Alin Tagar English Training School has been APPROVED by the Administrator.\n\nYou can now log in to your account and enrol in courses:\nhttps://alintagar-official-website.web.app/profile\n\nWelcome to Alin Tagar!`, 889 html: ` 890 <div style="font-family: sans-serif; padding: 24px; background: #f8fafc; color: #0f172a; border-radius: 16px; border: 1px solid #e2e8f0;"> 891 <div style="text-align: center; margin-bottom: 20px;"> 892 <h2 style="color: #05396b; margin: 0; font-size: 22px;">Alin Tagar English Training School</h2> 893 <p style="color: #64748b; font-size: 13px;">Official Account Approval Notification</p> 894 </div> 895 <div style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #cbd5e1;"> 896 <h3 style="color: #16a34a; margin-top: 0;">Account Approved! ð</h3> 897 <p>Hello <strong>${name}</strong>,</p> 898 <p>Great news! Your student account registration has been reviewed and <strong>APPROVED</strong> by our Administration team.</p> 899 <p>You now have full student access to enrol in courses and access learning materials.</p> 900 <p style="margin-top: 24px;"> 901 <a href="https://alintagar-official-website.web.app/profile" style="background: #05396b; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: bold; display: inline-block;">Log In to Your Profile →</a> 902 </p> 903 </div> 904 <p style="font-size: 12px; color: #94a3b8; text-align: center; margin-top: 20px;"> 905 Alin Tagar English Training School ⢠Dawei, Myanmar 906 </p> 907 </div>` 908 }, 909 status: "queued", 910 createdAt: new Date() 911 }); 912 console.log("Approval email queued successfully for:", email); 913 } catch(e) { 914 console.error("Email queue error:", e); 915 } 916} 917 918// ð¢ UPDATE USER ROLE 919window.updateUserRole = async function(uid, newRole) { 920 try { 921 await db.collection("users").doc(uid).set({ 922 role: newRole, 923 updatedAt: new Date() 924 }, { merge: true });
925 customToast(`User role updated to ${newRole}!`, "success"); 926 } catch(e) { 927 customToast(e.message, "error"); 928 } 929}; 930 931// ð¢ FIXED: DELETE USER ACCOUNT (Clean Firestore deletion that triggers auto-delete Auth) 932window.deleteUserAccount = async function(uid, email) { 933 const confirmed = await customConfirm( 934 "Delete User Account ðï¸", 935 `Are you sure you want to permanently delete account for ${email || 'this user'}?\n\nThis will remove their login from Authentication and delete their profile forever.` 936 ); 937 if (!confirmed) return; 938 939 try { 940 customToast("Deleting account...", "info"); 941 942 // Deleting the Firestore document automatically triggers the backend to delete Auth! 943 await db.collection("users").doc(uid).delete(); 944 945 customToast("User account permanently deleted!", "success"); 946 loadFullscreenUsers(); 947 948 } catch(e) { 949 console.error("Delete error:", e); 950 customToast("Error deleting user: " + e.message, "error"); 951 } 952}; 953 954// ð¢ Load All Student Registrations for Admin Approval 955async function loadAdminRegistrations() { 956 const container = document.getElementById('admin-registrations-list'); 957 if (!container) return; 958 959 try { 960 const snapshot = await db.collection("course_registrations") 961 .orderBy("createdAt", "desc") 962 .get(); 963 964 if (snapshot.empty) { 965 container.innerHTML = `<p class="italic text-gray-400 text-xs bg-gray-50 p-3 rounded-xl border">No course registrations found.</p>`; 966 return; 967 } 968 969 let html = ''; 970 snapshot.forEach(doc => { 971 const reg = doc.data(); 972 const docId = doc.id; 973 974 html += ` 975 <div class="bg-gray-50 p-3.5 rounded-xl border border-gray-200 text-xs space-y-2"> 976 <div class="flex justify-between items-start"> 977 <div> 978 <p class="font-bold text-gray-900 text-sm">${reg.courseName}</p> 979 <p class="text-gray-600 font-medium">${reg.studentName} (${reg.studentEmail})</p> 980 </div> 981 <span class="text-[10px] font-bold px-2 py-0.5 rounded-full ${reg.status === 'Approved' ? 'bg-green-100 text-green-800' : reg.status === 'Rejected' ? 'bg-red-100 text-red-800' : 'bg-amber-100 text-amber-800'}"> 982 ${reg.status} 983 </span> 984 </div> 985 <p class="text-gray-500">Phone: ${reg.studentPhone} | Schedule: ${reg.batch}</p> 986 987 <div class="flex gap-2 pt-1 border-t"> 988 <button onclick="updateRegistrationStatus('${docId}', 'Approved')" class="bg-green-700 text-white text-[11px] font-bold px-3 py-1 rounded-lg hover:bg-green-800 transition"> 989 â Approve 990 </button> 991 <button onclick="updateRegistrationStatus('${docId}', 'Rejected')" class="bg-red-600 text-white text-[11px] font-bold px-3 py-1 rounded-lg hover:bg-red-700 transition"> 992 â Reject 993 </button> 994 </div> 995 </div>`; 996 }); 997 998 container.innerHTML = html; 999 1000 } catch(e) { 1001 container.innerHTML = `<p class="text-xs text-red-500">Error loading registrations: ${e.message}</p>`; 1002 } 1003} 1004 1005// ð¢ Admin Action: Update Student Registration Status 1006window.updateRegistrationStatus = async function(docId, newStatus) { 1007 try { 1008 await db.collection("course_registrations").doc(docId).update({ 1009 status: newStatus, 1010 reviewedAt: new Date() 1011 }); 1012 customToast(`Registration set to '${newStatus}'!`, "success"); 1013 loadAdminRegistrations(); 1014 } catch(e) { 1015 customToast(e.message, "error"); 1016 } 1017}; 1018 1019// Render Page Nav Buttons inside Sidebar 1020function renderAdminPageNavButtons() { 1021 const listContainer = document.getElementById('admin-page-nav-list'); 1022 if (!listContainer) return; 1023 1024 let html = '';
1025 defaultPages.forEach(p => { 1026 const isActive = p.id === activeEditingPageId; 1027 html += ` 1028 <button onclick="selectAdminEditingPage('${p.id}', '${p.name}', '${p.url}')" class="flex items-center gap-2 text-left p-2.5 rounded-xl border text-xs font-bold transition-all ${isActive ? 'bg-brandBlue text-white border-brandBlue shadow-md' : 'bg-white text-gray-700 hover:bg-blue-50 hover:text-brandBlue'}"> 1029 <i class="fa-solid ${p.icon} text-sm"></i> 1030 <span class="truncate">${p.name}</span> 1031 </button>`; 1032 }); 1033 1034 listContainer.innerHTML = html; 1035} 1036 1037// Handle Selecting a Page to Edit in Sidebar 1038window.selectAdminEditingPage = function(id, name, url) { 1039 activeEditingPageId = id; 1040 renderAdminPageNavButtons(); 1041 1042 document.getElementById('active-editing-badge').innerText = `Editing: ${name}`; 1043 document.getElementById('admin-view-page-link').href = url; 1044 1045 loadAdminPageEditorData(id); 1046}; 1047 1048// Load Page Data from Firebase Firestore 1049async function loadAdminPageEditorData(pageId) { 1050 const titleInput = document.getElementById('admin-edit-title'); 1051 const videoInput = document.getElementById('admin-edit-video'); 1052 const descInput = document.getElementById('admin-edit-desc'); 1053 1054 if (!titleInput) return; 1055 1056 try { 1057 const doc = await db.collection("site_pages").doc(pageId).get(); 1058 if (doc.exists) { 1059 const data = doc.data(); 1060 titleInput.value = data.title || ""; 1061 videoInput.value = data.video || ""; 1062 descInput.value = data.desc || ""; 1063 } else { 1064 // Default placeholder values 1065 titleInput.value = `Alin Tagar ${pageId.toUpperCase()} Page`; 1066 videoInput.value = "https://www.youtube.com/embed/l0uEFPhIwgA"; 1067 descInput.value = "Explore a new learning experience at Alin Tagar English Centre..."; 1068 } 1069 } catch(e) { 1070 console.error("Error loading page data:", e); 1071 } 1072} 1073 1074// Load Registered User List for Admin 1075async function loadAdminUserData() { 1076 const listContainer = document.getElementById('admin-user-list'); 1077 if (!listContainer) return; 1078 1079 try { 1080 const snapshot = await db.collection("users").get(); 1081 if (snapshot.empty) { 1082 listContainer.innerHTML = `<p class="italic text-gray-500">No registered users found.</p>`; 1083 return; 1084 } 1085 1086 let html = `<ul class="divide-y border rounded-xl overflow-hidden bg-gray-50">`; 1087 snapshot.forEach(doc => { 1088 const u = doc.data(); 1089 html += ` 1090 <li class="p-3 bg-white flex items-center justify-between"> 1091 <div> 1092 <p class="font-bold text-gray-900">${u.fullName || 'User'}</p> 1093 <p class="text-gray-500 text-[11px]">${u.email}</p> 1094 </div> 1095 <span class="text-[10px] font-bold px-2 py-0.5 rounded-full ${u.role === 'admin' ? 'bg-amber-100 text-amber-800' : 'bg-blue-100 text-blue-800'}"> 1096 ${u.role || 'student'} 1097 </span> 1098 </li>`; 1099 }); 1100 html += `</ul>`; 1101 listContainer.innerHTML = html; 1102 } catch(e) { 1103 listContainer.innerHTML = `<p class="text-red-500">Error loading users: ${e.message}</p>`; 1104 } 1105} 1106 1107/* ========================================== 1108 ADVANCED AUTOMATED SITEMAP GENERATOR WORKSPACE 1109 ========================================== */ 1110 1111let discoveredSitemapUrls = []; 1112 1113function openFullscreenSitemapGenerator() { 1114 const existingWs = document.getElementById('admin-fullscreen-workspace'); 1115 if (existingWs) existingWs.remove(); 1116 1117 const workspaceHTML = ` 1118 <div id="admin-fullscreen-workspace"> 1119 <div class="bg-slate-950 text-white px-8 py-5 flex items-center justify-between border-b border-slate-800 shadow-xl"> 1120 <div class="flex items-center gap-4"> 1121 <div class="w-10 h-10 bg-amber-600 text-white rounded-xl flex items-center justify-center font-black text-xl"> 1122 <i class="fa-solid fa-sitemap"></i> 1123 </div> 1124 <div> 1125 <h2 class="font-black text-xl text-white">Dynamic SEO Sitemap & Index Manager</h2> 1126 <p class="text-xs text-gray-400">Scan database, manage index permissions, and export XML for Google</p> 1127 </div> 1128 </div> 1129 1130 <div class="flex items-center gap-3"> 1131 <button id="sitemap-copy-url-btn" class="bg-blue-600 hover:bg-blue-500 text-white font-black px-4 py-2.5 rounded-xl text-xs transition flex items-center gap-2 shadow-lg"> 1132 <i class="fa-solid fa-link"></i> Copy Sitemap URL 1133 </button> 1134 <button id="sitemap-copy-btn" class="bg-amber-500 hover:bg-amber-400 text-slate-950 font-black px-4 py-2.5 rounded-xl text-xs transition flex items-center gap-2 shadow-lg"> 1135 <i class="fa-solid fa-copy"></i> Copy XML Code 1136 </button> 1137 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 1138 â Close Panel 1139 </button> 1140 </div> 1141 </div> 1142 1143 <div class="p-8 md:p-12 max-w-7xl mx-auto w-full flex-1 grid grid-cols-1 lg:grid-cols-12 gap-8 overflow-y-auto"> 1144 1145 <!-- LEFT COLUMN: URL INDEX & PROHIBITION TOGGLES (7 Cols) --> 1146 <div class="lg:col-span-7 bg-slate-900 p-6 rounded-3xl border border-slate-800 shadow-2xl flex flex-col space-y-4"> 1147 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 1148 <h3 class="font-black text-sm text-white flex items-center gap-2"> 1149 <i class="fa-solid fa-list-check text-amber-400"></i> Discovered Pages, Categories & Posts 1150 </h3>
1151 <span id="sitemap-discovered-count" class="text-xs bg-slate-800 text-gray-300 px-3 py-1 rounded-full font-semibold">Scanning...</span> 1152 </div> 1153 <p class="text-xs text-gray-400 leading-relaxed"> 1154 Check or uncheck items below to <strong>include</strong> or <strong>prohibit (exclude)</strong> them from your generated sitemap XML. 1155 </p> 1156 1157 <div id="sitemap-url-checklist" class="space-y-2.5 max-h-[420px] overflow-y-auto pr-2 divide-y divide-slate-800/50"> 1158 <p class="italic text-gray-500 text-xs py-4 text-center">Loading database records...</p> 1159 </div> 1160 </div> 1161 1162 <!-- RIGHT COLUMN: LIVE XML OUTPUT (5 Cols) --> 1163 <div class="lg:col-span-5 bg-slate-900 p-6 rounded-3xl border border-slate-800 shadow-2xl flex flex-col space-y-4"> 1164 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 1165 <h3 class="font-black text-sm text-white flex items-center gap-2"> 1166 <i class="fa-solid fa-code text-emerald-400"></i> Live XML Preview 1167 </h3> 1168 <span class="text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2.5 py-0.5 rounded-full font-bold">Auto-Updating</span> 1169 </div> 1170 1171 <textarea id="sitemap-output-textarea" readonly rows="16" class="w-full p-4 bg-slate-950 border border-slate-800 rounded-2xl font-mono text-xs text-emerald-400 focus:outline-none leading-relaxed select-all flex-1"></textarea> 1172 </div> 1173 1174 </div> 1175 </div> 1176 `; 1177 1178 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 1179 scanDatabaseAndBuildSitemap(); 1180 1181 setTimeout(() => { 1182 autoSyncSitemapToDatabase(); 1183 }, 1000); 1184 1185 document.getElementById('sitemap-copy-btn').addEventListener('click', () => { 1186 const textarea = document.getElementById('sitemap-output-textarea'); 1187 textarea.select(); 1188 textarea.setSelectionRange(0, 99999); 1189 navigator.clipboard.writeText(textarea.value).then(() => { 1190 customToast("Sitemap XML code copied to clipboard!", "success"); 1191 }).catch(err => { 1192 customToast("Failed to copy: " + err, "error"); 1193 }); 1194 }); 1195 1196 document.getElementById('sitemap-copy-url-btn').addEventListener('click', () => { 1197 const sitemapUrl = "https://alintagar-official-website.web.app/sitemap.xml"; 1198 navigator.clipboard.writeText(sitemapUrl).then(() => { 1199 customToast("Sitemap URL copied: " + sitemapUrl, "success"); 1200 }).catch(err => { 1201 customToast("Failed to copy URL", "error"); 1202 }); 1203 }); 1204} 1205 1206/* ========================================== 1207 AUTO-SYNC SITEMAP TO FIREBASE 1208 ========================================== */ 1209 1210async function autoSyncSitemapToDatabase() { 1211 const textarea = document.getElementById('sitemap-output-textarea'); 1212 if (!textarea || !textarea.value) return; 1213 1214 try { 1215 // Save the raw XML string into Firestore under a public document 1216 await db.collection("site_seo").doc("sitemap_xml").set({ 1217 xmlContent: textarea.value, 1218 updatedAt: new Date() 1219 }); 1220 customToast("Sitemap automatically synced to Firebase Database!", "success"); 1221 } catch(e) { 1222 console.error("Error auto-syncing sitemap:", e); 1223 } 1224} 1225 1226async function scanDatabaseAndBuildSitemap() { 1227 const domain = "https://alintagar-official-website.web.app"; 1228 discoveredSitemapUrls = []; 1229 1230 // 1. Add Static System Pages 1231 defaultPages.forEach(p => { 1232 let route = p.url === '/' ? '' : `/${p.url}`; 1233 discoveredSitemapUrls.push({ 1234 url: `${domain}${route}`, 1235 type: 'System Page', 1236 title: p.name, 1237 enabled: true, 1238 priority: p.id === 'home' ? '1.0' : '0.8' 1239 }); 1240 }); 1241 1242 // 2. Scan Firestore: site_builder_pages (Custom builder pages) 1243 try { 1244 const builderSnap = await db.collection("site_builder_pages").get();
1245 builderSnap.forEach(doc => { 1246 const slug = doc.id; 1247 if (!defaultPages.some(p => p.url === slug || p.id === slug)) { 1248 discoveredSitemapUrls.push({ 1249 url: `${domain}/${slug}`, 1250 type: 'Custom Builder Page', 1251 title: `Builder Page (${slug})`, 1252 enabled: true, 1253 priority: '0.7' 1254 }); 1255 } 1256 }); 1257 } catch(e) { console.error(e); } 1258 1259 // 3. Scan Firestore: blog_posts (If you have a blog collection) 1260 try { 1261 const blogSnap = await db.collection("blog_posts").get(); 1262 blogSnap.forEach(doc => { 1263 const post = doc.data(); 1264 discoveredSitemapUrls.push({ 1265 url: `${domain}/blog/${doc.id}`, 1266 type: 'Blog Post', 1267 title: post.title || 'Untitled Post', 1268 enabled: true, 1269 priority: '0.6' 1270 }); 1271 }); 1272 } catch(e) {} 1273 1274 // 4. Scan Firestore: categories (If you have categories) 1275 try { 1276 const catSnap = await db.collection("categories").get(); 1277 catSnap.forEach(doc => { 1278 const cat = doc.data(); 1279 discoveredSitemapUrls.push({ 1280 url: `${domain}/category/${doc.id}`, 1281 type: 'Category', 1282 title: cat.name || doc.id, 1283 enabled: true, 1284 priority: '0.5' 1285 }); 1286 }); 1287 } catch(e) {} 1288 1289 renderSitemapChecklist(); 1290 generateSitemapXML(); 1291} 1292
1293function renderSitemapChecklist() { 1294 const container = document.getElementById('sitemap-url-checklist'); 1295 const countBadge = document.getElementById('sitemap-discovered-count'); 1296 if (!container) return; 1297 1298 if (discoveredSitemapUrls.length === 0) { 1299 container.innerHTML = `<p class="text-xs text-gray-500 italic py-4 text-center">No URLs discovered.</p>`; 1300 if (countBadge) countBadge.innerText = `0 URLs`; 1301 return; 1302 } 1303 1304 let html = ''; 1305 discoveredSitemapUrls.forEach((item, index) => { 1306 let badgeColor = 'bg-blue-500/10 text-blue-400 border-blue-500/20'; 1307 if (item.type === 'Blog Post') badgeColor = 'bg-purple-500/10 text-purple-400 border-purple-500/20'; 1308 if (item.type === 'Category') badgeColor = 'bg-amber-500/10 text-amber-400 border-amber-500/20'; 1309 if (item.type === 'Custom Builder Page') badgeColor = 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'; 1310 1311 html += ` 1312 <div class="pt-2.5 flex items-center justify-between gap-3"> 1313 <div class="flex items-center gap-3 truncate"> 1314 <input type="checkbox" id="sitemap-chk-${index}" ${item.enabled ? 'checked' : ''} onchange="toggleSitemapUrl(${index}, this.checked)" class="w-4 h-4 rounded text-amber-500 focus:ring-amber-500 bg-slate-950 border-slate-700 cursor-pointer"> 1315 <div class="truncate"> 1316 <p class="text-xs font-bold text-white truncate">${item.title}</p> 1317 <p class="text-[10px] text-gray-400 font-mono truncate">${item.url}</p> 1318 </div> 1319 </div> 1320 <span class="text-[10px] font-bold px-2.5 py-0.5 rounded-full border ${badgeColor} flex-shrink-0">${item.type}</span> 1321 </div>`; 1322 }); 1323 1324 container.innerHTML = html; 1325 if (countBadge) countBadge.innerText = `${discoveredSitemapUrls.length} Discovered`; 1326} 1327 1328window.toggleSitemapUrl = function(index, isEnabled) { 1329 if (discoveredSitemapUrls[index]) { 1330 discoveredSitemapUrls[index].enabled = isEnabled; 1331 generateSitemapXML(); 1332 } 1333}; 1334 1335function generateSitemapXML() { 1336 const textarea = document.getElementById('sitemap-output-textarea'); 1337 if (!textarea) return; 1338 1339 let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`; 1340 xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`; 1341 1342 discoveredSitemapUrls.forEach(item => { 1343 if (item.enabled) { 1344 xml += ` <url>\n`; 1345 xml += ` <loc>${item.url}</loc>\n`; 1346 xml += ` <changefreq>weekly</changefreq>\n`; 1347 xml += ` <priority>${item.priority}</priority>\n`; 1348 xml += ` </url>\n`; 1349 } 1350 }); 1351 1352 xml += `</urlset>`; 1353 textarea.value = xml; 1354} 1355 1356/* ========================================== 1357 DYNAMIC SITEMAP.XML ROUTE HANDLER 1358 ========================================== */ 1359document.addEventListener('DOMContentLoaded', async () => { 1360 if (window.location.pathname === '/sitemap.xml' || window.location.pathname === 'sitemap.xml') { 1361 const domain = "https://alintagar-official-website.web.app"; 1362 let urls = [...defaultPages]; 1363 1364 // Scan builder pages too 1365 try { 1366 const builderSnap = await db.collection("site_builder_pages").get(); 1367 builderSnap.forEach(doc => { 1368 const slug = doc.id; 1369 if (!urls.some(p => p.url === slug || p.id === slug)) { 1370 urls.push({ id: slug, url: slug }); 1371 } 1372 }); 1373 } catch(e) {} 1374 1375 let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`; 1376 xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`; 1377 1378 urls.forEach(p => { 1379 let route = p.url === '/' ? '' : `/${p.url.replace(/^\//, '')}`; 1380 let priority = p.id === 'home' ? '1.0' : p.url.includes('course') ? '0.9' : '0.8'; 1381 1382 xml += ` <url>\n`; 1383 xml += ` <loc>${domain}${route}</loc>\n`; 1384 xml += ` <changefreq>weekly</changefreq>\n`; 1385 xml += ` <priority>${priority}</priority>\n`; 1386 xml += ` </url>\n`; 1387 }); 1388 1389 xml += `</urlset>`; 1390
1391 // Output raw XML to browser 1392 document.open(); 1393 document.write(`<pre style="word-wrap: break-word; white-space: pre-wrap; font-family: monospace; padding: 20px;">${xml.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</pre>`); 1394 document.close(); 1395 1396 // Change document content type context visually 1397 document.contentType = 'application/xml'; 1398 } 1399}); 1400 1401/* ========================================== 1402 DYNAMIC MENU MANAGEMENT PANEL 1403 ========================================== */ 1404 1405window.openFullscreenMenuManager = function() { 1406 const existingWs = document.getElementById('admin-fullscreen-workspace'); 1407 if (existingWs) existingWs.remove(); 1408 1409 const workspaceHTML = ` 1410 <!-- ð¢ Shifted overflow-y-auto to the outermost wrapper --> 1411 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-y-auto"> 1412 1413 <!-- ð¢ Added sticky top-0 and z-50 so the header stays visible while scrolling --> 1414 <div class="sticky top-0 z-50 bg-slate-950 text-gray-300 py-4 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl"> 1415 <div class="flex items-center gap-3"> 1416 <div class="w-9 h-9 bg-rose-600 text-white rounded-xl flex items-center justify-center font-black text-lg"> 1417 <i class="fa-solid fa-bars-staggered"></i> 1418 </div> 1419 <div> 1420 <h2 class="font-black text-lg text-white leading-none">Navigation Menu Manager</h2> 1421 <p class="text-[11px] text-gray-400">Reorder pages, create dropdowns, and manage your main header navigation.</p> 1422 </div> 1423 </div> 1424 1425 <div class="flex items-center gap-3"> 1426 <button onclick="saveMenuToFirebase()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-black px-5 py-2 rounded-xl text-xs transition shadow-lg flex items-center gap-2"> 1427 <i class="fa-solid fa-cloud-arrow-up"></i> Save Menu Live 1428 </button> 1429 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 1430 â Close 1431 </button> 1432 </div> 1433 </div> 1434 1435 <!-- Main Panel --> 1436 <div class="flex-1 p-6 md:p-10 flex justify-center items-start"> 1437 1438 <!-- ð¢ 1. Changed to lg:grid-cols-3 for 3 equal-width columns --> 1439 <div class="w-full max-w-[1280px] grid grid-cols-1 lg:grid-cols-3 gap-8"> 1440 1441 <!-- ð¢ COLUMN 1: Pick Existing Content --> 1442 <!-- Added h-[650px] to lock the height --> 1443 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl h-[650px] flex flex-col space-y-4"> 1444 <div> 1445 <h3 class="font-black text-white text-lg mb-1">Pick Content</h3> 1446 <p class="text-xs text-gray-400">Select multiple pages or posts to bulk-add to your menu.</p> 1447 </div> 1448 1449 <div class="bg-slate-900 p-4 rounded-2xl border border-slate-800 flex flex-col flex-1 min-h-0"> 1450 <div class="flex justify-between items-center mb-4"> 1451 <label class="flex items-center gap-2 cursor-pointer group"> 1452 <input type="checkbox" id="menu-select-all-cb" onchange="toggleAllMenuContent(this)" class="w-3.5 h-3.5 rounded text-rose-500 bg-slate-950 border-slate-600 focus:ring-rose-500"> 1453 <span class="text-xs font-bold text-gray-300 uppercase group-hover:text-white transition">Select All</span> 1454 </label> 1455 1456 <select id="existing-content-type" onchange="renderExistingContentList()" class="bg-slate-950 border border-slate-700 rounded-lg text-gray-300 text-[10px] p-1.5 focus:outline-none"> 1457 <option value="all">All Types</option> 1458 <option value="physical">System Pages</option> 1459 <option value="builder">Custom Pages</option> 1460 <option value="post">Blog Posts</option> 1461 </select> 1462 </div> 1463 1464 <!-- ð¢ Added flex-1, overflow-y-auto, and min-h-0 so it scrolls perfectly inside the box --> 1465 <div id="existing-content-list" class="flex-1 overflow-y-auto space-y-1.5 mb-4 pr-2 min-h-0"> 1466 <p class="text-xs text-gray-500 italic text-center py-4">Loading database...</p> 1467 </div> 1468 1469 <button onclick="addSelectedContentToMenu()" id="load-content-btn" class="w-full mt-auto bg-rose-600/20 hover:bg-rose-600/30 text-rose-400 font-bold py-2.5 rounded-lg text-xs transition border border-rose-500/30 shadow-sm flex items-center justify-center gap-2 shrink-0">
1470 <span>+ Add Selected to Menu</span> 1471 </button> 1472 </div> 1473 </div> 1474 1475 <!-- ð¢ COLUMN 2: Create Custom Link / Dropdown --> 1476 <!-- Added h-[650px] to lock the height --> 1477 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl h-[650px] flex flex-col space-y-4"> 1478 <div> 1479 <h3 class="font-black text-white text-lg mb-1">Link Builder</h3> 1480 <p class="text-xs text-gray-400">Create a standard link or a dropdown folder.</p> 1481 </div> 1482 1483 <div class="space-y-4 overflow-y-auto pr-2 flex-1 min-h-0"> 1484 <!-- Box 1 --> 1485 <div class="bg-slate-900 p-4 rounded-2xl border border-slate-800 transition-colors" id="link-builder-box"> 1486 <h4 class="text-xs font-bold text-gray-300 uppercase mb-3">1. Add Standard Link</h4> 1487 <input type="text" id="new-link-title" placeholder="Link Title (e.g. About Us)" class="w-full p-2.5 mb-2 bg-slate-950 border border-slate-700 rounded-lg text-white text-xs focus:border-rose-500 outline-none transition"> 1488 <input type="text" id="new-link-url" placeholder="URL Slug (e.g. about)" class="w-full p-2.5 mb-3 bg-slate-950 border border-slate-700 rounded-lg text-white text-xs focus:border-rose-500 outline-none transition"> 1489 <button onclick="addStandardLinkToMenu()" class="w-full bg-slate-800 hover:bg-slate-700 text-rose-400 font-bold py-2.5 rounded-lg text-xs transition border border-slate-700 shadow-sm"> 1490 + Add Link to Menu 1491 </button> 1492 </div> 1493 1494 <!-- Box 2 --> 1495 <div class="bg-slate-900 p-4 rounded-2xl border border-slate-800"> 1496 <h4 class="text-xs font-bold text-gray-300 uppercase mb-3">2. Add Dropdown Folder</h4> 1497 <input type="text" id="new-dropdown-title" placeholder="Folder Title (e.g. Learn English)" class="w-full p-2.5 mb-3 bg-slate-950 border border-slate-700 rounded-lg text-white text-xs focus:border-rose-500 outline-none transition"> 1498 <button onclick="addDropdownToMenu()" class="w-full bg-slate-800 hover:bg-slate-700 text-rose-400 font-bold py-2.5 rounded-lg text-xs transition border border-slate-700 shadow-sm"> 1499 + Add Dropdown to Menu 1500 </button> 1501 </div> 1502 </div> 1503 </div> 1504 1505 <!-- ð¢ COLUMN 3: Menu Structure --> 1506 <!-- Removed lg:col-span-2 and added h-[650px] --> 1507 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl h-[650px] flex flex-col"> 1508 <div class="flex justify-between items-center mb-6"> 1509 <h3 class="font-black text-white text-lg">Menu Structure</h3> 1510 <span class="text-xs bg-rose-500/20 text-rose-400 border border-rose-500/30 px-3 py-1 rounded-full font-bold">Drag to Reorder</span> 1511 </div> 1512 1513 <!-- Builder List Container --> 1514 <!-- Added flex-1 overflow-y-auto and min-h-0 --> 1515 <div id="menu-builder-list" class="space-y-2 flex-1 overflow-y-auto pr-2 min-h-0"> 1516 1517 <!-- Visual Mockup --> 1518 <div class="bg-slate-900 border border-slate-700 p-3 rounded-xl flex items-center justify-between group"> 1519 <div class="flex items-center gap-3"> 1520 <i class="fa-solid fa-grip-vertical text-gray-600 cursor-grab"></i> 1521 <span class="font-bold text-sm text-white">Home</span> 1522 <span class="text-[10px] text-gray-500 font-mono">/</span> 1523 </div> 1524 <div class="flex gap-2 opacity-50 group-hover:opacity-100 transition"> 1525 <button class="text-gray-400 hover:text-blue-400"><i class="fa-solid fa-pen-to-square"></i></button> 1526 <button class="text-gray-400 hover:text-red-400">
1526<i class="fa-solid fa-trash-can"></i></button> 1527 </div> 1528 </div> 1529 1530 <div class="bg-slate-900 border border-slate-700 p-3 rounded-xl flex flex-col gap-2 group"> 1531 <div class="flex items-center justify-between"> 1532 <div class="flex items-center gap-3"> 1533 <i class="fa-solid fa-grip-vertical text-gray-600 cursor-grab"></i> 1534 <span class="font-bold text-sm text-amber-400"><i class="fa-solid fa-folder-open mr-1"></i> Learn English (Dropdown)</span> 1535 </div> 1536 <div class="flex gap-2 opacity-50 group-hover:opacity-100 transition"> 1537 <button class="text-gray-400 hover:text-blue-400"><i class="fa-solid fa-pen-to-square"></i></button> 1538 <button class="text-gray-400 hover:text-red-400"><i class="fa-solid fa-trash-can"></i></button> 1539 </div> 1540 </div> 1541 1542 <div class="ml-6 pl-3 border-l-2 border-slate-700 space-y-2 mt-2"> 1543 <div class="bg-slate-950 border border-slate-800 p-2.5 rounded-lg flex items-center justify-between"> 1544 <div class="flex items-center gap-3"> 1545 <i class="fa-solid fa-grip-vertical text-gray-600 cursor-grab text-xs"></i> 1546 <span class="font-semibold text-xs text-gray-300">Our Courses</span> 1547 <span class="text-[10px] text-gray-500 font-mono">/courses</span> 1548 </div> 1549 <button class="text-gray-500 hover:text-red-400 text-xs"><i class="fa-solid fa-xmark"></i></button> 1550 </div> 1551 <button class="w-full text-left p-2 border border-dashed border-slate-700 rounded-lg text-[11px] font-bold text-gray-500 hover:text-rose-400 hover:border-rose-500/50 transition"> 1552 + Add sub-link 1553 </button> 1554 </div> 1555 </div> 1556 1557 </div> 1558 </div> 1559 1560 </div> 1561 </div> 1562 1563 <!-- ð¢ MENU ACCESS MODAL --> 1564 <div id="menu-access-modal" class="fixed inset-0 z-[1000006] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4"> 1565 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-6 w-full max-w-sm shadow-2xl space-y-4"> 1566 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 1567 <h3 class="text-lg font-black text-amber-400"><i class="fa-solid fa-eye mr-2"></i> Menu Visibility</h3> 1568 <button type="button" onclick="closeMenuAccessModal()" class="text-gray-400 hover:text-white text-lg w-8 h-8 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark"></i></button> 1569 </div> 1570 <p class="text-xs text-gray-400">Who can see <strong id="menu-access-target-name" class="text-white">this item</strong> in the header?</p> 1571 1572 <div class="space-y-2 pt-2"> 1573 <label class="flex items-center gap-3 p-3 bg-slate-900 border border-slate-700 rounded-xl cursor-pointer hover:border-amber-500 transition"> 1574 <input type="radio" name="menu_vis_option" value="public" class="w-4 h-4 text-amber-500 bg-slate-950 border-slate-600 focus:ring-amber-500"> 1575 <div class="flex flex-col"> 1576 <span class="text-white text-xs font-bold">Public (Everyone)</span> 1577 <span class="text-gray-500 text-[10px]">Visible to all visitors.</span> 1578 </div> 1579 </label> 1580 <label class="flex items-center gap-3 p-3 bg-slate-900 border border-slate-700 rounded-xl cursor-pointer hover:border-amber-500 transition"> 1581 <input type="radio" name="menu_vis_option" value="logged_in" class="w-4 h-4 text-amber-500 bg-slate-950 border-slate-600 focus:ring-amber-500"> 1582 <div class="flex flex-col">
1583 <span class="text-white text-xs font-bold">Logged In Users Only</span> 1584 <span class="text-gray-500 text-[10px]">Hidden from public guests.</span> 1585 </div> 1586 </label> 1587 <label class="flex items-center gap-3 p-3 bg-slate-900 border border-slate-700 rounded-xl cursor-pointer hover:border-amber-500 transition"> 1588 <input type="radio" name="menu_vis_option" value="admin_only" class="w-4 h-4 text-amber-500 bg-slate-950 border-slate-600 focus:ring-amber-500"> 1589 <div class="flex flex-col"> 1590 <span class="text-white text-xs font-bold">Admins Only</span> 1591 <span class="text-gray-500 text-[10px]">Only visible to administrators.</span> 1592 </div> 1593 </label> 1594 </div> 1595 1596 <div class="pt-4 border-t border-slate-800 flex justify-end gap-2"> 1597 <button onclick="closeMenuAccessModal()" class="px-4 py-2 bg-slate-800 text-gray-300 text-xs font-bold rounded-xl hover:bg-slate-700">Cancel</button> 1598 <button onclick="saveMenuAccess()" class="px-4 py-2 bg-amber-500 text-slate-950 text-xs font-black rounded-xl hover:bg-amber-400 shadow-lg">Save Visibility</button> 1599 </div> 1600 </div> 1601 </div> 1602 </div> 1603 `; 1604 1605 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 1606 1607 // ð¢ Lock background page scrolling to kill the duplicate outer scrollbar 1608 document.body.classList.add('overflow-hidden'); 1609 1610 // Load data for the left column (Database Fetcher) 1611 loadAvailableContentForMenu(); 1612 1613 // ð¢ Load data for the right column (Menu Structure) 1614 loadMenuBuilderData(); 1615}; 1616 1617let availableMenuContent = []; 1618 1619async function loadAvailableContentForMenu() { 1620 availableMenuContent = []; 1621 const listEl = document.getElementById('existing-content-list'); 1622 if (!listEl) return; 1623 1624 // 1. Fetch Physical / System Pages (From your hardcoded array) 1625 if (typeof defaultPages !== 'undefined') { 1626 defaultPages.forEach(p => { 1627 availableMenuContent.push({ 1628 type: 'physical', label: 'System Page', title: p.name, url: p.url === '/' ? '/' : p.url 1629 }); 1630 }); 1631 } 1632 1633 // 2. Fetch Custom Page Creator Pages 1634 try { 1635 const builderSnap = await db.collection("site_builder_pages").get(); 1636 builderSnap.forEach(doc => { 1637 // Prevent duplicates if a builder page overwrites a physical page slug 1638 if (!availableMenuContent.some(c => c.url === doc.id)) { 1639 availableMenuContent.push({ 1640 type: 'builder', label: 'Custom Page', title: `Builder: ${doc.id}`, url: doc.id 1641 }); 1642 } 1643 }); 1644 } catch(e) { console.error(e); } 1645 1646 // 3. Fetch Dynamic Posts & Categories 1647 try { 1648 const postSnap = await db.collection("dynamic_posts").where("isPublished", "==", true).get(); 1649 postSnap.forEach(doc => { 1650 const data = doc.data(); 1651 availableMenuContent.push({ 1652 type: 'post', label: 'Blog Post', title: data.title || doc.id, url: `post.html?id=${doc.id}` 1653 }); 1654 }); 1655 } catch(e) { console.error(e); } 1656 1657 renderExistingContentList(); 1658} 1659 1660window.renderExistingContentList = function() { 1661 const listEl = document.getElementById('existing-content-list'); 1662 const filter = document.getElementById('existing-content-type').value; 1663 if (!listEl) return; 1664 1665 // Reset the Select All checkbox when the filter changes 1666 const selectAllCb = document.getElementById('menu-select-all-cb'); 1667 if (selectAllCb) selectAllCb.checked = false; 1668 1669 let html = ''; 1670 availableMenuContent.forEach((item, index) => { 1671 if (filter === 'all' || item.type === filter) { 1672 let badgeColor = 'bg-gray-600'; 1673 if (item.type === 'physical') badgeColor = 'bg-blue-600'; 1674 if (item.type === 'builder') badgeColor = 'bg-emerald-600'; 1675 if (item.type === 'post') badgeColor = 'bg-purple-600'; 1676 1677 // ð¢ Swapped radio input for checkbox input 1678 html += ` 1679 <label class="flex items-center gap-3 p-2 bg-slate-950 border border-slate-800 rounded-lg cursor-pointer hover:border-rose-500 transition group"> 1680 <input type="checkbox" name="menu_content_cb" value="${index}" class="w-3.5 h-3.5 rounded text-rose-500 bg-slate-900 border-slate-700 focus:ring-rose-500 focus:ring-offset-slate-950 cursor-pointer"> 1681 <div class="flex-1 truncate"> 1682 <p class="text-xs text-white font-bold truncate group-hover:text-rose-400 transition">${item.title}</p> 1683 <p class="text-[9px] text-gray-500 font-mono truncate">${item.url}</p> 1684 </div>
1685 <span class="text-[8px] font-bold px-1.5 py-0.5 rounded text-white ${badgeColor} shadow-sm">${item.label}</span> 1686 </label> 1687 `; 1688 } 1689 }); 1690 1691 listEl.innerHTML = html || '<p class="text-xs text-gray-500 italic text-center py-4">No content found.</p>'; 1692}; 1693 1694// ð¢ NEW: Select All Logic 1695window.toggleAllMenuContent = function(source) { 1696 const checkboxes = document.querySelectorAll('input[name="menu_content_cb"]'); 1697 checkboxes.forEach(cb => { 1698 cb.checked = source.checked; 1699 }); 1700}; 1701 1702// ð¢ NEW: Bulk Add Logic (Directly to Menu) 1703window.addSelectedContentToMenu = function() { 1704 const checkedBoxes = document.querySelectorAll('input[name="menu_content_cb"]:checked'); 1705 if (checkedBoxes.length === 0) { 1706 customToast("Please check at least one item from the list.", "error"); 1707 return; 1708 } 1709 1710 // Push every selected item directly into the live menu array 1711 checkedBoxes.forEach(cb => { 1712 const item = availableMenuContent[cb.value]; 1713 window.liveMenuData.push({ 1714 type: 'link', 1715 title: item.title.replace('Builder: ', ''), 1716 url: item.url, 1717 icon: 'fa-link' // Default icon 1718 }); 1719 cb.checked = false; // Uncheck after adding 1720 }); 1721 1722 // Re-render the menu structure immediately 1723 renderMenuBuilderList(); 1724 1725 // Uncheck "Select All" toggle 1726 const selectAllCb = document.getElementById('menu-select-all-cb'); 1727 if (selectAllCb) selectAllCb.checked = false; 1728 1729 // Button success feedback 1730 const btn = document.getElementById('load-content-btn'); 1731 const originalHTML = btn.innerHTML; 1732 btn.innerHTML = 'â Added to Menu!'; 1733 btn.classList.replace('text-rose-400', 'text-white'); 1734 btn.classList.replace('bg-rose-600/20', 'bg-green-600'); 1735 1736 setTimeout(() => { 1737 btn.innerHTML = originalHTML; 1738 btn.classList.replace('text-white', 'text-rose-400'); 1739 btn.classList.replace('bg-green-600', 'bg-rose-600/20'); 1740 }, 1500); 1741}; 1742 1743/* ========================================== 1744 MENU BUILDER: DRAG, DROP & RENDER LOGIC 1745 ========================================== */ 1746 1747window.liveMenuData = []; 1748let draggedMenuIndex = null; 1749 1750// 1. Load Initial Data (Called when panel opens) 1751window.loadMenuBuilderData = async function() { 1752 const container = document.getElementById('menu-builder-list'); 1753 if (container) container.innerHTML = '<p class="text-center text-gray-500 italic py-8">Loading menu data...</p>'; 1754 1755 try { 1756 const menuDoc = await db.collection("site_settings").doc("navigation").get(); 1757 if (menuDoc.exists && menuDoc.data().menuItems) { 1758 window.liveMenuData = menuDoc.data().menuItems; 1759 } else { 1760 window.liveMenuData = []; // Empty fallback 1761 } 1762 renderMenuBuilderList(); 1763 } catch (e) { 1764 console.error("Error loading menu:", e); 1765 if (container) container.innerHTML = '<p class="text-center text-red-400 italic py-8">Error loading menu.</p>'; 1766 } 1767}; 1768 1769// 2. Render the Array to HTML 1770window.renderMenuBuilderList = function() { 1771 const container = document.getElementById('menu-builder-list'); 1772 if (!container) return; 1773 1774 let html = ''; 1775 window.liveMenuData.forEach((item, index) => { 1776 // ð¢ FIXED: Split color classes from the FontAwesome icon class 1777 const isPublic = (!item.visibility || item.visibility === 'public'); 1778 const visIcon = isPublic ? 'fa-eye' : 'fa-eye-slash'; 1779 const visColor = isPublic ? 'text-gray-400 hover:text-amber-400' : 'text-amber-500 hover:text-amber-400'; 1780 1781 if (item.type === 'link') { 1782 const displayUrl = item.url === '/' ? '/' : (item.url.startsWith('/') ? item.url : `/${item.url}`); 1783 1784 html += ` 1785 <div class="menu-item-row bg-slate-900 border border-slate-700 p-3 rounded-xl flex items-center justify-between group transition-all" 1786 draggable="true" 1787 ondragstart="menuDragStart(event, ${index})" 1788 ondragover="menuDragOver(event)" 1789 ondrop="menuDrop(event, ${index})" 1790 ondragend="menuDragEnd(event)"> 1791 <div class="flex items-center gap-3"> 1792 <i class="fa-solid fa-grip-vertical text-gray-600 cursor-grab active:cursor-grabbing"></i>
1793 <span class="font-bold text-sm text-white">${item.title}</span> 1794 <span class="text-[10px] text-gray-500 font-mono">${displayUrl}</span> 1795 </div> 1796 <div class="flex gap-2 opacity-50 group-hover:opacity-100 transition"> 1797 <!-- ð¢ FIXED: Placed ${visIcon} inside the <i> tag --> 1798 <button onclick="openMenuAccessModal(${index})" class="${visColor}" title="Visibility Settings"><i class="fa-solid ${visIcon} fa-fw"></i></button> 1799 <button onclick="removeMenuItem(${index})" class="text-gray-400 hover:text-red-400" title="Delete"><i class="fa-solid fa-trash-can fa-fw"></i></button> 1800 </div> 1801 </div>`; 1802 1803 } else if (item.type === 'dropdown') { 1804 let subHtml = ''; 1805 if (item.subItems && item.subItems.length > 0) { 1806 item.subItems.forEach((sub, subIndex) => { 1807 const subDisplayUrl = sub.url === '/' ? '/' : (sub.url.startsWith('/') ? sub.url : `/${sub.url}`); 1808 1809 // ð¢ FIXED: Same fix for sub-links 1810 const isSubPublic = (!sub.visibility || sub.visibility === 'public'); 1811 const subVisIcon = isSubPublic ? 'fa-eye' : 'fa-eye-slash'; 1812 const subVisColor = isSubPublic ? 'text-gray-500 hover:text-amber-400' : 'text-amber-500 hover:text-amber-400'; 1813 1814 subHtml += ` 1815 <div class="bg-slate-950 border border-slate-800 p-2.5 rounded-lg flex items-center justify-between group/sub"> 1816 <div class="flex items-center gap-3"> 1817 <i class="fa-solid fa-minus text-gray-700 text-xs"></i> 1818 <span class="font-semibold text-xs text-gray-300">${sub.title}</span> 1819 <span class="text-[10px] text-gray-500 font-mono">${subDisplayUrl}</span> 1820 </div> 1821 <div class="flex items-center gap-2 opacity-50 group-hover/sub:opacity-100 transition"> 1822 <!-- ð¢ FIXED --> 1823 <button onclick="openMenuAccessModal(${index}, ${subIndex})" class="${subVisColor} text-xs px-1" title="Visibility"><i class="fa-solid ${subVisIcon} fa-fw"></i></button> 1824 <button onclick="moveSubMenuItem(${index}, ${subIndex}, -1)" class="text-gray-500 hover:text-amber-400 text-xs px-1" title="Move Up"><i class="fa-solid fa-arrow-up fa-fw"></i></button> 1825 <button onclick="moveSubMenuItem(${index}, ${subIndex}, 1)" class="text-gray-500 hover:text-amber-400 text-xs px-1" title="Move Down"><i class="fa-solid fa-arrow-down fa-fw"></i></button> 1826 <button onclick="removeSubMenuItem(${index}, ${subIndex})" class="text-gray-500 hover:text-red-400 text-xs ml-1" title="Remove Sub-link"><i class="fa-solid fa-xmark fa-fw"></i></button> 1827 </div> 1828 </div>`; 1829 }); 1830 } else { 1831 subHtml = `<p class="text-[10px] text-gray-500 italic py-1">No sub-links added yet.</p>`; 1832 } 1833 1834 html += ` 1835 <div class="menu-item-row bg-slate-900 border border-slate-700 p-3 rounded-xl flex flex-col gap-2 group transition-all" 1836 draggable="true" 1837 ondragstart="menuDragStart(event, ${index})" 1838 ondragover="menuDragOver(event)" 1839 ondrop="menuDrop(event, ${index})" 1840 ondragend="menuDragEnd(event)"> 1841 <div class="flex items-center justify-between"> 1842 <div class="flex items-center gap-3"> 1843 <i class="fa-solid fa-grip-vertical text-gray-600 cursor-grab active:cursor-grabbing"></i> 1844 <span class="font-bold text-sm text-amber-400"><i class="fa-solid fa-folder-open mr-1"></i> ${item.title}</span> 1845 </div> 1846 <div class="flex gap-2 opacity-50 group-hover:opacity-100 transition"> 1847 <!-- ð¢ FIXED --> 1848 <button onclick="openMenuAccessModal(${index})" class="${visColor}" title="Visibility Settings"><i class="fa-solid ${visIcon} fa-fw"></i></button> 1849 <button onclick="removeMenuItem(${index})" class="text-gray-400 hover:text-red-400" title="Delete Folder">
1849<i class="fa-solid fa-trash-can fa-fw"></i></button> 1850 </div> 1851 </div> 1852 1853 <div class="ml-6 pl-3 border-l-2 border-slate-700 space-y-2 mt-2" ondragover="event.stopPropagation();" ondrop="event.stopPropagation();"> 1854 ${subHtml} 1855 <button onclick="addSubLinkPrompt(${index})" class="w-full text-left p-2 border border-dashed border-slate-700 rounded-lg text-[11px] font-bold text-gray-500 hover:text-rose-400 hover:border-rose-500/50 transition"> 1856 + Add sub-link 1857 </button> 1858 </div> 1859 </div>`; 1860 } 1861 }); 1862 1863 container.innerHTML = html || '<div class="text-center py-8"><p class="text-gray-500 italic text-sm mb-2">Your menu is completely empty.</p></div>'; 1864}; 1865 1866window.customDoublePrompt = function(title, message, placeholder1 = "Title", placeholder2 = "URL") { 1867 return new Promise((resolve) => { 1868 injectUniversalUI(); 1869 const overlay = document.getElementById('universal-dialog-overlay'); 1870 document.getElementById('dialog-icon').innerHTML = '<i class="fa-solid fa-link text-brandBlue"></i>'; 1871 document.getElementById('dialog-title').innerText = title; 1872 1873 // ð¢ Injects two separate inputs into the same dialog box 1874 document.getElementById('dialog-message').innerHTML = ` 1875 <p class="text-gray-600 text-sm mb-4">${message}</p> 1876 <div class="space-y-3 text-left"> 1877 <div> 1878 <label class="block text-xs font-bold text-gray-500 mb-1">Link Title</label> 1879 <input type="text" id="prompt-input-1" placeholder="${placeholder1}" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:ring-2 focus:ring-brandBlue focus:outline-none text-sm text-gray-800"> 1880 </div> 1881 <div> 1882 <label class="block text-xs font-bold text-gray-500 mb-1">URL Slug</label> 1883 <input type="text" id="prompt-input-2" placeholder="${placeholder2}" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:ring-2 focus:ring-brandBlue focus:outline-none text-sm text-gray-800"> 1884 </div> 1885 </div> 1886 `; 1887 1888 document.getElementById('dialog-buttons').innerHTML = ` 1889 <button id="dialog-cancel-btn" class="w-1/2 bg-gray-200 text-gray-800 font-bold py-2.5 rounded-xl hover:bg-gray-300 transition text-sm">Cancel</button> 1890 <button id="dialog-submit-btn" class="w-1/2 bg-brandBlue text-white font-bold py-2.5 rounded-xl hover:bg-blue-900 transition shadow-md text-sm">Add Link</button> 1891 `; 1892 1893 overlay.classList.remove('hidden'); 1894 const input1 = document.getElementById('prompt-input-1'); 1895 const input2 = document.getElementById('prompt-input-2'); 1896 input1.focus(); 1897 1898 const submitFn = () => { 1899 const val1 = input1.value; 1900 const val2 = input2.value; 1901 if (!val1 || !val2) { 1902 customToast("Both fields are required.", "error"); 1903 return; 1904 } 1905 overlay.classList.add('hidden'); 1906 resolve({ title: val1, url: val2 }); 1907 }; 1908 1909 input2.onkeyup = (e) => { if (e.key === 'Enter') submitFn(); }; 1910 document.getElementById('dialog-submit-btn').onclick = submitFn; 1911 1912 document.getElementById('dialog-cancel-btn').onclick = () => { 1913 overlay.classList.add('hidden'); 1914 resolve(null); 1915 }; 1916 }); 1917}; 1918 1919// 3. HTML5 Drag & Drop Handlers 1920window.menuDragStart = function(e, index) { 1921 draggedMenuIndex = index; 1922 e.dataTransfer.effectAllowed = 'move'; 1923 // Delay adding the class so the dragged ghost image looks normal 1924 setTimeout(() => { 1925 e.target.classList.add('opacity-40', 'border-rose-500', 'border-dashed'); 1926 }, 0); 1927}; 1928 1929window.menuDragOver = function(e) { 1930 e.preventDefault(); 1931 e.dataTransfer.dropEffect = 'move'; 1932}; 1933 1934window.menuDrop = function(e, targetIndex) { 1935 e.preventDefault(); 1936 e.stopPropagation(); 1937 1938 if (draggedMenuIndex !== null && draggedMenuIndex !== targetIndex) { 1939 // Remove the dragged item from its original position 1940 const movedItem = window.liveMenuData.splice(draggedMenuIndex, 1)[0]; 1941 // Insert it into the new target position 1942 window.liveMenuData.splice(targetIndex, 0, movedItem); 1943 1944 // Re-render the UI to reflect the new array order 1945 renderMenuBuilderList(); 1946 } 1947}; 1948 1949window.menuDragEnd = function(e) { 1950 e.target.classList.remove('opacity-40', 'border-rose-500', 'border-dashed'); 1951 draggedMenuIndex = null; 1952 renderMenuBuilderList(); // Clean up any lingering visual artifacts 1953}; 1954 1955// 4. Delete Functions 1956window.removeMenuItem = async function(index) { 1957 const confirmed = await customConfirm("Remove Item", "Are you sure you want to remove this item from the menu?"); 1958 if (confirmed) { 1959 window.liveMenuData.splice(index, 1); 1960 renderMenuBuilderList(); 1961 } 1962}; 1963 1964window.removeSubMenuItem = async function(parentIndex, subIndex) { 1965 const confirmed = await customConfirm("Remove Sub-link", "Remove this link from the dropdown folder?"); 1966 if (confirmed) { 1967 window.liveMenuData[parentIndex].subItems.splice(subIndex, 1); 1968 renderMenuBuilderList(); 1969 } 1970}; 1971 1972// 5. Move Sub-Links Up and Down 1973window.moveSubMenuItem = function(parentIndex, subIndex, direction) { 1974 const subItems = window.liveMenuData[parentIndex].subItems; 1975 const newIndex = subIndex + direction; 1976 1977 // Stop if trying to move out of bounds (above the top or below the bottom) 1978 if (newIndex < 0 || newIndex >= subItems.length) return; 1979 1980 // Swap the items in the array 1981 const temp = subItems[subIndex]; 1982 subItems[subIndex] = subItems[newIndex]; 1983 subItems[newIndex] = temp; 1984 1985 // Instantly re-render the UI 1986 renderMenuBuilderList(); 1987}; 1988
1989// ð¢ 6. Menu Item Visibility Logic 1990window.activeMenuAccessParent = null; 1991window.activeMenuAccessSub = null; 1992 1993window.openMenuAccessModal = function(parentIndex, subIndex = null) { 1994 window.activeMenuAccessParent = parentIndex; 1995 window.activeMenuAccessSub = subIndex; 1996 1997 let targetItem; 1998 if (subIndex === null) { 1999 targetItem = window.liveMenuData[parentIndex]; 2000 } else { 2001 targetItem = window.liveMenuData[parentIndex].subItems[subIndex]; 2002 } 2003 2004 document.getElementById('menu-access-target-name').innerText = targetItem.title; 2005 2006 // Check the correct radio button 2007 const currentVis = targetItem.visibility || 'public'; 2008 const radio = document.querySelector(`input[name="menu_vis_option"][value="${currentVis}"]`); 2009 if (radio) radio.checked = true; 2010 2011 document.getElementById('menu-access-modal').classList.remove('hidden'); 2012}; 2013 2014window.closeMenuAccessModal = function() { 2015 document.getElementById('menu-access-modal').classList.add('hidden'); 2016}; 2017 2018window.saveMenuAccess = function() { 2019 const selected = document.querySelector('input[name="menu_vis_option"]:checked').value; 2020 2021 if (window.activeMenuAccessSub === null) { 2022 window.liveMenuData[window.activeMenuAccessParent].visibility = selected; 2023 } else { 2024 window.liveMenuData[window.activeMenuAccessParent].subItems[window.activeMenuAccessSub].visibility = selected; 2025 } 2026 2027 renderMenuBuilderList(); // Re-render to update the icons 2028 closeMenuAccessModal(); 2029 customToast("Visibility updated. Don't forget to 'Save Menu Live'!", "success"); 2030}; 2031 2032/* ========================================== 2033 MENU BUILDER: ADD & SAVE LOGIC 2034 ========================================== */ 2035 2036window.addStandardLinkToMenu = function() { 2037 const titleInput = document.getElementById('new-link-title'); 2038 const urlInput = document.getElementById('new-link-url'); 2039 2040 if (!titleInput.value.trim() || !urlInput.value.trim()) { 2041 return customToast("Please enter both a Title and a URL.", "error"); 2042 } 2043 2044 window.liveMenuData.push({ 2045 type: 'link', 2046 title: titleInput.value.trim(), 2047 url: urlInput.value.trim(), 2048 icon: 'fa-link' // Default icon for custom links 2049 }); 2050 2051 // Clear inputs and re-render 2052 titleInput.value = ''; 2053 urlInput.value = ''; 2054 renderMenuBuilderList(); 2055 customToast("Link added to menu!", "success"); 2056}; 2057 2058window.addDropdownToMenu = function() { 2059 const titleInput = document.getElementById('new-dropdown-title'); 2060 2061 if (!titleInput.value.trim()) { 2062 return customToast("Please enter a folder title.", "error"); 2063 } 2064 2065 window.liveMenuData.push({ 2066 type: 'dropdown', 2067 title: titleInput.value.trim(), 2068 icon: 'fa-folder', 2069 subItems: [] 2070 }); 2071 2072 // Clear input and re-render 2073 titleInput.value = ''; 2074 renderMenuBuilderList(); 2075 customToast("Dropdown folder created!", "success"); 2076}; 2077 2078window.addSubLinkPrompt = async function(parentIndex) { 2079 const result = await customDoublePrompt( 2080 "Add Sub-Link", 2081 "Enter the link title and URL slug below:", 2082 "e.g. Grammar Classes", 2083 "e.g. grammar" 2084 ); 2085 2086 // If the user hits cancel, do nothing 2087 if (!result) return; 2088 2089 window.liveMenuData[parentIndex].subItems.push({ 2090 title: result.title.trim(), 2091 url: result.url.trim() 2092 }); 2093 2094 renderMenuBuilderList(); 2095 customToast("Sub-link added to folder!", "success"); 2096}; 2097 2098// ð¢ Save the Live Menu to Firebase! 2099window.saveMenuToFirebase = async function() { 2100 try { 2101 const btn = document.querySelector('button[onclick="saveMenuToFirebase()"]'); 2102 const origHtml = btn.innerHTML; 2103 btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving...'; 2104 2105 await db.collection("site_settings").doc("navigation").set({ 2106 menuItems: window.liveMenuData, 2107 updatedAt: new Date() 2108 }); 2109 2110 customToast("Menu saved successfully! It is now live.", "success"); 2111 btn.innerHTML = origHtml; 2112 } catch (e) { 2113 customToast("Error saving menu: " + e.message, "error"); 2114 } 2115}; 2116 2117/* ========================================== 2118 DYNAMIC LIBRARY MANAGEMENT PANEL 2119 ========================================== */ 2120 2121window.allBookshelvesData = []; 2122 2123window.openFullscreenLibraryManager = function() { 2124 const existingWs = document.getElementById('admin-fullscreen-workspace'); 2125 if (existingWs) existingWs.remove(); 2126 2127 const workspaceHTML = ` 2128 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 2129 2130 <!-- Top Bar --> 2131 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl"> 2132 <div class="flex items-center gap-3"> 2133 <div class="w-9 h-9 bg-sky-600 text-white rounded-xl flex items-center justify-center font-black text-lg"> 2134 <i class="fa-solid fa-book-bookmark"></i> 2135 </div> 2136 <div> 2137 <h2 class="font-black text-lg text-white leading-none">Digital Library Management</h2> 2138 <p class="text-[11px] text-gray-400">Build 3D bookshelves, manage flipbooks, categories, themes, and pricing.</p> 2139 </div> 2140 </div> 2141 2142 <div class="flex items-center gap-3"> 2143 <button onclick="openBookshelfEditorModal()" class="bg-amber-500 hover:bg-amber-400 text-slate-950 font-black px-4 py-2 rounded-xl text-xs transition shadow-lg flex items-center gap-2"> 2144 <i class="fa-solid fa-plus"></i> Create New Bookshelf 2145 </button> 2146 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 2147 â Close Panel 2148 </button> 2149 </div> 2150 </div> 2151 2152 <!-- Main Panel Container (1280px Max Width) --> 2153 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center"> 2154 <div class="w-full max-w-[1280px] space-y-6"> 2155 2156 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl space-y-6"> 2157 <div class="flex justify-between items-center border-b border-slate-800/80 pb-4"> 2158 <div> 2159 <h3 class="text-2xl font-black text-white mb-1">Bookshelves & Digital Collections</h3> 2160 <p class="text-xs text-gray-400">Published bookshelves appear live on /library with 3D perspective themes.</p> 2161 </div> 2162 </div> 2163 2164 <!-- Bookshelves Grid List --> 2165 <div id="admin-bookshelves-list" class="grid grid-cols-1 md:grid-cols-2 gap-6"> 2166 <p class="text-xs text-gray-500 italic py-8 text-center col-span-2">Loading bookshelves...</p> 2167 </div> 2168 </div> 2169 2170 </div> 2171 </div> 2172 </div> 2173 2174 <!-- ð¢ 1280px BOOKSHELF EDITOR MODAL WITH SHORTENED INSET SCROLLBAR --> 2175 <div id="bookshelf-editor-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4 sm:p-6 overflow-y-auto"> 2176 2177 <style> 2178 /* ð¢ Shortened Inset Custom Dark Scrollbar (20px top/bottom gap prevents corner clipping) */ 2179 #bookshelf-modal-box::-webkit-scrollbar { 2180 width: 12px; 2181 } 2182 #bookshelf-modal-box::-webkit-scrollbar-track { 2183 background: #0f172a; 2184 border-radius: 10px; 2185 /* 20px gap at top & bottom keeps track away from rounded corners */ 2186 border-top: 20px solid transparent; 2187 border-bottom: 20px solid transparent; 2188 background-clip: padding-box; 2189 } 2190 #bookshelf-modal-box::-webkit-scrollbar-thumb { 2191 background: #334155; 2192 border-radius: 10px; 2193 border: 3px solid transparent; 2194 /* 20px gap at top & bottom */ 2195 border-top: 20px solid transparent; 2196 border-bottom: 20px solid transparent; 2197 background-clip: padding-box; 2198 } 2199 #bookshelf-modal-box::-webkit-scrollbar-thumb:hover { 2200 background: #f59e0b; 2201 background-clip: padding-box; 2202 } 2203 </style> 2204 2205 <div id="bookshelf-modal-box" class="bg-[#181d2a] border border-slate-800 rounded-3xl p-6 sm:p-8 max-w-[1280px] w-full shadow-2xl my-auto space-y-6 max-h-[90vh] overflow-y-auto text-xs relative"> 2206 2207 <!-- Sticky Modal Header --> 2208 <div class="flex justify-between items-center border-b border-slate-800 pb-4 sticky -top-6 sm:-top-8 bg-[#181d2a] z-30 pt-2"> 2209 <h3 class="text-xl sm:text-2xl font-black text-white" id="bs-modal-title">+ Create New Bookshelf</h3> 2210 2211 <!-- ð¢ NEW: SHELF LEVEL ACCESS BUTTON --> 2212 <div class="flex items-center gap-3 mr-4"> 2213 <button type="button" onclick="openAccessManager('shelf')" class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-4 py-2 rounded-xl text-xs sm:text-sm transition shadow-md flex items-center gap-2"> 2214 <i class="fa-solid fa-users-lock"></i>
2214 Shelf Access 2215 </button> 2216 <button type="button" onclick="closeBookshelfEditorModal()" class="text-gray-400 hover:text-white text-xl w-8 h-8 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark"></i></button> 2217 </div> 2218 </div> 2219 2220 <form id="bookshelf-editor-form" class="space-y-6"> 2221 <input type="hidden" id="bs-edit-id" value=""> 2222 <!-- ð¢ NEW: Hidden input to store allowed user IDs for the shelf --> 2223 <input type="hidden" id="bs-allowed-users" value="[]"> 2224 2225 <!-- Category & Pricing --> 2226 <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> 2227 <div class="sm:col-span-2"> 2228 <label class="block font-bold text-gray-300 uppercase mb-1">Category / Bookshelf Title *</label> 2229 <input type="text" id="bs-title" placeholder="e.g. Young Learners YLE Series" required class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white focus:outline-none focus:border-amber-400 text-xs sm:text-sm"> 2230 </div> 2231 <div> 2232 <label class="block font-bold text-gray-300 uppercase mb-1">Bookshelf Access Price</label> 2233 <input type="text" id="bs-price" placeholder="Free or e.g. 25,000 MMK" value="Free" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-amber-400 font-bold focus:outline-none focus:border-amber-400 text-xs sm:text-sm"> 2234 </div> 2235 </div> 2236 2237 <div> 2238 <label class="block font-bold text-gray-300 uppercase mb-1">Category Description</label> 2239 <textarea id="bs-desc" rows="2" placeholder="Brief explanation of books in this shelf..." class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white focus:outline-none focus:border-amber-400 text-xs sm:text-sm"></textarea> 2240 2241 <label class="flex items-center gap-2.5 mt-2.5 font-bold text-amber-400 text-xs cursor-pointer select-none"> 2242 <input type="checkbox" id="bs-collapse-desc" class="w-4 h-4 rounded text-amber-500 bg-slate-950 border-slate-700 cursor-pointer focus:ring-amber-500"> 2243 Collapse long description (Show "Read More" button) 2244 </label> 2245 </div> 2246 2247 <!-- Style / Theme Selector --> 2248 <div> 2249 <label class="block font-bold text-gray-300 uppercase mb-2">3D Bookshelf Theme & Material *</label> 2250 <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 text-xs"> 2251 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2252 <input type="radio" name="bs_theme" value="theme-oak" checked class="hidden"> ð² Natural Oak 2253 </label> 2254 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2255 <input type="radio" name="bs_theme" value="theme-walnut" class="hidden"> ð Dark Walnut 2256 </label> 2257 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2258 <input type="radio" name="bs_theme" value="theme-silver" class="hidden"> ð Silver Metallic 2259 </label> 2260 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2261 <input type="radio" name="bs_theme" value="theme-glass" class="hidden"> ð§ Frosted Glass 2262 </label> 2263 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2264 <input type="radio" name="bs_theme" value="theme-gold" class="hidden"> ðª Luxury Gold 2265 </label> 2266 <label class="p-3 border rounded-xl bg-slate-950 border-slate-700 text-gray-300 flex items-center justify-center gap-2 cursor-pointer has-[:checked]:border-amber-400 has-[:checked]:text-amber-400 font-bold text-center"> 2267 <input type="radio" name="bs_theme" value="theme-white" class="hidden"> ⪠Minimal White 2268 </label> 2269 </div> 2270 </div> 2271 2272 <!-- DYNAMIC BOOKS LIST BUILDER --> 2273 <div class="space-y-4 pt-4 border-t border-slate-800"> 2274 <div class="flex justify-between items-center"> 2275 <h4 class="font-black text-amber-400 text-sm sm:text-base"><i class="fa-solid fa-books mr-1.5"></i> Books on this Shelf</h4> 2276 <button type="button" onclick="addBookRowToEditor()" class="bg-blue-600 hover:bg-blue-500 text-white font-bold px-4 py-2 rounded-xl text-xs sm:text-sm transition shadow-md"> 2277 + Add Book Item 2278 </button> 2279 </div> 2280 2281 <div id="bs-books-container" class="space-y-3"> 2282 <!-- Book Items Injected Here --> 2283 </div> 2284 </div> 2285 2286 <!-- Action Buttons --> 2287 <div class="flex gap-3 pt-4 border-t border-slate-800 justify-end sticky -bottom-6 sm:-bottom-8 bg-[#181d2a] z-30 pb-2"> 2288 <button type="button" onclick="closeBookshelfEditorModal()" class="px-5 py-3 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold rounded-xl transition">Cancel</button> 2289 <button type="submit" name="status" value="draft" class="px-5 py-3 bg-slate-700 hover:bg-slate-600 text-amber-300 font-bold rounded-xl transition">Save as Draft</button> 2290 <button type="submit" name="status" value="published" class="px-6 py-3 bg-sky-600 hover:bg-sky-500 text-white font-black rounded-xl transition shadow-lg">â Publish Live</button> 2291 </div> 2292 </form> 2293 </div> 2294 </div> 2295 2296 <!-- ð¢ NEW: USER ACCESS MANAGER MODAL --> 2297 <div id="user-access-modal" class="fixed inset-0 z-[1000006] bg-black/90 backdrop-blur-sm hidden flex items-center justify-center p-4"> 2298 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-6 w-full max-w-lg shadow-2xl space-y-4 flex flex-col max-h-[80vh]"> 2299 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 2300 <h3 class="text-lg font-black text-indigo-400" id="access-modal-title"><i class="fa-solid fa-users-lock mr-2"></i> Manage Access</h3> 2301 <button type="button" onclick="closeAccessManager()" class="text-gray-400 hover:text-white text-lg w-8 h-8 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark"></i></button> 2302 </div> 2303 2304 <div class="flex items-center gap-3 pr-2"> 2305 <input type="text" id="access-user-search" placeholder="Search by name or email..." onkeyup="filterAccessUsers()" class="flex-1 p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-indigo-500"> 2306 2307 <label class="flex items-center gap-3 p-3 bg-slate-800/60 border border-slate-700 rounded-xl cursor-pointer hover:border-indigo-500 transition whitespace-nowrap shrink-0">
2308 <span class="text-white text-xs font-black">Select All</span> 2309 <input type="checkbox" id="access-select-all-cb" onchange="toggleAllAccessUsers(this)" class="w-4 h-4 rounded text-indigo-600 bg-slate-950 border-slate-600"> 2310 </label> 2311 </div> 2312 2313 <!-- ð¢ NEW: REQUIRE LOGIN TOGGLE --> 2314 <label class="flex items-center justify-between p-3 bg-indigo-900/20 border border-indigo-500/30 rounded-xl cursor-pointer hover:border-indigo-500 transition mr-2"> 2315 <div> 2316 <span class="text-indigo-300 text-xs font-black block">Require Login to View</span> 2317 <span class="text-gray-500 text-[10px]">If checked, random public visitors cannot see this.</span> 2318 </div> 2319 <input type="checkbox" id="access-require-login-cb" class="w-4 h-4 rounded text-indigo-600 bg-slate-950 border-slate-600"> 2320 </label> 2321 2322 <div id="access-users-list" class="flex-1 overflow-y-auto space-y-2 pr-2 border-t border-slate-800 pt-3"> 2323 <p class="text-gray-500 italic text-xs text-center py-4">Loading users...</p> 2324 </div> 2325 2326 <button type="button" onclick="saveAccessManager()" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black py-3 rounded-xl transition shadow-lg text-sm"> 2327 Save Access Permissions 2328 </button> 2329 </div> 2330 </div> 2331 `; 2332 2333 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 2334 loadAdminBookshelves(); 2335}; 2336 2337async function loadAdminBookshelves() { 2338 const list = document.getElementById('admin-bookshelves-list'); 2339 if (!list) return; 2340 2341 try { 2342 const snapshot = await db.collection("library_bookshelves").orderBy("createdAt", "desc").get(); 2343 window.allBookshelvesData = []; 2344 2345 if (snapshot.empty) { 2346 list.innerHTML = `<p class="text-sm text-gray-500 italic py-8 text-center col-span-2">No bookshelves created yet. Click "+ Create New Bookshelf" to build one!</p>`; 2347 return; 2348 } 2349 2350 let html = ''; 2351 snapshot.forEach(doc => { 2352 const data = doc.data(); 2353 data.id = doc.id; 2354 window.allBookshelvesData.push(data); 2355 2356 const isPub = data.isPublished !== false; 2357 const statusBadge = isPub 2358 ? `<span class="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 text-xs font-black px-3 py-1 rounded-full uppercase">Published</span>` 2359 : `<span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-xs font-black px-3 py-1 rounded-full uppercase">Draft</span>`; 2360 2361 const bookCount = (data.books || []).length; 2362 const rawDesc = data.categoryDescription || ''; 2363 const shouldCollapse = data.collapseDesc !== false && rawDesc.length > 120; // Only collapse if long text & enabled 2364 2365 let descBlock = ''; 2366 const safeShelfId = data.id.replace(/[^a-zA-Z0-9]/g, '_'); 2367 2368 if (rawDesc.trim() !== '') { 2369 if (shouldCollapse) { 2370 descBlock = ` 2371 <div class="space-y-2 mt-1"> 2372 <p id="admin-desc-${safeShelfId}" class="text-gray-300 text-sm leading-relaxed line-clamp-2 whitespace-pre-line overflow-hidden transition-all duration-300">${rawDesc}</p> 2373 <button type="button" onclick="toggleAdminShelfDescription('${safeShelfId}')" id="admin-desc-btn-${safeShelfId}" class="inline-flex items-center gap-1.5 text-xs font-black bg-slate-800 hover:bg-slate-700 text-amber-400 px-3 py-1 rounded-xl border border-slate-700 transition cursor-pointer select-none"> 2374 <span>Read More â¼</span> 2375 </button> 2376 </div>`; 2377 } else { 2378 descBlock = `<p class="text-gray-300 text-sm leading-relaxed whitespace-pre-line mt-1">${rawDesc}</p>`; 2379 } 2380 } else { 2381 descBlock = `<p class="text-gray-500 text-sm italic mt-1">
2381No description provided.</p>`; 2382 } 2383 2384 html += ` 2385 <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-4 relative group"> 2386 <div class="flex justify-between items-start gap-4"> 2387 <div> 2388 <h4 class="font-black text-white text-xl sm:text-2xl mb-1">${data.categoryName}</h4> 2389 ${descBlock} 2390 </div> 2391 ${statusBadge} 2392 </div> 2393 2394 <div class="flex flex-wrap items-center gap-3 text-sm text-gray-300 pt-1 font-medium"> 2395 <span class="bg-slate-950 px-3.5 py-1.5 rounded-xl border border-slate-800">ð Books: <strong class="text-white font-extrabold ml-1">${bookCount}</strong></span> 2396 <span class="bg-slate-950 px-3.5 py-1.5 rounded-xl border border-slate-800">ð¨ Theme: <strong class="text-amber-400 font-extrabold ml-1">${data.theme || 'theme-oak'}</strong></span> 2397 <span class="bg-slate-950 px-3.5 py-1.5 rounded-xl border border-slate-800">ð·ï¸ Price: <strong class="text-sky-400 font-extrabold ml-1">${data.shelfPrice || 'Free'}</strong></span> 2398 </div> 2399 2400 <div class="flex gap-3 pt-3 border-t border-slate-800/80 justify-end"> 2401 <button onclick="editBookshelf('${data.id}')" class="bg-sky-600 hover:bg-sky-500 text-white font-bold px-4 py-2 rounded-xl text-xs sm:text-sm transition shadow-md">Edit Shelf</button> 2402 <button onclick="deleteBookshelf('${data.id}')" class="bg-slate-800 hover:bg-red-600 text-gray-300 hover:text-white font-bold px-4 py-2 rounded-xl text-xs sm:text-sm transition">Delete</button> 2403 </div> 2404 </div>`; 2405 }); 2406 2407 list.innerHTML = html; 2408 } catch(e) { 2409 list.innerHTML = `<p class="text-sm text-red-400 py-8 text-center col-span-2">Error loading bookshelves: ${e.message}</p>`; 2410 } 2411} 2412 2413window.toggleAdminShelfDescription = function(safeId) { 2414 const textEl = document.getElementById(`admin-desc-${safeId}`); 2415 const btnEl = document.getElementById(`admin-desc-btn-${safeId}`); 2416 if (!textEl || !btnEl) return; 2417 2418 if (textEl.classList.contains('line-clamp-2')) { 2419 textEl.classList.remove('line-clamp-2'); 2420 btnEl.innerHTML = `<span>Show Less â²</span>`; 2421 } else { 2422 textEl.classList.add('line-clamp-2'); 2423 btnEl.innerHTML = `<span>Read More â¼</span>`; 2424 } 2425}; 2426 2427window.openBookshelfEditorModal = function(editId = null) { 2428 // ð¢ FIX: Always ensure the background Admin Library Management panel exists first! 2429 if (!document.getElementById('admin-fullscreen-workspace')) { 2430 if (typeof openFullscreenLibraryManager === 'function') { 2431 openFullscreenLibraryManager(); 2432 } 2433 } 2434 2435 // ð¢ Automatically inject Bookshelf Editor Modal HTML if it got wiped out 2436 if (!document.getElementById('bookshelf-editor-modal')) { 2437 // Re-inject modal template if needed or ensure it's present 2438 loadAdminBookshelves(); // Refreshes container structure 2439 } 2440 2441 const editInput = document.getElementById('bs-edit-id'); 2442 const modalTitle = document.getElementById('bs-modal-title'); 2443 const form = document.getElementById('bookshelf-editor-form'); 2444 const booksContainer = document.getElementById('bs-books-container'); 2445 2446 if (!editInput || !form) return; 2447 2448 editInput.value = editId || ''; 2449 if (modalTitle) modalTitle.innerText = editId ? 'Edit Bookshelf' : '+ Create New Bookshelf'; 2450 form.reset(); 2451 if (booksContainer) booksContainer.innerHTML = ''; 2452 2453 if (editId) { 2454 const data = (window.allBookshelvesData && window.allBookshelvesData.find(b => b.id === editId)) || 2455 (window.allPublicShelves && window.allPublicShelves.find(b => b.id === editId)); 2456 2457 if (data) { 2458 document.getElementById('bs-title').value = data.categoryName || ''; 2459 document.getElementById('bs-price').value = data.shelfPrice || 'Free'; 2460 document.getElementById('bs-desc').value = data.categoryDescription || ''; 2461 2462 // If editing, use saved value. Default to true if undefined. 2463 document.getElementById('bs-collapse-desc').checked = data.collapseDesc !== false; 2464 2465 const radio = document.querySelector(`input[name="bs_theme"][value="${data.theme || 'theme-oak'}"]`); 2466 if (radio) radio.checked = true; 2467 2468 if (data.books && data.books.length > 0) { 2469 data.books.forEach(b => addBookRowToEditor(b)); 2470 } 2471 } 2472 } else { 2473 document.getElementById('bs-collapse-desc').checked = true; 2474 addBookRowToEditor(); 2475 addBookRowToEditor(); 2476 addBookRowToEditor(); 2477 } 2478 2479 const modal = document.getElementById('bookshelf-editor-modal'); 2480 if (modal) { 2481 modal.classList.remove('hidden'); 2482 document.body.classList.add('overflow-hidden'); // Locks background scrolling 2483 } 2484}; 2485 2486window.closeBookshelfEditorModal = function() { 2487 const modal = document.getElementById('bookshelf-editor-modal'); 2488 if (modal) { 2489 modal.classList.add('hidden'); 2490 document.body.classList.remove('overflow-hidden'); // ð¢ Unlocks background scrolling 2491 } 2492}; 2493 2494let draggedBookRow = null; 2495 2496// ð¢ Helper to Renumber Book Labels (Book #1, Book #2, etc.) 2497window.renumberBookRows = function() { 2498 const container = document.getElementById('bs-books-container'); 2499 if (!container) return; 2500 const rows = container.querySelectorAll('.book-item-row'); 2501 rows.forEach((r, idx) => { 2502 const label = r.querySelector('.book-row-label'); 2503 if (label) label.innerText = `Book #${idx + 1}`; 2504 });
2505}; 2506 2507// ð¢ Helper for Up/Down Arrow Buttons 2508window.moveBookRow = function(btn, direction) { 2509 const row = btn.closest('.book-item-row'); 2510 if (!row) return; 2511 const container = document.getElementById('bs-books-container'); 2512 2513 if (direction === -1 && row.previousElementSibling) { 2514 container.insertBefore(row, row.previousElementSibling); 2515 renumberBookRows(); 2516 } else if (direction === 1 && row.nextElementSibling) { 2517 container.insertBefore(row.nextElementSibling, row); 2518 renumberBookRows(); 2519 } 2520}; 2521 2522// ð¢ Helper for Remove Row Button 2523window.removeBookRow = function(btn) { 2524 const row = btn.closest('.book-item-row'); 2525 if (row) { 2526 row.remove(); 2527 renumberBookRows(); 2528 } 2529}; 2530 2531window.addBookRowToEditor = function(bookData = {}) { 2532 const container = document.getElementById('bs-books-container'); 2533 const index = container.children.length + 1; 2534 2535 // ð¢ FIX: Clean JSON Object with isbn and publishedDate included 2536 const extDataJson = JSON.stringify({ 2537 author: bookData.author || '', 2538 isbn: bookData.isbn || '', 2539 publishedDate: bookData.publishedDate || '', 2540 publisher: bookData.publisher || '', 2541 level: bookData.level || '', 2542 type: bookData.type || '', 2543 cefr: bookData.cefr || '', 2544 language: bookData.language || 'British English', 2545 series: bookData.series || '', 2546 formats: bookData.formats || ['PDF Document'], 2547 description: bookData.description || '' 2548 }); 2549 2550 const row = document.createElement('div'); 2551 row.className = 'bg-slate-950 p-4 rounded-xl border border-slate-800 space-y-3 relative group book-item-row transition-all duration-200'; 2552 row.setAttribute('data-ext-info', extDataJson); 2553 row.setAttribute('draggable', 'true'); // ð¢ Enable HTML5 Dragging 2554 2555 row.innerHTML = ` 2556 <div class="flex justify-between items-center border-b border-slate-800/80 pb-2 select-none"> 2557 <div class="flex items-center gap-2"> 2558 <span class="cursor-grab active:cursor-grabbing text-gray-500 hover:text-amber-400 p-1" title="Click & Drag to reorder"> 2559 <i class="fa-solid fa-grip-lines text-sm"></i> 2560 </span> 2561 <span class="book-row-label font-extrabold text-amber-400 text-xs">Book #${index}</span> 2562 </div> 2563 <div class="flex items-center gap-1.5"> 2564 <!-- ð¢ NEW: Hidden input to store allowed user IDs for this specific book --> 2565 <input type="hidden" class="book-input-access" value='${JSON.stringify(bookData.allowedUsers || [])}'> 2566 2567 <button type="button" onclick="moveBookRow(this, -1)" class="bg-slate-900 hover:bg-slate-800 text-gray-400 hover:text-amber-400 px-2 py-1 rounded-lg text-xs font-bold transition" title="Move Up"><i class="fa-solid fa-arrow-up"></i></button> 2568 <button type="button" onclick="moveBookRow(this, 1)" class="bg-slate-900 hover:bg-slate-800 text-gray-400 hover:text-amber-400 px-2 py-1 rounded-lg text-xs font-bold transition" title="Move Down"><i class="fa-solid fa-arrow-down"></i></button> 2569 2570 <!-- ð¢ NEW: BOOK LEVEL ACCESS BUTTON --> 2571 <button type="button" onclick="openAccessManager('book', this)" class="bg-indigo-600/30 text-indigo-300 border border-indigo-500/30 hover:bg-indigo-600 hover:text-white px-3 py-1 rounded-lg text-xs font-bold transition flex items-center gap-1.5 ml-1"> 2572 <i class="fa-solid fa-users-lock"></i> User Access 2573 </button> 2574 2575 <button type="button" onclick="openAdminMoreBookInfoModal(this)" class="bg-amber-600/30 text-amber-300 border border-amber-500/30 hover:bg-amber-600 hover:text-white px-3 py-1 rounded-lg text-xs font-bold transition flex items-center gap-1.5 ml-1"> 2576 <i class="fa-solid fa-circle-info"></i> More Book Info 2577 </button> 2578 <button type="button" onclick="removeBookRow(this)" class="bg-red-600/30 text-red-400 border border-red-500/30 hover:bg-red-600 hover:text-white px-3 py-1 rounded-lg text-xs font-bold transition flex items-center gap-1.5 ml-1"> 2579 <i class="fa-solid fa-xmark"></i> Remove 2580 </button> 2581 </div> 2582 </div> 2583 2584 <div class="grid grid-cols-1 sm:grid-cols-4 gap-3"> 2585 <div> 2586 <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Book Title *</label> 2587 <input type="text" class="book-input-title w-full p-2.5 bg-slate-900 border border-slate-700 rounded-lg text-white text-xs" value="${bookData.title || ''}" placeholder="e.g. Troy" required> 2588 </div> 2589 <div> 2590 <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Cover Image URL *</label> 2591 <input type="url" class="book-input-cover w-full p-2.5 bg-slate-900 border border-slate-700 rounded-lg text-white text-xs" value="${bookData.coverUrl || ''}" placeholder="https://..." required> 2592 </div> 2593 <div> 2594 <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Flipbook / PDF Link *</label> 2595 <input type="url" class="book-input-link w-full p-2.5 bg-slate-900 border border-slate-700 rounded-lg text-white text-xs" value="${bookData.bookUrl || ''}" placeholder="https://..." required> 2596 </div> 2597 <div> 2598 <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Book Price (Optional)</label> 2599 <input type="text" class="book-input-price w-full p-2.5 bg-slate-900 border border-slate-700 rounded-lg text-amber-400 font-bold text-xs" value="${bookData.price || 'Free'}" placeholder="e.g. Free or 5,000 MMK"> 2600 </div> 2601 </div> 2602 `; 2603 2604 // ð¢ Attach HTML5 Drag & Drop Event Listeners 2605 row.addEventListener('dragstart', (e) => { 2606 draggedBookRow = row; 2607 e.dataTransfer.effectAllowed = 'move'; 2608 row.classList.add('opacity-40', 'border-amber-400'); 2609 });
2610 2611 row.addEventListener('dragover', (e) => { 2612 e.preventDefault(); 2613 e.dataTransfer.dropEffect = 'move'; 2614 const targetRow = e.target.closest('.book-item-row'); 2615 if (targetRow && targetRow !== draggedBookRow) { 2616 const bounding = targetRow.getBoundingClientRect(); 2617 const offset = e.clientY - bounding.top - (bounding.height / 2); 2618 if (offset > 0) { 2619 container.insertBefore(draggedBookRow, targetRow.nextSibling); 2620 } else { 2621 container.insertBefore(draggedBookRow, targetRow); 2622 } 2623 } 2624 }); 2625 2626 row.addEventListener('drop', (e) => { 2627 e.preventDefault(); 2628 renumberBookRows(); 2629 }); 2630 2631 row.addEventListener('dragend', () => { 2632 row.classList.remove('opacity-40', 'border-amber-400'); 2633 draggedBookRow = null; 2634 renumberBookRows(); 2635 }); 2636 2637 container.appendChild(row); 2638}; 2639 2640// Handle Bookshelf Form Submit 2641document.addEventListener('submit', async (e) => { 2642 if (e.target && e.target.id === 'bookshelf-editor-form') { 2643 e.preventDefault(); 2644 2645 const editId = document.getElementById('bs-edit-id').value; 2646 const categoryName = document.getElementById('bs-title').value.trim(); 2647 const categoryDescription = document.getElementById('bs-desc').value.trim(); 2648 const shelfPrice = document.getElementById('bs-price').value.trim() || 'Free'; 2649 const theme = document.querySelector('input[name="bs_theme"]:checked')?.value || 'theme-oak'; 2650 2651 const submitBtn = e.submitter; 2652 2653 // ð¢ FIXED: Prevent Firebase 'undefined' crash during background auto-saves 2654 let isPublished = true; 2655 if (submitBtn) { 2656 isPublished = (submitBtn.value === 'published'); 2657 } 2658 2659 // ð¢ Get Shelf Level Access 2660 let shelfAllowedUsers = []; 2661 try { shelfAllowedUsers = JSON.parse(document.getElementById('bs-allowed-users').value || '[]'); } catch(e){} 2662 2663 // Collect Books 2664 const bookRows = document.querySelectorAll('#bs-books-container > .book-item-row'); 2665 const books = []; 2666 bookRows.forEach(row => { 2667 const title = row.querySelector('.book-input-title').value.trim(); 2668 const coverUrl = row.querySelector('.book-input-cover').value.trim(); 2669 const bookUrl = row.querySelector('.book-input-link').value.trim(); 2670 const price = row.querySelector('.book-input-price')?.value.trim() || 'Free'; 2671 2672 // ð¢ Get Book Level Access 2673 let bookAllowedUsers = []; 2674 try { bookAllowedUsers = JSON.parse(row.querySelector('.book-input-access').value || '[]'); } catch(e){} 2675 2676 let extData = {}; 2677 try { extData = JSON.parse(row.getAttribute('data-ext-info') || '{}'); } catch(e) {} 2678 2679 if (title && coverUrl && bookUrl) { 2680 books.push({ 2681 title, 2682 coverUrl, 2683 bookUrl, 2684 price, 2685 author: extData.author || '', 2686 isbn: extData.isbn || '', 2687 publishedDate: extData.publishedDate || '', 2688 publisher: extData.publisher || '', 2689 level: extData.level || '', 2690 type: extData.type || '', 2691 cefr: extData.cefr || '', 2692 language: extData.language || 'British English', 2693 series: extData.series || '', 2694 formats: extData.formats || ['PDF Document'], 2695 description: extData.description || '', 2696 2697 // ð¢ Package Toggles for Database 2698 enableDownload: extData.enableDownload || false, 2699 enableShare: extData.enableShare || false, 2700 allowedUsers: bookAllowedUsers // ð¢ Save book access to DB 2701 }); 2702 } 2703 }); 2704 2705 const docId = editId || categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); 2706 2707 try { 2708 await db.collection("library_bookshelves").doc(docId).set({ 2709 categoryName, 2710 categoryDescription, 2711 shelfPrice, 2712 theme, 2713 books, 2714 isPublished, 2715 allowedUsers: shelfAllowedUsers, 2716 collapseDesc: document.getElementById('bs-collapse-desc')?.checked || false, // ð¢ Save setting 2717 createdAt: new Date() 2718 }, { merge: true });
2719 2720 customToast("Bookshelf saved successfully!", "success"); 2721 closeBookshelfEditorModal(); 2722 loadAdminBookshelves(); 2723 } catch(err) { 2724 customToast(err.message, "error"); 2725 } 2726 } 2727}); 2728 2729window.editBookshelf = function(id) { 2730 openBookshelfEditorModal(id); 2731}; 2732 2733window.deleteBookshelf = async function(id) { 2734 const confirmed = await customConfirm("Delete Bookshelf", "Are you sure you want to delete this entire bookshelf?"); 2735 if (confirmed) { 2736 try { 2737 await db.collection("library_bookshelves").doc(id).delete(); 2738 customToast("Bookshelf deleted.", "info"); 2739 loadAdminBookshelves(); 2740 } catch(e) { 2741 customToast(e.message, "error"); 2742 } 2743 } 2744}; 2745 2746 2747// ========================================== 2748// DYNAMIC PAGE MANAGEMENT SYSTEM (With System Files & Pagination) 2749// ========================================== 2750 2751window.allPagesData = []; 2752window.currentPageStatusFilter = 'all'; 2753window.currentPageManagerPage = 1; 2754window.pagesPerPage = 10; 2755 2756window.openFullscreenPageManager = function() { 2757 const existingWs = document.getElementById('admin-fullscreen-workspace'); 2758 if (existingWs) existingWs.remove(); 2759 2760 const workspaceHTML = ` 2761 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 2762 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl flex-shrink-0"> 2763 <div class="flex items-center gap-3"> 2764 <div class="w-9 h-9 bg-emerald-600 text-white rounded-xl flex items-center justify-center font-black text-lg"><i class="fa-solid fa-file-lines"></i></div> 2765 <div> 2766 <h2 class="font-black text-lg text-white leading-none">Page Management Panel</h2> 2767 <p class="text-[11px] text-gray-400">View, manage, and delete custom website pages.</p> 2768 </div> 2769 </div> 2770 <div class="flex items-center gap-3"> 2771 <button onclick="openPageCreatorStudio()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-black px-4 py-2 rounded-xl text-xs transition shadow-lg flex items-center gap-2"><i class="fa-solid fa-wand-magic-sparkles"></i> Open Page Creator</button> 2772 <button onclick="document.getElementById('admin-fullscreen-workspace').remove(); document.body.classList.remove('overflow-hidden');" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition">â Close</button> 2773 </div> 2774 </div> 2775 2776 <div class="flex-1 overflow-y-auto p-6 md:p-10 pb-24 flex justify-center items-start"> 2777 <div class="w-full max-w-[1280px] space-y-6"> 2778 2779 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl"> 2780 <div class="mb-6"> 2781 <h3 class="text-2xl font-black text-white mb-1">Page Manager</h3> 2782 <p class="text-xs text-gray-400">Filter, search, and manage your website pages.</p> 2783 </div> 2784 2785 <div class="flex flex-wrap items-center gap-2 text-xs font-semibold text-gray-400 mb-6 pb-4 border-b border-slate-800/80"> 2786 <button onclick="filterPagesByStatus('all')" id="page-pill-all" class="text-blue-400 hover:text-white transition font-black underline">Total Pages - <span id="page-stat-total">0</span></button> <span class="text-slate-700">|</span> 2787 <button onclick="filterPagesByStatus('published')" id="page-pill-pub" class="text-gray-400 hover:text-white transition font-medium">Published - <span id="page-stat-published">0</span></button> <span class="text-slate-700">|</span> 2788 <button onclick="filterPagesByStatus('draft')" id="page-pill-draft" class="text-gray-400 hover:text-white transition font-medium">Hidden / Drafts - <span id="page-stat-drafts">0</span></button> 2789 </div> 2790 2791 <div class="flex flex-wrap justify-between items-center mb-6 gap-4 text-xs"> 2792 <div class="flex items-center gap-2"> 2793 <select id="page-bulk-action" class="bg-slate-950 border border-slate-700 text-white rounded-xl px-3 py-2 focus:outline-none focus:border-emerald-500 cursor-pointer"> 2794 <option value="">Bulk actions</option> 2795 <option value="publish">Set to Public</option> 2796 <option value="unpublish">Set to Hidden</option> 2797 <option value="delete">Move to Trash</option> 2798 </select> 2799 <button onclick="executePageBulkAction()" class="bg-slate-800 hover:bg-slate-700 text-white font-bold px-4 py-2 rounded-xl transition shadow">Apply</button> 2800 </div> 2801 2802 <div class="flex items-center gap-3"> 2803 <div class="flex items-center gap-2">
2804 <span class="text-gray-400 font-bold">Per Page:</span> 2805 <select id="page-per-page" onchange="changePagesPerPage()" class="bg-slate-950 border border-slate-700 text-white rounded-xl px-3 py-2 focus:outline-none focus:border-emerald-500 cursor-pointer"> 2806 <option value="10">10</option> 2807 <option value="20">20</option> 2808 <option value="50">50</option> 2809 <option value="100">100</option> 2810 </select> 2811 </div> 2812 <input type="text" id="page-filter-search" onkeyup="renderPagesTable()" class="bg-slate-950 border border-slate-700 text-white rounded-xl px-4 py-2 text-xs focus:ring-2 focus:ring-emerald-500 focus:outline-none w-56" placeholder="Search page slugs..."> 2813 </div> 2814 </div> 2815 2816 <div class="overflow-x-auto"> 2817 <table class="w-full text-left text-xs border-collapse"> 2818 <thead> 2819 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 pb-3"> 2820 <th class="py-3 px-4 w-10 text-center"><input type="checkbox" id="page-cb-all" onchange="toggleAllPageCheckboxes(this)" class="w-4 h-4 rounded text-emerald-600 bg-slate-950 border-slate-600 cursor-pointer"></th> 2821 <th class="py-3 px-4 w-1/3">Page Slug / Name</th> 2822 <th class="py-3 px-4">Builder Tool</th> 2823 <th class="py-3 px-4 text-center">Header</th> 2824 <th class="py-3 px-4 text-center">Footer</th> 2825 <th class="py-3 px-4">Last Updated</th> 2826 </tr> 2827 </thead> 2828 <tbody id="dynamic-pages-list" class="divide-y divide-slate-800/80"> 2829 <tr><td colspan="6" class="py-8 text-center text-gray-500 italic">Loading pages...</td></tr> 2830 </tbody> 2831 </table> 2832 </div> 2833 2834 <!-- Pagination Controls --> 2835 <div class="flex flex-wrap justify-between items-center pt-6 border-t border-slate-800/80 text-xs text-gray-400 gap-4 mt-6"> 2836 <div class="flex items-center gap-2"> 2837 <span>Showing <span id="page-showing-start" class="font-bold text-white">0</span> - <span id="page-showing-end" class="font-bold text-white">0</span> of <span id="page-showing-total" class="font-bold text-white">0</span></span> 2838 </div> 2839 <div class="flex items-center gap-3"> 2840 <button onclick="changePageManagerPage(-1)" id="page-prev-btn" class="bg-slate-800 hover:bg-slate-700 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold px-4 py-2 rounded-xl transition shadow">â¹ Prev</button> 2841 <span class="font-bold text-white px-2">Page <span id="page-current-page" class="text-emerald-400">1</span> of <span id="page-total-pages">1</span></span> 2842 <button onclick="changePageManagerPage(1)" id="page-next-btn" class="bg-slate-800 hover:bg-slate-700 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold px-4 py-2 rounded-xl transition shadow">Next âº</button> 2843 </div> 2844 </div> 2845 2846 </div> 2847 </div> 2848 </div> 2849 </div> 2850 `; 2851 2852 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 2853 document.body.classList.add('overflow-hidden'); 2854 window.currentPageStatusFilter = 'all'; 2855 window.currentPageManagerPage = 1; 2856 loadAdminPages(); 2857}; 2858 2859window.changePagesPerPage = function() { 2860 window.pagesPerPage = parseInt(document.getElementById('page-per-page').value); 2861 window.currentPageManagerPage = 1; 2862 renderPagesTable(); 2863}; 2864 2865window.changePageManagerPage = function(delta) { 2866 window.currentPageManagerPage += delta; 2867 renderPagesTable(); 2868}; 2869 2870async function loadAdminPages() { 2871 const container = document.getElementById('dynamic-pages-list'); 2872 if (!container) return; 2873 2874 window.allPagesData = []; 2875 2876 // ð¢ 1. Inject physical system HTML files from defaultPages array 2877 if (typeof defaultPages !== 'undefined') {
2878 defaultPages.forEach(p => { 2879 const slug = p.url === '/' ? 'index' : p.url.replace(/^\//, ''); 2880 window.allPagesData.push({ 2881 id: slug, 2882 name: p.name, 2883 isPublished: true, 2884 type: 'system', 2885 parsedDate: new Date(0), // Set older base date for system files to help sorting 2886 showHeader: true, 2887 showFooter: true 2888 }); 2889 }); 2890 } 2891 2892 try { 2893 // ð¢ 2. Inject custom pages from Firestore 2894 const snapshot = await db.collection("site_builder_pages").get(); 2895 snapshot.forEach(doc => { 2896 const data = doc.data(); 2897 data.id = doc.id; 2898 data.type = 'builder'; 2899 2900 if (data.updatedAt && typeof data.updatedAt.toDate === 'function') { 2901 data.parsedDate = data.updatedAt.toDate(); 2902 } else if (data.createdAt && typeof data.createdAt.toDate === 'function') { 2903 data.parsedDate = data.createdAt.toDate(); 2904 } else { 2905 data.parsedDate = new Date(); 2906 } 2907 2908 data.isPublished = data.isPublished !== false; 2909 2910 // Prevent duplicates if builder overrides a system page 2911 if (!window.allPagesData.some(p => p.id === data.id)) { 2912 window.allPagesData.push(data); 2913 } 2914 }); 2915 2916 // ð¢ FIX: Sort first by type ('builder' on top), then by newest date 2917 window.allPagesData.sort((a, b) => { 2918 if (a.type === 'builder' && b.type === 'system') return -1; 2919 if (a.type === 'system' && b.type === 'builder') return 1; 2920 return b.parsedDate - a.parsedDate; 2921 }); 2922 2923 renderPagesTable(); 2924 2925 } catch(e) { 2926 container.innerHTML = `<tr><td colspan="6" class="py-6 text-center text-red-500">Error loading pages: ${e.message}</td></tr>`; 2927 } 2928} 2929 2930window.filterPagesByStatus = function(status) { 2931 window.currentPageStatusFilter = status; 2932 document.getElementById('page-pill-all').className = status === 'all' ? 'text-blue-400 hover:text-white transition font-black underline' : 'text-gray-400 hover:text-white transition font-medium'; 2933 document.getElementById('page-pill-pub').className = status === 'published' ? 'text-green-400 hover:text-white transition font-black underline' : 'text-gray-400 hover:text-white transition font-medium'; 2934 document.getElementById('page-pill-draft').className = status === 'draft' ? 'text-amber-400 hover:text-white transition font-black underline' : 'text-gray-400 hover:text-white transition font-medium'; 2935 renderPagesTable(); 2936}; 2937 2938window.renderPagesTable = function() { 2939 const container = document.getElementById('dynamic-pages-list'); 2940 const searchFilter = document.getElementById('page-filter-search').value.toLowerCase(); 2941 2942 const filteredData = window.allPagesData.filter(page => { 2943 let match = true; 2944 if (window.currentPageStatusFilter === 'published' && !page.isPublished) match = false; 2945 if (window.currentPageStatusFilter === 'draft' && page.isPublished) match = false; 2946 if (searchFilter && !page.id.toLowerCase().includes(searchFilter)) match = false; 2947 return match; 2948 }); 2949 2950 // Update Top Counts 2951 let pubCount = 0, draftCount = 0; 2952 window.allPagesData.forEach(p => p.isPublished ? pubCount++ : draftCount++); 2953 document.getElementById('page-stat-total').innerText = window.allPagesData.length; 2954 document.getElementById('page-stat-published').innerText = pubCount; 2955 document.getElementById('page-stat-drafts').innerText = draftCount; 2956 2957 if (filteredData.length === 0) { 2958 container.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-gray-500 italic">No pages match your filters.</td></tr>`; 2959 updatePagePaginationControls(0, 0, 0, 1); 2960 return; 2961 } 2962 2963 // Pagination Calculation 2964 const totalItems = filteredData.length; 2965 const totalPages = Math.ceil(totalItems / window.pagesPerPage); 2966 if (window.currentPageManagerPage > totalPages) window.currentPageManagerPage = totalPages; 2967 if (window.currentPageManagerPage < 1) window.currentPageManagerPage = 1; 2968 2969 const startIndex = (window.currentPageManagerPage - 1) * window.pagesPerPage; 2970 const endIndex = Math.min(startIndex + window.pagesPerPage, totalItems); 2971 const paginatedData = filteredData.slice(startIndex, endIndex); 2972 2973 let html = ''; 2974 paginatedData.forEach(page => { 2975 const isSystem = page.type === 'system'; 2976 const fileExt = isSystem ? '.html' : ''; 2977 const titleDisplay = page.isPublished ? page.id : `${page.id} <span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-black px-2 py-0.5 rounded-full uppercase ml-2">Hidden</span>`; 2978 const publishActionText = page.isPublished ? 'Hide Page' : 'Make Public'; 2979 2980 const headerIcon = page.showHeader !== false ? '<i class="fa-solid fa-check text-emerald-500"></i>' : '<i class="fa-solid fa-xmark text-gray-600"></i>'; 2981 const footerIcon = page.showFooter !== false ? '<i class="fa-solid fa-check text-emerald-500"></i>' : '<i class="fa-solid fa-xmark text-gray-600"></i>'; 2982 2983 const builderBadge = isSystem 2984 ? `<span class="bg-blue-900/30 border border-blue-700 text-blue-400 px-2.5 py-1 rounded-lg text-[10px] uppercase font-bold flex items-center gap-1.5 w-max"><i class="fa-solid fa-code"></i> Core System File</span>` 2985 : `<span class="bg-slate-950 border border-slate-700 text-gray-400 px-2.5 py-1 rounded-lg text-[10px] uppercase font-bold flex items-center gap-1.5 w-max"><i class="fa-solid fa-wand-magic-sparkles text-emerald-500"></i> Creator Studio</span>`; 2986 2987 html += ` 2988 <tr class="hover:bg-slate-800/40 transition group"> 2989 <td class="py-4 px-4 align-top text-center"> 2990 <input type="checkbox" class="page-checkbox w-4 h-4 rounded text-emerald-600 bg-slate-950 border-slate-600 cursor-pointer focus:ring-emerald-500 mt-0.5" value="${page.id}" ${isSystem ? 'disabled' : ''}> 2991 </td> 2992 <td class="py-4 px-4 align-top"> 2993 <a href="${isSystem ? '/' + page.id + '.html' : '/page.html?slug=' + page.id}" target="_blank" class="font-bold text-white hover:text-emerald-400 transition block mb-1 text-sm">/${titleDisplay}${fileExt}</a> 2994 <div class="text-[11px] text-gray-500 invisible group-hover:visible flex gap-2 mt-2 font-medium transition-all"> 2995 <a href="${isSystem ? '/' + page.id + '.html' : '/page.html?slug=' + page.id}" target="_blank" class="hover:text-emerald-400 transition">View Live</a> 2996 ${!isSystem ? `
2997 <span class="text-slate-700">|</span> <button onclick="togglePageStatus('${page.id}', ${page.isPublished})" class="hover:text-amber-400 transition">${publishActionText}</button> 2998 <span class="text-slate-700">|</span> <button onclick="deleteAdminPage('${page.id}')" class="hover:text-red-400 transition">Trash</button>` : ''} 2999 </div> 3000 </td> 3001 <td class="py-4 px-4 align-top">${builderBadge}</td> 3002 <td class="py-4 px-4 align-top text-center">${headerIcon}</td> 3003 <td class="py-4 px-4 align-top text-center">${footerIcon}</td> 3004 <td class="py-4 px-4 align-top leading-tight text-gray-400 text-[11px]">System Defaults<br><span class="text-gray-500 text-[10px]">Managed by Admin</span></td> 3005 </tr>`; 3006 }); 3007 3008 container.innerHTML = html; 3009 updatePagePaginationControls(startIndex + 1, endIndex, totalItems, totalPages); 3010}; 3011 3012function updatePagePaginationControls(start, end, total, totalPages) { 3013 document.getElementById('page-showing-start').innerText = start; 3014 document.getElementById('page-showing-end').innerText = end; 3015 document.getElementById('page-showing-total').innerText = total; 3016 document.getElementById('page-current-page').innerText = window.currentPageManagerPage; 3017 document.getElementById('page-total-pages').innerText = totalPages; 3018 document.getElementById('page-prev-btn').disabled = window.currentPageManagerPage <= 1; 3019 document.getElementById('page-next-btn').disabled = window.currentPageManagerPage >= totalPages || totalPages === 0; 3020} 3021 3022window.toggleAllPageCheckboxes = function(source) { 3023 document.querySelectorAll('.page-checkbox').forEach(cb => cb.checked = source.checked); 3024}; 3025 3026window.executePageBulkAction = async function() { 3027 const action = document.getElementById('page-bulk-action').value; 3028 if (!action) return alert("Please select a bulk action."); 3029 3030 const checkedBoxes = document.querySelectorAll('.page-checkbox:checked:not([disabled])'); 3031 if (checkedBoxes.length === 0) return alert("Please check at least one valid custom page. System files cannot be bulk edited."); 3032 3033 const pageIds = Array.from(checkedBoxes).map(cb => cb.value); 3034 3035 if (action === 'delete') { 3036 if(!confirm(`Are you sure you want to permanently delete ${pageIds.length} custom page(s)?`)) return; 3037 try { 3038 await Promise.all(pageIds.map(id => db.collection("site_builder_pages").doc(id).delete())); 3039 if(typeof customToast === 'function') customToast(`Successfully deleted ${pageIds.length} page(s).`, "success"); 3040 loadAdminPages(); 3041 } catch(e) { alert("Error: " + e.message); } 3042 } else if (action === 'publish' || action === 'unpublish') { 3043 const isPub = action === 'publish'; 3044 try { 3045 await Promise.all(pageIds.map(id => db.collection("site_builder_pages").doc(id).update({ isPublished: isPub }))); 3046 if(typeof customToast === 'function') customToast(`Updated ${pageIds.length} page(s).`, "success"); 3047 loadAdminPages(); 3048 } catch(e) { alert("Error: " + e.message); } 3049 } 3050 3051 document.getElementById('page-bulk-action').value = ""; 3052 document.getElementById('page-cb-all').checked = false; 3053}; 3054 3055window.togglePageStatus = async function(pageId, currentState) { 3056 try { 3057 await db.collection("site_builder_pages").doc(pageId).update({ isPublished: !currentState }); 3058 if(typeof customToast === 'function') customToast(!currentState ? "Page is now Public!" : "Page is now Hidden.", "success"); 3059 loadAdminPages(); 3060 } catch (e) { 3061 alert("Error: " + e.message); 3062 } 3063}; 3064 3065window.deleteAdminPage = async function(pageId) { 3066 const confirmed = confirm("Are you sure you want to permanently delete this page? This cannot be undone."); 3067 if (confirmed) { 3068 try { 3069 await db.collection("site_builder_pages").doc(pageId).delete(); 3070 if(typeof customToast === 'function') customToast("Page deleted.", "success"); 3071 loadAdminPages(); 3072 } catch (e) { 3073 alert("Error: " + e.message); 3074 } 3075 } 3076}; 3077 3078 3079/* ========================================== 3080 ATVEO-STYLE COMMENT MODERATION PANEL (1280px) 3081 ========================================== */ 3082 3083window.openFullscreenCommentModeration = function() { 3084 const existingWs = document.getElementById('admin-fullscreen-workspace'); 3085 if (existingWs) existingWs.remove(); 3086 3087 const workspaceHTML = ` 3088 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 3089 3090 <!-- Top Bar --> 3091 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl"> 3092 <div class="flex items-center gap-3">
3093 <div class="w-9 h-9 bg-amber-500 text-slate-950 rounded-xl flex items-center justify-center font-black text-lg"> 3094 <i class="fa-solid fa-comments"></i> 3095 </div> 3096 <div> 3097 <h2 class="font-black text-lg text-white leading-none">Comment Moderation</h2> 3098 <p class="text-[11px] text-gray-400">Review, edit, reply, or remove community discussions.</p> 3099 </div> 3100 </div> 3101 3102 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 3103 â Close Panel 3104 </button> 3105 </div> 3106 3107 <!-- Main Panel Container (1280px Max Width) --> 3108 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center"> 3109 <div class="w-full max-w-[1280px] space-y-6"> 3110 3111 <!-- Comment Table Card (ATVEO Dark Theme) --> 3112 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl"> 3113 <div class="mb-6"> 3114 <h3 class="text-2xl font-black text-white mb-1">Comment Moderation</h3> 3115 <p class="text-xs text-gray-400">Review, edit, reply, or remove community discussions across all website pages.</p> 3116 </div> 3117 3118 <!-- Table --> 3119 <div class="overflow-x-auto"> 3120 <table class="w-full text-left text-xs border-collapse"> 3121 <thead> 3122 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 pb-3"> 3123 <th class="py-3 px-4">User Info</th> 3124 <th class="py-3 px-4">Page / Post</th> 3125 <th class="py-3 px-4">Comment & Rating</th> 3126 <th class="py-3 px-4">Posted Date</th> 3127 <th class="py-3 px-4 text-center">Action</th> 3128 </tr> 3129 </thead> 3130 <tbody id="admin-comments-table-body" class="divide-y divide-slate-800/80"> 3131 <tr> 3132 <td colspan="5" class="py-8 text-center text-gray-400 italic">Loading comments...</td> 3133 </tr> 3134 </tbody> 3135 </table> 3136 </div> 3137 </div> 3138 3139 </div> 3140 </div> 3141 </div> 3142 3143 <!-- ADMIN WARNING MODAL (ATVEO STYLE) --> 3144 <div id="admin-warning-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4"> 3145 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-8 max-w-lg w-full shadow-2xl space-y-6"> 3146 <h3 class="text-2xl font-black text-white text-center">Admin Warning</h3> 3147 3148 <div class="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl"> 3149 <p class="text-[10px] font-extrabold text-amber-400 uppercase tracking-wider mb-1">TARGET USER:</p> 3150 <p class="text-base font-black text-white" id="warn-target-name">User Name</p> 3151 <p class="text-xs text-gray-400" id="warn-target-id">ID: USER_ID</p> 3152 </div> 3153 3154 <div> 3155 <label class="block text-xs font-bold text-gray-300 mb-2">Enter private message for this user:</label> 3156 <textarea id="warn-message-text" rows="4" placeholder="Type your warning here..." class="w-full p-4 bg-slate-950 border border-amber-500/50 rounded-2xl text-white text-sm focus:outline-none focus:border-amber-400"></textarea> 3157 </div> 3158 3159 <div class="flex gap-4"> 3160 <button onclick="closeAdminWarningModal()" class="w-1/2 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold py-3.5 rounded-2xl text-xs transition">Cancel</button> 3161 <button id="send-warning-btn" class="w-1/2 bg-amber-400 hover:bg-amber-300 text-slate-950 font-black py-3.5 rounded-2xl text-xs transition shadow-lg">Send Warning</button> 3162 </div> 3163 </div> 3164 </div> 3165 3166 <!-- ADMIN REPLY MODAL --> 3167 <div id="admin-reply-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4"> 3168 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-8 max-w-lg w-full shadow-2xl space-y-6"> 3169 <h3 class="text-2xl font-black text-white text-center">Reply to Comment</h3> 3170 3171 <div class="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl"> 3172 <p class="text-[10px] font-extrabold text-blue-400 uppercase tracking-wider mb-1">USER COMMENT:</p> 3173 <p class="text-xs text-gray-300 italic" id="reply-user-comment">"Comment text..."</p> 3174 </div> 3175 3176 <div> 3177 <label class="block text-xs font-bold text-gray-300 mb-2">Type your Official Admin Reply:</label> 3178 <textarea id="reply-message-text" rows="4" placeholder="Type official reply here..." class="w-full p-4 bg-slate-950 border border-blue-500/50 rounded-2xl text-white text-sm focus:outline-none focus:border-blue-400"></textarea> 3179 </div> 3180 3181 <div class="flex gap-4"> 3182 <button onclick="closeAdminReplyModal()" class="w-1/2 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold py-3.5 rounded-2xl text-xs transition">Cancel</button> 3183 <button id="send-reply-btn" class="w-1/2 bg-blue-600 hover:bg-blue-500 text-white font-black py-3.5 rounded-2xl text-xs transition shadow-lg">Post Reply</button> 3184 </div> 3185 </div> 3186 </div> 3187 `; 3188 3189 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 3190 loadAdminCommentsTable(); 3191}; 3192 3193async function loadAdminCommentsTable() { 3194 const tableBody = document.getElementById('admin-comments-table-body'); 3195 if (!tableBody) return; 3196 3197 try { 3198 const snapshot = await db.collection("site_comments").orderBy("createdAt", "desc").get(); 3199 if (snapshot.empty) { 3200 tableBody.innerHTML = `<tr><td colspan="5" class="py-8 text-center text-gray-500 italic">No comments or reviews found.</td></tr>`; 3201 return; 3202 } 3203 3204 let html = ''; 3205 snapshot.forEach(doc => { 3206 const c = doc.data(); 3207 const commentId = doc.id; 3208 3209 let dateStr = "Recently"; 3210 if (c.createdAt && typeof c.createdAt.toDate === 'function') { 3211 const d = c.createdAt.toDate(); 3212 dateStr = d.toLocaleDateString('en-GB') + ', ' + d.toLocaleTimeString('en-US', { hour12: false }); 3213 } 3214 3215 // Star Rating HTML 3216 let starsHtml = ''; 3217 if (c.rating) { 3218 starsHtml = `<div class="text-amber-400 text-xs mb-1">` + 'â '.repeat(c.rating) + 'â'.repeat(5 - c.rating) + `</div>`; 3219 } 3220 3221 // Admin Replies badge 3222 let replyBadge = ''; 3223 if (c.replies && c.replies.length > 0) { 3224 replyBadge = `<div class="mt-2 pl-3 border-l-2 border-blue-500 text-[11px] text-blue-300"><strong>Admin Reply:</strong> ${c.replies[c.replies.length - 1].text}</div>`; 3225 } 3226 3227 html += ` 3228 <tr class="hover:bg-slate-900/50 transition border-b border-slate-800/50"> 3229 <td class="py-4 px-4 align-top"> 3230 <div class="flex items-center gap-3"> 3231 <div class="w-8 h-8 rounded-full bg-brandBlue text-white font-bold flex items-center justify-center text-xs flex-shrink-0 border border-blue-400/30"> 3232 ${(c.userName || 'User')[0].toUpperCase()} 3233 </div> 3234 <div> 3235 <p class="font-bold text-white text-xs">${c.userName || 'Anonymous'}</p> 3236 <p class="text-[10px] text-gray-400">${c.userEmail || ''}</p> 3237 </div> 3238 </div> 3239 </td> 3240 <td class="py-4 px-4 align-top"> 3241 <p class="font-bold text-white text-xs truncate max-w-[180px]">${c.pageTitle || 'Website Page'}</p> 3242 <p class="text-[10px] text-blue-400 font-mono truncate max-w-[180px]">${c.pageUrl || '/'}</p> 3243 </td> 3244 <td class="py-4 px-4 align-top"> 3245 ${starsHtml} 3246 <p class="text-gray-200 text-xs leading-relaxed max-w-sm">${c.commentText || ''}</p> 3247 ${replyBadge} 3248 </td> 3249 <td class="py-4 px-4 align-top text-gray-400 text-xs font-mono"> 3250 ${dateStr} 3251 </td> 3252 <td class="py-4 px-4 align-top text-center"> 3253 <div class="flex items-center justify-center gap-2"> 3254 <button onclick="openAdminWarningModal('${commentId}', '${c.userName || 'User'}', '${c.userId || commentId}')" class="bg-amber-400 hover:bg-amber-300 text-slate-950 font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3255 Warn 3256 </button> 3257 <button onclick="openAdminReplyModal('${commentId}', '${(c.commentText || '').replace(/'/g, "\\'")}')" class="bg-blue-600 hover:bg-blue-500 text-white font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3258 Reply 3259 </button> 3260 <button onclick="deleteAdminComment('${commentId}')" class="bg-slate-800 hover:bg-red-600 text-gray-300 hover:text-white font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3261 Remove 3262 </button> 3263 </div> 3264 </td> 3265 </tr>`; 3266 }); 3267 3268 tableBody.innerHTML = html; 3269 3270 } catch(e) { 3271 tableBody.innerHTML = `<tr><td colspan="5" class="py-8 text-center text-red-400 italic">Error loading comments: ${e.message}</td></tr>`; 3272 } 3273} 3274 3275// Warn Modal Logic (Upgraded with Automated Email Dispatch) 3276let activeWarnCommentId = null; 3277window.openAdminWarningModal = function(commentId, userName, userId) { 3278 activeWarnCommentId = commentId; 3279 document.getElementById('warn-target-name').innerText = userName; 3280 document.getElementById('warn-target-id').innerText = 'ID: ' + userId;
3281 document.getElementById('warn-message-text').value = ''; 3282 document.getElementById('admin-warning-modal').classList.remove('hidden'); 3283 3284 document.getElementById('send-warning-btn').onclick = async () => { 3285 const msg = document.getElementById('warn-message-text').value.trim(); 3286 if (!msg) return customToast("Please enter a warning message.", "error"); 3287 3288 const logoUrl = "https://firebasestorage.googleapis.com/v0/b/alintagar-official-website.firebasestorage.app/o/photos%2FAlintagarlogosmall.png?alt=media&token=68c0cef0-b24c-4e40-9deb-17ebe6cfc10b"; 3289 3290 try { 3291 // 1. Update comment/user status in Firestore 3292 await db.collection("site_comments").doc(activeWarnCommentId).update({ 3293 status: 'warned', 3294 warningMessage: msg, 3295 warnedAt: new Date() 3296 }); 3297 3298 // 2. Fetch user email to send the warning notification 3299 let userEmail = ""; 3300 let targetName = userName; 3301 3302 // Try fetching from users collection if comment contains a userId 3303 const commentDoc = await db.collection("site_comments").doc(activeWarnCommentId).get(); 3304 if (commentDoc.exists) { 3305 const cData = commentDoc.data(); 3306 if (cData.userEmail) userEmail = cData.userEmail; 3307 if (cData.userName) targetName = cData.userName; 3308 3309 if (!userEmail && cData.userId) { 3310 const uDoc = await db.collection("users").doc(cData.userId).get(); 3311 if (uDoc.exists) { 3312 userEmail = uDoc.data().email || ""; 3313 targetName = uDoc.data().fullName || targetName; 3314 } 3315 } 3316 } 3317 3318 // 3. Queue the Branded Warning Email 3319 if (userEmail) { 3320 await db.collection("email_queue").add({ 3321 to: userEmail, 3322 message: { 3323 subject: "Important Notice from Alin Tagar Administration â ï¸", 3324 text: `Hello ${targetName},\n\nYou have received a formal warning from the Alin Tagar Administration:\n\n"${msg}"\n\nPlease ensure you follow our terms and conditions.`, 3325 html: ` 3326 <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 650px; margin: 0 auto; background-color: #ffffff; overflow: hidden;"> 3327 3328 <!-- Header --> 3329 <div style="padding: 20px 10px; border-bottom: 2px solid #05396b;"> 3330 <table style="width: 100%; border-collapse: collapse;"> 3331 <tr> 3332 <td style="vertical-align: middle; width: 70px;"> 3333 <img src="${logoUrl}" alt="Alin Tagar Logo" style="height: 50px;"> 3334 </td> 3335 <td style="vertical-align: middle; text-align: right;"> 3336 <h2 style="color: #05396b; margin: 0; font-size: 18px; font-weight: 900; letter-spacing: 1px;">ALIN TAGAR</h2> 3337 <p style="color: #64748b; margin: 2px 0 0 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; font-weight: bold;">Alin Tagar English Language Training School</p> 3338 </td> 3339 </tr> 3340 </table> 3341 </div> 3342 3343 <!-- Body --> 3344 <div style="padding: 30px 10px;"> 3345 <h3 style="color: #d97706; margin-top: 0; font-size: 20px; font-weight: 800;">Administrator Warning Notice â ï¸</h3> 3346 <p style="color: #0f172a; font-size: 16px;">Hello <strong>${targetName}</strong>,</p> 3347 <p style="color: #334155; font-size: 14px; line-height: 1.6;">You have received a formal notice regarding your recent activity or comments on the Alin Tagar platform.</p> 3348 3349 <div style="background-color: #fefce8; border-left: 4px solid #f59e0b; padding: 18px; border-radius: 8px; border-top: 1px solid #fef3c7; border-right: 1px solid #fef3c7; border-bottom: 1px solid #fef3c7; margin: 20px 0; color: #78350f; font-size: 14px; line-height: 1.6; white-space: pre-wrap;"><strong>Admin Message:</strong><br>"${msg}"</div> 3350 3351 <p style="color: #334155; font-size: 14px; line-height: 1.6;">Please review our community guidelines and ensure your future interactions comply with our terms and conditions. Continued violations may lead to account suspension.</p> 3352 </div> 3353 3354 <!-- Signature --> 3355 <div style="padding: 0 10px 30px 10px;"> 3356 <p style="color: #333333; font-size: 14px; margin-bottom: 15px;">Best regards,</p> 3357 <table style="width: 100%; border-collapse: collapse;"> 3358 <tr> 3359 <td style="width: 90px; vertical-align: top; padding-right: 15px;"> 3360 <img src="${logoUrl}" alt="Alin Tagar Logo" style="width: 90px; height: auto;"> 3361 </td> 3362 <td style="vertical-align: top; font-size: 13px; color: #333333; line-height: 1.5;"> 3363 <strong style="font-size: 15px; color: #000000;">Administrator</strong><br> 3364 CEO, Admin, Customer Relations Manager (CRM)<br> 3365 <strong>âï¸</strong> +95 (0) 9 781 010 356<br> 3366 <strong>ð§</strong> <a href="mailto:[email protected]" style="color: #05396b; text-decoration: none;">[email protected]</a><br> 3367 <strong>ð</strong> <a href="https://alintagar.com" style="color: #05396b; text-decoration: none;">alintagar.com</a><br> 3368 3369 <div style="margin-top: 8px;">
3370 <span style="background-color: #00b67a; color: white; font-size: 10px; font-weight: bold; padding: 3px 8px; border-radius: 4px; display: inline-block;">â Trustindex</span><br> 3371 <span style="color: #00b67a; font-size: 18px; letter-spacing: 2px;">â â â â â </span><br> 3372 <span style="font-size: 12px; font-weight: bold; color: #000000;">40 reviews</span> 3373 </div> 3374 </td> 3375 </tr> 3376 </table> 3377 3378 <div style="margin-top: 30px; font-size: 10px; color: #777777; line-height: 1.5; text-align: justify; border-top: 1px solid #e2e8f0; padding-top: 15px;"> 3379 <p style="margin-bottom: 8px;">Please do not print this e-mail unless it is necessary. Every unprinted e-mail helps the environment.</p> 3380 <p style="margin: 0;">This e-mail and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. Please notify the sender immediately by e-mail if you have received this e-mail by mistake and delete this e-mail from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing or taking any action in reliance on the contents of this information is strictly prohibited.</p> 3381 </div> 3382 </div> 3383 </div>` 3384 }, 3385 status: "queued", 3386 createdAt: new Date() 3387 }); 3388 } 3389 3390 customToast(`Warning sent to ${userName}!`, "success"); 3391 closeAdminWarningModal(); 3392 loadAdminCommentsTable(); 3393 } catch(e) { 3394 customToast(e.message, "error"); 3395 } 3396 }; 3397};
3398 3399window.closeAdminWarningModal = function() { 3400 document.getElementById('admin-warning-modal').classList.add('hidden'); 3401}; 3402 3403// Reply Modal Logic 3404let activeReplyCommentId = null; 3405window.openAdminReplyModal = function(commentId, commentText) { 3406 activeReplyCommentId = commentId; 3407 document.getElementById('reply-user-comment').innerText = `"${commentText}"`; 3408 document.getElementById('reply-message-text').value = ''; 3409 document.getElementById('admin-reply-modal').classList.remove('hidden'); 3410 3411 document.getElementById('send-reply-btn').onclick = async () => { 3412 const replyMsg = document.getElementById('reply-message-text').value.trim(); 3413 if (!replyMsg) return customToast("Please enter a reply message.", "error"); 3414 3415 // ð¢ FIXED: Declared BEFORE calling update() 3416 const currentAdminName = (auth.currentUser && auth.currentUser.displayName) 3417 ? `${auth.currentUser.displayName} (Admin)` 3418 : 'School Admin'; 3419 3420 try { 3421 await db.collection("site_comments").doc(activeReplyCommentId).update({ 3422 replies: firebase.firestore.FieldValue.arrayUnion({ 3423 adminName: currentAdminName, 3424 text: replyMsg, 3425 date: new Date().toLocaleDateString('en-GB') 3426 }) 3427 }); 3428 customToast("Official reply posted!", "success"); 3429 closeAdminReplyModal(); 3430 loadAdminCommentsTable(); 3431 } catch(e) { 3432 customToast(e.message, "error"); 3433 } 3434 }; 3435};
3436 3437window.closeAdminReplyModal = function() { 3438 document.getElementById('admin-reply-modal').classList.add('hidden'); 3439}; 3440 3441// Remove Comment Logic 3442window.deleteAdminComment = async function(commentId) { 3443 const confirmed = await customConfirm("Delete Comment", "Are you sure you want to delete this comment forever?"); 3444 if (confirmed) { 3445 try { 3446 await db.collection("site_comments").doc(commentId).delete(); 3447 customToast("Comment removed.", "success"); 3448 loadAdminCommentsTable(); 3449 } catch(e) { 3450 customToast(e.message, "error"); 3451 } 3452 } 3453}; 3454 3455/* ========================================== 3456 ATVEO-STYLE COMMENT MODERATION PANEL 3457 ========================================== */ 3458 3459window.openFullscreenCommentModeration = function() { 3460 const existingWs = document.getElementById('admin-fullscreen-workspace'); 3461 if (existingWs) existingWs.remove(); 3462 3463 const workspaceHTML = ` 3464 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 3465 3466 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl"> 3467 <div class="flex items-center gap-3"> 3468 <div class="w-9 h-9 bg-amber-500 text-slate-950 rounded-xl flex items-center justify-center font-black text-lg"> 3469 <i class="fa-solid fa-comments"></i> 3470 </div> 3471 <div> 3472 <h2 class="font-black text-lg text-white leading-none">Comment Moderation</h2> 3473 <p class="text-[11px] text-gray-400">Review, edit, approve, or remove community discussions.</p> 3474 </div> 3475 </div> 3476 3477 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 3478 â Close Panel 3479 </button> 3480 </div> 3481 3482 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center"> 3483 <div class="w-full max-w-[1280px] space-y-6"> 3484 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl"> 3485 <div class="mb-6 flex justify-between items-center"> 3486 <div> 3487 <h3 class="text-2xl font-black text-white mb-1">Comment Moderation Panel</h3> 3488 <p class="text-xs text-gray-400">Approve pending reviews to publish them live to your website.</p> 3489 </div> 3490 </div> 3491 3492 <div class="overflow-x-auto"> 3493 <table class="w-full text-left text-xs border-collapse"> 3494 <thead> 3495 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 pb-3"> 3496 <th class="py-3 px-4">User Info</th> 3497 <th class="py-3 px-4">Page / Post</th> 3498 <th class="py-3 px-4">Comment & Rating</th> 3499 <th class="py-3 px-4">Status</th> 3500 <th class="py-3 px-4">Posted Date</th> 3501 <th class="py-3 px-4 text-center">Action</th> 3502 </tr> 3503 </thead> 3504 <tbody id="admin-comments-table-body" class="divide-y divide-slate-800/80"> 3505 <tr> 3506 <td colspan="6" class="py-8 text-center text-gray-400 italic">Loading comments...</td> 3507 </tr> 3508 </tbody> 3509 </table> 3510 </div> 3511 </div> 3512 </div> 3513 </div> 3514 </div> 3515 3516 <!-- ADMIN WARNING MODAL --> 3517 <div id="admin-warning-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4"> 3518 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-8 max-w-lg w-full shadow-2xl space-y-6"> 3519 <h3 class="text-2xl font-black text-white text-center">Admin Warning</h3> 3520 <div class="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl"> 3521 <p class="text-[10px] font-extrabold text-amber-400 uppercase tracking-wider mb-1">TARGET USER:</p> 3522 <p class="text-base font-black text-white" id="warn-target-name">User Name</p> 3523 <p class="text-xs text-gray-400" id="warn-target-id">
3523ID: USER_ID</p> 3524 </div> 3525 <div> 3526 <label class="block text-xs font-bold text-gray-300 mb-2">Enter private message for this user:</label> 3527 <textarea id="warn-message-text" rows="4" placeholder="Type your warning here..." class="w-full p-4 bg-slate-950 border border-amber-500/50 rounded-2xl text-white text-sm focus:outline-none focus:border-amber-400"></textarea> 3528 </div> 3529 <div class="flex gap-4"> 3530 <button onclick="closeAdminWarningModal()" class="w-1/2 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold py-3.5 rounded-2xl text-xs transition">Cancel</button> 3531 <button id="send-warning-btn" class="w-1/2 bg-amber-400 hover:bg-amber-300 text-slate-950 font-black py-3.5 rounded-2xl text-xs transition shadow-lg">Send Warning</button> 3532 </div> 3533 </div> 3534 </div> 3535 3536 <!-- ADMIN REPLY MODAL --> 3537 <div id="admin-reply-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4"> 3538 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-8 max-w-lg w-full shadow-2xl space-y-6"> 3539 <h3 class="text-2xl font-black text-white text-center">Reply to Comment</h3> 3540 <div class="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl"> 3541 <p class="text-[10px] font-extrabold text-blue-400 uppercase tracking-wider mb-1">USER COMMENT:</p> 3542 <p class="text-xs text-gray-300 italic" id="reply-user-comment">"Comment text..."</p> 3543 </div> 3544 <div> 3545 <label class="block text-xs font-bold text-gray-300 mb-2">Type your Official Admin Reply:</label> 3546 <textarea id="reply-message-text" rows="4" placeholder="Type official reply here..." class="w-full p-4 bg-slate-950 border border-blue-500/50 rounded-2xl text-white text-sm focus:outline-none focus:border-blue-400"></textarea> 3547 </div> 3548 <div class="flex gap-4"> 3549 <button onclick="closeAdminReplyModal()" class="w-1/2 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold py-3.5 rounded-2xl text-xs transition">Cancel</button> 3550 <button id="send-reply-btn" class="w-1/2 bg-blue-600 hover:bg-blue-500 text-white font-black py-3.5 rounded-2xl text-xs transition shadow-lg">Post Reply</button> 3551 </div> 3552 </div> 3553 </div> 3554 `; 3555 3556 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 3557 loadAdminCommentsTable(); 3558}; 3559 3560async function loadAdminCommentsTable() { 3561 const tableBody = document.getElementById('admin-comments-table-body'); 3562 if (!tableBody) return; 3563 3564 try { 3565 const snapshot = await db.collection("site_comments").orderBy("createdAt", "desc").get(); 3566 if (snapshot.empty) { 3567 tableBody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-gray-500 italic">No comments found.</td></tr>`; 3568 return; 3569 } 3570 3571 let html = ''; 3572 snapshot.forEach(doc => { 3573 const c = doc.data(); 3574 const commentId = doc.id; 3575 3576 let dateStr = "Recently"; 3577 if (c.createdAt && typeof c.createdAt.toDate === 'function') { 3578 const d = c.createdAt.toDate(); 3579 dateStr = d.toLocaleDateString('en-GB') + ', ' + d.toLocaleTimeString('en-US', { hour12: false }); 3580 } 3581 3582 let starsHtml = ''; 3583 if (c.rating) { 3584 starsHtml = `<div class="text-amber-400 text-xs mb-1">` + 'â '.repeat(c.rating) + 'â'.repeat(5 - c.rating) + `</div>`; 3585 } 3586 3587 let replyBadge = ''; 3588 if (c.replies && c.replies.length > 0) { 3589 replyBadge = `<div class="mt-2 pl-3 border-l-2 border-blue-500 text-[11px] text-blue-300"><strong>Admin Reply:</strong> ${c.replies[c.replies.length - 1].text}</div>`; 3590 } 3591 3592 // Status Badge 3593 const isApproved = c.status === 'approved'; 3594 const statusBadge = isApproved 3595 ? `<span class="bg-green-500/20 text-green-400 border border-green-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Approved</span>` 3596 : `<span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase animate-pulse">Pending</span>`; 3597 3598 html += ` 3599 <tr class="hover:bg-slate-900/50 transition border-b border-slate-800/50"> 3600 <td class="py-4 px-4 align-top"> 3601 <div class="flex items-center gap-3"> 3602 <div class="w-8 h-8 rounded-full bg-brandBlue text-white font-bold flex items-center justify-center text-xs flex-shrink-0 border border-blue-400/30"> 3603 ${(c.userName || 'U')[0].toUpperCase()} 3604 </div> 3605 <div> 3606 <!-- ð¢ USER FULL NAME --> 3607 <p class="font-bold text-white text-xs">${c.userName || 'Anonymous'}</p> 3608 <p class="text-[10px] text-gray-400">${c.userEmail || ''}</p> 3609 </div> 3610 </div> 3611 </td> 3612 <td class="py-4 px-4 align-top"> 3613 <p class="font-bold text-white text-xs truncate max-w-[180px]">${c.pageTitle || 'Website Page'}</p> 3614 <p class="text-[10px] text-blue-400 font-mono truncate max-w-[180px]">${c.pageUrl || '/'}</p> 3615 </td> 3616 <td class="py-4 px-4 align-top"> 3617 ${starsHtml} 3618 <p class="text-gray-200 text-xs leading-relaxed max-w-sm">${c.commentText || ''}</p> 3619 ${replyBadge} 3620 </td> 3621 <td class="py-4 px-4 align-top"> 3622 ${statusBadge} 3623 </td> 3624 <td class="py-4 px-4 align-top text-gray-400 text-xs font-mono"> 3625 ${dateStr} 3626 </td> 3627 <td class="py-4 px-4 align-top text-center"> 3628 <div class="flex items-center justify-center gap-2"> 3629 <!-- ð¢ APPROVE BUTTON --> 3630 ${!isApproved ? `<button onclick="approveAdminComment('${commentId}')" class="bg-green-600 hover:bg-green-500 text-white font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]">â Approve</button>` : ''} 3631 3632 <button onclick="openAdminReplyModal('${commentId}', '${(c.commentText || '').replace(/'/g, "\\'")}')" class="bg-blue-600 hover:bg-blue-500 text-white font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3633 Reply 3634 </button> 3635 <button onclick="openAdminWarningModal('${commentId}', '${c.userName || 'User'}', '${c.userId || commentId}')" class="bg-amber-400 hover:bg-amber-300 text-slate-950 font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3636 Warn 3637 </button> 3638 <button onclick="deleteAdminComment('${commentId}')" class="bg-slate-800 hover:bg-red-600 text-gray-300 hover:text-white font-extrabold px-3 py-1.5 rounded-lg transition text-[11px]"> 3639 Remove 3640 </button> 3641 </div> 3642 </td> 3643 </tr>`; 3644 }); 3645 3646 tableBody.innerHTML = html; 3647 3648 } catch(e) { 3649 tableBody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-red-400 italic">Error loading comments: ${e.message}</td></tr>`; 3650 } 3651} 3652 3653// ð¢ ADMIN APPROVE COMMENT ACTION 3654window.approveAdminComment = async function(commentId) { 3655 try { 3656 await db.collection("site_comments").doc(commentId).update({ 3657 status: 'approved', 3658 approvedAt: new Date() 3659 }); 3660 customToast("Comment Approved & Published Live!", "success"); 3661 loadAdminCommentsTable(); 3662 } catch(e) { 3663 customToast(e.message, "error"); 3664 } 3665}; 3666 3667/* ========================================== 3668 ADVANCED LIVE PAGE BUILDER ENGINE (V6 - CUSTOM MODALS) 3669 ========================================== */ 3670 3671let savedSelectionRange = null; 3672let currentSelectedBlock = null; 3673 3674// HISTORY STACK FOR UNDO / REDO 3675let historyStack = []; 3676let historyIndex = -1; 3677let isSnapshotSuppressed = false; 3678 3679function saveHistorySnapshot() { 3680 if (isSnapshotSuppressed) return; 3681 const wrapper = document.getElementById('live-editable-content'); 3682 if (!wrapper) return; 3683
3684 const currentHTML = wrapper.innerHTML; 3685 3686 if (historyStack[historyIndex] === currentHTML) return; 3687 3688 if (historyIndex < historyStack.length - 1) { 3689 historyStack = historyStack.slice(0, historyIndex + 1); 3690 } 3691 3692 historyStack.push(currentHTML); 3693 historyIndex++; 3694 3695 if (historyStack.length > 30) { 3696 historyStack.shift(); 3697 historyIndex--; 3698 } 3699} 3700 3701window.undo = function() { 3702 if (historyIndex > 0) { 3703 isSnapshotSuppressed = true; 3704 historyIndex--; 3705 const wrapper = document.getElementById('live-editable-content'); 3706 wrapper.innerHTML = historyStack[historyIndex]; 3707 isSnapshotSuppressed = false; 3708 customToast("Undo âª", "info"); 3709 } else { 3710 customToast("Nothing to undo", "info"); 3711 } 3712}; 3713 3714window.redo = function() { 3715 if (historyIndex < historyStack.length - 1) { 3716 isSnapshotSuppressed = true; 3717 historyIndex++; 3718 const wrapper = document.getElementById('live-editable-content'); 3719 wrapper.innerHTML = historyStack[historyIndex]; 3720 isSnapshotSuppressed = false; 3721 customToast("Redo â©", "info"); 3722 } else { 3723 customToast("Nothing to redo", "info"); 3724 } 3725}; 3726 3727// KEYBOARD SHORTCUTS (Ctrl+Z, Ctrl+Shift+Z, Ctrl+Y) 3728document.addEventListener('keydown', (e) => { 3729 if (!document.getElementById('live-edit-toolbar')) return; 3730 3731 const isCtrlOrCmd = e.ctrlKey || e.metaKey; 3732 if (isCtrlOrCmd) { 3733 if (e.key.toLowerCase() === 'z') { 3734 e.preventDefault(); 3735 if (e.shiftKey) { 3736 redo(); 3737 } else { 3738 undo(); 3739 } 3740 } else if (e.key.toLowerCase() === 'y') { 3741 e.preventDefault(); 3742 redo(); 3743 } 3744 } 3745}); 3746 3747document.addEventListener('selectionchange', () => { 3748 const sel = window.getSelection(); 3749 const wrapper = document.getElementById('live-editable-content'); 3750 if (sel.rangeCount > 0 && wrapper && wrapper.contains(sel.anchorNode)) { 3751 savedSelectionRange = sel.getRangeAt(0); 3752 } 3753}); 3754 3755let preventLinkClick = function(e) { e.preventDefault(); }; 3756 3757window.enableLiveEditMode = function() { 3758 const drawer = document.getElementById('admin-sidebar-drawer'); 3759 if (drawer) drawer.classList.add('translate-x-full'); 3760 3761 const wrapper = document.getElementById('live-editable-content'); 3762 if (!wrapper) { 3763 customToast("Error: Missing <main id='live-editable-content'> wrapper on this page.", "error"); 3764 return; 3765 } 3766 3767 historyStack = []; 3768 historyIndex = -1; 3769 3770 // 1. Make Text Editable 3771 const textElements = wrapper.querySelectorAll('h1, h2, h3, h4, p, span, li, a, button'); 3772 textElements.forEach(el => { 3773 el.contentEditable = "true"; 3774 el.style.outline = "2px dashed #f59e0b"; // Amber outline 3775 el.style.outlineOffset = "2px"; 3776 el.style.cursor = "text"; 3777 3778 if(el.tagName === 'A') { 3779 el.title = "Edit text, or double-click to edit URL"; 3780 el.addEventListener('click', preventLinkClick); 3781 el.ondblclick = async (e) => { 3782 e.preventDefault(); 3783 const newUrl = await customPrompt("Update Link URL", "Enter destination web address:", el.href); 3784 if(newUrl !== null) { 3785 el.href = newUrl; 3786 saveHistorySnapshot(); 3787 } 3788 }; 3789 } 3790 }); 3791 3792 // 2. Make Images Editable 3793 const images = wrapper.querySelectorAll('img'); 3794 images.forEach(img => { 3795 img.style.outline = "3px dashed #3b82f6"; // Blue outline 3796 img.style.cursor = "pointer"; 3797 img.title = "Double-click to replace image URL"; 3798 img.ondblclick = async (e) => { 3799 e.preventDefault(); 3800 const newUrl = await customPrompt("Update Image URL", "Enter new image web address:", img.src); 3801 if(newUrl) { 3802 img.src = newUrl; 3803 saveHistorySnapshot(); 3804 } 3805 }; 3806 });
3807 3808 // 3. YouTube iFrames Overlay 3809 const iframes = wrapper.querySelectorAll('iframe'); 3810 iframes.forEach(iframe => { 3811 if(window.getComputedStyle(iframe.parentElement).position === 'static') { 3812 iframe.parentElement.style.position = 'relative'; 3813 } 3814 const overlay = document.createElement('div'); 3815 overlay.className = 'iframe-edit-overlay absolute inset-0 bg-blue-500/40 flex items-center justify-center cursor-pointer border-4 border-dashed border-blue-500 z-40 rounded-xl'; 3816 overlay.innerHTML = '<span class="bg-blue-900 text-white font-bold px-4 py-2 rounded-full shadow-lg"><i class="fa-brands fa-youtube mr-2"></i>Double-click to Change Video</span>'; 3817 3818 overlay.ondblclick = async () => { 3819 const newUrl = await customPrompt("Update Video URL", "Enter new YouTube embed URL:", iframe.src); 3820 if(newUrl) { 3821 iframe.src = newUrl; 3822 saveHistorySnapshot(); 3823 } 3824 }; 3825 iframe.parentElement.appendChild(overlay); 3826 }); 3827 3828 // 4. INJECT SECTION CONTROL BADGES 3829 const sections = wrapper.querySelectorAll('section, article'); 3830 sections.forEach((sec, idx) => { 3831 if (window.getComputedStyle(sec).position === 'static') { 3832 sec.style.position = 'relative'; 3833 } 3834 3835 sec.style.outline = '3px dashed #8b5cf6'; // Purple dashed outline 3836 sec.style.outlineOffset = '-3px'; 3837 3838 const badge = document.createElement('div'); 3839 badge.className = 'live-section-badge absolute top-3 left-3 z-[60] bg-slate-900/90 text-white text-xs font-bold px-3 py-1.5 rounded-xl shadow-2xl backdrop-blur border border-slate-700 flex items-center gap-2 select-none'; 3840 badge.innerHTML = ` 3841 <span class="text-amber-400 font-extrabold flex items-center gap-1"> 3842 <i class="fa-solid fa-layer-group"></i> Section ${idx + 1} 3843 </span> 3844 <div class="h-3 w-[1px] bg-slate-700 mx-1"></div> 3845 <button onclick="event.stopPropagation(); moveSection(this, -1)" class="hover:text-amber-400 p-1" title="Move Section Up"><i class="fa-solid fa-arrow-up"></i></button> 3846 <button onclick="event.stopPropagation(); moveSection(this, 1)" class="hover:text-amber-400 p-1" title="Move Section Down"><i class="fa-solid fa-arrow-down"></i></button> 3847 <button onclick="event.stopPropagation(); deleteSection(this)" class="hover:text-red-400 p-1 ml-1" title="Delete Section"><i class="fa-solid fa-trash-can"></i></button> 3848 `; 3849 sec.appendChild(badge); 3850 }); 3851 3852 // 5. INNER BLOCK SELECTION 3853 wrapper.addEventListener('click', function(e) { 3854 if (e.target.closest('.live-section-badge') || e.target.closest('.iframe-edit-overlay')) return; 3855 3856 const target = e.target.closest('section, article, img, a, .grid > div, .flex > div'); 3857 3858 if (target && target !== wrapper) { 3859 deselectBlock(); 3860 currentSelectedBlock = target; 3861 currentSelectedBlock.style.outline = '4px solid #a855f7'; 3862 currentSelectedBlock.style.outlineOffset = '-4px'; 3863 } else { 3864 deselectBlock(); 3865 } 3866 }); 3867
3868 // Debounce Typing Input to Save History Snapshots 3869 let typingTimer = null; 3870 wrapper.addEventListener('input', () => { 3871 clearTimeout(typingTimer); 3872 typingTimer = setTimeout(() => { 3873 saveHistorySnapshot(); 3874 }, 500); 3875 }); 3876 3877 saveHistorySnapshot(); 3878 3879 // 6. Inject Floating Toolbar 3880 if(!document.getElementById('live-edit-toolbar')) { 3881 const toolbar = document.createElement('div'); 3882 toolbar.id = "live-edit-toolbar"; 3883 toolbar.style.cssText = "position: fixed; top: 80px; left: 20px; z-index: 10000; width: 64px;"; 3884 toolbar.className = "bg-slate-900 text-white rounded-2xl shadow-2xl border border-slate-700 flex flex-col items-center py-3 overflow-hidden select-none"; 3885 3886 toolbar.innerHTML = ` 3887 <!-- Drag Handle --> 3888 <div id="drag-handle" class="w-full text-center cursor-grab active:cursor-grabbing text-gray-500 hover:text-white pb-3 border-b border-slate-800 mb-2" title="Drag to move panel"> 3889 <i class="fa-solid fa-grip-lines text-xl"></i> 3890 </div> 3891 3892 <!-- History Tools --> 3893 <div class="flex flex-col gap-1 w-full px-2 mb-2 border-b border-slate-800 pb-2"> 3894 <button onclick="undo()" class="w-10 h-10 flex items-center justify-center text-gray-400 hover:bg-slate-800 hover:text-amber-400 rounded-xl transition" title="Undo (Ctrl+Z)"><i class="fa-solid fa-rotate-left"></i></button> 3895 <button onclick="redo()" class="w-10 h-10 flex items-center justify-center text-gray-400 hover:bg-slate-800 hover:text-amber-400 rounded-xl transition" title="Redo (Ctrl+Shift+Z / Ctrl+Y)"><i class="fa-solid fa-rotate-right"></i></button> 3896 </div> 3897 3898 <!-- Text Formatting Tools --> 3899 <div class="flex flex-col gap-1 w-full px-2 mb-2 border-b border-slate-800 pb-2"> 3900 <button onmousedown="event.preventDefault(); document.execCommand('bold'); saveHistorySnapshot();" class="w-10 h-10 flex items-center justify-center text-gray-300 hover:bg-slate-800 hover:text-amber-400 rounded-xl transition" title="Bold"><i class="fa-solid fa-bold"></i></button> 3901 <button onmousedown="event.preventDefault(); document.execCommand('italic'); saveHistorySnapshot();" class="w-10 h-10 flex items-center justify-center text-gray-300 hover:bg-slate-800 hover:text-amber-400 rounded-xl transition" title="Italic"><i class="fa-solid fa-italic"></i></button> 3902 <button onmousedown="event.preventDefault(); document.execCommand('insertUnorderedList'); saveHistorySnapshot();" class="w-10 h-10 flex items-center justify-center text-gray-300 hover:bg-slate-800 hover:text-amber-400 rounded-xl transition" title="Bullet List"><i class="fa-solid fa-list-ul"></i></button> 3903 3904 <!-- Color Picker --> 3905 <div class="relative w-10 h-10 flex items-center justify-center hover:bg-slate-800 rounded-xl transition cursor-pointer" title="Change Text Color"> 3906 <i class="fa-solid fa-palette text-amber-400 absolute pointer-events-none"></i> 3907 <input type="color" id="text-color-picker" class="opacity-0 w-full h-full cursor-pointer"> 3908 </div> 3909 </div> 3910 3911 <!-- Inner Element Actions --> 3912 <div class="flex flex-col gap-1 w-full px-2 mb-3 border-b border-slate-800 pb-2"> 3913 <button onclick="deselectBlock()" class="w-10 h-10 flex items-center justify-center text-gray-400 hover:bg-slate-800 hover:text-white rounded-xl transition" title="Deselect Item"><i class="fa-regular fa-square"></i></button> 3914 <button onclick="moveSelectedBlock(-1)" class="w-10 h-10 flex items-center justify-center text-blue-400 hover:bg-blue-900/50 rounded-xl transition" title="Move Selected Item UP"><i class="fa-solid fa-arrow-up"></i></button> 3915 <button onclick="moveSelectedBlock(1)" class="w-10 h-10 flex items-center justify-center text-blue-400 hover:bg-blue-900/50 rounded-xl transition" title="Move Selected Item DOWN"><i class="fa-solid fa-arrow-down"></i></button> 3916 <button onclick="deleteSelectedBlock()" class="w-10 h-10 flex items-center justify-center text-red-500 hover:bg-red-900/50 rounded-xl transition" title="Delete Selected Item">
3916<i class="fa-solid fa-trash-can"></i></button> 3917 </div> 3918 3919 <!-- Save / Cancel Actions --> 3920 <div class="flex flex-col gap-2 w-full px-2 mt-1"> 3921 <button onclick="saveLiveEdits()" class="w-10 h-10 flex items-center justify-center bg-green-600 hover:bg-green-500 rounded-xl shadow-lg transition" title="Save Changes to Firebase"> 3922 <i class="fa-solid fa-cloud-arrow-up text-white"></i> 3923 </button> 3924 <button onclick="cancelLiveEdits()" class="w-10 h-10 flex items-center justify-center bg-red-600/20 text-red-400 hover:bg-red-600/40 rounded-xl transition" title="Cancel & Exit"> 3925 <i class="fa-solid fa-xmark text-lg"></i> 3926 </button> 3927 </div> 3928 `; 3929 document.body.appendChild(toolbar); 3930 3931 // Color Picker Event 3932 document.getElementById('text-color-picker').addEventListener('change', (e) => { 3933 if (savedSelectionRange) { 3934 const sel = window.getSelection(); 3935 sel.removeAllRanges(); 3936 sel.addRange(savedSelectionRange); 3937 document.execCommand('foreColor', false, e.target.value); 3938 saveHistorySnapshot(); 3939 } 3940 }); 3941 3942 // Make Panel Draggable 3943 dragElement(toolbar); 3944 } 3945 3946 customToast("Live Edit Mode Activated!", "success"); 3947}; 3948 3949/* --- SECTION BADGE FUNCTIONS --- */ 3950window.moveSection = function(btn, direction) { 3951 const sec = btn.closest('section, article'); 3952 if (!sec) return; 3953 const parent = sec.parentNode; 3954 3955 if (direction === -1 && sec.previousElementSibling) { 3956 parent.insertBefore(sec, sec.previousElementSibling); 3957 sec.scrollIntoView({ behavior: 'smooth', block: 'center' }); 3958 saveHistorySnapshot(); 3959 customToast("Section moved UP", "success"); 3960 } else if (direction === 1 && sec.nextElementSibling) { 3961 parent.insertBefore(sec.nextElementSibling, sec); 3962 sec.scrollIntoView({ behavior: 'smooth', block: 'center' }); 3963 saveHistorySnapshot(); 3964 customToast("Section moved DOWN", "success"); 3965 } else { 3966 customToast("Cannot move section further.", "info"); 3967 } 3968}; 3969 3970window.deleteSection = async function(btn) { 3971 const sec = btn.closest('section, article'); 3972 if (!sec) return; 3973 const confirmed = await customConfirm("Delete Section", "Are you sure you want to delete this ENTIRE section?"); 3974 if (confirmed) { 3975 sec.remove(); 3976 saveHistorySnapshot(); 3977 customToast("Section deleted.", "success"); 3978 } 3979}; 3980 3981/* --- INNER BLOCK FUNCTIONS --- */ 3982window.deselectBlock = function() { 3983 if (currentSelectedBlock) { 3984 currentSelectedBlock.style.outline = ''; 3985 currentSelectedBlock.style.outlineOffset = ''; 3986 currentSelectedBlock = null; 3987 } 3988}; 3989 3990window.moveSelectedBlock = function(direction) { 3991 if(!currentSelectedBlock) return customToast("Click an item to select it first!", "info"); 3992 3993 const parent = currentSelectedBlock.parentNode; 3994 if (direction === -1 && currentSelectedBlock.previousElementSibling) { 3995 parent.insertBefore(currentSelectedBlock, currentSelectedBlock.previousElementSibling); 3996 currentSelectedBlock.scrollIntoView({behavior: "smooth", block: "center"}); 3997 saveHistorySnapshot(); 3998 } else if (direction === 1 && currentSelectedBlock.nextElementSibling) { 3999 parent.insertBefore(currentSelectedBlock.nextElementSibling, currentSelectedBlock); 4000 currentSelectedBlock.scrollIntoView({behavior: "smooth", block: "center"}); 4001 saveHistorySnapshot(); 4002 } else { 4003 customToast("Cannot move any further.", "info"); 4004 } 4005}; 4006 4007window.deleteSelectedBlock = async function() { 4008 if(!currentSelectedBlock) return customToast("Click an item to select it first!", "info"); 4009 const confirmed = await customConfirm("Delete Element", "Are you sure you want to delete this element?"); 4010 if (confirmed) { 4011 currentSelectedBlock.remove(); 4012 currentSelectedBlock = null; 4013 saveHistorySnapshot(); 4014 customToast("Element deleted.", "success"); 4015 } 4016}; 4017 4018window.cancelLiveEdits = async function() { 4019 const confirmed = await customConfirm("Cancel Live Editing", "Discard all unsaved changes and exit edit mode?"); 4020 if (confirmed) { 4021 window.location.reload(); 4022 } 4023}; 4024 4025// Panel Draggable Logic 4026function dragElement(elmnt) { 4027 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; 4028 document.getElementById("drag-handle").onmousedown = dragMouseDown; 4029 4030 function dragMouseDown(e) { 4031 e.preventDefault(); 4032 pos3 = e.clientX; 4033 pos4 = e.clientY; 4034 document.onmouseup = closeDragElement; 4035 document.onmousemove = elementDrag; 4036 } 4037 4038 function elementDrag(e) { 4039 e.preventDefault(); 4040 pos1 = pos3 - e.clientX; 4041 pos2 = pos4 - e.clientY; 4042 pos3 = e.clientX; 4043 pos4 = e.clientY; 4044 4045 let newTop = elmnt.offsetTop - pos2; 4046 let newLeft = elmnt.offsetLeft - pos1; 4047 4048 if(newTop < 0) newTop = 0; 4049 if(newLeft < 0) newLeft = 0; 4050 if(newTop + elmnt.offsetHeight > window.innerHeight) newTop = window.innerHeight - elmnt.offsetHeight; 4051 if(newLeft + elmnt.offsetWidth > window.innerWidth) newLeft = window.innerWidth - elmnt.offsetWidth; 4052 4053 elmnt.style.top = newTop + "px"; 4054 elmnt.style.left = newLeft + "px"; 4055 } 4056 4057 function closeDragElement() { 4058 document.onmouseup = null; 4059 document.onmousemove = null; 4060 } 4061} 4062 4063window.saveLiveEdits = async function() { 4064 const wrapper = document.getElementById('live-editable-content'); 4065 if(!wrapper) return; 4066 4067 // 1. CLEANUP SWIPER 4068 const swipers = wrapper.querySelectorAll('.swiper');
4069 swipers.forEach(s => { 4070 if(s.swiper) s.swiper.destroy(true, true); 4071 const pag = s.querySelector('.swiper-pagination'); 4072 if(pag) pag.innerHTML = ''; 4073 }); 4074 4075 // 2. CLEANUP ALL EDITOR TRASH 4076 deselectBlock(); 4077 wrapper.querySelectorAll('.live-section-badge').forEach(b => b.remove()); 4078 wrapper.querySelectorAll('.iframe-edit-overlay').forEach(el => el.remove()); 4079 4080 wrapper.querySelectorAll('*').forEach(el => { 4081 el.contentEditable = "false"; 4082 el.style.outline = ""; 4083 el.style.outlineOffset = ""; 4084 el.style.cursor = ""; 4085 el.title = ""; 4086 if(el.tagName === 'IMG') el.ondblclick = null; 4087 if(el.tagName === 'A') { 4088 el.removeEventListener('click', preventLinkClick); 4089 el.ondblclick = null; 4090 } 4091 }); 4092 4093 // 3. SAVE TO FIREBASE 4094 const finalHTML = wrapper.innerHTML; 4095 const pageSlug = window.location.pathname.replace(/^\/|\.html$/g, '') || 'home'; 4096 4097 try { 4098 const saveBtn = document.querySelector('.fa-cloud-arrow-up').parentNode; 4099 saveBtn.innerHTML = "<i class='fa-solid fa-spinner fa-spin text-white'></i>"; 4100 4101 await db.collection("live_pages").doc(pageSlug).set({ 4102 htmlContent: finalHTML, updatedAt: new Date() 4103 }); 4104 4105 customToast("Page successfully saved!", "success"); 4106 setTimeout(() => window.location.reload(), 1000); 4107 } catch(e) { 4108 customToast("Error saving: " + e.message, "error"); 4109 } 4110}; 4111 4112// â¡ LOAD SAVED CONTENT INSTANTLY FROM CACHE ON PAGE LOAD 4113document.addEventListener('DOMContentLoaded', async () => { 4114 const wrapper = document.getElementById('live-editable-content'); 4115 if (!wrapper) return; 4116 4117 const pageSlug = window.location.pathname.replace(/^\/|\.html$/g, '') || 'home'; 4118 4119 try { 4120 // Read from local cache first for 0ms instant loading 4121 let doc; 4122 try { 4123 doc = await db.collection("live_pages").doc(pageSlug).get({ source: 'cache' }); 4124 } catch(cacheErr) { 4125 doc = await db.collection("live_pages").doc(pageSlug).get(); 4126 } 4127 4128 if (doc && doc.exists && doc.data().htmlContent) { 4129 wrapper.innerHTML = doc.data().htmlContent; 4130 } 4131 } catch(e) { 4132 console.log("Live page load:", e); 4133 } 4134 4135 // Initialize Swiper Carousel if present 4136 if (document.querySelector('.testimonial-swiper')) { 4137 new Swiper('.testimonial-swiper', { 4138 slidesPerView: 1, spaceBetween: 24, loop: true, 4139 autoplay: { delay: 4000, disableOnInteraction: false }, 4140 pagination: { el: '.swiper-pagination', clickable: true }, 4141 navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }, 4142 breakpoints: { 640: { slidesPerView: 1 }, 768: { slidesPerView: 2 }, 1024: { slidesPerView: 3 } } 4143 }); 4144 } 4145}); 4146 4147 4148/* ========================================== 4149 UNIVERSAL DYNAMIC LIST MANAGER FOR BOOKS (A-Z SORTED) 4150 ========================================== */ 4151 4152const libraryDefaults = { 4153 publishers: ["Alin Tagar School", "Cambridge University Press", "Macmillan Education", "Oxford University Press", "Pearson Education", "Scholastic"], 4154 levels: ["Beginner A1", "Elementary A2", "Intermediate B1+", "Level 1", "Level 2", "Pre-Intermediate B1", "Quick Starter", "Starter (YLE)", "Upper-Intermediate B2"], 4155 types: ["Audiobook", "Coursebook", "Graded Reader", "Grammar Guide", "Workbook"], 4156 cefrs: ["A1 Starter", "A1+ Elementary", "A2 Elementary", "B1 Pre-Intermediate", "B1+ Intermediate", "B2 Upper-Intermediate", "C1 Advanced"], 4157 languages: ["American English", "British English", "International English", "Myanmar & English"], 4158 seriesList: ["Bookworms", "Dominoes", "Express Publishing", "Penguin Readers"], 4159 formats: ["App", "Downloadable Pack", "MP3 Audio", "MP4 Video", "Paperback", "PDF Document", "Software", "Zip Archive"] 4160}; 4161 4162// ð¢ Get Merged List Sorted Alphabetically (A-Z) 4163window.getSortedLibList = function(key) { 4164 const defaults = libraryDefaults[key] || [];
4165 let custom = []; 4166 try { custom = JSON.parse(localStorage.getItem('custom_' + key) || '[]'); } catch(e){} 4167 4168 const merged = Array.from(new Set([...defaults, ...custom])); 4169 merged.sort((a, b) => a.localeCompare(b)); // A-Z Sort 4170 return merged; 4171}; 4172 4173// ð¢ Save Custom Item 4174window.saveCustomLibItem = function(key, typedVal) { 4175 if (!typedVal) return; 4176 let custom = []; 4177 try { custom = JSON.parse(localStorage.getItem('custom_' + key) || '[]'); } catch(e){} 4178 if (!custom.includes(typedVal)) { 4179 custom.push(typedVal); 4180 localStorage.setItem('custom_' + key, JSON.stringify(custom)); 4181 } 4182}; 4183 4184// ð¢ Populate Any Dropdown with A-Z Items + Add New Option 4185window.populateLibDropdown = function(selectId, key, selectedVal = '') { 4186 const selectEl = document.getElementById(selectId); 4187 if (!selectEl) return; 4188 4189 const items = window.getSortedLibList(key); 4190 let label = key.toUpperCase(); 4191 if (key === 'seriesList') label = 'SERIES'; 4192 4193 let html = `<option value="">Select ${label}...</option>`; 4194 4195 items.forEach(item => { 4196 html += `<option value="${item}">${item}</option>`; 4197 }); 4198 html += `<option value="NEW" class="font-bold text-amber-400">+ Add New...</option>`; 4199 4200 selectEl.innerHTML = html; 4201 4202 const inputBox = document.getElementById(`new-${key}-box`); 4203 const inputEl = document.getElementById(`m-book-${key}-new`); 4204 4205 if (selectedVal && items.includes(selectedVal)) { 4206 selectEl.value = selectedVal; 4207 if (inputBox) inputBox.classList.add('hidden'); 4208 } else if (selectedVal) { 4209 selectEl.value = 'NEW'; 4210 if (inputBox) inputBox.classList.remove('hidden'); 4211 if (inputEl) inputEl.value = selectedVal; 4212 } else { 4213 if (inputBox) inputBox.classList.add('hidden'); 4214 } 4215}; 4216 4217// ð¢ Interactive Prompt Handler for "+ Add New..." Dropdown Option 4218window.handleLibDropdownSelect = async function(key, val, selectEl) { 4219 if (val === 'NEW') { 4220 let label = key.toUpperCase(); 4221 if (key === 'seriesList') label = 'SERIES'; 4222 if (key === 'publishers') label = 'PUBLISHER'; 4223 if (key === 'levels') label = 'LEVEL'; 4224 if (key === 'types') label = 'TYPE'; 4225 if (key === 'cefrs') label = 'CEFR LEVEL'; 4226 if (key === 'languages') label = 'LANGUAGE'; 4227 4228 const newItem = await customPrompt(`Add New ${label}`, `Enter new ${label.toLowerCase()} name:`, ""); 4229 4230 if (newItem && newItem.trim() !== '') { 4231 const cleanVal = newItem.trim(); 4232 window.saveCustomLibItem(key, cleanVal); 4233 4234 // Re-populate dropdown with new item included & selected 4235 window.populateLibDropdown(selectEl.id, key, cleanVal); 4236 if (typeof customToast === 'function') customToast(`Added "${cleanVal}" to ${label.toLowerCase()} list!`, "success"); 4237 } else { 4238 // Reset selection if cancelled 4239 selectEl.value = ''; 4240 } 4241 } 4242}; 4243 4244// ð¢ Render Dynamic Format Chip Tags 4245window.renderFormatChips = function(selectedFormats = ['PDF Document']) { 4246 const container = document.getElementById('format-tags-container'); 4247 if (!container) return; 4248 4249 const formats = window.getSortedLibList('formats'); 4250 let html = ''; 4251 4252 formats.forEach(f => { 4253 const isSelected = selectedFormats.includes(f); 4254 const btnClass = isSelected 4255 ? 'format-chip-btn px-3.5 py-2 rounded-xl border text-xs font-bold transition flex items-center gap-1.5 bg-blue-600 text-white border-blue-500' 4256 : 'format-chip-btn px-3.5 py-2 rounded-xl border text-xs font-bold transition flex items-center gap-1.5 bg-slate-900 text-gray-300 border-slate-700'; 4257 4258 html += `<button type="button" onclick="toggleFormatTag(this)" data-format="${f}" class="${btnClass}">${f}</button>`; 4259 }); 4260 4261 // Add "+ Add New Format" button 4262 html += `<button type="button" onclick="addNewFormatTagPrompt()" class="px-3.5 py-2 rounded-xl border border-dashed border-amber-500/60 text-amber-400 hover:bg-amber-500/10 text-xs font-bold transition flex items-center gap-1.5">+ Add New Format</button>`; 4263 4264 container.innerHTML = html; 4265}; 4266 4267window.addNewFormatTagPrompt = async function() { 4268 const newFormat = await customPrompt("Add New Format Tag", "Enter format name (e.g. EPUB, Flashcards, Interactive HTML):", ""); 4269 if (newFormat && newFormat.trim() !== '') { 4270 const cleanVal = newFormat.trim(); 4271 window.saveCustomLibItem('formats', cleanVal); 4272 4273 const selected = []; 4274 document.querySelectorAll('.format-chip-btn.bg-blue-600').forEach(b => selected.push(b.getAttribute('data-format'))); 4275 selected.push(cleanVal); 4276 4277 window.renderFormatChips(selected); 4278 customToast(`Added "${cleanVal}" to formats!`, "success"); 4279 } 4280}; 4281
4282window.toggleFormatTag = function(btn) { 4283 if (btn.classList.contains('bg-blue-600')) { 4284 btn.className = 'format-chip-btn px-3.5 py-2 rounded-xl border text-xs font-bold transition flex items-center gap-1.5 bg-slate-900 text-gray-300 border-slate-700'; 4285 } else { 4286 btn.className = 'format-chip-btn px-3.5 py-2 rounded-xl border text-xs font-bold transition flex items-center gap-1.5 bg-blue-600 text-white border-blue-500'; 4287 } 4288}; 4289 4290// Helper to get final value from dropdown or new input 4291window.getFinalLibValue = function(key, selectId, inputId) { 4292 let val = document.getElementById(selectId)?.value; 4293 if (val === 'NEW') { 4294 const typed = document.getElementById(inputId)?.value.trim(); 4295 if (typed) { 4296 window.saveCustomLibItem(key, typed); 4297 return typed; 4298 } 4299 return ''; 4300 } 4301 return val || ''; 4302}; 4303 4304/* ========================================== 4305 1280px ADMIN MORE BOOK INFO MODAL (SAMPLE 2) 4306 ========================================== */ 4307 4308let activeEditingRow = null; 4309 4310/* ð¢ CORRECT SYNTAX (Single catch block): */ 4311window.openAdminMoreBookInfoModal = function(btn) { 4312 activeEditingRow = btn.closest('.book-item-row'); 4313 if (!activeEditingRow) return; 4314 4315 let extData = {}; 4316 try { 4317 let rawJson = activeEditingRow.getAttribute('data-ext-info') || '{}'; 4318 rawJson = rawJson.replace(/"/g, '"'); 4319 extData = JSON.parse(rawJson); 4320 } catch(e) { 4321 console.error("Error parsing book metadata JSON:", e); 4322 } 4323 4324 // Inject modal HTML if missing 4325 if (!document.getElementById('admin-book-details-modal')) { 4326 const modalHtml = ` 4327 <div id="admin-book-details-modal" class="fixed inset-0 z-[1000005] bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-4 sm:p-6 overflow-y-auto"> 4328 4329 <style> 4330 /* Shortened Inset Dark Scrollbar (20px top & bottom gap) */ 4331 #admin-book-details-content::-webkit-scrollbar { 4332 width: 10px; 4333 } 4334 #admin-book-details-content::-webkit-scrollbar-track { 4335 background: #0f172a; 4336 border-radius: 10px; 4337 border-top: 20px solid transparent; 4338 border-bottom: 20px solid transparent; 4339 background-clip: padding-box; 4340 } 4341 #admin-book-details-content::-webkit-scrollbar-thumb { 4342 background: #334155; 4343 border-radius: 10px; 4344 border: 2px solid transparent; 4345 border-top: 20px solid transparent; 4346 border-bottom: 20px solid transparent; 4347 background-clip: padding-box; 4348 } 4349 #admin-book-details-content::-webkit-scrollbar-thumb:hover { 4350 background: #f59e0b; 4351 background-clip: padding-box; 4352 } 4353 </style> 4354 4355 <div id="admin-book-details-content" class="bg-[#181d2a] border border-slate-800 rounded-3xl p-6 sm:p-8 max-w-[1280px] w-full shadow-2xl space-y-6 text-xs text-white my-auto max-h-[90vh] overflow-y-auto relative"> 4356 4357 <!-- Sticky Header --> 4358 <div class="flex justify-between items-center border-b border-slate-800 pb-4 sticky -top-6 sm:-top-8 bg-[#181d2a] z-30 pt-2"> 4359 <h3 class="text-xl sm:text-2xl font-black text-amber-400"><i class="fa-solid fa-book mr-2"></i> Extended Book Details (Sample 2)</h3> 4360 <button type="button" onclick="document.getElementById('admin-book-details-modal').classList.add('hidden')" class="text-gray-400 hover:text-white text-xl w-10 h-10 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors focus:outline-none"> 4361 <i class="fa-solid fa-xmark"></i> 4362 </button> 4363 </div> 4364 4365 <!-- ð¢ 4-COLUMN ROW 1: Author | Publisher (+ Add) | Level (+ Add) | Type (+ Add)
4365--> 4366 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> 4367 <div> 4368 <label class="block font-bold text-gray-300 uppercase mb-1">Author Name</label> 4369 <input type="text" id="m-book-author" placeholder="e.g. Bill Bowler" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"> 4370 </div> 4371 <div> 4372 <label class="block font-bold text-gray-300 uppercase mb-1">Publisher (AâZ)</label> 4373 <!-- ð¢ FIXED: Added 'this' parameter --> 4374 <select id="m-book-publisher" onchange="handleLibDropdownSelect('publishers', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4375 <div id="new-publishers-box" class="hidden mt-2"> 4376 <input type="text" id="m-book-publishers-new" placeholder="Type new publisher..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4377 </div> 4378 </div> 4379 <div> 4380 <label class="block font-bold text-gray-300 uppercase mb-1">Level (AâZ)</label> 4381 <!-- ð¢ FIXED: Added 'this' parameter --> 4382 <select id="m-book-level" onchange="handleLibDropdownSelect('levels', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4383 <div id="new-levels-box" class="hidden mt-2"> 4384 <input type="text" id="m-book-levels-new" placeholder="Type new level..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4385 </div> 4386 </div> 4387 <div> 4388 <label class="block font-bold text-gray-300 uppercase mb-1">Type (AâZ)</label> 4389 <!-- ð¢ FIXED: Added 'this' parameter --> 4390 <select id="m-book-type" onchange="handleLibDropdownSelect('types', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4391 <div id="new-types-box" class="hidden mt-2"> 4392 <input type="text" id="m-book-types-new" placeholder="Type new type..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4393 </div> 4394 </div> 4395 </div> 4396 4397 <!-- Formats Available Clickable Tags + Add New Format --> 4398 <div> 4399 <label class="block font-bold text-gray-300 uppercase mb-2">Formats Available (Click to Toggle)</label> 4400 <div class="flex flex-wrap gap-2" id="format-tags-container"> 4401 <!-- Rendered Dynamically --> 4402 </div> 4403 </div> 4404 4405 <!-- ð¢ 3-COLUMN ROW 2: CEFR (+ Add) | Language (+ Add) | Series (+ Add) --> 4406 <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> 4407 <div> 4408 <label class="block font-bold text-gray-300 uppercase mb-1">CEFR Level (AâZ)</label> 4409 <!-- ð¢ FIXED: Added 'this' parameter --> 4410 <select id="m-book-cefr" onchange="handleLibDropdownSelect('cefrs', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4411 <div id="new-cefrs-box" class="hidden mt-2"> 4412 <input type="text" id="m-book-cefrs-new" placeholder="Type new CEFR..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4413 </div> 4414 </div> 4415 <div> 4416 <label class="block font-bold text-gray-300 uppercase mb-1">Language (AâZ)</label> 4417 <!-- ð¢ FIXED: Added 'this' parameter --> 4418 <select id="m-book-language" onchange="handleLibDropdownSelect('languages', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4419 <div id="new-languages-box" class="hidden mt-2"> 4420 <input type="text" id="m-book-languages-new" placeholder="Type new language..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4421 </div> 4422 </div> 4423 <div> 4424 <label class="block font-bold text-gray-300 uppercase mb-1">Series (AâZ)</label> 4425 <!-- ð¢ FIXED: Added 'this' parameter --> 4426 <select id="m-book-seriesList" onchange="handleLibDropdownSelect('seriesList', this.value, this)" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"></select> 4427 <div id="new-seriesList-box" class="hidden mt-2"> 4428 <input type="text" id="m-book-seriesList-new" placeholder="Type new series..." class="w-full p-2.5 bg-slate-900 border border-amber-500/50 rounded-xl text-amber-300 text-xs focus:outline-none"> 4429 </div> 4430 </div> 4431 </div> 4432 4433 <!-- ð¢ NEW ROW: ISBN & Published Date (with Calendar Picker) --> 4434 <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> 4435 <div> 4436 <label class="block font-bold text-gray-300 uppercase mb-1">ISBN Number</label> 4437 <input type="text" id="m-book-isbn" placeholder="e.g. 978-0-19-450000-0" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400"> 4438 </div> 4439 <div> 4440 <label class="block font-bold text-gray-300 uppercase mb-1">Published Date (Calendar Picker)</label> 4441 <input type="date" id="m-book-pubdate" onclick="this.showPicker()" class="w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-amber-400 text-xs focus:outline-none focus:border-amber-400 cursor-pointer [color-scheme:dark]"> 4442 </div> 4443 </div> 4444
4445 <!-- ð¢ NEW ROW: Flipbook Viewer Settings --> 4446 <div class="bg-slate-900 p-4 rounded-xl border border-slate-700 flex flex-col sm:flex-row gap-6 mt-2"> 4447 <label class="flex items-center gap-2.5 font-bold text-amber-400 text-xs cursor-pointer"> 4448 <input type="checkbox" id="m-book-enable-download" class="w-4 h-4 rounded text-amber-500 bg-slate-950 border-slate-700 cursor-pointer focus:ring-amber-500 focus:ring-offset-slate-900"> 4449 Enable PDF Download Button 4450 </label> 4451 <label class="flex items-center gap-2.5 font-bold text-amber-400 text-xs cursor-pointer"> 4452 <input type="checkbox" id="m-book-enable-share" class="w-4 h-4 rounded text-amber-500 bg-slate-950 border-slate-700 cursor-pointer focus:ring-amber-500 focus:ring-offset-slate-900"> 4453 Enable Social Share Button 4454 </label> 4455 </div> 4456 4457 <!-- ð¢ 2-VERSION DESCRIPTION EDITOR (VISUAL MODE & HTML MODE) --> 4458 <div class="space-y-2"> 4459 <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 border-b border-slate-800 pb-2"> 4460 <label class="block font-bold text-gray-300 uppercase">Description / Extra Info</label> 4461 4462 <div class="flex items-center gap-2"> 4463 <!-- Version Tab Switcher --> 4464 <div class="flex bg-slate-950 p-1 rounded-xl border border-slate-800 text-[11px] font-bold"> 4465 <button type="button" id="desc-tab-visual" onclick="switchDescTab('visual')" class="px-3 py-1 rounded-lg bg-amber-500 text-slate-950 shadow-sm transition">Visual Version</button> 4466 <button type="button" id="desc-tab-html" onclick="switchDescTab('html')" class="px-3 py-1 rounded-lg text-gray-400 hover:text-white transition">HTML Version</button> 4467 </div> 4468 4469 <!-- Visual Toolbar Buttons --> 4470 <div id="desc-visual-toolbar" class="flex items-center gap-1"> 4471 <button type="button" onclick="formatVisualDesc('bold')" class="px-2.5 py-1 bg-slate-800 hover:bg-amber-500 hover:text-slate-950 text-white rounded-lg font-black text-xs transition" title="Bold">B</button> 4472 <button type="button" onclick="formatVisualDesc('heading')" class="px-2.5 py-1 bg-slate-800 hover:bg-amber-500 hover:text-slate-950 text-white rounded-lg font-black text-xs transition" title="Heading">H (Heading)</button> 4473 <button type="button" onclick="formatVisualDesc('insertUnorderedList')" class="px-2.5 py-1 bg-slate-800 hover:bg-amber-500 hover:text-slate-950 text-white rounded-lg font-bold text-xs transition" title="List">⢠List</button> 4474 </div> 4475 </div> 4476 </div> 4477 4478 <!-- 1. VISUAL EDITOR CONTAINER (ContentEditable Live WYSIWYG) --> 4479 <div id="m-book-desc-visual" contenteditable="true" class="w-full min-h-[130px] max-h-[250px] overflow-y-auto p-3 bg-slate-950 border border-slate-700 rounded-xl text-white text-xs focus:outline-none focus:border-amber-400 leading-relaxed font-sans" placeholder="Type book description visually..."></div> 4480 4481 <!-- 2. HTML CODE EDITOR CONTAINER (Raw Source Code Textarea) --> 4482 <textarea id="m-book-desc-html" rows="6" class="hidden w-full p-3 bg-slate-950 border border-slate-700 rounded-xl text-emerald-400 text-xs focus:outline-none focus:border-amber-400 leading-relaxed font-mono" placeholder="<p>Write raw HTML code here...</p>"></textarea> 4483 </div> 4484 4485 <!-- Sticky Action Buttons --> 4486 <div class="flex gap-3 pt-4 border-t border-slate-800 justify-end sticky -bottom-6 sm:-bottom-8 bg-[#181d2a] z-30 pb-2"> 4487 <button type="button" onclick="document.getElementById('admin-book-detail
4487s-modal').classList.add('hidden')" class="px-5 py-3 bg-slate-800 text-gray-300 font-bold rounded-xl text-xs">Cancel</button> 4488 <button type="button" onclick="saveAdminMoreBookInfo()" class="px-6 py-3 bg-amber-500 hover:bg-amber-400 text-slate-950 font-black rounded-xl text-xs shadow-lg">â Save Book Info</button> 4489 </div> 4490 </div> 4491 </div>`; 4492 document.body.insertAdjacentHTML('beforeend', modalHtml); 4493 } 4494 4495 // Populate all A-Z dropdowns with + Add New 4496 populateLibDropdown('m-book-publisher', 'publishers', extData.publisher || ''); 4497 populateLibDropdown('m-book-level', 'levels', extData.level || ''); 4498 populateLibDropdown('m-book-type', 'types', extData.type || ''); 4499 populateLibDropdown('m-book-cefr', 'cefrs', extData.cefr || ''); 4500 populateLibDropdown('m-book-language', 'languages', extData.language || 'British English'); 4501 populateLibDropdown('m-book-seriesList', 'seriesList', extData.series || ''); 4502 4503 // Render format chips 4504 renderFormatChips(extData.formats || ['PDF Document']); 4505 4506 // Other values 4507 if (document.getElementById('m-book-author')) document.getElementById('m-book-author').value = extData.author || ''; 4508 if (document.getElementById('m-book-isbn')) document.getElementById('m-book-isbn').value = extData.isbn || ''; 4509 if (document.getElementById('m-book-pubdate')) document.getElementById('m-book-pubdate').value = extData.publishedDate || ''; 4510 4511 // ð¢ Load Checkbox States 4512 if (document.getElementById('m-book-enable-download')) document.getElementById('m-book-enable-download').checked = extData.enableDownload === true; 4513 if (document.getElementById('m-book-enable-share')) document.getElementById('m-book-enable-share').checked = extData.enableShare === true; 4514 4515 // Description (Visual & HTML Versions) 4516 const descContent = extData.description || ''; 4517 const visualEl = document.getElementById('m-book-desc-visual'); 4518 const htmlEl = document.getElementById('m-book-desc-html'); 4519 4520 if (visualEl) visualEl.innerHTML = descContent; 4521 if (htmlEl) htmlEl.value = descContent; 4522 4523 // Reset to Visual Version tab by default 4524 if (typeof switchDescTab === 'function') switchDescTab('visual'); 4525 4526 document.getElementById('admin-book-details-modal').classList.remove('hidden'); 4527}; 4528 4529// ð¢ Helper to format text inside Description Textarea 4530window.formatDescText = function(tag) { 4531 const textarea = document.getElementById('m-book-desc'); 4532 if (!textarea) return; 4533 4534 const start = textarea.selectionStart; 4535 const end = textarea.selectionEnd; 4536 const selected = textarea.value.substring(start, end); 4537 4538 let formatted = ''; 4539 if (tag === 'bold') { 4540 formatted = `<strong>${selected || 'Bold Text'}</strong>`; 4541 } else if (tag === 'heading') { 4542 formatted = `<h4 class="text-base font-extrabold text-brandBlue mt-2 mb-1">${selected || 'Larger Heading Title'}</h4>`; 4543 } else if (tag === 'list') { 4544 formatted = `<ul class="list-disc pl-5 my-2"><li>${selected || 'List Item'}</li></ul>`; 4545 } 4546 4547 textarea.value = textarea.value.substring(0, start) + formatted + textarea.value.substring(end); 4548 textarea.focus(); 4549}; 4550 4551window.activeDescTab = 'visual'; 4552 4553// ð¢ Switch between Visual Version & HTML Version 4554window.switchDescTab = function(tab) { 4555 const visualEl = document.getElementById('m-book-desc-visual'); 4556 const htmlEl = document.getElementById('m-book-desc-html'); 4557 const btnVisual = document.getElementById('desc-tab-visual'); 4558 const btnHtml = document.getElementById('desc-tab-html'); 4559 const toolbar = document.getElementById('desc-visual-toolbar'); 4560 4561 if (!visualEl || !htmlEl) return; 4562 4563 if (tab === 'html') { 4564 // Sync Visual -> HTML 4565 htmlEl.value = visualEl.innerHTML; 4566 4567 visualEl.classList.add('hidden'); 4568 htmlEl.classList.remove('hidden'); 4569 if (toolbar) toolbar.classList.add('hidden'); 4570 4571 btnHtml.className = "px-3 py-1 rounded-lg bg-amber-500 text-slate-950 shadow-sm transition"; 4572 btnVisual.className = "px-3 py-1 rounded-lg text-gray-400 hover:text-white transition"; 4573 window.activeDescTab = 'html'; 4574 } else { 4575 // Sync HTML -> Visual 4576 visualEl.innerHTML = htmlEl.value; 4577 4578 htmlEl.classList.add('hidden'); 4579 visualEl.classList.remove('hidden'); 4580 if (toolbar) toolbar.classList.remove('hidden'); 4581 4582 btnVisual.className = "px-3 py-1 rounded-lg bg-amber-500 text-slate-950 shadow-sm transition"; 4583 btnHtml.className = "px-3 py-1 rounded-lg text-gray-400 hover:text-white transition"; 4584 window.activeDescTab = 'visual'; 4585 } 4586}; 4587 4588// ð¢ Visual Toolbar Formatter 4589window.formatVisualDesc = function(command) { 4590 const visualEl = document.getElementById('m-book-desc-visual'); 4591 if (!visualEl) return; 4592 visualEl.focus(); 4593 4594 if (command === 'heading') { 4595 document.execCommand('formatBlock', false, '<h4>'); 4596 } else { 4597 document.execCommand(command, false, null); 4598 } 4599}; 4600 4601// ð¢ Instant Cloud Auto-Save Version 4602window.saveAdminMoreBookInfo = async function() { 4603 if (!activeEditingRow) { 4604 if (typeof customToast === 'function') customToast("Error: No active book row selected.", "error"); 4605 return; 4606 } 4607 4608 const publisherVal = getFinalLibValue('publishers', 'm-book-publisher', 'm-book-publishers-new'); 4609 const levelVal = getFinalLibValue('levels', 'm-book-level', 'm-book-levels-new'); 4610 const typeVal = getFinalLibValue('types', 'm-book-type', 'm-book-types-new'); 4611 const cefrVal = getFinalLibValue('cefrs', 'm-book-cefr', 'm-book-cefrs-new'); 4612 const languageVal = getFinalLibValue('languages', 'm-book-language', 'm-book-languages-new'); 4613 const seriesVal = getFinalLibValue('seriesList', 'm-book-seriesList', 'm-book-seriesList-new'); 4614 4615 const selectedFormats = []; 4616 document.querySelectorAll('.format-chip-btn.bg-blue-600').forEach(btn => { 4617 selectedFormats.push(btn.getAttribute('data-format'));
4618 }); 4619 4620 const visualEl = document.getElementById('m-book-desc-visual'); 4621 const htmlEl = document.getElementById('m-book-desc-html'); 4622 4623 let finalDescription = ''; 4624 if (window.activeDescTab === 'html' && htmlEl) { 4625 finalDescription = htmlEl.value.trim(); 4626 } else if (visualEl) { 4627 finalDescription = visualEl.innerHTML.trim(); 4628 } 4629 4630 const extData = { 4631 author: (document.getElementById('m-book-author')?.value || '').trim(), 4632 isbn: (document.getElementById('m-book-isbn')?.value || '').trim(), 4633 publishedDate: (document.getElementById('m-book-pubdate')?.value || '').trim(), 4634 publisher: publisherVal, 4635 level: levelVal, 4636 type: typeVal, 4637 cefr: cefrVal, 4638 language: languageVal, 4639 series: seriesVal, 4640 formats: selectedFormats.length > 0 ? selectedFormats : ['PDF Document'], 4641 description: finalDescription, 4642 4643 // ð¢ Save Checkbox States 4644 enableDownload: document.getElementById('m-book-enable-download')?.checked || false, 4645 enableShare: document.getElementById('m-book-enable-share')?.checked || false 4646 }; 4647 4648 // 1. Update row attribute locally 4649 activeEditingRow.setAttribute('data-ext-info', JSON.stringify(extData)); 4650 document.getElementById('admin-book-details-modal').classList.add('hidden'); 4651 4652 // 2. ð¢ INSTANT AUTO-SAVE TO CLOUD FIRESTORE 4653 const bsForm = document.getElementById('bookshelf-editor-form'); 4654 if (bsForm) { 4655 // Automatically triggers form submission directly to Cloud Firestore! 4656 bsForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); 4657 } else { 4658 if (typeof customToast === 'function') customToast("Book details saved successfully!", "success"); 4659 } 4660}; 4661 4662// ð¢ USER ACCESS MANAGER LOGIC 4663let activeAccessTargetType = null; 4664let activeAccessTargetInput = null; 4665let loadedAccessUsers = []; 4666 4667window.openAccessManager = async function(type, btn = null) { 4668 activeAccessTargetType = type; 4669 4670 if (type === 'shelf') { 4671 document.getElementById('access-modal-title').innerHTML = '<i class="fa-solid fa-users-lock mr-2"></i> Manage Entire Shelf Access'; 4672 activeAccessTargetInput = document.getElementById('bs-allowed-users'); 4673 } else { 4674 document.getElementById('access-modal-title').innerHTML = '<i class="fa-solid fa-users-lock mr-2"></i> Manage Specific Book Access'; 4675 const row = btn.closest('.book-item-row'); 4676 activeAccessTargetInput = row.querySelector('.book-input-access'); 4677 } 4678 4679 document.getElementById('user-access-modal').classList.remove('hidden'); 4680 4681 const selectAllCb = document.getElementById('access-select-all-cb'); 4682 if (selectAllCb) selectAllCb.checked = false; 4683 4684 const listEl = document.getElementById('access-users-list'); 4685 const requireLoginCb = document.getElementById('access-require-login-cb'); 4686 4687 // Parse currently allowed users 4688 let currentAllowed = []; 4689 try { currentAllowed = JSON.parse(activeAccessTargetInput.value || '[]'); } catch(e){} 4690 4691 // ð¢ NEW: If "LOGGED_IN_USERS_ONLY" is in the array, check the box! 4692 if (requireLoginCb) { 4693 requireLoginCb.checked = currentAllowed.includes('LOGGED_IN_USERS_ONLY'); 4694 } 4695 4696 try { 4697 if (loadedAccessUsers.length === 0) { 4698 const snap = await db.collection("users").get(); 4699 snap.forEach(doc => loadedAccessUsers.push({ id: doc.id, ...doc.data() })); 4700 } 4701 4702 let html = ''; 4703 loadedAccessUsers.forEach(u => { 4704 const isChecked = currentAllowed.includes(u.id) ? 'checked' : ''; 4705 html += ` 4706 <label class="access-user-item flex items-center justify-between p-3 bg-slate-900 border border-slate-700 rounded-xl cursor-pointer hover:border-indigo-500 transition"> 4707 <div> 4708 <p class="text-white text-xs font-bold user-name">${u.fullName || 'Unknown User'}</p> 4709 <p class="text-gray-400 text-[10px] user-email">${u.email || u.displayEmail || 'No Email'}</p> 4710 </div> 4711 <input type="checkbox" value="${u.id}" class="user-access-cb w-4 h-4 rounded text-indigo-600 bg-slate-950 border-slate-600" ${isChecked}> 4712 </label>`; 4713 }); 4714 listEl.innerHTML = html; 4715 } catch(e) { 4716 listEl.innerHTML = `<p class="text-red-400 text-xs text-center">Error loading users.</p>`; 4717 } 4718}; 4719 4720window.closeAccessManager = function() { 4721 document.getElementById('user-access-modal').classList.add('hidden'); 4722}; 4723
4724window.saveAccessManager = function() { 4725 const checkboxes = document.querySelectorAll('.user-access-cb:checked'); 4726 const selectedIds = Array.from(checkboxes).map(cb => cb.value); 4727 4728 // ð¢ NEW: Add the special flag if the require login box is checked 4729 const requireLoginCb = document.getElementById('access-require-login-cb'); 4730 if (requireLoginCb && requireLoginCb.checked) { 4731 selectedIds.push('LOGGED_IN_USERS_ONLY'); 4732 } 4733 4734 if (activeAccessTargetInput) { 4735 activeAccessTargetInput.value = JSON.stringify(selectedIds); 4736 activeAccessTargetInput.setAttribute('value', JSON.stringify(selectedIds)); 4737 } 4738 4739 customToast(`Access settings saved!`, "success"); 4740 closeAccessManager(); 4741}; 4742 4743window.filterAccessUsers = function() { 4744 const query = document.getElementById('access-user-search').value.toLowerCase(); 4745 document.querySelectorAll('.access-user-item').forEach(item => { 4746 const name = item.querySelector('.user-name').innerText.toLowerCase(); 4747 const email = item.querySelector('.user-email').innerText.toLowerCase(); 4748 item.style.display = (name.includes(query) || email.includes(query)) ? 'flex' : 'none'; 4749 }); 4750}; 4751 4752// ð¢ NEW: SELECT ALL VISIBLE USERS LOGIC 4753window.toggleAllAccessUsers = function(source) { 4754 const checkboxes = document.querySelectorAll('.user-access-cb'); 4755 checkboxes.forEach(cb => { 4756 const parentLabel = cb.closest('.access-user-item'); 4757 // Only check/uncheck if the user is currently visible in the search list 4758 if (parentLabel && parentLabel.style.display !== 'none') { 4759 cb.checked = source.checked; 4760 } 4761 }); 4762}; 4763 4764/* ========================================== 4765 ATVEO-STYLE CENTRALIZED PAYMENT PANEL 4766 ========================================== */ 4767window.allPaymentMethods = []; 4768window.activePaymentTab = 'methods'; 4769 4770window.openFullscreenPaymentManager = function() { 4771 const existingWs = document.getElementById('admin-fullscreen-workspace'); 4772 if (existingWs) existingWs.remove(); 4773 4774 const workspaceHTML = ` 4775 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 4776 4777 <!-- Top Bar --> 4778 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl flex-shrink-0 z-50"> 4779 <div class="flex items-center gap-3"> 4780 <div class="w-9 h-9 bg-sky-500 text-white rounded-xl flex items-center justify-center font-black text-lg"> 4781 <i class="fa-solid fa-credit-card"></i> 4782 </div> 4783 <div> 4784 <h2 class="font-black text-lg text-white leading-none">Payment Control & Transactions</h2> 4785 <p class="text-[11px] text-gray-400">Manage global payment methods and review student transaction ledgers.</p> 4786 </div> 4787 </div> 4788 4789 <button onclick="document.getElementById('admin-fullscreen-workspace').remove()" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 4790 â Close Panel 4791 </button> 4792 </div> 4793 4794 <!-- Dynamic Header Switch Tabs --> 4795 <div class="bg-slate-900 border-b border-slate-800 h-12 flex items-center px-8 gap-4 flex-shrink-0"> 4796 <button onclick="switchPaymentPanelTab('methods')" id="pay-tab-methods" class="h-full px-4 border-b-2 border-sky-500 text-sky-400 text-xs font-bold uppercase tracking-wider transition focus:outline-none"> 4797 ð³ Payment Methods 4798 </button> 4799 <button onclick="switchPaymentPanelTab('transactions')" id="pay-tab-transactions" class="h-full px-4 border-b-2 border-transparent text-gray-400 hover:text-white text-xs font-bold uppercase tracking-wider transition focus:outline-none"> 4800 ð Transaction History Ledger 4801 </button> 4802 </div> 4803 4804 <!-- Main Workspace Area --> 4805 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center items-start"> 4806 <div class="w-full max-w-[1280px]"> 4807 4808 <!-- TAB 1: PAYMENT METHOD MANAGER --> 4809 <div id="pay-view-methods" class="space-y-6"> 4810 <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900 border border-slate-800 p-6 rounded-3xl"> 4811 <div> 4812 <h3 class="text-xl font-black text-white">Centralized Payment Settings</h3> 4813 <p class="text-xs text-gray-400">Configured methods are automatically loaded into site checkout forms.</p> 4814 </div> 4815 <button onclick="openPaymentMethodFormModal()" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-black px-4 py-2.5 rounded-xl text-xs transition shadow-lg flex items-center gap-1.5 self-start sm:self-ce
4815nter"> 4816 <i class="fa-solid fa-plus"></i> Add Payment Method 4817 </button> 4818 </div> 4819 4820 <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> 4821 <!-- Left Container: Online Methods --> 4822 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl space-y-4"> 4823 <h4 class="text-sm font-black text-amber-400 uppercase tracking-wider border-b border-slate-800 pb-2 flex items-center gap-2"> 4824 <i class="fa-solid fa-globe"></i> Online Payment gateways 4825 </h4> 4826 <div id="online-gateways-list" class="space-y-3"> 4827 <p class="text-xs text-gray-500 italic py-4 text-center">Loading gateways...</p> 4828 </div> 4829 </div> 4830 4831 <!-- Right Container: Offline Methods --> 4832 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl space-y-4"> 4833 <h4 class="text-sm font-black text-sky-400 uppercase tracking-wider border-b border-slate-800 pb-2 flex items-center gap-2"> 4834 <i class="fa-solid fa-wallet"></i> Offline & Local Mobile Transfers 4835 </h4> 4836 <div id="offline-transfers-list" class="space-y-3"> 4837 <p class="text-xs text-gray-500 italic py-4 text-center">Loading mobile transfers...</p> 4838 </div> 4839 </div> 4840 </div> 4841 </div> 4842 4843 <!-- TAB 2: TRANSACTION LEDGER --> 4844 <div id="pay-view-transactions" class="hidden space-y-6"> 4845 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 md:p-8 shadow-2xl space-y-6"> 4846 <div> 4847 <h3 class="text-xl font-black text-white mb-1">Dynamic Transaction Ledger</h3> 4848 <p class="text-xs text-gray-400">Review enrolments, verify transaction reference numbers (TxID), and approve access.</p> 4849 </div> 4850 4851 <div class="overflow-x-auto"> 4852 <table class="w-full text-left text-xs border-collapse"> 4853 <thead> 4854 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 pb-3"> 4855 <th class="py-3 px-4">Student Info</th> 4856 <th class="py-3 px-4">Course Package</th> 4857 <th class="py-3 px-4">Method / Transaction ID</th> 4858 <th class="py-3 px-4">Date</th> 4859 <th class="py-3 px-4">Status</th> 4860 <th class="py-3 px-4 text-center">Actions</th> 4861 </tr> 4862 </thead> 4863 <tbody id="pay-ledger-table-body" class="divide-y divide-slate-800/80"> 4864 <tr> 4865 <td colspan="6" class="py-8 text-center text-gray-500 italic">Loading ledger transactions...</td> 4866 </tr> 4867 </tbody> 4868 </table> 4869 </div> 4870 </div> 4871 4872 </div> 4873 </div> 4874 </div> 4875 4876 <!-- ð¢ DYNAMIC PAYMENT METHOD FORM MODAL --> 4877 <div id="payment-method-form-modal" class="fixed inset-0 z-[1000000] bg-black/80 backdrop-blur-xs hidden flex items-center justify-center p-4"> 4878 <div class="bg-[#181d2a] border border-slate-800 rounded-3xl p-6 sm:p-8 max-w-2xl w-full shadow-2xl space-y-6 text-xs text-white"> 4879 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 4880 <h3 class="text-lg font-black text-sky-400" id="method-modal-title">+ Add Payment Method</h3> 4881 <button type="button" onclick="closePaymentMethodFormModal()" class="text-gray-400 hover:text-white text-lg w-8 h-8 rounded-full hover:bg-slate-800 flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark"></i></button> 4882 </div> 4883 4884 <form id="payment-method-editor-form" class="space-y-4" onsubmit="savePaymentMethod(event)"> 4885 <input type="hidden" id="method-edit-id" value=""> 4886 4887 <div class="grid grid-cols-2 gap-3"> 4888 <div class="control-group"> 4889 <label class="block font-bold text-gray-300 uppercase mb-1">Method Type *</label> 4890 <select id="method-type" required onchange="togglePaymentFormFields(this.value)" class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4891 <option value="offline">Offline / Local Transfer</option> 4892 <option value="online">Online Payment Gateway</option> 4893 </select> 4894 </div> 4895 <div class="control-group"> 4896 <label class="block font-bold text-gray-300 uppercase mb-1">Method Name *</label> 4897 <input type="text" id="method-name" placeholder="e.g. KBZ Pay" required class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4898 </div> 4899 </div> 4900 4901 <div class="control-group"> 4902 <label class="block font-bold text-gray-300 uppercase mb-1">Official Logo URL</label> 4903 <input type="text" id="method-logo" placeholder="https://... (Leave empty to auto-detect)" class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4904 </div> 4905 4906 <!-- Fields for Offline Type --> 4907 <div id="method-offline-fields" class="space-y-4"> 4908 <div class="grid grid-cols-2 gap-3"> 4909 <div class="control-group"> 4910 <label class="block font-bold text-gray-300 uppercase mb-1">Account Holder Name *</label> 4911 <input type="text" id="method-account-name" placeholder="e.g. Saya Victor" class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4912 </div> 4913 <div class="control-group"> 4914 <label class="block font-bold text-gray-300 uppercase mb-1">Account / Phone Number *</label> 4915 <input type="text" id="method-account-number" placeholder="e.g. 09781010356" class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4916 </div> 4917 </div> 4918 <div class="control-group"> 4919 <label class="block font-bold text-gray-300 uppercase mb-1">Instructions / Note</label> 4920 <textarea id="method-note" rows="2" placeholder="e.g. Please transfer the fee and take a screenshot." class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"></textarea> 4921 </div> 4922 </div> 4923 4924 <!-- Fields for Online Type --> 4925 <div id="method-online-fields" class="hidden space-y-4"> 4926 <div class="control-group"> 4927 <label class="block font-bold text-gray-300 uppercase mb-1">PayPal Merchant Email *</label> 4928 <input type="email" id="method-paypal-email" placeholder="e.g. [email protected]" class="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-xl text-white outline-none"> 4929 </div> 4930 </div> 4931 4932 <div class="pt-4 border-t border-slate-800 flex justify-end gap-2"> 4933 <button type="button" onclick="closePaymentMethodFormModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-gray-300 font-bold rounded-xl transition">Cancel</button> 4934 <button type="submit" class="px-5 py-2 bg-sky-600 hover:bg-sky-500 text-white font-black rounded-xl shadow-lg transition">Save Method</button> 4935 </div> 4936 </form> 4937 </div> 4938 </div> 4939 `; 4940 4941 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 4942 loadPaymentMethodsData(); 4943 loadTransactionsLedger(); 4944}; 4945 4946window.switchPaymentPanelTab = function(tab) { 4947 const tabMethods = document.getElementById('pay-tab-methods'); 4948 const tabTrans = document.getElementById('pay-tab-transactions'); 4949 const viewMethods = document.getElementById('pay-view-methods'); 4950 const viewTrans = document.getElementById('pay-view-transactions'); 4951 4952 if (tab === 'methods') { 4953 tabMethods.className = 'h-full px-4 border-b-2 border-sky-500 text-sky-400 text-xs font-bold uppercase tracking-wider transition focus:outline-none'; 4954 tabTrans.className = 'h-full px-4 border-b-2 border-transparent text-gray-400 hover:text-white text-xs font-bold uppercase tracking-wider transition focus:outline-none'; 4955 viewMethods.classList.remove('hidden'); 4956 viewTrans.classList.add('hidden'); 4957 } else { 4958 tabTrans.className = 'h-full px-4 border-b-2 border-sky-500 text-sky-400 text-xs font-bold uppercase tracking-wider transition focus:outline-none'; 4959 tabMethods.className = 'h-full px-4 border-b-2 border-transparent text-gray-400 hover:text-white text-xs font-bold uppercase tracking-wider transition focus:outline-none'; 4960 viewTrans.classList.remove('hidden'); 4961 viewMethods.classList.add('hidden'); 4962 } 4963}; 4964 4965window.togglePaymentFormFields = function(type) { 4966 const offBlock = document.getElementById('method-offline-fields'); 4967 const onBlock = document.getElementById('method-online-fields'); 4968 if (type === 'online') { 4969 onBlock.classList.remove('hidden'); 4970 offBlock.classList.add('hidden'); 4971 } else { 4972 offBlock.classList.remove('hidden'); 4973 onBlock.classList.add('hidden'); 4974 } 4975}; 4976 4977window.openPaymentMethodFormModal = function(editId = null) { 4978 const modal = document.getElementById('payment-method-form-modal'); 4979 const form = document.getElementById('payment-method-editor-form'); 4980 const titleEl = document.getElementById('method-modal-title'); 4981 const idInput = document.getElementById('method-edit-id'); 4982 4983 form.reset(); 4984 idInput.value = editId || ''; 4985 titleEl.innerText = editId ? 'Edit Payment Method' : '+ Add Payment Method'; 4986 togglePaymentFormFields('offline'); 4987 4988 if (editId) { 4989 const item = window.allPaymentMethods.find(p => p.id === editId); 4990 if (item) { 4991 document.getElementById('method-type').value = item.type; 4992 document.getElementById('method-name').value = item.name; 4993 document.getElementById('method-logo').value = item.logo || ''; 4994 togglePaymentFormFields(item.type); 4995 4996 if (item.type === 'online') { 4997 document.getElementById('method-paypal-email').value = item.paypalEmail || ''; 4998 } else { 4999 document.getElementById('method-account-name').value = item.accountName || ''; 5000 document.getElementById('method-account-number').value = item.accountNumber || ''; 5001 document.getElementById('method-note').value = item.note || ''; 5002 } 5003 } 5004 } 5005 5006 modal.classList.remove('hidden'); 5007}; 5008 5009window.closePaymentMethodFormModal = function() { 5010 document.getElementById('payment-method-form-modal').classList.add('hidden'); 5011}; 5012 5013window.loadPaymentMethodsData = function() { 5014 const onlineList = document.getElementById('online-gateways-list'); 5015 const offlineList = document.getElementById('offline-transfers-list'); 5016 if (!onlineList || !offlineList) return; 5017 5018 db.collection("payment_methods").orderBy("createdAt", "asc").onSnapshot(snap => { 5019 window.allPaymentMethods = []; 5020 let onlineHtml = ''; 5021 let offlineHtml = ''; 5022
5023 snap.forEach(doc => { 5024 const data = doc.data(); 5025 data.id = doc.id; 5026 window.allPaymentMethods.push(data); 5027 5028 const resolvedLogo = data.logo || getPaymentMethodLogo(data.name); 5029 const statusColor = data.enabled ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' : 'bg-slate-800 text-gray-500 border-slate-700'; 5030 const statusLabel = data.enabled ? 'Enabled' : 'Disabled'; 5031 5032 const cardHtml = ` 5033 <div class="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl flex items-center justify-between gap-4"> 5034 <div class="flex items-center gap-3 min-w-0"> 5035 <img src="${resolvedLogo}" class="w-10 h-10 object-contain rounded-lg flex-shrink-0" onerror="this.src='https://img.icons8.com/color/100/bank-card.png'"> 5036 <div class="min-w-0"> 5037 <h5 class="font-bold text-white text-xs truncate">${data.name}</h5> 5038 <p class="text-[10px] text-gray-500 truncate">${data.type === 'online' ? (data.paypalEmail || 'PayPal Gateway') : (data.accountNumber || 'Transfer details')}</p> 5039 </div> 5040 </div> 5041 <div class="flex items-center gap-2"> 5042 <button onclick="togglePaymentMethodStatus('${data.id}', ${data.enabled})" class="border px-2.5 py-1 rounded-full text-[10px] font-black uppercase transition ${statusColor}"> 5043 ${statusLabel} 5044 </button> 5045 <button onclick="openPaymentMethodFormModal('${data.id}')" class="text-sky-400 hover:text-sky-300 p-1.5"><i class="fa-solid fa-pen-to-square"></i></button> 5046 <button onclick="deletePaymentMethod('${data.id}')" class="text-red-500 hover:text-red-400 p-1.5"><i class="fa-solid fa-trash-can"></i></button> 5047 </div> 5048 </div>`; 5049 5050 if (data.type === 'online') { 5051 onlineHtml += cardHtml; 5052 } else { 5053 offlineHtml += cardHtml; 5054 } 5055 }); 5056 5057 onlineList.innerHTML = onlineHtml || '<p class="text-xs text-gray-500 italic text-center py-4">No online payment gateways configured.</p>'; 5058 offlineList.innerHTML = offlineHtml || '<p class="text-xs text-gray-500 italic text-center py-4">No offline transfer methods configured.</p>'; 5059 }); 5060}; 5061 5062window.savePaymentMethod = async function(e) { 5063 e.preventDefault(); 5064 const id = document.getElementById('method-edit-id').value; 5065 const type = document.getElementById('method-type').value; 5066 const name = document.getElementById('method-name').value.trim(); 5067 const logo = document.getElementById('method-logo').value.trim(); 5068 5069 let payData = { 5070 type, 5071 name, 5072 logo, 5073 enabled: true, 5074 updatedAt: new Date() 5075 }; 5076 5077 if (type === 'online') { 5078 payData.paypalEmail = document.getElementById('method-paypal-email').value.trim(); 5079 } else { 5080 payData.accountName = document.getElementById('method-account-name').value.trim(); 5081 payData.accountNumber = document.getElementById('method-account-number').value.trim(); 5082 payData.note = document.getElementById('method-note').value.trim(); 5083 } 5084 5085 try { 5086 const docId = id || name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); 5087 if (!id) payData.createdAt = new Date(); 5088 5089 await db.collection("payment_methods").doc(docId).set(payData, { merge: true }); 5090 closePaymentMethodFormModal(); 5091 customToast("Payment method saved globally!", "success"); 5092 } catch(err) { 5093 customToast(err.message, "error"); 5094 } 5095}; 5096 5097window.togglePaymentMethodStatus = async function(id, currentStatus) { 5098 try { 5099 await db.collection("payment_methods").doc(id).update({ enabled: !currentStatus }); 5100 customToast(`Payment method ${!currentStatus ? 'enabled' : 'disabled'} globally!`, "success"); 5101 } catch(e) { 5102 customToast(e.message, "error"); 5103 } 5104}; 5105 5106window.deletePaymentMethod = async function(id) { 5107 const confirmed = await customConfirm("Delete Method", "Are you sure you want to permanently delete this payment option? It will be removed from your checkout popup."); 5108 if (confirmed) { 5109 try { 5110 await db.collection("payment_methods").doc(id).delete(); 5111 customToast("Payment method deleted.", "info"); 5112 } catch(e) { 5113 customToast(e.message, "error"); 5114 } 5115 } 5116}; 5117 5118window.loadTransactionsLedger = function() { 5119 const tbody = document.getElementById('pay-ledger-table-body'); 5120 if (!tbody) return; 5121 5122 db.collection("course_registrations").orderBy("createdAt", "desc").onSnapshot(snap => { 5123 if (snap.empty) { 5124 tbody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-gray-500 italic">No transactions or course enrolments logged.</td></tr>`; 5125 return; 5126 } 5127 5128 let html = '';
5129 snap.forEach(doc => { 5130 const reg = doc.data(); 5131 const docId = doc.id; 5132 5133 let dateStr = "Recently"; 5134 if (reg.createdAt && typeof reg.createdAt.toDate === 'function') { 5135 const d = reg.createdAt.toDate(); 5136 dateStr = d.toLocaleDateString('en-GB') + ' ' + d.toLocaleTimeString('en-US', { hour12: false, hour: 'numeric', minute: '2-digit' }); 5137 } 5138 5139 const isApproved = reg.status === 'Approved'; 5140 const isRejected = reg.status === 'Rejected'; 5141 5142 const statusBadge = isApproved 5143 ? `<span class="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Approved</span>` 5144 : isRejected 5145 ? `<span class="bg-red-500/20 text-red-400 border border-red-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase">Rejected</span>` 5146 : `<span class="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-black px-2.5 py-1 rounded-full uppercase animate-pulse">Pending Review</span>`; 5147 5148 html += ` 5149 <tr class="hover:bg-slate-900/50 transition border-b border-slate-800/50"> 5150 <td class="py-4 px-4 align-middle"> 5151 <div class="flex items-center gap-3"> 5152 <div class="w-8 h-8 rounded-full bg-blue-600 text-white font-bold flex items-center justify-center text-xs flex-shrink-0"> 5153 ${(reg.studentName || 'S')[0].toUpperCase()} 5154 </div> 5155 <div> 5156 <p class="font-bold text-white text-xs">${reg.studentName || 'Student'}</p> 5157 <p class="text-[10px] text-gray-400">${reg.studentEmail || ''}</p> 5158 </div> 5159 </div> 5160 </td> 5161 <td class="py-4 px-4 align-middle text-gray-200 font-medium">${reg.courseName || 'N/A'}</td> 5162 <td class="py-4 px-4 align-middle text-gray-300 font-mono select-all hover:text-amber-400 transition cursor-pointer" title="Double click to copy Transaction detail">${reg.paymentMethod || 'Local Transfer'}</td> 5163 <td class="py-4 px-4 align-middle text-gray-400 font-mono">${dateStr}</td> 5164 <td class="py-4 px-4 align-middle">${statusBadge}</td> 5165 <td class="py-4 px-4 align-middle text-center"> 5166 <div class="flex items-center justify-center gap-1.5"> 5167 ${!isApproved ? `<button onclick="updatePaymentLedgerStatus('${docId}', 'Approved')" class="bg-emerald-600 hover:bg-emerald-500 text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Approve</button>` : ''} 5168 ${!isRejected ? `<button onclick="updatePaymentLedgerStatus('${docId}', 'Rejected')" class="bg-slate-800 hover:bg-red-600 text-gray-300 hover:text-white font-extrabold px-2.5 py-1.5 rounded-lg transition text-[11px]">Reject</button>` : ''} 5169 </div> 5170 </td> 5171 </tr>`; 5172 }); 5173 5174 tbody.innerHTML = html; 5175 }, err => { 5176 tbody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-red-400 italic">Error loading transactions: ${err.message}</td></tr>`; 5177 }); 5178}; 5179 5180window.updatePaymentLedgerStatus = async function(docId, newStatus) { 5181 try { 5182 await db.collection("course_registrations").doc(docId).update({ 5183 status: newStatus, 5184 reviewedAt: new Date() 5185 }); 5186 customToast(`Enrolment set to '${newStatus}'!`, "success"); 5187 } catch(e) { 5188 customToast(e.message, "error"); 5189 } 5190}; 5191 5192 5193//===================================// 5194//--- DATABASE BACKUP AND RESTORE ---// 5195//===================================// 5196window.openFullscreenBackupManager = function() { 5197 const existingWs = document.getElementById('admin-fullscreen-workspace'); 5198 if (existingWs) existingWs.remove(); 5199 5200 const workspaceHTML = ` 5201 <div id="admin-fullscreen-workspace" class="fixed inset-0 z-[999999] bg-[#0b0f19] text-white flex flex-col font-sans overflow-hidden"> 5202 5203 <!-- Top Bar --> 5204 <div class="bg-slate-950 text-gray-300 h-14 px-8 flex items-center justify-between border-b border-slate-800 shadow-xl flex-shrink-0"> 5205 <div class="flex items-center gap-3"> 5206 <div class="w-9 h-9 bg-teal-600 text-white rounded-xl flex items-center justify-center font-black text-lg"> 5207 <i class="fa-solid fa-database"></i> 5208 </div> 5209 <div> 5210 <h2 class="font-black text-lg text-white leading-none">Universal Database Backup & Restore Suite</h2> 5211 <p class="text-[11px] text-gray-400">Granular partial export, selective restore, and clean database wipeout.</p> 5212 </div> 5213 </div> 5214 5215 <button onclick="document.getElementById('admin-fullscreen-workspace').remove(); document.body.classList.remove('overflow-hidden');" class="bg-red-600/20 text-red-400 border border-red-500/30 px-5 py-2 rounded-xl text-xs font-bold hover:bg-red-600/40 transition"> 5216 â Close Panel 5217 </button> 5218 </div> 5219 5220 <!-- Main Panel Container (1280px Max Width - 2 Column Grid) --> 5221 <div class="flex-1 overflow-y-auto p-6 md:p-10 flex justify-center items-start"> 5222 <div class="w-full max-w-[1280px] grid grid-cols-1 lg:grid-cols-12 gap-8"> 5223 5224 <!-- LEFT COLUMN: Collections Table of Contents with Checkboxes (7 Cols) --> 5225 <div class="lg:col-span-7 bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl flex flex-col space-y-4"> 5226 <div class="flex justify-between items-center border-b border-slate-800 pb-3"> 5227 <div class="flex items-center gap-2"> 5228 <h3 class="font-black text-sm text-white flex items-center gap-2"> 5229 <i class="fa-solid fa-table-cells text-teal-400"></i> Collections Selector & Total Sizes 5230 </h3> 5231 </div>
5232 <span id="total-db-size-badge" class="text-xs bg-slate-800 text-teal-400 border border-slate-700 px-3 py-1 rounded-full font-bold">Total Footprint: Calculating...</span> 5233 </div> 5234 <p class="text-[11px] text-gray-400 leading-relaxed"> 5235 Check or uncheck specific tables below to include or exclude them from your <strong>Partial Backup</strong> or <strong>Partial Restore</strong> operations. 5236 </p> 5237 5238 <!-- Expanded table view without harsh internal cutoffs --> 5239 <div class="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/50"> 5240 <table class="w-full text-left text-xs border-collapse"> 5241 <thead> 5242 <tr class="text-gray-400 font-extrabold uppercase border-b border-slate-800 bg-slate-950"> 5243 <!-- ð¢ Master Checkbox moved into the header column --> 5244 <th class="py-3.5 px-4 w-12 text-center"> 5245 <input type="checkbox" id="backup-select-all-cb" checked onchange="toggleAllBackupCollections(this)" class="w-4 h-4 rounded text-teal-600 bg-slate-900 border-slate-700 cursor-pointer" title="Select All"> 5246 </th> 5247 <th class="py-3.5 px-4">Collection Name</th> 5248 <th class="py-3.5 px-4 text-center">Records (Size)</th> 5249 <th class="py-3.5 px-4 text-right">Status</th> 5250 </tr> 5251 </thead> 5252 <tbody id="backup-schema-table-body" class="divide-y divide-slate-800/60"> 5253 <tr> 5254 <td colspan="4" class="py-8 text-center text-gray-400 italic">Scanning database schema...</td> 5255 </tr> 5256 </tbody> 5257 </table> 5258 </div> 5259 </div> 5260 5261 <!-- RIGHT COLUMN: Database Management Actions (5 Cols) --> 5262 <div class="lg:col-span-5 space-y-6"> 5263 5264 <!-- Action Card 1: Partial / Full Export --> 5265 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl space-y-4"> 5266 <h3 class="text-base font-black text-white flex items-center gap-2 border-b border-slate-800 pb-3"> 5267 <i class="fa-solid fa-cloud-arrow-down text-teal-400"></i> Export Selected Data 5268 </h3> 5269 <p class="text-xs text-gray-400 leading-relaxed"> 5270 Downloads a <code>.json</code> file containing only the checked collections on the left. 5271 </p> 5272 <button onclick="executeDynamicDatabaseExport()" class="w-full bg-teal-600 hover:bg-teal-500 text-white font-black py-3 rounded-xl text-xs transition shadow-lg flex items-center justify-center gap-2"> 5273 <i class="fa-solid fa-download"></i> Download Partial/Full Backup (.json) 5274 </button> 5275 </div> 5276 5277 <!-- Action Card 2: Partial / Full Restore --> 5278 <div class="bg-[#131927] border border-slate-800 rounded-3xl p-6 shadow-2xl space-y-4"> 5279 <h3 class="text-base font-black text-white flex items-center gap-2 border-b border-slate-800 pb-3"> 5280 <i class="fa-solid fa-cloud-arrow-up text-amber-400"></i> Restore Selected Data 5281 </h3> 5282 <p class="text-xs text-gray-400 leading-relaxed"> 5283 Upload a backup file. Only the checked collections on the left will be restored into Firestore. 5284 </p> 5285 <input type="file" id="restore-json-file-input" accept=".json" class="w-full text-xs text-gray-400 file:mr-4 file:py-2.5 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-bold file:bg-slate-800 file:text-white hover:file:bg-slate-700 cursor-pointer mb-2"> 5286 <button onclick="executeDynamicDatabaseRestore()" class="w-full bg-amber-500 hover:bg-amber-400 text-slate-950 font-black py-3 rounded-xl text-xs transition shadow-lg flex items-center justify-center gap-2"> 5287 <i class="fa-solid fa-upload"></i> Restore Selected Collections 5288 </button> 5289 </div> 5290 5291 <!-- Action Card 3: Danger Zone - Database Wipeout --> 5292 <div class="bg-slate-950 border border-red-900/40 rounded-3xl p-6 shadow-2xl space-y-4"> 5293 <h3 class="text-base font-black text-red-400 flex items-center gap-2 border-b border-slate-800 pb-3"> 5294 <i class="fa-solid fa-triangle-exclamation"></i> Danger Zone: Database Wipeout 5295 </h3> 5296 <p class="text-xs text-gray-400 leading-relaxed"> 5297 Permanently delete all documents inside the checked collections on the left. This action cannot be undone! 5298 </p> 5299 <button onclick="executeDatabaseWipeout()" class="w-full bg-red-600 hover:bg-red-500 text-white font-black py-3 rounded-xl text-xs transition shadow-lg flex items-center justify-center gap-2"> 5300 <i class="fa-solid fa-skull"></i> Wipe Out Selected Collections 5301 </button> 5302 </div> 5303 5304 </div> 5305 5306 </div> 5307 </div> 5308 </div> 5309 `; 5310 5311 document.body.insertAdjacentHTML('beforeend', workspaceHTML); 5312 document.body.classList.add('overflow-hidden'); 5313 loadBackupSchemaTable(); 5314}; 5315 5316const collectionsMeta = [ 5317 { name: "users", desc: "User accounts & roles" }, 5318 { name: "site_builder_pages", desc: "Custom visual builder pages" }, 5319 { name: "blog_posts", desc: "Articles & event posts" }, 5320 { name: "library_bookshelves", desc: "Digital library 3D shelves" }, 5321 { name: "course_registrations", desc: "Student course enrollments" }, 5322 { name: "site_comments", desc: "Community reviews & ratings" }, 5323 { name: "payment_methods", desc: "Payment gateways" }, 5324 { name: "site_settings", desc: "Navigation menus & settings" }, 5325 { name: "site_seo", desc: "Sitemap XML strings" }, 5326 { name: "live_pages", desc: "Cached live edits" }, 5327 { name: "email_queue", desc: "Notification emails queue" }, 5328 { name: "contact_messages", desc: "Contact inquiries" } 5329]; 5330 5331async function loadBackupSchemaTable() { 5332 const tbody = document.getElementById('backup-schema-table-body'); 5333 const sizeBadge = document.getElementById('total-db-size-badge'); 5334 if (!tbody) return; 5335 5336 try { 5337 let html = ''; 5338 let totalDatabaseBytes = 0; 5339 let totalRecordsCount = 0; 5340 5341 for (let [idx, col] of collectionsMeta.entries()) { 5342 let count = 0; 5343 let collectionBytes = 0; 5344 let statusHtml = '<span class="text-gray-500 font-medium">Empty</span>'; 5345 5346 try { 5347 const snap = await db.collection(col.name).get(); 5348 count = snap.size; 5349 totalRecordsCount += count; 5350
5351 snap.forEach(doc => { 5352 const docString = JSON.stringify(doc.data()); 5353 const docBytes = new TextEncoder().encode(docString).length; 5354 collectionBytes += docBytes; 5355 }); 5356 5357 totalDatabaseBytes += collectionBytes; 5358 5359 if (count > 0) { 5360 statusHtml = '<span class="text-teal-400 font-bold bg-teal-500/10 px-2 py-0.5 rounded text-[10px]">Active</span>'; 5361 } 5362 } catch(err) { 5363 statusHtml = '<span class="text-red-400 font-medium">Restricted</span>'; 5364 } 5365 5366 let formattedSize = '0 B'; 5367 if (collectionBytes > 1024 * 1024) { 5368 formattedSize = (collectionBytes / (1024 * 1024)).toFixed(2) + ' MB'; 5369 } else if (collectionBytes > 1024) { 5370 formattedSize = (collectionBytes / 1024).toFixed(1) + ' KB'; 5371 } else if (collectionBytes > 0) { 5372 formattedSize = collectionBytes + ' B'; 5373 } 5374 5375 html += ` 5376 <tr class="hover:bg-slate-900/40 transition"> 5377 <td class="py-3 px-4 text-center"> 5378 <input type="checkbox" name="backup_col_cb" value="${col.name}" checked class="w-4 h-4 rounded text-teal-600 bg-slate-900 border-slate-700 cursor-pointer"> 5379 </td> 5380 <td class="py-3 px-4 font-mono font-bold text-amber-400">${col.name}</td> 5381 <td class="py-3 px-4 text-center font-black text-white">${count} <span class="text-[10px] text-gray-500 font-normal">(${formattedSize})</span></td> 5382 <td class="py-3 px-4 text-right">${statusHtml}</td> 5383 </tr>`; 5384 } 5385 5386 tbody.innerHTML = html; 5387 5388 // Display entire website database size badge at the top 5389 let totalFormatted = totalDatabaseBytes > 1024 * 1024 5390 ? (totalDatabaseBytes / (1024 * 1024)).toFixed(2) + ' MB' 5391 : (totalDatabaseBytes / 1024).toFixed(1) + ' KB'; 5392 5393 if (sizeBadge) { 5394 sizeBadge.innerText = `Total Footprint: ${totalFormatted} (${totalRecordsCount} Records)`; 5395 } 5396 5397 } catch(e) { 5398 tbody.innerHTML = `<tr><td colspan="4" class="py-6 text-center text-red-400 italic">Error scanning schema: ${e.message}</td></tr>`; 5399 } 5400} 5401 5402window.toggleAllBackupCollections = function(source) { 5403 const checkboxes = document.querySelectorAll('input[name="backup_col_cb"]'); 5404 checkboxes.forEach(cb => cb.checked = source.checked); 5405}; 5406 5407// ð¢ PARTIAL EXPORT FUNCTION (Using custom toast) 5408window.executeDynamicDatabaseExport = async function() { 5409 try { 5410 const checkedBoxes = document.querySelectorAll('input[name="backup_col_cb"]:checked'); 5411 if (checkedBoxes.length === 0) { 5412 if (typeof customToast === 'function') customToast("Please check at least one collection to export.", "error"); 5413 else alert("Please check at least one collection to export."); 5414 return; 5415 } 5416 5417 if (typeof customToast === 'function') customToast("Scanning selected collections for export...", "info"); 5418 let backupData = {}; 5419 5420 for (let cb of checkedBoxes) { 5421 const colName = cb.value; 5422 const snap = await db.collection(colName).get(); 5423 if (!snap.empty) { 5424 backupData[colName] = {}; 5425 snap.forEach(doc => { 5426 const docData = doc.data(); 5427 5428 // ð¢ SAFETY: Skip exporting the master admin user account 5429 if (colName === 'users' && (docData.email || '').toLowerCase() === '[email protected]') { 5430 return; 5431 } 5432 5433 backupData[colName][doc.id] = docData; 5434 }); 5435 } 5436 } 5437 5438 const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(backupData, null, 2)); 5439 const downloadAnchor = document.createElement('a'); 5440 const dateStr = new Date().toISOString().split('T')[0]; 5441 5442 downloadAnchor.setAttribute("href", dataStr); 5443 downloadAnchor.setAttribute("download", `alin-tagar-partial-backup-${dateStr}.json`); 5444 document.body.appendChild(downloadAnchor); 5445 downloadAnchor.click(); 5446 downloadAnchor.remove(); 5447 5448 if (typeof customToast === 'function') customToast("Database exported successfully as JSON!", "success"); 5449 else alert("Database exported successfully as JSON!"); 5450 } catch(e) { 5451 console.error("Export error:", e); 5452 if (typeof customToast === 'function') customToast("Export error: " + e.message, "error"); 5453 else alert("Export error: " + e.message); 5454 } 5455}; 5456 5457// ð¢ PARTIAL RESTORE FUNCTION (Using custom modal confirm & toast) 5458window.executeDynamicDatabaseRestore = async function() { 5459 const fileInput = document.getElementById('restore-json-file-input'); 5460 if (!fileInput || !fileInput.files || fileInput.files.length === 0) { 5461 if (typeof customToast === 'function') customToast("Please select a valid backup .json file first.", "error"); 5462 else alert("Please select a valid backup .json file first."); 5463 return; 5464 } 5465 5466 const checkedBoxes = document.querySelectorAll('input[name="backup_col_cb"]:checked'); 5467 if (checkedBoxes.length === 0) { 5468 if (typeof customToast === 'function') customToast("Please check at least one collection on the left to restore into.", "error"); 5469 else alert("Please check at least one collection on the left to restore into."); 5470 return; 5471 } 5472 const allowedCols = Array.from(checkedBoxes).map(cb => cb.value); 5473 5474 const file = fileInput.files[0]; 5475 const reader = new FileReader(); 5476 5477 reader.onload = async function(e) { 5478 try { 5479 const backupData = JSON.parse(e.target.result); 5480 5481 const confirmed = typeof customConfirm === 'function' 5482 ? await customConfirm("Restore Database", "Are you sure you want to restore selected collections? Existing records with matching IDs will be updated.") 5483 : confirm("Are you sure you want to restore selected collections? Existing records with matching IDs will be updated."); 5484 5485 if (!confirmed) return; 5486 5487 if (typeof customToast === 'function') customToast("Restoring selected records... Please wait.", "info"); 5488 5489 let restoredCount = 0; 5490 for (let colName in backupData) { 5491 if (allowedCols.includes(colName)) { 5492 const colDocs = backupData[colName]; 5493 for (let docId in colDocs) { 5494 let docData = colDocs[docId]; 5495
5496 // ð¢ SAFETY: Never touch or overwrite the master admin user account during restore 5497 if (colName === 'users' && (docData.email || '').toLowerCase() === '[email protected]') { 5498 continue; 5499 } 5500 5501 await db.collection(colName).doc(docId).set(docData, { merge: true }); 5502 restoredCount++; 5503 } 5504 } 5505 } 5506 5507 if (typeof customToast === 'function') customToast(`Successfully restored ${restoredCount} document(s)! Refreshing...`, "success"); 5508 else alert(`Successfully restored ${restoredCount} document(s)!`); 5509 5510 // ð¢ Auto-refresh the page to reflect restored counts instantly 5511 setTimeout(() => { 5512 window.location.reload(); 5513 }, 1500); 5514 } catch(err) { 5515 console.error("Restore error:", err); 5516 if (typeof customToast === 'function') customToast("Restore error: " + err.message, "error"); 5517 else alert("Restore error: " + err.message); 5518 } 5519 }; 5520 5521 reader.readAsText(file); 5522}; 5523 5524// ð¢ DATABASE WIPEOUT FUNCTION (Using custom modal confirm & toast) 5525window.executeDatabaseWipeout = async function() { 5526 const checkedBoxes = document.querySelectorAll('input[name="backup_col_cb"]:checked'); 5527 if (checkedBoxes.length === 0) { 5528 if (typeof customToast === 'function') customToast("Please check at least one collection to wipe out.", "error"); 5529 else alert("Please check at least one collection to wipe out."); 5530 return; 5531 } 5532 5533 const confirmed = typeof customConfirm === 'function' 5534 ? await customConfirm("â ï¸ DANGER: Database Wipeout", "Are you completely sure you want to delete ALL documents inside the selected collections? This cannot be undone!") 5535 : confirm("â ï¸ DANGER: Are you completely sure you want to delete ALL documents inside the selected collections?"); 5536 5537 if (!confirmed) return; 5538 5539 const userVerification = await customPrompt( 5540 "ð Final Security Verification", 5541 "To protect your data, this action requires explicit confirmation. Please type the word 'WIPE' below to unlock deletion:", 5542 "" 5543 ); 5544 5545 if (!userVerification || userVerification.trim().toUpperCase() !== 'WIPE') { 5546 if (typeof customToast === 'function') customToast("Wipeout cancelled. Verification word did not match.", "info"); 5547 else alert("Wipeout cancelled. Verification word did not match."); 5548 return; 5549 } 5550 5551 try { 5552 if (typeof customToast === 'function') customToast("Wiping out selected collections... Please wait.", "info"); 5553 5554 for (let cb of checkedBoxes) { 5555 const colName = cb.value; 5556 const snap = await db.collection(colName).get(); 5557 5558 const batch = db.batch(); 5559 snap.docs.forEach(doc => { 5560 const docData = doc.data(); 5561 5562 // ð¢ ULTIMATE ADMIN SAFETY LOCK: Never delete the admin account document! 5563 if (colName === 'users') { 5564 const docEmail = (docData.email || '').toLowerCase().trim(); 5565 if (docEmail === '[email protected]' || doc.id === 'qpJ1ojwul2YL6LO1udqJOkQzUI23') { 5566 return; // Skips deleting the admin document entirely! 5567 } 5568 } 5569 5570 batch.delete(doc.ref); 5571 }); 5572 await batch.commit(); 5573 } 5574 5575 if (typeof customToast === 'function') customToast("Selected collections have been wiped out safely (Admin protected).", "success"); 5576 else alert("Selected collections have been wiped out safely."); 5577 5578 loadBackupSchemaTable(); 5579 } catch(err) { 5580 console.error("Wipeout error:", err); 5581 if (typeof customToast === 'function') customToast("Wipeout error: " + err.message, "error"); 5582 else alert("Wipeout error: " + err.message); 5583 } 5584}; 5585
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.