dptxa-proxy / src /pages /admin /binh-luan.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
31.1 kB
---
import AdminLayout from '../../layouts/AdminLayout.astro';
import { txasupabase } from '../../lib/txasupabase.js';
// Kiểm tra quyền truy cập Admin trước khi render
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) {}
}
if (!user || user.role !== 'admin') {
return Astro.redirect('/dang-nhap');
}
// Lấy danh sách bình luận ban đầu từ API
const bannedUsers = await txasupabase.getSetting('banned_commenters') || [];
// Để tối ưu hóa tốc độ, chúng ta load danh sách thông qua Client-side fetch trong script,
// Nhưng chúng ta vẫn truyền danh sách bannedUsers từ Server-side.
---
<AdminLayout title="Quản lý Bình luận Nhanh">
<div class="mb-12 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-4xl font-black tracking-tighter text-white">Quản lý Bình luận</h1>
<p class="mt-2 text-gray-500">Giám sát, kiểm duyệt nội dung phản hồi, gắn thẻ spoil và cấm tài khoản vi phạm chính sách chỉ với 1-click.</p>
</div>
<!-- Stats Cards -->
<div class="flex gap-4">
<div class="glass border border-white/5 rounded-2xl px-5 py-3 text-left">
<span class="text-[9px] font-black uppercase tracking-wider text-gray-500">Người dùng bị cấm</span>
<div class="text-lg font-black text-red-500 mt-0.5" id="banned-count">{bannedUsers.length}</div>
</div>
</div>
</div>
<div class="glass rounded-[40px] p-8 md:p-10 border border-white/5 space-y-6 relative">
<!-- Filters & Actions Area -->
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between border-b border-white/5 pb-6">
<!-- Search bar -->
<div class="relative w-full md:max-w-md">
<i class="fas fa-search absolute left-5 top-1/2 -translate-y-1/2 text-gray-500 text-sm"></i>
<input
type="text"
id="search-input"
placeholder="Tìm bình luận theo nội dung, người dùng, slug phim..."
class="w-full bg-[#12141d] border border-white/5 rounded-2xl pl-12 pr-5 py-3.5 text-sm text-white focus:outline-none focus:border-primary/50 transition-all"
/>
</div>
<!-- Filter pills -->
<div class="flex flex-wrap gap-2">
<button class="filter-pill px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider bg-primary text-dark border border-primary/20 cursor-pointer" data-filter="all">Tất cả</button>
<button class="filter-pill px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-400 hover:text-white border border-white/5 hover:bg-white/5 cursor-pointer" data-filter="spoiler">Tiết lộ (Spoiler)</button>
<button class="filter-pill px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-400 hover:text-white border border-white/5 hover:bg-white/5 cursor-pointer" data-filter="banned">User bị cấm</button>
</div>
</div>
<!-- Table Area -->
<div class="overflow-x-auto min-h-[400px]">
<table class="w-full text-left table-auto">
<thead>
<tr class="border-b border-white/5">
<th class="pb-6 w-12 text-center">
<label class="relative flex items-center justify-center cursor-pointer">
<input type="checkbox" id="chk-all" class="peer sr-only" />
<div class="h-5 w-5 rounded-lg border border-white/10 bg-[#12141d] flex items-center justify-center transition-all peer-checked:bg-primary peer-checked:border-primary">
<i class="fas fa-check text-[10px] text-dark opacity-0 peer-checked:opacity-100 transition-opacity"></i>
</div>
</label>
</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500 w-48">Thành viên</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500 w-44">Phim / Tập</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Nội dung bình luận</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500 w-36">Thời gian</th>
<th class="pb-6 text-right text-[10px] font-black uppercase tracking-widest text-gray-500 w-28">Thao tác</th>
</tr>
</thead>
<tbody id="table-body" class="divide-y divide-white/5">
<tr>
<td colspan="6" class="py-20 text-center text-gray-500 text-sm">
<i class="fas fa-spinner fa-spin text-3xl mb-3 text-primary"></i>
<p class="font-bold">Đang nạp toàn bộ bình luận của hệ thống...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Floating Bulk Action Pill (Viên Thuốc Hành Động Hàng Loạt) -->
<div id="bulk-pill" class="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 transform translate-y-28 opacity-0 pointer-events-none transition-all duration-500 ease-out bg-[#0d0e15]/95 border border-primary/30 rounded-full py-4 px-6 flex items-center gap-6 shadow-[0_0_50px_rgba(124,58,237,0.35)] backdrop-blur-xl">
<div class="flex items-center gap-2 border-r border-white/10 pr-4">
<span class="h-2.5 w-2.5 rounded-full bg-primary animate-ping"></span>
<span class="text-xs font-black uppercase tracking-widest text-primary">Đã chọn: <span id="selected-count-pill" class="text-white font-mono font-black text-sm">0</span></span>
</div>
<div class="flex items-center gap-2">
<button id="pill-delete" class="flex items-center gap-2 rounded-full bg-red-500/10 hover:bg-red-500 border border-red-500/20 text-red-400 hover:text-white px-5 py-2.5 text-[9px] font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer" data-txatooltip="Xóa toàn bộ các bình luận đã chọn">
<i class="fas fa-trash-alt"></i> Xóa
</button>
<button id="pill-spoiler" class="flex items-center gap-2 rounded-full bg-yellow-500/10 hover:bg-yellow-500 border border-yellow-500/20 text-yellow-400 hover:text-white px-5 py-2.5 text-[9px] font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer" data-txatooltip="Bật/Tắt chế độ Spoiler hàng loạt">
<i class="fas fa-exclamation-triangle"></i> Spoiler
</button>
<button id="pill-ban" class="flex items-center gap-2 rounded-full bg-red-600/10 hover:bg-red-600 border border-red-600/20 text-red-500 hover:text-white px-5 py-2.5 text-[9px] font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer" data-txatooltip="Cấm viết bình luận hàng loạt các user đã chọn">
<i class="fas fa-user-slash"></i> Cấm viết
</button>
</div>
<button id="pill-cancel" class="text-gray-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all border border-transparent hover:border-white/10 px-4 py-2 rounded-full cursor-pointer" data-txatooltip="Bỏ chọn tất cả">Hủy</button>
</div>
</AdminLayout>
<script is:inline define:vars={{ initialBanned: bannedUsers }}>
let comments = [];
let bannedUsers = initialBanned || [];
let userProfiles = {};
let movieMap = {};
let searchQuery = '';
let activeFilter = 'all'; // 'all', 'spoiler', 'banned'
// Định dạng thời gian tương đối
function formatRelativeTime(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
if (isNaN(date.getTime()) || diffMs < 0) return 'Vừa xong';
const diffSec = Math.floor(diffMs / 1000);
if (diffSec < 60) return 'Vừa xong';
const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60) return `${diffMin} phút trước`;
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return `${diffHour} giờ trước`;
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
// Tải danh sách bình luận qua API
async function loadComments() {
try {
const res = await fetch('/api/movie/comments?slug=all');
if (res.ok) {
const data = await res.json();
comments = data.comments || [];
bannedUsers = data.bannedUsers || [];
userProfiles = data.userProfiles || {};
movieMap = data.movieMap || {};
// Cập nhật số lượng user bị cấm lên UI
const bannedCountEl = document.getElementById('banned-count');
if (bannedCountEl) bannedCountEl.textContent = bannedUsers.length;
renderTable();
if (window.txaPollNotifications) {
window.txaPollNotifications();
}
} else {
const errData = await res.json();
window.txatoast.error(errData.error || 'Không tải được danh sách bình luận!');
}
} catch (e) {
window.txatoast.error('Lỗi kết nối API bình luận!');
}
}
// Render bảng danh sách bình luận
function renderTable() {
const tbody = document.getElementById('table-body');
if (!tbody) return;
// Lọc theo search query và filter pill
const filtered = comments.filter(c => {
const matchesSearch =
c.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.movie_slug.toLowerCase().includes(searchQuery.toLowerCase()) ||
(movieMap[c.movie_slug] && movieMap[c.movie_slug].toLowerCase().includes(searchQuery.toLowerCase()));
if (!matchesSearch) return false;
if (activeFilter === 'spoiler') return c.is_spoiler;
if (activeFilter === 'banned') return bannedUsers.includes(c.username);
return true;
});
if (filtered.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="py-20 text-center text-gray-500 text-sm">
<i class="fas fa-comment-slash text-3xl mb-3 block opacity-20"></i>
<p class="font-bold">Không tìm thấy bình luận nào phù hợp</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = filtered.map(c => {
const isSpoiler = c.is_spoiler;
const isUserBanned = bannedUsers.includes(c.username);
const charCode = c.username ? c.username.charCodeAt(0) : 65;
const avatarColors = [
'bg-primary text-dark', 'bg-emerald-500 text-white', 'bg-blue-500 text-white',
'bg-pink-500 text-white', 'bg-yellow-500 text-dark', 'bg-purple-500 text-white'
];
const avatarStyle = avatarColors[charCode % avatarColors.length];
// Tên phim
const movieName = movieMap[c.movie_slug] || c.movie_slug;
const episodeInfo = c.episode_name ? `${c.episode_name} (${c.server_name || 'Server'})` : 'Chi tiết phim';
// Nơi ở & Profile popup
const profile = userProfiles[c.username] || { province: '', ward: '', commentsCount: 0, likesCount: 0 };
let locationStr = 'Chưa cập nhật';
if (profile.ward || profile.province) {
const parts = [];
if (profile.ward) parts.push(`Xã ${profile.ward}`);
if (profile.province) parts.push(`Tỉnh ${profile.province}`);
locationStr = parts.join(', ');
}
return `
<tr class="hover:bg-white/[0.01] transition-colors group">
<td class="py-5 text-center">
<label class="relative flex items-center justify-center cursor-pointer">
<input type="checkbox" class="chk-comment peer sr-only" data-id="${c.id}" data-username="${c.username}" />
<div class="h-5 w-5 rounded-lg border border-white/10 bg-[#12141d] flex items-center justify-center transition-all peer-checked:bg-primary peer-checked:border-primary">
<i class="fas fa-check text-[10px] text-dark opacity-0 peer-checked:opacity-100 transition-opacity"></i>
</div>
</label>
</td>
<td class="py-5">
<div class="flex items-center gap-3">
<!-- Avatar -->
${(() => {
const hasAvatar = profile && profile.avatar_url;
const avatarInner = hasAvatar
? `<img src="${profile.avatar_url}" class="h-full w-full object-cover" />`
: (c.username ? c.username[0] : 'U');
return `
<div class="h-8 w-8 rounded-full overflow-hidden flex-shrink-0 flex items-center justify-center font-black uppercase text-xs ${hasAvatar ? '' : avatarStyle}">
${avatarInner}
</div>
<!-- Username with hover preview -->
<div class="relative group/usercard text-left">
<span class="text-xs font-bold text-white hover:text-primary transition-all cursor-pointer">@${c.username}</span>
${isUserBanned ? '<span class="ml-1 px-1.5 py-0.5 rounded-full bg-red-500/10 border border-red-500/20 text-[7px] font-black uppercase tracking-wider text-red-500">Bị cấm</span>' : ''}
<!-- Hover User Card -->
<div class="absolute bottom-full left-0 mb-3 w-60 p-4 rounded-2xl bg-[#090b11]/98 border border-white/10 shadow-2xl backdrop-blur-md opacity-0 pointer-events-none group-hover/usercard:opacity-100 group-hover/usercard:pointer-events-auto transition-all duration-200 z-50 transform translate-y-1 group-hover/usercard:translate-y-0 text-left">
<div class="absolute -inset-px rounded-2xl bg-gradient-to-r from-primary/10 to-blue-500/10 opacity-40 blur-sm pointer-events-none"></div>
<div class="relative space-y-3">
<div class="flex items-center gap-2">
<div class="h-7 w-7 rounded-full overflow-hidden flex items-center justify-center font-black uppercase text-xs ${hasAvatar ? '' : avatarStyle}">
${avatarInner}
</div>
<div>
<div class="text-[11px] font-black text-white">@${c.username}</div>
<div class="text-[8px] text-gray-500 font-bold uppercase tracking-wider">Thành viên TPhimX</div>
</div>
</div>
<div class="border-t border-white/5 my-1"></div>
<div class="space-y-1.5 text-[10px] text-gray-400">
<div>Số bình luận: <strong class="text-white">${profile.commentsCount}</strong></div>
<div>Thích: <strong class="text-white">${profile.likesCount} phim</strong></div>
<div class="truncate">Nơi ở: <strong class="text-white">${locationStr}</strong></div>
</div>
</div>
</div>
</div>
`;
})()}
</div>
</td>
<td class="py-5">
<div class="flex flex-col min-w-0">
<a href="/phim/${c.movie_slug}" target="_blank" class="text-xs font-bold text-gray-300 hover:text-primary transition-all truncate leading-snug" data-txatooltip="Xem chi tiết phim">${movieName}</a>
<span class="text-[9px] text-gray-500 font-bold uppercase tracking-wider mt-0.5">${episodeInfo}</span>
</div>
</td>
<td class="py-5 pr-6">
<div class="flex flex-col gap-1 text-left">
<p class="text-xs text-gray-300 leading-relaxed font-medium break-all whitespace-pre-wrap">${c.content}</p>
<div class="flex gap-2.5 mt-1 items-center">
${isSpoiler ? '<span class="bg-red-500/10 border border-red-500/20 text-[8px] font-black uppercase tracking-wider px-2 py-0.5 rounded text-red-400"><i class="fas fa-exclamation-triangle"></i> Spoiler</span>' : ''}
<span class="text-[10px] text-gray-500 font-semibold flex items-center gap-1">
<i class="far fa-thumbs-up"></i> ${c.likes?.length || 0}
</span>
<span class="text-[10px] text-gray-500 font-semibold flex items-center gap-1">
<i class="far fa-thumbs-down"></i> ${c.dislikes?.length || 0}
</span>
</div>
</div>
</td>
<td class="py-5 text-xs text-gray-400 font-medium" data-txatooltip="Timestamp: ${new Date(c.created_at).toLocaleString('vi-VN')}">
${formatRelativeTime(c.created_at)}
</td>
<td class="py-5 text-right">
<div class="flex justify-end gap-1.5">
<!-- Toggle Spoiler button -->
<button
class="btn-toggle-spoiler h-8 w-8 rounded-lg bg-white/5 hover:bg-yellow-500/20 text-gray-500 hover:text-yellow-400 transition-all border border-white/5 flex items-center justify-center cursor-pointer"
data-id="${c.id}"
data-spoiler="${c.is_spoiler}"
data-txatooltip="${isSpoiler ? 'Gỡ nhãn Spoiler' : 'Đánh dấu Spoiler'}"
>
<i class="fas fa-mask text-[10px]"></i>
</button>
<!-- Ban User button -->
<button
class="btn-ban-user h-8 w-8 rounded-lg bg-white/5 hover:bg-red-500/20 text-gray-500 hover:text-red-500 transition-all border border-white/5 flex items-center justify-center cursor-pointer"
data-username="${c.username}"
data-action="${isUserBanned ? 'unban' : 'ban'}"
data-txatooltip="${isUserBanned ? 'Bỏ cấm bình luận' : 'Chặn bình luận người dùng'}"
>
<i class="fas ${isUserBanned ? 'fa-user-check text-emerald-400' : 'fa-user-slash'} text-[10px]"></i>
</button>
<!-- Delete Comment button -->
<button
class="btn-delete h-8 w-8 rounded-lg bg-white/5 hover:bg-red-500/30 text-gray-500 hover:text-red-400 transition-all border border-white/5 flex items-center justify-center cursor-pointer"
data-id="${c.id}"
data-txatooltip="Xóa bình luận này"
>
<i class="fas fa-trash-alt text-[10px]"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
bindTableEvents();
updateBulkPill();
if (window.initTxaTooltips) {
setTimeout(() => window.initTxaTooltips(), 50);
}
}
// Gắn sự kiện cho các hàng & nút trong bảng
function bindTableEvents() {
// Checkbox items
document.querySelectorAll('.chk-comment').forEach(chk => {
chk.onchange = () => {
updateBulkPill();
};
});
// 1. Toggle Spoiler
document.querySelectorAll('.btn-toggle-spoiler').forEach(btn => {
btn.onclick = async () => {
const commentId = btn.getAttribute('data-id');
const isCurrentlySpoiler = btn.getAttribute('data-spoiler') === 'true';
try {
const res = await fetch('/api/admin/movie/edit-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commentId, action: 'toggle_spoiler', isSpoiler: !isCurrentlySpoiler })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi cập nhật bình luận!');
}
};
});
// 2. Ban / Unban User
document.querySelectorAll('.btn-ban-user').forEach(btn => {
btn.onclick = () => {
const username = btn.getAttribute('data-username');
const action = btn.getAttribute('data-action');
const actionText = action === 'ban' ? 'cấm' : 'mở cấm';
window.txamodal.show({
title: `${action === 'ban' ? 'Cấm' : 'Mở cấm'} bình luận`,
message: `Bạn chắc chắn muốn <b>${actionText}</b> thành viên <b>@${username}</b> viết bình luận trên hệ thống?`,
type: action === 'ban' ? 'danger' : 'success',
confirmText: action === 'ban' ? 'Cấm ngay' : 'Mở cấm',
onConfirm: async () => {
try {
const res = await fetch('/api/movie/comments', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, targetUsername: username })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Gặp lỗi khi xử lý cấm!');
}
return true;
}
});
};
});
// 3. Delete Comment
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.onclick = () => {
const commentId = btn.getAttribute('data-id');
window.txamodal.show({
title: 'Xóa bình luận',
message: 'Bạn có chắc chắn muốn xóa vĩnh viễn bình luận này? Hành động này không thể khôi phục lại.',
type: 'danger',
confirmText: 'Xóa ngay',
onConfirm: async () => {
try {
const res = await fetch('/api/movie/comments', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commentId })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi khi xóa bình luận!');
}
return true;
}
});
};
});
}
// Cập nhật hiển thị Viên Thuốc (Bulk Action Pill)
function updateBulkPill() {
const checked = document.querySelectorAll('.chk-comment:checked');
const totalCount = document.querySelectorAll('.chk-comment').length;
const chkAll = document.getElementById('chk-all');
const pill = document.getElementById('bulk-pill');
const countSpan = document.getElementById('selected-count-pill');
if (chkAll) {
chkAll.checked = totalCount > 0 && checked.length === totalCount;
}
if (checked.length > 0) {
if (pill) {
pill.classList.remove('translate-y-28', 'opacity-0', 'pointer-events-none');
pill.classList.add('translate-y-0', 'opacity-100', 'pointer-events-auto');
}
if (countSpan) countSpan.textContent = checked.length;
} else {
if (pill) {
pill.classList.add('translate-y-28', 'opacity-0', 'pointer-events-none');
pill.classList.remove('translate-y-0', 'opacity-100', 'pointer-events-auto');
}
}
}
// Khởi tạo các sự kiện toàn trang
function init() {
loadComments();
// Tìm kiếm
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.oninput = (e) => {
searchQuery = e.target.value.trim();
renderTable();
};
}
// Filter pills
document.querySelectorAll('.filter-pill').forEach(pill => {
pill.onclick = () => {
document.querySelectorAll('.filter-pill').forEach(p => {
p.className = 'filter-pill px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-400 hover:text-white border border-white/5 hover:bg-white/5 cursor-pointer';
});
pill.className = 'filter-pill px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider bg-primary text-dark border border-primary/20 cursor-pointer';
activeFilter = pill.getAttribute('data-filter');
renderTable();
};
});
// Checkbox All
const chkAll = document.getElementById('chk-all');
if (chkAll) {
chkAll.onchange = () => {
document.querySelectorAll('.chk-comment').forEach(chk => {
chk.checked = chkAll.checked;
});
updateBulkPill();
};
}
// --- SỰ KIỆN VIÊN THUỐC HÀNG LOẠT (BULK PILL ACTIONS) ---
// 1. Cancel Selection
const pillCancel = document.getElementById('pill-cancel');
if (pillCancel) {
pillCancel.onclick = () => {
document.querySelectorAll('.chk-comment').forEach(chk => chk.checked = false);
if (chkAll) chkAll.checked = false;
updateBulkPill();
};
}
// 2. Bulk Delete
const pillDelete = document.getElementById('pill-delete');
if (pillDelete) {
pillDelete.onclick = () => {
const checked = document.querySelectorAll('.chk-comment:checked');
const commentIds = Array.from(checked).map(chk => chk.getAttribute('data-id'));
window.txamodal.show({
title: 'Xóa hàng loạt bình luận',
message: `Bạn chắc chắn muốn xóa vĩnh viễn <b>${commentIds.length}</b> bình luận đã chọn?`,
type: 'danger',
confirmText: 'Xóa tất cả',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/movie/edit-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commentIds, action: 'bulk_delete' })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi khi xóa hàng loạt!');
}
return true;
}
});
};
}
// 3. Bulk Spoiler
const pillSpoiler = document.getElementById('pill-spoiler');
if (pillSpoiler) {
pillSpoiler.onclick = () => {
const checked = document.querySelectorAll('.chk-comment:checked');
const commentIds = Array.from(checked).map(chk => chk.getAttribute('data-id'));
window.txamodal.show({
title: 'Bật/Tắt nhãn Spoiler hàng loạt',
message: `Lựa chọn hành động áp dụng cho <b>${commentIds.length}</b> bình luận đã chọn:`,
type: 'warning',
confirmText: 'Bật Spoiler',
cancelText: 'Tắt Spoiler',
onConfirm: async () => {
// Confirm -> Bật Spoiler
await applyBulkSpoiler(commentIds, true);
return true;
},
onCancel: async () => {
// Cancel -> Tắt Spoiler
await applyBulkSpoiler(commentIds, false);
return true;
}
});
};
}
async function applyBulkSpoiler(commentIds, isSpoiler) {
try {
const res = await fetch('/api/admin/movie/edit-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commentIds, action: 'bulk_spoiler', isSpoiler })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi khi cập nhật spoiler hàng loạt!');
}
}
// 4. Bulk Ban User
const pillBan = document.getElementById('pill-ban');
if (pillBan) {
pillBan.onclick = () => {
const checked = document.querySelectorAll('.chk-comment:checked');
const usernames = [...new Set(Array.from(checked).map(chk => chk.getAttribute('data-username')))];
window.txamodal.show({
title: 'Cấm bình luận hàng loạt',
message: `Bạn chắc chắn muốn cấm toàn bộ <b>${usernames.length}</b> thành viên viết bình luận trên hệ thống?`,
type: 'danger',
confirmText: 'Chặn tất cả',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/movie/edit-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ usernames, action: 'bulk_ban' })
});
const data = await res.json();
if (res.ok) {
window.txatoast.success(data.message);
loadComments();
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi chặn hàng loạt!');
}
return true;
}
});
};
}
}
document.addEventListener('astro:page-load', init);
</script>
<style>
.glass {
background: rgba(18, 20, 29, 0.4);
backdrop-filter: blur(40px) saturate(200%);
}
.glass-sub {
background: rgba(255, 255, 255, 0.01);
}
/* Bouncing ease out animation */
.ease-out-back {
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>