"use client" import Link from "next/link" import { FormEvent, useEffect, useMemo, useState } from "react" import { Loader2, Search, Star, Trash2, UserRoundCheck } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" type NovelLite = { id: string title: string slug: string authorName: string coverUrl: string | null status: string totalChapters: number } type RecommendationItem = { id: string createdAt: string | null recommendCount: number novel: NovelLite editor: { id: string name: string } } type SummaryItem = { novel: NovelLite recommendCount: number } type CandidateNovel = NovelLite & { alreadyRecommended: boolean recommendCount: number } type RecommendationResponse = { items: RecommendationItem[] summary: SummaryItem[] candidates: CandidateNovel[] myNovelIds: string[] currentUser: { id: string role: string recommendationCount: number maxRecommendationCount: number } } function formatRelativeTime(value: string | null): string { if (!value) return "Vừa đề cử" const date = new Date(value) if (Number.isNaN(date.getTime())) return "Vừa đề cử" const diff = Date.now() - date.getTime() const minute = 60 * 1000 const hour = 60 * minute const day = 24 * hour if (diff < minute) return "Vừa xong" if (diff < hour) return `${Math.floor(diff / minute)} phút trước` if (diff < day) return `${Math.floor(diff / hour)} giờ trước` if (diff < day * 30) return `${Math.floor(diff / day)} ngày trước` return date.toLocaleDateString("vi-VN") } export function RecommendationClient() { const [loading, setLoading] = useState(true) const [searching, setSearching] = useState(false) const [submittingNovelId, setSubmittingNovelId] = useState(null) const [deletingId, setDeletingId] = useState(null) const [keyword, setKeyword] = useState("") const [activeQuery, setActiveQuery] = useState("") const [items, setItems] = useState([]) const [summary, setSummary] = useState([]) const [candidates, setCandidates] = useState([]) const [currentUser, setCurrentUser] = useState<{ id: string role: string recommendationCount: number maxRecommendationCount: number } | null>(null) const fetchData = async (query: string, initial = false) => { if (initial) { setLoading(true) } else { setSearching(true) } try { const url = query.trim() ? `/api/mod/de-cu?q=${encodeURIComponent(query.trim())}` : "/api/mod/de-cu" const res = await fetch(url) const data = (await res.json()) as RecommendationResponse & { error?: string } if (!res.ok) { throw new Error(data.error || "Không thể tải dữ liệu đề cử") } setItems(data.items || []) setSummary(data.summary || []) setCandidates(data.candidates || []) setCurrentUser(data.currentUser || null) setActiveQuery(query.trim()) } catch (error) { const message = error instanceof Error ? error.message : "Không thể tải dữ liệu đề cử" toast.error(message) } finally { setLoading(false) setSearching(false) } } useEffect(() => { fetchData("", true) }, []) const handleSearch = async (e: FormEvent) => { e.preventDefault() await fetchData(keyword) } const handleRecommend = async (novelId: string) => { setSubmittingNovelId(novelId) try { const res = await fetch("/api/mod/de-cu", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ novelId }), }) const data = (await res.json()) as { error?: string } if (!res.ok) { throw new Error(data.error || "Không thể thêm đề cử") } toast.success("Đã thêm đề cử") await fetchData(activeQuery) } catch (error) { const message = error instanceof Error ? error.message : "Không thể thêm đề cử" toast.error(message) } finally { setSubmittingNovelId(null) } } const handleDelete = async (id: string) => { if (!confirm("Bạn có chắc muốn xóa đề cử này?")) return setDeletingId(id) try { const res = await fetch(`/api/mod/de-cu?id=${encodeURIComponent(id)}`, { method: "DELETE" }) const data = (await res.json()) as { error?: string } if (!res.ok) { throw new Error(data.error || "Không thể xóa đề cử") } toast.success("Đã xóa đề cử") await fetchData(activeQuery) } catch (error) { const message = error instanceof Error ? error.message : "Không thể xóa đề cử" toast.error(message) } finally { setDeletingId(null) } } const canDelete = (item: RecommendationItem) => { if (!currentUser) return false return currentUser.role === "ADMIN" || currentUser.id === item.editor.id } const rankedSummary = useMemo(() => summary.slice(0, 12), [summary]) const hasReachedLimit = Boolean(currentUser) && (currentUser?.recommendationCount || 0) >= (currentUser?.maxRecommendationCount || 0) return (

Quản lý đề cử biên tập

Mỗi biên tập viên có thể đề cử truyện thủ công. Trang chủ sẽ lấy dữ liệu từ danh sách này.

{currentUser && (

Bạn đang dùng {currentUser.recommendationCount}/{currentUser.maxRecommendationCount} đề cử.

)}

Tìm truyện để đề cử

setKeyword(e.target.value)} placeholder="Nhập tên truyện, tác giả hoặc slug" />
{searching ? (
Đang tìm...
) : candidates.length === 0 ? (

Nhập từ khóa rồi bấm tìm để hiện danh sách truyện.

) : ( candidates.map((novel) => (
{novel.title}
{novel.title}

{novel.authorName}

{novel.recommendCount} đề cử

)) )}

Top theo số lượng đề cử

{rankedSummary.length === 0 ? (

Chưa có dữ liệu đề cử.

) : ( rankedSummary.map((item, index) => ( {index + 1} {item.novel.title}

{item.novel.title}

{item.recommendCount} đề cử

)) )}

Danh sách đề cử hiện tại

{loading ? ( ) : items.length === 0 ? ( ) : ( items.map((item) => ( )) )}
Truyện Biên tập viên Tổng đề cử Thời gian Thao tác
Chưa có đề cử nào
{item.novel.title}

{item.novel.authorName}

{item.editor.name} {item.recommendCount} {formatRelativeTime(item.createdAt)} {canDelete(item) ? ( ) : ( Không có quyền xóa )}
) }