dptxa-proxy / src /layouts /AdminLayout.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
20.4 kB
---
import '../styles/global.css';
import { ClientRouter } from 'astro:transitions';
interface Props {
title: string;
}
const { title } = Astro.props;
// Mock admin check - in real app, check role from cookie/session
const token = Astro.cookies.get('auth_token')?.value;
let user = null;
if (token) {
try {
user = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch(e) {}
}
let forceRedirect = false;
if (!user || user.role !== 'admin') {
forceRedirect = true;
}
const sidebarLinks = [
{ href: '/admin', label: 'Tổng quan', icon: 'fas fa-chart-line' },
{ href: '/admin/duyet-zalo', label: 'Duyệt Zalo', icon: 'fas fa-user-check', badgeId: 'nav-badge-zalo' },
{ href: '/admin/bao-cao-loi', label: 'Báo cáo lỗi', icon: 'fas fa-exclamation-triangle', badgeId: 'nav-badge-loi' },
{ href: '/admin/phim', label: 'Quản lý phim', icon: 'fas fa-film' },
{ href: '/admin/crawl', label: 'Thu thập phim', icon: 'fas fa-spider' },
{ href: '/admin/crawl-nang-cao', label: 'Cào nâng cao', icon: 'fas fa-search-plus' },
{ href: '/admin/server', label: 'Quản lý Server', icon: 'fas fa-server' },
{ href: '/admin/the-loai', label: 'Thể loại', icon: 'fas fa-tags' },
{ href: '/admin/quoc-gia', label: 'Quốc gia', icon: 'fas fa-globe' },
{ href: '/admin/dien-vien', label: 'Diễn viên', icon: 'fas fa-user-friends' },
{ href: '/admin/nguoi-dung', label: 'Người dùng', icon: 'fas fa-users' },
{ href: '/admin/binh-luan', label: 'Bình luận', icon: 'fas fa-comments', badgeId: 'nav-badge-comment' },
{ href: '/admin/email', label: 'Lịch sử Email', icon: 'fas fa-envelope-open-text' },
{ href: '/admin/cai-dat', label: 'Cài đặt', icon: 'fas fa-cog' },
];
---
<!doctype html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{forceRedirect && <meta http-equiv="refresh" content="0;url=/dang-nhap" />}
<title>{title} - TPHIMX Admin</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<!-- TXA Core Libraries -->
<script is:inline src="/lib/txa/txaformat.js" defer></script>
<script is:inline src="/lib/txa/txatooltip.js" defer></script>
<script is:inline src="/lib/txa/txatoast.js" defer></script>
<script is:inline src="/lib/txa/txamodal.js" type="module" defer></script>
<ClientRouter />
</head>
<body class="bg-[#0b0c10] text-white selection:bg-primary selection:text-dark overflow-x-hidden w-full max-w-full">
{forceRedirect ? (
<div class="fixed inset-0 z-[10000] flex flex-col items-center justify-center bg-[#0b0c10]">
<div class="w-10 h-10 rounded-full border-4 border-white/5 border-t-primary animate-spin shadow-[0_0_30px_rgba(124,58,237,0.4)] mb-4"></div>
<p class="text-xs font-black uppercase tracking-widest text-primary animate-pulse">Vui lòng đăng nhập...</p>
</div>
) : (
<>
<!-- Premium Cinematic Splash Loading Screen -->
<div id="txa-splash-loader" class="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-[#0b0c10] transition-all duration-700">
<!-- Glowing aura behind logo -->
<div class="absolute w-[400px] h-[400px] bg-primary/10 rounded-full blur-[120px] animate-pulse"></div>
<div class="relative flex flex-col items-center gap-6 scale-95 animate-fade-in">
<!-- Spinning Outer Ring -->
<div class="relative flex items-center justify-center">
<div class="w-24 h-24 rounded-full border-4 border-white/5 border-t-primary animate-spin shadow-[0_0_30px_rgba(124,58,237,0.4)]"></div>
<!-- Logo Icon inside -->
<div class="absolute w-14 h-14 overflow-hidden rounded-2xl shadow-lg">
<img src="/logo-icon.png" class="h-full w-full object-cover" />
</div>
</div>
<!-- Branding & Subtext -->
<div class="flex flex-col items-center">
<div class="text-3xl font-black tracking-tighter text-white animate-pulse">
T<span class="text-primary">PHIM</span>X
</div>
<div class="mt-2 text-[10px] font-black uppercase tracking-[0.2em] text-gray-500">
Hệ thống Quản trị Cao cấp
</div>
</div>
<!-- Premium Horizontal Progress Bar -->
<div class="w-48 h-1 bg-white/5 rounded-full overflow-hidden border border-white/5">
<div class="h-full bg-gradient-to-r from-primary to-violet-400 w-1/3 rounded-full animate-progress-loading"></div>
</div>
</div>
</div>
<div class="flex min-h-screen w-full max-w-full overflow-x-hidden">
<!-- Desktop Sidebar -->
<aside class="fixed left-0 top-0 hidden h-screen w-72 border-r border-white/5 bg-dark-light lg:block z-[100]">
<div class="flex h-full flex-col p-8">
<div class="mb-12 flex items-center gap-4">
<div class="h-10 w-10 overflow-hidden rounded-xl shadow-[0_0_20px_var(--primary)]">
<img src="/logo-icon.png" class="h-full w-full object-cover" />
</div>
<div class="text-2xl font-black tracking-tighter">
T<span class="text-primary">PHIM</span>X
</div>
</div>
<nav class="flex-1 space-y-1.5 overflow-y-auto pr-2 scrollbar-thin">
{sidebarLinks.map(link => {
const isActive = Astro.url.pathname === link.href || (link.href !== '/admin' && Astro.url.pathname.startsWith(link.href + '/'));
return (
<a
href={link.href}
class={`flex items-center gap-4 rounded-2xl px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all group ${
isActive
? 'bg-primary/15 text-primary border-l-4 border-primary shadow-[0_0_15px_rgba(124,58,237,0.1)]'
: 'text-gray-500 hover:bg-white/5 hover:text-white active:bg-primary/10'
}`}
>
<i class={`${link.icon} text-base ${isActive ? 'text-primary animate-pulse' : 'group-hover:text-primary transition-colors'}`}></i>
<span>{link.label}</span>
{link.badgeId && (
<span
id={link.badgeId}
class={`hidden ml-auto px-2 py-0.5 rounded-full text-[9px] font-black text-white min-w-[18px] text-center animate-pulse ${
link.badgeId === 'nav-badge-loi' ? 'bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.4)]' :
link.badgeId === 'nav-badge-comment' ? 'bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.4)]' :
'bg-primary shadow-[0_0_10px_rgba(124,58,237,0.4)]'
}`}
>
0
</span>
)}
</a>
);
})}
</nav>
<div class="mt-auto pt-6 border-t border-white/5">
<div class="flex items-center gap-4">
{user?.avatar_url ? (
<div class="h-10 w-10 overflow-hidden rounded-full shrink-0 shadow-lg">
<img src={user.avatar_url} class="h-full w-full object-cover" />
</div>
) : (
<div class="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-dark font-black shrink-0">
{user?.username?.[0]?.toUpperCase() || 'A'}
</div>
)}
<div class="flex flex-col">
<span class="text-sm font-bold text-white">{user?.username || 'Admin'}</span>
<span class="text-[10px] font-black uppercase tracking-widest text-gray-500">Quản trị viên</span>
</div>
<a href="/" class="ml-auto text-gray-500 hover:text-red-500 transition-colors">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
</aside>
<!-- Mobile Top Header -->
<header class="fixed top-0 left-0 right-0 z-[90] flex h-16 items-center justify-between border-b border-white/5 bg-[#12141d]/80 px-6 backdrop-blur-xl lg:hidden">
<div class="flex items-center gap-3">
<div class="h-8 w-8 overflow-hidden rounded-lg shadow-[0_0_15px_rgba(124,58,237,0.5)]">
<img src="/logo-icon.png" class="h-full w-full object-cover" />
</div>
<span class="text-lg font-black tracking-tighter text-white">T<span class="text-primary">PHIM</span>X</span>
</div>
<button id="mobile-menu-trigger" class="h-10 w-10 rounded-xl bg-white/5 flex items-center justify-center text-gray-400 hover:text-white border border-white/5 active:scale-95 transition-all">
<i class="fas fa-bars"></i>
</button>
</header>
<!-- Mobile Slide-out Drawer -->
<div id="mobile-drawer" class="fixed inset-0 z-[200] hidden bg-black/60 opacity-0 backdrop-blur-md transition-all duration-300 pointer-events-none">
<div class="absolute inset-y-0 right-0 w-[280px] bg-[#12141d] border-l border-white/5 p-8 flex flex-col justify-between transform translate-x-full transition-transform duration-300 pointer-events-auto overflow-y-auto scrollbar-thin">
<div>
<div class="flex items-center justify-between mb-12">
<div class="flex items-center gap-3">
<div class="h-8 w-8 overflow-hidden rounded-lg shadow-[0_0_15px_rgba(124,58,237,0.5)]">
<img src="/logo-icon.png" class="h-full w-full object-cover" />
</div>
<span class="text-lg font-black tracking-tighter text-white">T<span class="text-primary">PHIM</span>X</span>
</div>
<button id="mobile-menu-close" class="h-8 w-8 rounded-lg bg-white/5 flex items-center justify-center text-gray-400 hover:text-white border border-white/5">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<nav class="space-y-1.5 pr-1">
{sidebarLinks.map(link => {
const isActive = Astro.url.pathname === link.href || (link.href !== '/admin' && Astro.url.pathname.startsWith(link.href + '/'));
return (
<a
href={link.href}
class={`flex items-center gap-4 rounded-xl px-5 py-3.5 text-xs font-black uppercase tracking-widest transition-all group ${
isActive
? 'bg-primary/15 text-primary border-l-4 border-primary shadow-[0_0_15px_rgba(124,58,237,0.1)]'
: 'text-gray-500 hover:bg-white/5 hover:text-white active:bg-primary/10'
}`}
>
<i class={`${link.icon} text-base ${isActive ? 'text-primary' : 'group-hover:text-primary transition-colors'}`}></i>
<span>{link.label}</span>
{link.badgeId && (
<span
id={`${link.badgeId}-mobile`}
class={`hidden ml-auto px-2 py-0.5 rounded-full text-[9px] font-black text-white min-w-[18px] text-center animate-pulse ${
link.badgeId === 'nav-badge-loi' ? 'bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.4)]' :
link.badgeId === 'nav-badge-comment' ? 'bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.4)]' :
'bg-primary shadow-[0_0_10px_rgba(124,58,237,0.4)]'
}`}
>
0
</span>
)}
</a>
);
})}
</nav>
</div>
<div class="pt-8 border-t border-white/5">
<div class="flex items-center gap-4">
{user?.avatar_url ? (
<div class="h-10 w-10 overflow-hidden rounded-full shrink-0 shadow-lg">
<img src={user.avatar_url} class="h-full w-full object-cover" />
</div>
) : (
<div class="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-dark font-black shrink-0">
{user?.username?.[0]?.toUpperCase() || 'A'}
</div>
)}
<div class="flex flex-col">
<span class="text-sm font-bold text-white">{user?.username || 'Admin'}</span>
<span class="text-[10px] font-black uppercase tracking-widest text-gray-500">Quản trị viên</span>
</div>
<a href="/" class="ml-auto text-gray-500 hover:text-red-500 transition-colors">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<main class="flex-1 lg:ml-72 p-4 sm:p-6 pt-24 pb-24 lg:p-12 lg:pt-12 overflow-x-hidden max-w-full">
<slot />
</main>
</div>
</>
)}
</body>
</html>
<script>
// Hide splash loader on initial load with a smooth buffer delay
const hideInitialSplash = () => {
const splash = document.getElementById('txa-splash-loader');
if (splash) {
setTimeout(() => {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
setTimeout(() => {
splash.style.display = 'none';
}, 700);
}, 450); // 450ms buffer delay to let admin panels mount seamlessly
}
};
if (document.readyState === 'complete') {
hideInitialSplash();
} else {
window.addEventListener('load', () => {
setTimeout(hideInitialSplash, 150);
});
}
// Handle transition page load
document.addEventListener('astro:before-preparation', () => {
const splash = document.getElementById('txa-splash-loader');
if (splash) {
splash.style.display = 'flex';
void splash.offsetWidth;
splash.style.opacity = '1';
splash.style.pointerEvents = 'auto';
}
});
document.addEventListener('astro:page-load', () => {
// Hide splash loader on navigation complete
hideInitialSplash();
if ((window as any).initTxaTooltips) (window as any).initTxaTooltips();
// Mobile Drawer Logic
const trigger = document.getElementById('mobile-menu-trigger');
const closeBtn = document.getElementById('mobile-menu-close');
const drawer = document.getElementById('mobile-drawer');
const panel = drawer?.querySelector('.pointer-events-auto');
const openDrawer = () => {
if (drawer && panel) {
drawer.classList.remove('hidden');
// Force reflow
void drawer.offsetWidth;
drawer.classList.add('opacity-100');
drawer.classList.remove('pointer-events-none');
panel.classList.remove('translate-x-full');
}
};
const closeDrawer = () => {
if (drawer && panel) {
drawer.classList.remove('opacity-100');
drawer.classList.add('pointer-events-none');
panel.classList.add('translate-x-full');
setTimeout(() => {
drawer.classList.add('hidden');
}, 300);
}
};
trigger?.addEventListener('click', openDrawer);
closeBtn?.addEventListener('click', closeDrawer);
drawer?.addEventListener('click', (e) => {
if (e.target === drawer) closeDrawer();
});
// Real-time notifications logic (8 seconds polling)
const pollNotifications = async () => {
try {
const res = await fetch('/api/admin/notifications');
if (!res.ok) return;
const data = await res.json();
if (!data.success) return;
// 1. Zalo badge
const zaloCount = data.pendingZalo;
['nav-badge-zalo', 'nav-badge-zalo-mobile'].forEach(id => {
const badge = document.getElementById(id);
if (badge) {
if (zaloCount > 0) {
badge.textContent = zaloCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
});
// 2. Report badge
const reportCount = data.pendingReports;
['nav-badge-loi', 'nav-badge-loi-mobile'].forEach(id => {
const badge = document.getElementById(id);
if (badge) {
if (reportCount > 0) {
badge.textContent = reportCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
});
// 3. Comments badge
if (window.location.pathname.startsWith('/admin/binh-luan')) {
localStorage.setItem('admin_last_viewed_comments', new Date().toISOString());
}
const lastViewedStr = localStorage.getItem('admin_last_viewed_comments');
let newCommentsCount = 0;
if (lastViewedStr && data.comments) {
const lastViewedTime = new Date(lastViewedStr).getTime();
newCommentsCount = data.comments.filter(c => new Date(c.created_at).getTime() > lastViewedTime).length;
} else if (data.comments) {
const last24h = Date.now() - 24 * 60 * 60 * 1000;
newCommentsCount = data.comments.filter(c => new Date(c.created_at).getTime() > last24h).length;
}
['nav-badge-comment', 'nav-badge-comment-mobile'].forEach(id => {
const badge = document.getElementById(id);
if (badge) {
if (newCommentsCount > 0) {
badge.textContent = newCommentsCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
});
// 4. Comparison and Toast alerts
const prevZalo = parseInt(sessionStorage.getItem('prev_pending_zalo') || '0', 10);
const prevReports = parseInt(sessionStorage.getItem('prev_pending_reports') || '0', 10);
const prevComments = parseInt(sessionStorage.getItem('prev_new_comments') || '0', 10);
// Make sure toast objects are present
if ((window as any).txatoast) {
if (zaloCount > prevZalo) {
(window as any).txatoast.info(`🔔 Có yêu cầu duyệt Zalo mới đang chờ phê duyệt!`);
}
if (reportCount > prevReports) {
(window as any).txatoast.warning(`⚠️ Có báo cáo lỗi phim mới chưa được xử lý!`);
}
if (newCommentsCount > prevComments) {
(window as any).txatoast.success(`💬 Có bình luận mới của người xem trên trang phim!`);
}
}
// Cache counts
sessionStorage.setItem('prev_pending_zalo', zaloCount.toString());
sessionStorage.setItem('prev_pending_reports', reportCount.toString());
sessionStorage.setItem('prev_new_comments', newCommentsCount.toString());
} catch (err) {
console.error('Failed to fetch notifications:', err);
}
};
// Expose notification fetcher globally so admin sub-pages can refresh counts immediately after any actions
(window as any).txaPollNotifications = pollNotifications;
// Initial fetch
pollNotifications();
// Clear previous interval if any
if ((window as any).txaNotificationInterval) {
clearInterval((window as any).txaNotificationInterval);
}
// Set polling interval
(window as any).txaNotificationInterval = setInterval(pollNotifications, 8000);
});
</script>
<style>
@theme {
--color-primary: #7c3aed;
--color-dark: #0b0c10;
--color-dark-light: #12141d;
}
@keyframes progress-loading {
0% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
.animate-progress-loading {
animation: progress-loading 2s infinite linear;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.animate-fade-in {
animation: fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>