dptxa-proxy / src /pages /admin /index.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
10.2 kB
---
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>