"use client" import { useState, useEffect, Suspense, useRef } from "react" import { useSearchParams } from "next/navigation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { FileText, Loader2, Plus, ArrowLeft, Wand2, Edit, Trash2, Upload, Search } from "lucide-react" import { toast } from "sonner" import Link from "next/link" // @ts-ignore import * as mammoth from "mammoth" interface Chapter { _id: string number: number title: string views: number createdAt: string } const generatePagination = (currentPage: number, totalPages: number) => { if (totalPages <= 7) { return Array.from({ length: totalPages }, (_, i) => i + 1) } if (currentPage <= 3) { return [1, 2, 3, 4, '...', totalPages] } if (currentPage >= totalPages - 2) { return [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages] } return [1, '...', currentPage - 1, currentPage, currentPage + 1, '...', totalPages] } function ChapterManager() { const searchParams = useSearchParams() const novelId = searchParams.get("novelId") const [chapters, setChapters] = useState([]) const [loading, setLoading] = useState(true) const [openAdd, setOpenAdd] = useState(false) const [submitting, setSubmitting] = useState(false) // Pagination states const [currentPage, setCurrentPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [totalChapters, setTotalChapters] = useState(0) // Optimization states const [openOptimize, setOpenOptimize] = useState(false) const [previewMode, setPreviewMode] = useState(false) const [optimizing, setOptimizing] = useState(false) const [optRemovePrefix, setOptRemovePrefix] = useState(true) const [optRenumber, setOptRenumber] = useState(true) const [optimizedChapters, setOptimizedChapters] = useState([]) // Edit states const [openEdit, setOpenEdit] = useState(false) const [editingChapterId, setEditingChapterId] = useState(null) const [loadingEditData, setLoadingEditData] = useState(false) // Delete states const [openDelete, setOpenDelete] = useState(false) const [deletingChapterId, setDeletingChapterId] = useState(null) // Form states const [number, setNumber] = useState("") const [title, setTitle] = useState("") const [content, setContent] = useState("") // Multi-upload states const fileInputRef = useRef(null) const [uploadingMulti, setUploadingMulti] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [totalUpload, setTotalUpload] = useState(0) const fetchChapters = async (pageToFetch = 1) => { if (!novelId) return setLoading(true) try { const res = await fetch(`/api/mod/chuong?novelId=${novelId}&page=${pageToFetch}&limit=50`) if (!res.ok) throw new Error("Lỗi fetch") const data = await res.json() setChapters(data.chapters) setTotalPages(data.totalPages) setCurrentPage(data.currentPage) setTotalChapters(data.totalChapters) if (data.chapters.length > 0) { setNumber((data.chapters[data.chapters.length - 1].number + 1).toString()) } else { setNumber("1") } } catch { toast.error("Không tải được danh sách chương") } finally { setLoading(false) } } useEffect(() => { if (novelId) { fetchChapters(currentPage) } }, [novelId, currentPage]) const handleAddSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!number || !title || !content || !novelId) { toast.error("Vui lòng điền đầy đủ") return } setSubmitting(true) try { const res = await fetch("/api/mod/chuong", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ novelId, number: parseInt(number), title, content }), }) const resData = await res.json() if (!res.ok) throw new Error(resData.error || "Thêm mới thất bại") toast.success("Đã đăng chương mới thành công!") setOpenAdd(false) setTitle("") setContent("") setNumber((parseInt(number) + 1).toString()) fetchChapters() } catch (error: any) { toast.error(error.message) } finally { setSubmitting(false) } } const handleMultiFileUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length === 0 || !novelId) return setUploadingMulti(true) setTotalUpload(files.length) setUploadProgress(0) // Sort files by name to ensure order (e.g. Chapter 1, Chapter 2) files.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })) try { let currentNumber = parseInt(number) || 1 for (let i = 0; i < files.length; i++) { const file = files[i] let content = "" if (file.name.endsWith(".txt")) { content = await file.text() } else if (file.name.endsWith(".docx")) { const arrayBuffer = await file.arrayBuffer() const result = await mammoth.extractRawText({ arrayBuffer }) content = result.value } else { continue } if (!content.trim()) continue // Bỏ qua file rỗng let fileTitle = file.name.replace(/\.[^/.]+$/, "") // Loại bỏ "Chương X: " khỏi file title nếu cần thiết let cleanedTitle = fileTitle.replace(/^(Chương|Ch\.)\s*\d+\s*[:-]?\s*/i, "") if (!cleanedTitle) cleanedTitle = fileTitle const res = await fetch("/api/mod/chuong", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ novelId, number: currentNumber, title: cleanedTitle, content }), }) if (!res.ok) { const data = await res.json() throw new Error(data.error || `Lỗi khi lưu file ${file.name}`) } currentNumber++ setUploadProgress(i + 1) } toast.success(`Đã tải lên thành công ${files.length} chương!`) fetchChapters() } catch (error: any) { toast.error(error.message) } finally { setUploadingMulti(false) if (fileInputRef.current) fileInputRef.current.value = "" } } const handlePreviewOptimize = () => { let newChapters = [...chapters] if (optRenumber) { newChapters.sort((a, b) => a.number - b.number) newChapters = newChapters.map((ch, idx) => ({ ...ch, number: idx + 1 })) } if (optRemovePrefix) { newChapters = newChapters.map((ch, i) => { let newTitle = ch.title.replace(/^(Chương|Ch\.)\s*\d+\s*[:-]?\s*/i, "") if (!newTitle) newTitle = `Chương ${ch.number}` return { ...ch, title: newTitle } }) } setOptimizedChapters(newChapters) setPreviewMode(true) } const handleApplyOptimize = async () => { if (optimizedChapters.length === 0) return setOptimizing(true) try { const updates = optimizedChapters.map(ch => ({ id: ch._id, title: ch.title, number: ch.number })) const res = await fetch("/api/mod/chuong/optimize", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ novelId, updates }), }) const data = await res.json() if (!res.ok) throw new Error(data.error || "Lỗi tối ưu hóa") toast.success(`Đã tổi ưu ${data.modifiedCount} chương!`) setOpenOptimize(false) setPreviewMode(false) fetchChapters(currentPage) } catch (error: any) { toast.error(error.message) } finally { setOptimizing(false) } } // handleOpenEdit has been removed because edit is now via dedicated page // handleDelete remains the same const handleDelete = async () => { if (!deletingChapterId || !novelId) return setSubmitting(true) try { const res = await fetch(`/api/mod/chuong?id=${deletingChapterId}&novelId=${novelId}`, { method: "DELETE", }) if (!res.ok) { const data = await res.json() throw new Error(data.error || "Xóa thất bại") } toast.success("Đã xóa chương thành công") setOpenDelete(false) fetchChapters() } catch (error: any) { toast.error(error.message) } finally { setSubmitting(false) } } if (!novelId) { return (
Vui lòng chọn một truyện từ mục Quản lý truyện để xem danh sách chương.
) } return (

Quản lý chương

Đăng Chương Mới Thêm nội dung một chương truyện để gửi đến độc giả.
setNumber(e.target.value)} required />
setTitle(e.target.value)} placeholder="Ví dụ: Thiếu niên kỳ bạt" required autoFocus />