Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-03-23 11:12:56 +07:00
parent e345d9ccce
commit ffd177718f
39 changed files with 5258 additions and 520 deletions
+118 -33
View File
@@ -13,11 +13,12 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { BookOpen, Loader2, Plus, Upload, Edit, Trash2, LayoutGrid, List, Image as ImageIcon, Search, FileText, X, Check, FolderOpen, ChevronLeft, ChevronRight } from "lucide-react"
import { BookOpen, Loader2, Plus, Upload, Edit, Trash2, LayoutGrid, List, Image as ImageIcon, Search, FileText, X, Check, FolderOpen, ChevronLeft, ChevronRight, WandSparkles } from "lucide-react"
import { toast } from "sonner"
import Link from "next/link"
import { getNovelStatusBadgeClass } from "@/lib/novel-status"
import { Progress } from "@/components/ui/progress"
import { MOD_AI_PREFILL_STORAGE_KEY, type AINovelPrefillPayload } from "@/lib/mod-ai-tools"
interface Novel {
id: string
@@ -106,24 +107,19 @@ interface EpubUploadResponseData {
const CHAPTER_REGEX_PRESETS = [
{
id: "vi_chuong",
name: "VN - Chương 1: ...",
pattern: "^(?:Chương|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
},
{
id: "en_chapter",
name: "EN - Chapter 1: ...",
pattern: "^(?:Chapter|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
id: "vi_chuong_hoi",
name: "VN - Chương/Hồi/Tiết/Phần 1: ...",
pattern: "^(?:Chương|Hồi|Tiết|Phần|Thứ|Quyển|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
},
{
id: "mix_chapter",
name: "Mixed - Chương/Chapter",
pattern: "^(?:Chương|Chapter|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
name: "Mixed - Chương/Hồi/Chapter...",
pattern: "^(?:Chương|Hồi|Tiết|Phần|Thứ|Quyển|Chapter|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
},
{
id: "bracket_chapter",
name: "[Chương 01] ...",
pattern: "^\\[?\\s*(?:Chương|Chapter)\\s*\\d+(?:\\.\\d+)?\\s*\\]?[^\\n]*$",
id: "numeric_only",
name: "Chỉ có số (1. ...)",
pattern: "^\\d+(?:\\.\\d+)?\\s*[\\.\\:\\-\\]\\)]?(?:\\s+|$)[^\\n]*$",
},
]
@@ -146,7 +142,7 @@ export function NovelClient() {
const [epubSeriesId, setEpubSeriesId] = useState("")
const [epubSeriesName, setEpubSeriesName] = useState("")
const [epubSplitMode, setEpubSplitMode] = useState<"toc" | "regex">("toc")
const [epubRegexPreset, setEpubRegexPreset] = useState<string>("vi_chuong")
const [epubRegexPreset, setEpubRegexPreset] = useState<string>("vi_chuong_hoi")
const [epubCustomRegex, setEpubCustomRegex] = useState("")
const epubInputRef = useRef<HTMLInputElement>(null)
const epubFolderInputRef = useRef<HTMLInputElement>(null)
@@ -191,6 +187,7 @@ export function NovelClient() {
const [pageSize, setPageSize] = useState(20)
const [bulkProgress, setBulkProgress] = useState<Record<string, BulkUploadProgressItem>>({})
const [bulkDuplicateHandling, setBulkDuplicateHandling] = useState<BulkDuplicateHandling>("ask")
const [pendingAIPrefill, setPendingAIPrefill] = useState<AINovelPrefillPayload | null>(null)
const getSelectedChapterRegex = () => {
if (epubRegexPreset === "custom") {
@@ -360,6 +357,99 @@ export function NovelClient() {
fetchSeries()
}, [])
const normalizeGenreName = (value: string) => value.trim().toLowerCase()
const resetAddForm = () => {
setTitle("")
setOriginalTitle("")
setAuthorName("")
setOriginalAuthorName("")
setDescription("")
setCoverUrl("")
setSeriesMode("none")
setSelectedSeriesId("")
setNewSeriesName("")
setStatus("Đang ra")
setSelectedGenres([])
setGenreQuery("")
}
const applyAIPrefillToAddForm = (prefill: AINovelPrefillPayload) => {
const nextTitle = prefill.title?.trim() || ""
const nextAuthor = prefill.authorName?.trim() || ""
setTitle(nextTitle)
setOriginalTitle((prefill.originalTitle || nextTitle).trim())
setAuthorName(nextAuthor)
setOriginalAuthorName((prefill.originalAuthorName || nextAuthor).trim())
setDescription((prefill.description || "").trim())
setCoverUrl((prefill.coverUrl || "").trim())
setSeriesMode("none")
setSelectedSeriesId("")
setNewSeriesName("")
setGenreQuery("")
const validStatus = ["Đang ra", "Hoàn thành", "Tạm ngưng"].includes(prefill.status || "")
? (prefill.status as "Đang ra" | "Hoàn thành" | "Tạm ngưng")
: "Đang ra"
setStatus(validStatus)
const suggestedGenres = Array.isArray(prefill.genresSuggested) ? prefill.genresSuggested : []
if (suggestedGenres.length === 0 || genres.length === 0) {
setSelectedGenres([])
return
}
const byName = new Map(genres.map((genre) => [normalizeGenreName(genre.name), genre.id]))
const pickedIds: string[] = []
const missing: string[] = []
for (const name of suggestedGenres) {
const id = byName.get(normalizeGenreName(name))
if (!id) {
missing.push(name)
continue
}
if (!pickedIds.includes(id)) pickedIds.push(id)
}
setSelectedGenres(pickedIds)
if (missing.length > 0) {
toast.info(`The loai goi y chua co san: ${missing.slice(0, 4).join(", ")}`)
}
}
useEffect(() => {
if (typeof window === "undefined") return
const raw = window.localStorage.getItem(MOD_AI_PREFILL_STORAGE_KEY)
if (!raw) return
window.localStorage.removeItem(MOD_AI_PREFILL_STORAGE_KEY)
try {
const parsed = JSON.parse(raw) as AINovelPrefillPayload
setPendingAIPrefill(parsed)
setOpenAdd(true)
} catch {
toast.error("Du lieu AI Tool khong hop le")
}
}, [])
useEffect(() => {
if (!pendingAIPrefill) return
const needsGenres = Array.isArray(pendingAIPrefill.genresSuggested)
&& pendingAIPrefill.genresSuggested.length > 0
&& genres.length === 0
if (needsGenres) return
applyAIPrefillToAddForm(pendingAIPrefill)
setPendingAIPrefill(null)
toast.success("Da nap du lieu de xuat tu AI Tool")
}, [pendingAIPrefill, genres])
const filteredNovels = useMemo(() => {
const keyword = searchKeyword.trim().toLowerCase()
if (!keyword) return novels
@@ -671,18 +761,7 @@ export function NovelClient() {
if (!res.ok) throw new Error("Thêm mới thất bại")
toast.success("Đã thêm truyện thành công!")
setOpenAdd(false)
setTitle("")
setOriginalTitle("")
setAuthorName("")
setOriginalAuthorName("")
setDescription("")
setCoverUrl("")
setSeriesMode("none")
setSelectedSeriesId("")
setNewSeriesName("")
setStatus("Đang ra")
setSelectedGenres([])
setGenreQuery("")
resetAddForm()
fetchNovels()
fetchSeries()
} catch {
@@ -707,7 +786,7 @@ export function NovelClient() {
setEpubSeriesId("")
setEpubSeriesName("")
setEpubSplitMode("toc")
setEpubRegexPreset("vi_chuong")
setEpubRegexPreset("vi_chuong_hoi")
setEpubCustomRegex("")
if (epubInputRef.current) {
epubInputRef.current.value = ""
@@ -799,7 +878,7 @@ export function NovelClient() {
try {
await requestEpubPreview(file, {
splitMode: "toc",
regexPreset: "vi_chuong",
regexPreset: "vi_chuong_hoi",
regexInput: CHAPTER_REGEX_PRESETS[0].pattern,
preserveEditedMetadata: false,
})
@@ -1273,6 +1352,12 @@ export function NovelClient() {
<FolderOpen className="h-4 w-4" />
Chọn thư mục EPUB
</Button>
<Button variant="outline" className="gap-2" asChild>
<Link href="/mod/ai-tool">
<WandSparkles className="h-4 w-4" />
AI Tool
</Link>
</Button>
<Dialog
open={openEpubPreview}
@@ -1706,13 +1791,13 @@ export function NovelClient() {
</Dialog>
<Dialog open={openAdd} onOpenChange={(val) => {
setOpenAdd(val);
if (val) {
setTitle(""); setOriginalTitle(""); setAuthorName(""); setOriginalAuthorName(""); setDescription(""); setCoverUrl(""); setSeriesMode("none"); setSelectedSeriesId(""); setNewSeriesName(""); setSelectedGenres([]); setGenreQuery("");
setOpenAdd(val)
if (!val) {
resetAddForm()
}
}}>
<DialogTrigger asChild>
<Button className="gap-2">
<Button className="gap-2" onClick={resetAddForm}>
<Plus className="h-4 w-4" /> Thêm truyện
</Button>
</DialogTrigger>