Spaces:
Sleeping
Sleeping
File size: 10,202 Bytes
4bea261 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | ---
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>
|