Spaces:
Sleeping
Sleeping
| --- | |
| import AdminLayout from '../../layouts/AdminLayout.astro'; | |
| import { txasupabase } from '../../lib/txasupabase.js'; | |
| // Fetch real statistics from Supabase | |
| const localMovies = await txasupabase.getLocalMovies(); | |
| const totalMovies = Object.keys(localMovies).length; | |
| const users = await txasupabase.getUsers(); | |
| const totalUsers = users.length; | |
| // Fetch real count for comments | |
| const { count: commentsCount } = await txasupabase.supabase | |
| .from('comments') | |
| .select('*', { count: 'exact', head: true }); | |
| // Fetch real count for email logs (báo lỗi/gửi mail) | |
| const { count: emailLogsCount } = await txasupabase.supabase | |
| .from('email_logs') | |
| .select('*', { count: 'exact', head: true }); | |
| const stats = [ | |
| { label: 'Tổng số phim (Cục bộ)', value: String(totalMovies), icon: 'fas fa-film', color: 'text-blue-500' }, | |
| { label: 'Tổng thành viên', value: String(totalUsers), icon: 'fas fa-user-plus', color: 'text-green-500' }, | |
| { label: 'Tổng bình luận', value: String(commentsCount || 0), icon: 'fas fa-comments', color: 'text-purple-500' }, | |
| { label: 'Lịch sử gửi Email', value: String(emailLogsCount || 0), icon: 'fas fa-envelope-open-text', color: 'text-red-500' }, | |
| ]; | |
| // Phim mới cập nhật từ Database cục bộ | |
| const newestMovies = Object.values(localMovies) | |
| .sort((a: any, b: any) => { | |
| const timeA = new Date(a.movie.modified?.time || a.movie.created?.time || 0).getTime(); | |
| const timeB = new Date(b.movie.modified?.time || b.movie.created?.time || 0).getTime(); | |
| return timeB - timeA; | |
| }) | |
| .slice(0, 5); | |
| const formatImageUrl = (url: string) => { | |
| if (!url) return '/placeholder-poster.jpg'; | |
| if (url.startsWith('http')) return url; | |
| const cleanUrl = url.startsWith('/') ? url : `/${url}`; | |
| return `https://phimimg.com${cleanUrl}`; | |
| }; | |
| --- | |
| <AdminLayout title="Bảng điều khiển"> | |
| <div class="mb-12 flex items-center justify-between"> | |
| <div> | |
| <h1 class="text-4xl font-black tracking-tighter text-white">Bảng điều khiển</h1> | |
| <p class="mt-2 text-gray-500">Chào mừng bạn trở lại, hệ thống đang hoạt động ổn định.</p> | |
| </div> | |
| <div class="flex gap-4"> | |
| <button class="flex items-center gap-2 rounded-2xl bg-white/5 px-6 py-3 text-xs font-black uppercase tracking-widest text-white hover:bg-white/10 transition-all border border-white/5" data-txatooltip="Tải báo cáo chi tiết"> | |
| <i class="fas fa-download"></i> | |
| Xuất báo cáo | |
| </button> | |
| <button id="add-movie-btn" class="flex items-center gap-2 rounded-2xl bg-primary px-6 py-3 text-xs font-black uppercase tracking-widest text-dark hover:scale-105 transition-all shadow-[0_0_20px_var(--primary)]" data-txatooltip="Thêm phim vào hệ thống"> | |
| <i class="fas fa-plus"></i> | |
| Thêm phim mới | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4"> | |
| {stats.map(stat => ( | |
| <div class="glass rounded-[32px] p-8 border border-white/5 relative overflow-hidden group"> | |
| <div class="relative z-10 flex flex-col gap-4"> | |
| <div class={`h-12 w-12 rounded-2xl bg-white/5 flex items-center justify-center ${stat.color} text-xl group-hover:scale-110 transition-transform`}> | |
| <i class={stat.icon}></i> | |
| </div> | |
| <div> | |
| <p class="text-[10px] font-black uppercase tracking-widest text-gray-500">{stat.label}</p> | |
| <h3 class="text-3xl font-black text-white mt-1 txa-format-number" data-value={stat.value.replace(/,/g, '')}>{stat.value}</h3> | |
| </div> | |
| </div> | |
| <!-- Decoration --> | |
| <div class={`absolute -bottom-4 -right-4 h-24 w-24 rounded-full bg-white/5 blur-3xl ${stat.color} opacity-20`}></div> | |
| </div> | |
| ))} | |
| </div> | |
| <div class="mt-12 grid grid-cols-1 gap-12 lg:grid-cols-3"> | |
| <div class="lg:col-span-2 glass rounded-[40px] p-10 border border-white/5"> | |
| <div class="mb-8 flex items-center justify-between"> | |
| <h2 class="text-xl font-black tracking-tighter text-white">Phim mới cập nhật</h2> | |
| <a href="/admin/phim" class="text-[10px] font-black uppercase tracking-widest text-primary hover:underline">Xem tất cả</a> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-left"> | |
| <thead> | |
| <tr class="border-b border-white/5"> | |
| <th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Phim</th> | |
| <th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Ngày cập nhật</th> | |
| <th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Lượt xem</th> | |
| <th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Hành động</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-white/5"> | |
| {newestMovies.length > 0 ? newestMovies.map((m: any) => { | |
| const movie = m.movie; | |
| const poster = formatImageUrl(movie.poster_url || movie.thumb_url); | |
| const genres = movie.category ? movie.category.map((c: any) => c.name).join(', ') : 'Chưa phân loại'; | |
| const modifiedTime = movie.modified?.time || movie.created?.time || new Date().toISOString(); | |
| const views = movie.view || 0; | |
| return ( | |
| <tr> | |
| <td class="py-6"> | |
| <div class="flex items-center gap-4"> | |
| <div class="h-14 w-10 overflow-hidden rounded-xl bg-white/5 border border-white/10 shrink-0"> | |
| <img src={poster} alt={movie.name} class="h-full w-full object-cover" /> | |
| </div> | |
| <div class="flex flex-col min-w-0"> | |
| <span class="text-sm font-bold text-white truncate">{movie.name}</span> | |
| <span class="text-[10px] font-medium text-gray-500 uppercase tracking-widest truncate">{genres}</span> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="py-6 text-xs text-gray-400 txa-format-date" data-value={modifiedTime}></td> | |
| <td class="py-6 text-xs font-bold text-white txa-format-short" data-value={views}></td> | |
| <td class="py-6"> | |
| <div class="flex gap-2"> | |
| <a href={`/admin/phim/chinh-sua?slug=${movie.slug}`} class="h-8 w-8 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 hover:text-primary transition-colors border border-white/5" data-txatooltip="Chỉnh sửa phim"> | |
| <i class="fas fa-edit text-[10px]"></i> | |
| </a> | |
| <button class="h-8 w-8 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 hover:text-red-500 transition-colors border border-white/5" data-txatooltip="Xóa phim" onclick={`window.location.href='/admin/phim?keyword=${movie.slug}'`}> | |
| <i class="fas fa-trash text-[10px]"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| ); | |
| }) : ( | |
| <tr> | |
| <td colspan="4" class="py-12 text-center text-gray-500 text-xs font-bold">Chưa có phim nào trong cơ sở dữ liệu cục bộ.</td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="glass rounded-[40px] p-10 border border-white/5"> | |
| <h2 class="mb-8 text-xl font-black tracking-tighter text-white">Báo cáo gần đây</h2> | |
| <div class="space-y-6"> | |
| {[1,2,3,4].map(i => ( | |
| <div class="flex gap-4 p-4 rounded-3xl bg-white/5 border border-white/5 group cursor-pointer hover:bg-white/10 transition-colors" data-txatooltip="Xem chi tiết lỗi"> | |
| <div class="h-10 w-10 rounded-2xl bg-red-500/10 flex items-center justify-center text-red-500 group-hover:scale-110 transition-transform"> | |
| <i class="fas fa-bug"></i> | |
| </div> | |
| <div class="flex flex-col"> | |
| <span class="text-xs font-bold text-white">Lỗi không xem được tập {i}</span> | |
| <span class="text-[10px] font-medium text-gray-500 uppercase tracking-widest mt-1">Phim: Trục Ngọc</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </AdminLayout> | |
| <script> | |
| function formatData() { | |
| const { txaformat, initTxaTooltips, txamodal, txatoast } = (window as any); | |
| if (!txaformat) return; | |
| // Format numbers | |
| document.querySelectorAll('.txa-format-number').forEach(el => { | |
| const val = parseInt(el.getAttribute('data-value') || '0'); | |
| el.textContent = txaformat.number(val); | |
| }); | |
| // Format short numbers | |
| document.querySelectorAll('.txa-format-short').forEach(el => { | |
| const val = parseInt(el.getAttribute('data-value') || '0'); | |
| el.textContent = txaformat.shortNumber(val); | |
| }); | |
| // Format dates | |
| document.querySelectorAll('.txa-format-date').forEach(el => { | |
| const val = el.getAttribute('data-value'); | |
| el.textContent = txaformat.dateTime(val).split(',')[0]; // Just date | |
| }); | |
| // Init Tooltips | |
| if (initTxaTooltips) initTxaTooltips(); | |
| // Add Movie Action | |
| const addBtn = document.getElementById('add-movie-btn'); | |
| if (addBtn) { | |
| addBtn.onclick = () => { | |
| window.location.href = '/admin/phim/them-moi'; | |
| }; | |
| } | |
| } | |
| document.addEventListener('astro:page-load', formatData); | |
| </script> | |
| <style> | |
| .glass { | |
| background: rgba(18, 20, 29, 0.4); | |
| backdrop-filter: blur(40px) saturate(200%); | |
| } | |
| </style> | |