Spaces:
Sleeping
Sleeping
| --- | |
| import { txasupabase } from '../lib/txasupabase.js'; | |
| const genres = await txasupabase.getGenres().catch(() => []); | |
| const countries = await txasupabase.getCountries().catch(() => []); | |
| const navLinks = [ | |
| { href: '/', label: 'Trang chủ' }, | |
| { href: '/danh-sach/phim-le', label: 'Phim Lẻ' }, | |
| { href: '/danh-sach/phim-bo', label: 'Phim Bộ' }, | |
| ]; | |
| --- | |
| <header | |
| id="main-header" | |
| class="fixed top-0 left-0 right-0 z-[100] transition-all duration-500 bg-gradient-to-b from-black/35 via-transparent to-transparent" | |
| > | |
| <div id="header-inner" class="w-full flex items-center justify-between px-4 md:px-6 xl:px-10 py-4 md:py-6 transition-all duration-300"> | |
| <!-- Left: Logo & Search --> | |
| <div class="flex items-center gap-2 md:gap-4 lg:gap-6 flex-1 min-w-0"> | |
| <button id="menu-drawer-trigger" class="xl:hidden text-white hover:text-primary transition-all text-lg p-2.5 rounded-xl hover:bg-white/5 cursor-pointer shrink-0 flex items-center justify-center" aria-label="Mở menu"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <a href="/" class="flex items-center shrink-0 transition-transform hover:scale-[1.02] active:scale-95 group"> | |
| <!-- SVG Play Circle Logo (Matching TPHIMX Premium Theme) --> | |
| <svg class="h-9 md:h-10 w-auto animate-pulse" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" style="height: 42px; width: 42px;"> | |
| <circle cx="50" cy="50" r="44" stroke="url(#purple-grad)" stroke-width="5" fill="black" fill-opacity="0.3"/> | |
| <circle cx="50" cy="50" r="36" stroke="url(#purple-grad)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.8"/> | |
| <path d="M44 32.5L68 50L44 67.5V32.5Z" fill="url(#purple-grad)" stroke="url(#purple-grad)" stroke-width="1" stroke-linejoin="round"/> | |
| <defs> | |
| <linearGradient id="purple-grad" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" stop-color="#a78bfa"/> | |
| <stop offset="50%" stop-color="#7c3aed"/> | |
| <stop offset="100%" stop-color="#4c1d95"/> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| <div class="flex flex-col ml-2.5 justify-center leading-none"> | |
| <span class="text-lg md:text-[22px] font-black tracking-tight text-white flex items-center leading-none"> | |
| D<span class="text-primary">P</span>TXA | |
| </span> | |
| <span class="hidden sm:inline-block text-[8px] font-black text-gray-400 mt-2 tracking-widest uppercase leading-none">Động Phim Đỉnh Cao</span> | |
| </div> | |
| </a> | |
| <!-- Search Bar (Pill-shaped, next to Logo) --> | |
| <div class="relative hidden xl:block w-40 focus-within:w-52 xl:w-56 xl:focus-within:w-72 transition-all duration-300 ease-out group shrink-0"> | |
| <form id="header-search-form" class="flex items-center h-9 rounded-full bg-white/[0.06] border border-white/[0.08] px-3.5 focus-within:bg-white/10 focus-within:border-white/15 transition-all cursor-pointer w-full"> | |
| <i class="fas fa-search text-[11px] text-gray-400 shrink-0 select-none"></i> | |
| <input | |
| type="text" | |
| id="header-search-input" | |
| placeholder="Tìm phim, diễn viên..." | |
| class="flex-1 bg-transparent text-[11px] font-bold text-white outline-none placeholder:text-gray-400/40 min-w-0 ml-2 transition-all duration-300" | |
| autocomplete="off" | |
| /> | |
| <span id="search-shortcut-badge" class="hidden sm:inline-flex items-center justify-center text-[8px] font-black text-gray-400 bg-white/10 border border-white/5 px-2 py-0.5 rounded ml-2 shrink-0 leading-none group-focus-within:hidden select-none">/</span> | |
| </form> | |
| <div id="search-suggestions" class="absolute left-0 right-0 top-full mt-2 max-h-[400px] overflow-y-auto hidden rounded-2xl border border-white/10 p-2 shadow-2xl glass-dropdown z-[120] scrollbar-thin"> | |
| <div id="suggestions-content" class="flex flex-col gap-1"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4 xl:gap-6 shrink-0"> | |
| <!-- Desktop Navigation Menu (Inline in Header!) --> | |
| <nav class="hidden xl:flex items-center gap-1 xl:gap-2 2xl:gap-3 text-[11px] xl:text-[13px] 2xl:text-[14px] font-black uppercase tracking-wider text-gray-300"> | |
| <a href="/chu-de" data-astro-prefetch class="px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap">Chủ đề</a> | |
| <div class="relative group"> | |
| <button class="flex items-center gap-1 px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all cursor-pointer whitespace-nowrap"> | |
| Thể loại <i class="fas fa-caret-down text-[10px] opacity-50 ml-0.5"></i> | |
| </button> | |
| <div class="absolute left-1/2 -translate-x-1/2 top-[80%] pt-4 w-[560px] opacity-0 translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-300 z-[120]"> | |
| <div class="glass-dropdown rounded-3xl p-5 border border-white/10 shadow-2xl grid grid-cols-4 gap-1.5 max-h-[380px] overflow-y-auto scrollbar-thin"> | |
| {genres.length > 0 ? genres.map((g: any) => ( | |
| <a href={`/the-loai/${g.slug}`} class="px-3.5 py-2.5 rounded-xl text-[12px] font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all truncate">{g.name}</a> | |
| )) : ( | |
| <span class="col-span-4 text-center text-gray-600 text-[12px] py-4">Chưa có thể loại</span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <a href="/danh-sach/phim-le" data-astro-prefetch class="px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap">Phim Lẻ</a> | |
| <a href="/danh-sach/phim-bo" data-astro-prefetch class="px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap">Phim Bộ</a> | |
| <a href="#" id="xem-chung-btn" class="flex items-center gap-1 xl:gap-1.5 px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap group" data-txatooltip="Xem phim cùng bạn bè thời gian thực - Sắp ra mắt!"> | |
| Xem Chung | |
| <span class="bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white text-[8px] font-black px-1.5 py-0.5 rounded shadow-[0_0_12px_rgba(168,85,247,0.5)] tracking-wide uppercase leading-none scale-90 origin-left">Sắp ra mắt</span> | |
| </a> | |
| <div class="relative group"> | |
| <button class="flex items-center gap-1 px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all cursor-pointer whitespace-nowrap"> | |
| Quốc gia <i class="fas fa-caret-down text-[10px] opacity-50 ml-0.5"></i> | |
| </button> | |
| <div class="absolute left-1/2 -translate-x-1/2 top-[80%] pt-4 w-[460px] opacity-0 translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-300 z-[120]"> | |
| <div class="glass-dropdown rounded-3xl p-5 border border-white/10 shadow-2xl grid grid-cols-3 gap-1.5 max-h-[380px] overflow-y-auto scrollbar-thin"> | |
| {countries.length > 0 ? countries.map((c: any) => ( | |
| <a href={`/quoc-gia/${c.slug}`} class="px-3.5 py-2.5 rounded-xl text-[12px] font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all truncate">{c.name}</a> | |
| )) : ( | |
| <span class="col-span-3 text-center text-gray-600 text-[12px] py-4">Chưa có quốc gia</span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <a href="/dien-vien" class="px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap">Diễn viên</a> | |
| <a href="/lich-chieu" class="px-2 xl:px-3 py-1.5 xl:py-2 hover:text-white rounded-xl hover:bg-white/[0.06] transition-all whitespace-nowrap">Lịch chiếu</a> | |
| </nav> | |
| <!-- Auth + Mobile Buttons --> | |
| <div class="flex items-center gap-3 shrink-0"> | |
| <div id="auth-container" class="relative group"> | |
| <div id="auth-loading" class="flex items-center gap-3"> | |
| <div class="h-8 w-24 animate-pulse rounded-lg bg-white/5"></div> | |
| </div> | |
| <div id="auth-guest" class="hidden"> | |
| <a href="/dang-nhap" data-astro-prefetch class="flex items-center justify-center gap-2 rounded-full bg-white/[0.08] border border-white/[0.08] px-5 py-2 text-xs font-black text-white hover:bg-white hover:text-black hover:border-white transition-all"> | |
| <i class="fas fa-user text-xs"></i> | |
| Thành viên | |
| </a> | |
| </div> | |
| <div id="auth-user" class="hidden flex items-center gap-3 cursor-pointer group"> | |
| <div class="hidden sm:flex flex-col items-end leading-none"> | |
| <span class="text-[9.5px] font-black uppercase tracking-widest text-gray-500 mb-1">Thành viên</span> | |
| <span id="user-display-name" class="text-[13px] font-bold text-white group-hover:text-primary transition-colors">Admin</span> | |
| </div> | |
| <div class="relative"> | |
| <div id="user-avatar" class="h-9 w-9 rounded-xl bg-gradient-to-br from-primary to-primary-dark flex items-center justify-center text-white font-black shadow-lg ring-2 ring-white/10 overflow-hidden"> | |
| <img id="user-avatar-img" src="" class="h-full w-full object-cover hidden" /> | |
| <span id="user-avatar-text" class="text-xs">A</span> | |
| </div> | |
| <div class="absolute right-0 top-full pt-3 w-64 translate-y-2 opacity-0 pointer-events-none group-hover:translate-y-0 group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 z-[110]"> | |
| <div class="glass-dropdown rounded-2xl p-2 border border-white/10 shadow-2xl overflow-hidden"> | |
| <div class="px-4 py-3.5 border-b border-white/5 mb-1.5"> | |
| <div id="user-email" class="text-xs font-semibold text-gray-500 truncate">user@example.com</div> | |
| </div> | |
| <a href="/ca-nhan/yeu-thich" class="flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all"><i class="fas fa-heart text-[15px] text-primary/60"></i> Phim yêu thích</a> | |
| <a href="/ca-nhan/lich-su" class="flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all"><i class="fas fa-history text-[15px] text-primary/60"></i> Lịch sử xem</a> | |
| <div id="admin-link-container"></div> | |
| <button id="logout-btn" class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold text-gray-400 hover:bg-red-500/10 hover:text-red-500 transition-all"><i class="fas fa-sign-out-alt text-[15px]"></i> Đăng xuất</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mobile triggers --> | |
| <div class="flex items-center gap-2 xl:hidden"> | |
| <button id="mobile-search-trigger" class="text-gray-400 hover:text-white h-9 w-9 flex items-center justify-center rounded-lg bg-white/5"><i class="fas fa-search text-xs"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mobile Search Overlay --> | |
| <div id="mobile-search-overlay" class="fixed inset-0 z-[110] hidden bg-black/95 backdrop-blur-3xl animate-in fade-in duration-300"> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <div class="text-2xl font-black tracking-tighter text-white">D<span class="text-primary">P</span>TXA</div> | |
| <button id="mobile-search-close" class="h-12 w-12 flex items-center justify-center rounded-full bg-white/5 text-white"> | |
| <i class="fas fa-times text-xl"></i> | |
| </button> | |
| </div> | |
| <form id="mobile-search-form" class="relative"> | |
| <input | |
| type="text" | |
| id="mobile-search-input" | |
| placeholder="Nhập tên phim bạn muốn tìm..." | |
| class="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-5 text-lg font-bold text-white outline-none focus:border-primary/50 focus:bg-white/10 transition-all" | |
| autocomplete="off" | |
| /> | |
| <button type="submit" class="absolute right-4 top-1/2 -translate-y-1/2 h-12 w-12 flex items-center justify-center rounded-xl bg-primary text-white shadow-[0_0_20px_var(--primary)]"> | |
| <i class="fas fa-search"></i> | |
| </button> | |
| </form> | |
| <!-- Mobile Search Suggestions --> | |
| <div id="mobile-search-suggestions" class="mt-6 max-h-[calc(100vh-220px)] overflow-y-auto hidden rounded-3xl border border-white/10 p-2 glass-dropdown"> | |
| <div id="mobile-suggestions-content" class="flex flex-col gap-1"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Slide-out Navigation Drawer (Glassmorphic) --> | |
| <div | |
| id="menu-drawer" | |
| class="fixed inset-y-0 left-0 w-80 max-w-[85vw] z-[120] -translate-x-full hidden flex-col h-full max-h-screen overflow-y-auto scrollbar-hide bg-black/95 backdrop-blur-3xl border-r border-white/10 p-6 transition-transform duration-300 ease-out overscroll-contain" | |
| style="-webkit-overflow-scrolling: touch;" | |
| > | |
| <!-- Close button & Logo --> | |
| <div class="flex items-center justify-between mb-6 pb-4 border-b border-white/5 shrink-0"> | |
| <div class="text-[17px] font-black tracking-tight text-white flex items-center"> | |
| D<span class="text-primary mr-0.5">P</span>TXA | |
| </div> | |
| <button id="menu-drawer-close" class="h-8 w-8 flex items-center justify-center rounded-xl bg-white/5 text-white hover:bg-white/10 transition-all cursor-pointer"> | |
| <i class="fas fa-times text-sm"></i> | |
| </button> | |
| </div> | |
| <!-- Navigation Links inside Drawer --> | |
| <div class="space-y-1.5 pr-1"> | |
| <a href="/" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-home text-sm text-primary/60 w-5"></i> Trang chủ | |
| </a> | |
| <a href="/chu-de" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-compass text-sm text-primary/60 w-5"></i> Chủ đề | |
| </a> | |
| <a href="/danh-sach/phim-le" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-film text-sm text-primary/60 w-5"></i> Phim Lẻ | |
| </a> | |
| <a href="/danh-sach/phim-bo" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-tv text-sm text-primary/60 w-5"></i> Phim Bộ | |
| </a> | |
| <a href="#" id="drawer-xem-chung-btn" class="flex items-center justify-between rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all group"> | |
| <span class="flex items-center gap-3"> | |
| <i class="fas fa-users text-sm text-primary/60 w-5"></i> Xem Chung | |
| </span> | |
| <span class="bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white text-[8px] font-black px-1.5 py-0.5 rounded leading-none">Sắp ra mắt</span> | |
| </a> | |
| <a href="/dien-vien" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-user-friends text-sm text-primary/60 w-5"></i> Diễn viên | |
| </a> | |
| <a href="/lich-chieu" class="flex items-center gap-3 rounded-xl hover:bg-white/5 px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-gray-300 hover:text-white transition-all"> | |
| <i class="fas fa-calendar-alt text-sm text-primary/60 w-5"></i> Lịch chiếu | |
| </a> | |
| <!-- Dividers for lists --> | |
| <div class="h-px bg-white/5 my-4"></div> | |
| <!-- Countries list --> | |
| <div class="px-4 py-2 text-[10px] font-black tracking-widest text-gray-500 uppercase">Quốc gia</div> | |
| <div class="grid grid-cols-2 gap-1.5 px-2"> | |
| {countries.length > 0 ? countries.map((c: any) => ( | |
| <a href={`/quoc-gia/${c.slug}`} class="px-2.5 py-2 rounded-lg text-[11px] font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all truncate">{c.name}</a> | |
| )) : ( | |
| <span class="col-span-2 text-center text-gray-600 text-[11px] py-2">Chưa có quốc gia</span> | |
| )} | |
| </div> | |
| <!-- Genres list --> | |
| <div class="h-px bg-white/5 my-4"></div> | |
| <div class="px-4 py-2 text-[10px] font-black tracking-widest text-gray-500 uppercase">Thể loại</div> | |
| <div class="grid grid-cols-2 gap-1.5 px-2 pb-6"> | |
| {genres.length > 0 ? genres.map((g: any) => ( | |
| <a href={`/the-loai/${g.slug}`} class="px-2.5 py-2 rounded-lg text-[11px] font-bold text-gray-400 hover:bg-white/5 hover:text-white transition-all truncate">{g.name}</a> | |
| )) : ( | |
| <span class="col-span-2 text-center text-gray-600 text-[11px] py-2">Chưa có thể loại</span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Drawer Backdrop Overlay --> | |
| <div | |
| id="drawer-backdrop" | |
| class="fixed inset-0 z-[115] bg-black/60 backdrop-blur-sm hidden opacity-0 transition-opacity duration-300" | |
| ></div> | |
| </header> | |
| <script> | |
| function initHeader() { | |
| const header = document.getElementById('main-header'); | |
| if (!header || header.dataset.initialized) return; | |
| header.dataset.initialized = 'true'; | |
| const drawerTrigger = document.getElementById('menu-drawer-trigger'); | |
| const drawer = document.getElementById('menu-drawer'); | |
| const backdrop = document.getElementById('drawer-backdrop'); | |
| const drawerClose = document.getElementById('menu-drawer-close'); | |
| const searchForm = document.getElementById('header-search-form'); | |
| const searchInput = document.getElementById('header-search-input'); | |
| // Shortcut badge label detection | |
| const shortcutBadge = document.getElementById('search-shortcut-badge'); | |
| const isMac = navigator.userAgent.toLowerCase().includes('mac'); | |
| if (shortcutBadge) { | |
| shortcutBadge.textContent = isMac ? '⌘K' : '/'; | |
| } | |
| // Global Keydown search shortcut focus | |
| if (!window._hasSearchShortcut) { | |
| window._hasSearchShortcut = true; | |
| window.addEventListener('keydown', (e) => { | |
| const activeEl = document.activeElement; | |
| if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.hasAttribute('contenteditable'))) { | |
| return; | |
| } | |
| const isMacUser = navigator.userAgent.toLowerCase().includes('mac'); | |
| const sInput = document.getElementById('header-search-input'); | |
| if ((isMacUser && e.metaKey && e.key.toLowerCase() === 'k') || (!isMacUser && e.key === '/')) { | |
| e.preventDefault(); | |
| sInput?.focus(); | |
| sInput?.select(); | |
| } | |
| }); | |
| } | |
| if (!header) return; | |
| // Scroll Effect | |
| const headerInner = document.getElementById('header-inner'); | |
| const handleScroll = () => { | |
| if (window.scrollY > 30) { | |
| header.classList.add('glass-scrolled', 'shadow-2xl'); | |
| header.classList.remove('bg-gradient-to-b', 'from-black/35', 'via-transparent', 'to-transparent'); | |
| headerInner?.classList.add('py-3'); | |
| headerInner?.classList.remove('py-4', 'md:py-6'); | |
| } else { | |
| header.classList.remove('glass-scrolled', 'shadow-2xl'); | |
| header.classList.add('bg-gradient-to-b', 'from-black/35', 'via-transparent', 'to-transparent'); | |
| headerInner?.classList.add('py-4', 'md:py-6'); | |
| headerInner?.classList.remove('py-3'); | |
| } | |
| }; | |
| window.addEventListener('scroll', handleScroll); | |
| handleScroll(); // Init state | |
| // Drawer Xem Chung click handling with txatoast | |
| const drawerXemChungBtn = document.getElementById('drawer-xem-chung-btn'); | |
| drawerXemChungBtn?.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const { txatoast } = window; | |
| if (txatoast) { | |
| txatoast.info('Tính năng Xem Chung phim trực tuyến đang được phát triển và sẽ sớm ra mắt!'); | |
| } else { | |
| alert('Tính năng Xem Chung phim trực tuyến sắp ra mắt!'); | |
| } | |
| }); | |
| // Auto-focus search input when search form is clicked | |
| searchForm?.addEventListener('click', () => { | |
| searchInput?.focus(); | |
| }); | |
| // Drawer Open & Close Animation Logic | |
| const openDrawer = () => { | |
| drawer?.classList.remove('hidden'); | |
| backdrop?.classList.remove('hidden'); | |
| setTimeout(() => { | |
| drawer?.classList.remove('-translate-x-full'); | |
| backdrop?.classList.add('opacity-100'); | |
| }, 10); | |
| document.body.style.overflow = 'hidden'; | |
| }; | |
| const closeDrawer = () => { | |
| drawer?.classList.add('-translate-x-full'); | |
| backdrop?.classList.remove('opacity-100'); | |
| setTimeout(() => { | |
| drawer?.classList.add('hidden'); | |
| backdrop?.classList.add('hidden'); | |
| }, 300); | |
| document.body.style.overflow = ''; | |
| }; | |
| drawerTrigger?.addEventListener('click', openDrawer); | |
| drawerClose?.addEventListener('click', closeDrawer); | |
| backdrop?.addEventListener('click', closeDrawer); | |
| // Mobile Search Overlay Logic | |
| const mobileSearchTrigger = document.getElementById('mobile-search-trigger'); | |
| const mobileSearchOverlay = document.getElementById('mobile-search-overlay'); | |
| const mobileSearchClose = document.getElementById('mobile-search-close'); | |
| const mobileSearchForm = document.getElementById('mobile-search-form'); | |
| const mobileSearchInput = document.getElementById('mobile-search-input'); | |
| mobileSearchTrigger?.addEventListener('click', () => { | |
| mobileSearchOverlay?.classList.remove('hidden'); | |
| document.body.style.overflow = 'hidden'; | |
| setTimeout(() => mobileSearchInput?.focus(), 300); | |
| }); | |
| mobileSearchClose?.addEventListener('click', () => { | |
| mobileSearchOverlay?.classList.add('hidden'); | |
| if (drawer?.classList.contains('hidden')) { | |
| document.body.style.overflow = ''; | |
| } | |
| }); | |
| mobileSearchForm?.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const q = mobileSearchInput?.value.trim(); | |
| if (q) window.location.href = `/tim-kiem?q=${encodeURIComponent(q)}`; | |
| }); | |
| // Search Suggestions and Logic | |
| const searchSuggestions = document.getElementById('search-suggestions'); | |
| const suggestionsContent = document.getElementById('suggestions-content'); | |
| const mobileSearchSuggestions = document.getElementById('mobile-search-suggestions'); | |
| const mobileSuggestionsContent = document.getElementById('mobile-suggestions-content'); | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function(...args) { | |
| clearTimeout(timeout); | |
| timeout = setTimeout(() => func.apply(this, args), wait); | |
| }; | |
| } | |
| const formatImageUrl = (url) => { | |
| if (!url) return ''; | |
| if (url.startsWith('http')) return url; | |
| const cleanUrl = url.startsWith('/') ? url : `/${url}`; | |
| return `https://phimimg.com${cleanUrl}`; | |
| }; | |
| function highlightMatch(text, query) { | |
| if (!query || !text) return text; | |
| const normalize = (str) => { | |
| return str | |
| .normalize("NFD") | |
| .replace(/[\u0300-\u036f]/g, "") | |
| .toLowerCase() | |
| .replace(/đ/g, "d"); | |
| }; | |
| const normalizedText = normalize(text); | |
| const normalizedQuery = normalize(query); | |
| let result = ""; | |
| let lastIdx = 0; | |
| let idx = normalizedText.indexOf(normalizedQuery); | |
| if (idx === -1) return text; | |
| while (idx !== -1) { | |
| result += text.substring(lastIdx, idx); | |
| result += `<span class="text-primary font-black">${text.substring(idx, idx + query.length)}`; | |
| lastIdx = idx + query.length; | |
| idx = normalizedText.indexOf(normalizedQuery, lastIdx); | |
| } | |
| result += text.substring(lastIdx); | |
| return result; | |
| } | |
| const performSearch = async (query, container, dropdown) => { | |
| const q = query.trim(); | |
| if (!q) { | |
| dropdown?.classList.add('hidden'); | |
| if (container) container.innerHTML = ''; | |
| return; | |
| } | |
| try { | |
| const res = await fetch(`/api/movie/search?q=${encodeURIComponent(q)}`); | |
| const { items } = await res.json(); | |
| if (!items || items.length === 0) { | |
| if (container) { | |
| container.innerHTML = ` | |
| `; | |
| } | |
| dropdown?.classList.remove('hidden'); | |
| return; | |
| } | |
| if (container) { | |
| container.innerHTML = items.slice(0, 5).map((movie: any) => { | |
| const poster = formatImageUrl(movie.poster_url || movie.thumb_url); | |
| const titleHtml = highlightMatch(movie.name, q); | |
| const originHtml = highlightMatch(movie.origin_name, q); | |
| const status = movie.episode_current || 'HD'; | |
| const year = movie.year || ''; | |
| const type = movie.type === 'single' ? 'Phim Lẻ' : 'Phim Bộ'; | |
| return ` | |
| <a href="/phim/${movie.slug}" class="flex items-center gap-3 p-2 rounded-2xl hover:bg-white/5 transition-all group"> | |
| <div class="h-14 w-10 overflow-hidden rounded-xl border border-white/10 shrink-0"> | |
| <img src="${poster} | |
| <div class="min-w-0 flex-1"> | |
| <div class="text-[11px] font-black uppercase text-white truncate group-hover:text-primary transition-colors">${titleHtml} | |
| <div class="text-[9px] font-bold text-gray-500 truncate">${originHtml} | |
| <div class="flex items-center gap-2 mt-1"> | |
| <span class="text-[8px] font-black uppercase text-primary px-1.5 py-0.5 rounded bg-primary/10 border border-primary/20">${status} | |
| ${year ? `<span class="text-[8px] font-bold text-gray-400">${year}` : ''} | |
| <span class="text-[8px] font-bold text-gray-500">${type} | |
| </a> | |
| `; | |
| }).join(''); | |
| } | |
| dropdown?.classList.remove('hidden'); | |
| } catch (err) { | |
| console.error('Search suggestion error:', err); | |
| } | |
| }; | |
| const debouncedDesktopSearch = debounce((q: string) => { | |
| performSearch(q, suggestionsContent, searchSuggestions); | |
| }, 150); | |
| const debouncedMobileSearch = debounce((q) => { | |
| performSearch(q, mobileSuggestionsContent, mobileSearchSuggestions); | |
| }, 150); | |
| searchInput?.addEventListener('input', (e) => { | |
| debouncedDesktopSearch(e.target.value); | |
| }); | |
| mobileSearchInput?.addEventListener('input', (e) => { | |
| debouncedMobileSearch(e.target.value); | |
| }); | |
| // Hide dropdown when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!searchForm?.contains(e.target) && !searchSuggestions?.contains(e.target)) { | |
| searchSuggestions?.classList.add('hidden'); | |
| } | |
| }); | |
| // Focus triggers again if there's value | |
| searchInput?.addEventListener('focus', () => { | |
| if (searchInput.value.trim()) { | |
| searchSuggestions?.classList.remove('hidden'); | |
| } | |
| }); | |
| searchForm?.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const query = searchInput?.value.trim(); | |
| if (query) { | |
| window.location.href = `/tim-kiem?q=${encodeURIComponent(query)}`; | |
| } | |
| }); | |
| // Close menu on link click inside drawer | |
| drawer?.querySelectorAll('a').forEach(link => { | |
| link.addEventListener('click', closeDrawer); | |
| }); | |
| } | |
| async function checkAuth() { | |
| const authContainer = document.getElementById('auth-container'); | |
| if (!authContainer || authContainer.dataset.initialized) return; | |
| authContainer.dataset.initialized = 'true'; | |
| const loading = document.getElementById('auth-loading'); | |
| const guest = document.getElementById('auth-guest'); | |
| const userEl = document.getElementById('auth-user'); | |
| const mobileContainer = document.getElementById('mobile-auth-menu'); | |
| try { | |
| const res = await fetch('/api/auth/me'); | |
| const { user } = await res.json(); | |
| if (loading) loading.classList.add('hidden'); | |
| if (user) { | |
| // Desktop UI | |
| if (userEl) { | |
| userEl.classList.remove('hidden'); | |
| const nameEl = document.getElementById('user-display-name'); | |
| const emailEl = document.getElementById('user-email'); | |
| const avatarText = document.getElementById('user-avatar-text'); | |
| const adminContainer = document.getElementById('admin-link-container'); | |
| if (nameEl) nameEl.textContent = user.username; | |
| if (emailEl) emailEl.textContent = user.email || `${user.username}@tphimx.com`; | |
| const avatarImg = document.getElementById('user-avatar-img'); | |
| if (user.avatar_url && avatarImg) { | |
| avatarImg.src = user.avatar_url; | |
| avatarImg.classList.remove('hidden'); | |
| avatarText?.classList.add('hidden'); | |
| } else if (avatarText) { | |
| avatarText.textContent = user.username[0].toUpperCase(); | |
| avatarText.classList.remove('hidden'); | |
| avatarImg?.classList.add('hidden'); | |
| } | |
| if (user.role === 'admin' && adminContainer) { | |
| adminContainer.innerHTML = ` | |
| <a href="/admin" class="flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold text-primary hover:bg-primary/10 transition-all"> | |
| <i class="fas fa-user-shield text-[15px]"></i> | |
| Bảng quản trị | |
| </a> | |
| `; | |
| } | |
| document.getElementById('logout-btn')?.addEventListener('click', async () => { | |
| await fetch('/api/auth/logout', { method: 'POST' }); | |
| window.location.reload(); | |
| }); | |
| } | |
| // Mobile UI | |
| if (mobileContainer) { | |
| mobileContainer.innerHTML = ` | |
| <div class="w-full flex flex-col gap-4"> | |
| <div class="flex items-center justify-between rounded-3xl bg-white/5 p-6 border border-white/10"> | |
| <div class="flex items-center gap-4"> | |
| ${user.avatar_url ? ` | |
| <div class="h-14 w-14 rounded-2xl overflow-hidden shadow-[0_0_20px_var(--primary)] ring-2 ring-white/10 shrink-0"> | |
| <img src="${user.avatar_url}" class="h-full w-full object-cover" /> | |
| </div> | |
| ` : ` | |
| <div class="h-14 w-14 rounded-2xl bg-primary flex items-center justify-center text-2xl font-black text-white shadow-[0_0_20px_var(--primary)] shrink-0">${user.username[0].toUpperCase()}</div> | |
| `} | |
| <div class="flex flex-col"> | |
| <span class="text-[10px] font-black uppercase tracking-widest text-gray-500">Thành viên TPHIMX</span> | |
| <span class="text-lg font-black uppercase text-white">${user.username}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <a href="/ca-nhan/yeu-thich" class="flex flex-col items-center gap-3 rounded-3xl bg-white/5 p-6 border border-white/10 text-[10px] font-black uppercase tracking-widest text-gray-400"> | |
| <i class="fas fa-heart text-2xl text-primary"></i> | |
| Yêu thích | |
| </a> | |
| <a href="/ca-nhan/lich-su" class="flex flex-col items-center gap-3 rounded-3xl bg-white/5 p-6 border border-white/10 text-[10px] font-black uppercase tracking-widest text-gray-400"> | |
| <i class="fas fa-history text-2xl text-primary"></i> | |
| Lịch sử | |
| </a> | |
| </div> | |
| ${user.role === 'admin' ? ` | |
| <a href="/admin" class="flex items-center justify-center gap-3 rounded-3xl bg-primary/20 border border-primary/30 p-5 text-[10px] font-black uppercase tracking-widest text-primary"> | |
| <i class="fas fa-user-shield"></i> | |
| Quản trị hệ thống | |
| </a> | |
| ` : ''} | |
| <button id="mobile-logout-btn" class="flex items-center justify-center gap-3 rounded-3xl bg-red-500/10 border border-red-500/20 p-5 text-[10px] font-black uppercase tracking-widest text-red-500"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| Đăng xuất tài khoản | |
| </button> | |
| </div> | |
| `; | |
| document.getElementById('mobile-logout-btn')?.addEventListener('click', async () => { | |
| await fetch('/api/auth/logout', { method: 'POST' }); | |
| window.location.reload(); | |
| }); | |
| } | |
| } else { | |
| if (guest) guest.classList.remove('hidden'); | |
| } | |
| if (window.initTxaTooltips) window.initTxaTooltips(); | |
| } catch (err) { | |
| console.error('Auth check failed:', err); | |
| if (guest) guest.classList.remove('hidden'); | |
| } | |
| } | |
| // Run immediately since DOM is ready when script executes | |
| initHeader(); | |
| checkAuth(); | |
| // Support Astro View Transitions | |
| document.addEventListener('astro:page-load', () => { | |
| initHeader(); | |
| checkAuth(); | |
| }); | |
| </script> | |
| <style> | |
| .glass-scrolled { | |
| backdrop-filter: blur(20px) saturate(180%); | |
| background: rgba(10, 10, 10, 0.85); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .glass-dropdown { | |
| background: rgba(15, 15, 15, 0.92); | |
| backdrop-filter: blur(30px) saturate(180%); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| } | |
| </style> | |