dptxa-proxy / src /pages /admin /the-loai.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
17.4 kB
---
import AdminLayout from '../../layouts/AdminLayout.astro';
import { getGenres } from '../../lib/localDb.js';
const genres = await getGenres();
---
<AdminLayout title="Quản lý Thể loại">
<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ý Thể loại</h1>
<p class="mt-2 text-gray-500">Danh mục thể loại phim dùng để phân loại và lọc tìm kiếm trên hệ thống.</p>
</div>
<div class="flex flex-wrap gap-3">
<button id="sync-genres-btn" class="flex items-center gap-2 rounded-2xl bg-emerald-500/10 hover:bg-emerald-500 text-emerald-400 hover:text-white border border-emerald-500/20 px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer">
<i class="fas fa-sync-alt"></i>
Đồng bộ từ Phim
</button>
<button id="create-genre-btn" class="flex items-center gap-2 rounded-2xl bg-primary px-6 py-3.5 text-xs font-black uppercase tracking-widest text-dark hover:scale-105 transition-all shadow-[0_0_20px_var(--primary)] cursor-pointer">
<i class="fas fa-plus"></i>
Thêm Thể loại mới
</button>
</div>
</div>
<!-- Bulk Action Bar -->
<div id="bulk-bar" class="hidden glass rounded-3xl p-6 border border-primary/20 mb-8 items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xs font-black uppercase tracking-widest text-primary"><i class="fas fa-check-square"></i> Đã chọn <span id="selected-count">0</span> thể loại</span>
</div>
<div class="flex gap-2">
<button id="bulk-delete-btn" class="flex items-center gap-2 rounded-xl bg-red-500/10 hover:bg-red-500 text-red-400 hover:text-white border border-red-500/20 px-4 py-2.5 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer">
<i class="fas fa-trash"></i> Xóa hàng loạt
</button>
</div>
</div>
<div class="glass rounded-[40px] p-8 md:p-10 border border-white/5 space-y-6">
<!-- Filter section -->
<div class="flex items-center justify-between border-b border-white/5 pb-6">
<div class="relative w-full 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 kiếm tên hoặc slug..." 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>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5">
<th class="pb-6 w-12">
<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">Tên Thể loại</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Slug</th>
<th class="pb-6 text-right text-[10px] font-black uppercase tracking-widest text-gray-500">Thao tác</th>
</tr>
</thead>
<tbody id="table-body" class="divide-y divide-white/5">
<!-- Rendered via Javascript -->
</tbody>
</table>
</div>
</div>
</AdminLayout>
<script is:inline define:vars={{ initialGenres: genres }}>
let genres = initialGenres || [];
let searchQuery = '';
// Hàm chuyển tiếng Việt có dấu thành slug
function toSlug(str) {
str = str.toLowerCase().trim();
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, "a");
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, "e");
str = str.replace(/ì|í|ị|ỉ|ĩ/g, "i");
str = str.replace(/ò|á|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, "o");
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, "u");
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, "y");
str = str.replace(/đ/g, "d");
str = str.replace(/[^a-z0-9 -]/g, ""); // Xóa ký tự đặc biệt
str = str.replace(/\s+/g, "-"); // Thay khoảng trắng bằng -
str = str.replace(/-+/g, "-"); // Xóa nhiều - liên tiếp
return str;
}
function renderTable() {
const tbody = document.getElementById('table-body');
if (!tbody) return;
const filtered = genres.filter(g =>
g.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
g.slug.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filtered.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4" class="py-12 text-center text-gray-500 text-sm">
<i class="fas fa-tags text-3xl mb-3 block opacity-20"></i>
Không tìm thấy thể loại nào
</td>
</tr>
`;
return;
}
tbody.innerHTML = filtered.map(g => `
<tr class="hover:bg-white/[0.01] transition-colors group">
<td class="py-5">
<label class="relative flex items-center justify-center cursor-pointer">
<input type="checkbox" class="chk-item peer sr-only" data-slug="${g.slug}" />
<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 text-sm font-bold text-white">${g.name}</td>
<td class="py-5 text-xs text-gray-400 font-mono">${g.slug}</td>
<td class="py-5 text-right">
<div class="flex justify-end gap-2">
<button class="edit-btn h-9 w-9 rounded-xl bg-white/5 hover:bg-primary/20 text-gray-400 hover:text-primary transition-all border border-white/5 flex items-center justify-center" data-slug="${g.slug}" data-name="${g.name}">
<i class="fas fa-edit text-xs"></i>
</button>
<button class="delete-btn h-9 w-9 rounded-xl bg-white/5 hover:bg-red-500/20 text-gray-400 hover:text-red-500 transition-all border border-white/5 flex items-center justify-center" data-slug="${g.slug}" data-name="${g.name}">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
</td>
</tr>
`).join('');
bindEvents();
updateBulkBar();
}
function bindEvents() {
// Edit Genre
document.querySelectorAll('.edit-btn').forEach(btn => {
btn.onclick = () => {
const slug = btn.getAttribute('data-slug');
const name = btn.getAttribute('data-name');
window.txamodal.show({
title: 'Sửa Thể loại',
message: `
<div class="space-y-4 text-left">
<div>
<label class="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Tên thể loại</label>
<input type="text" id="edit-genre-name" value="${name}" class="w-full bg-[#12141d] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50" />
</div>
<div>
<label class="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Slug (Đường dẫn tĩnh)</label>
<input type="text" id="edit-genre-slug" value="${slug}" readonly class="w-full bg-white/5 border border-white/5 rounded-xl px-4 py-2.5 text-sm text-gray-400 cursor-not-allowed focus:outline-none" />
</div>
</div>`,
type: 'warning',
confirmText: 'Lưu thay đổi',
onConfirm: async () => {
const nameInput = document.getElementById('edit-genre-name') as HTMLInputElement;
const nameVal = nameInput ? nameInput.value.trim() : '';
if (!nameVal) {
window.txatoast.error('Tên không được để trống!');
return false;
}
try {
const res = await fetch('/api/admin/genres', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, name: nameVal })
});
const data = await res.json();
if (res.ok) {
genres = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
});
// Delete Genre
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.onclick = () => {
const slug = btn.getAttribute('data-slug');
const name = btn.getAttribute('data-name');
window.txamodal.show({
title: 'Xác nhận xóa thể loại',
message: `Bạn có chắc chắn muốn xóa thể loại <b>${name}</b>? Việc này sẽ gỡ thể loại khỏi các phim liên quan.`,
type: 'error',
confirmText: 'Xóa ngay',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/genres', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug })
});
const data = await res.json();
if (res.ok) {
genres = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
});
document.querySelectorAll('.chk-item').forEach(chk => {
chk.onchange = () => {
updateBulkBar();
};
});
}
function updateBulkBar() {
const chkAll = document.getElementById('chk-all');
const checked = document.querySelectorAll('.chk-item:checked');
const totalCount = document.querySelectorAll('.chk-item').length;
const bulkBar = document.getElementById('bulk-bar');
const selectedCount = document.getElementById('selected-count');
if (chkAll) {
chkAll.checked = totalCount > 0 && checked.length === totalCount;
}
if (checked.length > 0) {
if (bulkBar) {
bulkBar.style.display = 'flex';
}
if (selectedCount) selectedCount.textContent = checked.length;
} else {
if (bulkBar) {
bulkBar.style.display = 'none';
}
}
}
function init() {
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.oninput = (e) => {
searchQuery = e.target.value.trim();
renderTable();
};
}
const chkAll = document.getElementById('chk-all');
if (chkAll) {
chkAll.onchange = () => {
document.querySelectorAll('.chk-item').forEach(chk => {
chk.checked = chkAll.checked;
});
updateBulkBar();
};
}
// Create Genre
const createBtn = document.getElementById('create-genre-btn');
if (createBtn) {
createBtn.onclick = () => {
window.txamodal.show({
title: 'Thêm Thể loại mới',
message: `
<div class="space-y-4 text-left">
<div>
<label class="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Tên thể loại</label>
<input type="text" id="new-genre-name" placeholder="Ví dụ: Cổ Trang" class="w-full bg-[#12141d] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50" />
</div>
<div>
<label class="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Slug (Tự động sinh)</label>
<input type="text" id="new-genre-slug" placeholder="co-trang" class="w-full bg-[#12141d] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50" />
</div>
</div>`,
type: 'success',
confirmText: 'Tạo thể loại',
onConfirm: async () => {
const nameInput = document.getElementById('new-genre-name') as HTMLInputElement;
const slugInput = document.getElementById('new-genre-slug') as HTMLInputElement;
const name = nameInput ? nameInput.value.trim() : '';
const slug = slugInput ? slugInput.value.trim() : '';
if (!name || !slug) {
window.txatoast.error('Vui lòng điền đủ Tên và Slug!');
return false;
}
try {
const res = await fetch('/api/admin/genres', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, name })
});
const data = await res.json();
if (res.ok) {
genres = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
// Auto-generate slug on typing name in modal
setTimeout(() => {
const nameInput = document.getElementById('new-genre-name');
const slugInput = document.getElementById('new-genre-slug');
if (nameInput && slugInput) {
nameInput.addEventListener('input', (e) => {
slugInput.value = toSlug(e.target.value);
});
}
}, 100);
};
}
// Sync Genres
const syncBtn = document.getElementById('sync-genres-btn');
if (syncBtn) {
syncBtn.onclick = async () => {
window.txatoast.info('Đang đồng bộ thể loại từ kho phim...');
try {
const res = await fetch('/api/admin/genres', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sync: true })
});
const data = await res.json();
if (res.ok) {
genres = data.list;
renderTable();
window.txamodal.show({
title: 'Đồng bộ thể loại thành công',
message: data.message,
type: 'success'
});
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi đồng bộ thể loại!');
}
};
}
// Bulk Delete
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
if (bulkDeleteBtn) {
bulkDeleteBtn.onclick = () => {
const checked = document.querySelectorAll('.chk-item:checked');
const slugs = Array.from(checked).map(c => c.getAttribute('data-slug'));
window.txamodal.show({
title: 'Xác nhận xóa hàng loạt',
message: `Bạn có chắc chắn muốn xóa <b>${slugs.length}</b> thể loại đã chọn?`,
type: 'error',
confirmText: 'Xóa tất cả',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/genres', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slugs })
});
const data = await res.json();
if (res.ok) {
genres = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
}
renderTable();
}
document.addEventListener('astro:page-load', init);
</script>
<style>
.glass {
background: rgba(18, 20, 29, 0.4);
backdrop-filter: blur(40px) saturate(200%);
}
</style>