refactor: Remove unused volume fields and improve error handling in novel and chapter management
Build and Push Reader Image / docker (push) Successful in 40s
Build and Push Reader Image / docker (push) Successful in 40s
This commit is contained in:
@@ -39,9 +39,6 @@ export function EditorClient({ chapterId }: { chapterId: string }) {
|
|||||||
|
|
||||||
// Core states
|
// Core states
|
||||||
const [number, setNumber] = useState("")
|
const [number, setNumber] = useState("")
|
||||||
const [volumeNumber, setVolumeNumber] = useState("")
|
|
||||||
const [volumeTitle, setVolumeTitle] = useState("")
|
|
||||||
const [volumeChapterNumber, setVolumeChapterNumber] = useState("")
|
|
||||||
const [title, setTitle] = useState("")
|
const [title, setTitle] = useState("")
|
||||||
const [content, setContent] = useState("")
|
const [content, setContent] = useState("")
|
||||||
const [originalNovelId, setOriginalNovelId] = useState("")
|
const [originalNovelId, setOriginalNovelId] = useState("")
|
||||||
@@ -80,9 +77,6 @@ export function EditorClient({ chapterId }: { chapterId: string }) {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
setNumber(data.number.toString())
|
setNumber(data.number.toString())
|
||||||
setVolumeNumber(data.volumeNumber ? String(data.volumeNumber) : "")
|
|
||||||
setVolumeTitle(data.volumeTitle || "")
|
|
||||||
setVolumeChapterNumber(data.volumeChapterNumber ? String(data.volumeChapterNumber) : "")
|
|
||||||
setTitle(data.title)
|
setTitle(data.title)
|
||||||
setContent(data.content)
|
setContent(data.content)
|
||||||
setOriginalNovelId(data.novelId)
|
setOriginalNovelId(data.novelId)
|
||||||
@@ -195,9 +189,6 @@ export function EditorClient({ chapterId }: { chapterId: string }) {
|
|||||||
id: chapterId,
|
id: chapterId,
|
||||||
novelId: originalNovelId,
|
novelId: originalNovelId,
|
||||||
number: parseInt(number),
|
number: parseInt(number),
|
||||||
volumeNumber: volumeNumber ? parseInt(volumeNumber) : null,
|
|
||||||
volumeTitle: volumeTitle.trim() || null,
|
|
||||||
volumeChapterNumber: volumeChapterNumber ? parseInt(volumeChapterNumber) : null,
|
|
||||||
title,
|
title,
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
@@ -486,23 +477,11 @@ export function EditorClient({ chapterId }: { chapterId: string }) {
|
|||||||
|
|
||||||
{/* Editor Workspace */}
|
{/* Editor Workspace */}
|
||||||
<div className="flex flex-col flex-1 pb-4 min-h-0">
|
<div className="flex flex-col flex-1 pb-4 min-h-0">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4 mb-4 shrink-0">
|
<div className="grid grid-cols-1 gap-4 mb-4 shrink-0">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-semibold uppercase text-muted-foreground">Chương số</label>
|
<label className="text-xs font-semibold uppercase text-muted-foreground">Chương số</label>
|
||||||
<Input type="number" value={number} onChange={(e) => setNumber(e.target.value)} className="font-mono" />
|
<Input type="number" value={number} onChange={(e) => setNumber(e.target.value)} className="font-mono" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-xs font-semibold uppercase text-muted-foreground">Quyển số</label>
|
|
||||||
<Input type="number" value={volumeNumber} onChange={(e) => setVolumeNumber(e.target.value)} className="font-mono" placeholder="VD: 1" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-xs font-semibold uppercase text-muted-foreground">Chương trong quyển</label>
|
|
||||||
<Input type="number" value={volumeChapterNumber} onChange={(e) => setVolumeChapterNumber(e.target.value)} className="font-mono" placeholder="VD: 3" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 md:col-span-3">
|
|
||||||
<label className="text-xs font-semibold uppercase text-muted-foreground">Tên quyển</label>
|
|
||||||
<Input value={volumeTitle} onChange={(e) => setVolumeTitle(e.target.value)} placeholder="VD: Quyển 1 - Khởi đầu" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 mb-4 shrink-0">
|
<div className="grid grid-cols-1 gap-4 mb-4 shrink-0">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@@ -134,9 +134,6 @@ function ChapterManager() {
|
|||||||
|
|
||||||
// Form states
|
// Form states
|
||||||
const [number, setNumber] = useState("")
|
const [number, setNumber] = useState("")
|
||||||
const [volumeNumber, setVolumeNumber] = useState("")
|
|
||||||
const [volumeTitle, setVolumeTitle] = useState("")
|
|
||||||
const [volumeChapterNumber, setVolumeChapterNumber] = useState("")
|
|
||||||
const [title, setTitle] = useState("")
|
const [title, setTitle] = useState("")
|
||||||
const [content, setContent] = useState("")
|
const [content, setContent] = useState("")
|
||||||
|
|
||||||
@@ -349,9 +346,6 @@ function ChapterManager() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
novelId,
|
novelId,
|
||||||
number: parseInt(number),
|
number: parseInt(number),
|
||||||
volumeNumber: volumeNumber ? parseInt(volumeNumber) : null,
|
|
||||||
volumeTitle: volumeTitle.trim() || null,
|
|
||||||
volumeChapterNumber: volumeChapterNumber ? parseInt(volumeChapterNumber) : null,
|
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
}),
|
}),
|
||||||
@@ -364,9 +358,6 @@ function ChapterManager() {
|
|||||||
setOpenAdd(false)
|
setOpenAdd(false)
|
||||||
setTitle("")
|
setTitle("")
|
||||||
setContent("")
|
setContent("")
|
||||||
setVolumeNumber("")
|
|
||||||
setVolumeTitle("")
|
|
||||||
setVolumeChapterNumber("")
|
|
||||||
setNumber((parseInt(number) + 1).toString())
|
setNumber((parseInt(number) + 1).toString())
|
||||||
fetchChapters()
|
fetchChapters()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -790,24 +781,6 @@ function ChapterManager() {
|
|||||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Ví dụ: Thiếu niên kỳ bạt" required autoFocus />
|
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Ví dụ: Thiếu niên kỳ bạt" required autoFocus />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<details className="group border rounded-lg [&_summary::-webkit-details-marker]:hidden">
|
|
||||||
<summary className="flex cursor-pointer items-center justify-between px-4 py-2 bg-muted/30 font-medium">
|
|
||||||
<span className="text-sm">Tùy chọn nâng cao (Quyển / Tập)</span>
|
|
||||||
<span className="transition duration-300 group-open:-rotate-180">
|
|
||||||
<svg fill="none" height="18" shape-rendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" viewBox="0 0 24 24" width="18"><path d="M6 9l6 6 6-6"></path></svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="grid grid-cols-6 gap-4 p-4 text-muted-foreground bg-card">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<label className="text-xs font-medium">Quyển số</label>
|
|
||||||
<Input type="number" value={volumeNumber} onChange={(e) => setVolumeNumber(e.target.value)} placeholder="VD: 1" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-4">
|
|
||||||
<label className="text-xs font-medium">Tên quyển</label>
|
|
||||||
<Input value={volumeTitle} onChange={(e) => setVolumeTitle(e.target.value)} placeholder="VD: Khởi đầu mới" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<div className="space-y-2 flex-1 flex flex-col h-full">
|
<div className="space-y-2 flex-1 flex flex-col h-full">
|
||||||
<label className="text-sm font-medium">Nội dung văn bản (Hỗ trợ xuống dòng)</label>
|
<label className="text-sm font-medium">Nội dung văn bản (Hỗ trợ xuống dòng)</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ export function ImportClient() {
|
|||||||
const [uploadingCover, setUploadingCover] = useState(false)
|
const [uploadingCover, setUploadingCover] = useState(false)
|
||||||
|
|
||||||
const [title, setTitle] = useState("")
|
const [title, setTitle] = useState("")
|
||||||
|
const [originalTitle, setOriginalTitle] = useState("")
|
||||||
const [author, setAuthor] = useState("")
|
const [author, setAuthor] = useState("")
|
||||||
|
const [originalAuthorName, setOriginalAuthorName] = useState("")
|
||||||
|
const [status, setStatus] = useState("Đang ra")
|
||||||
const [shortDescription, setShortDescription] = useState("")
|
const [shortDescription, setShortDescription] = useState("")
|
||||||
const [genres, setGenres] = useState<Genre[]>([])
|
const [genres, setGenres] = useState<Genre[]>([])
|
||||||
const [selectedGenreIds, setSelectedGenreIds] = useState<string[]>([])
|
const [selectedGenreIds, setSelectedGenreIds] = useState<string[]>([])
|
||||||
@@ -201,7 +204,10 @@ export function ImportClient() {
|
|||||||
setCoverDetected(Boolean(data?.coverDetected))
|
setCoverDetected(Boolean(data?.coverDetected))
|
||||||
setCoverPreviewUrl(typeof data?.coverPreviewDataUrl === "string" ? data.coverPreviewDataUrl : "")
|
setCoverPreviewUrl(typeof data?.coverPreviewDataUrl === "string" ? data.coverPreviewDataUrl : "")
|
||||||
setTitle(suggested.title || item.title || "")
|
setTitle(suggested.title || item.title || "")
|
||||||
|
setOriginalTitle("")
|
||||||
setAuthor(suggested.author || item.author || "Unknown")
|
setAuthor(suggested.author || item.author || "Unknown")
|
||||||
|
setOriginalAuthorName("")
|
||||||
|
setStatus("Đang ra")
|
||||||
setShortDescription(suggested.shortDescription || "")
|
setShortDescription(suggested.shortDescription || "")
|
||||||
const genreList = await fetchGenres()
|
const genreList = await fetchGenres()
|
||||||
const suggestedGenres: string[] = suggested.genres || []
|
const suggestedGenres: string[] = suggested.genres || []
|
||||||
@@ -286,10 +292,16 @@ export function ImportClient() {
|
|||||||
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data?.detail || "Parse preview thất bại")
|
if (!res.ok) throw new Error(data?.detail || "Parse preview thất bại")
|
||||||
const chapters = Array.isArray(data?.chaptersPreview) ? data.chaptersPreview : []
|
const sampled = Array.isArray(data?.sample) ? data.sample : []
|
||||||
setPreviewItems(chapters.map((c: any) => ({ bucket: "preview", number: c.number || 0, title: c.title || "", chars: (c.excerpt || "").length, preview: c.excerpt || "" })))
|
if (sampled.length > 0) {
|
||||||
setChapterCount(Number(data?.novel?.totalChapters || chapters.length || 0))
|
setPreviewItems(sampled.map((c: any) => ({ bucket: c.bucket || "preview", number: c.number || 0, title: c.title || "", chars: c.chars || 0, preview: c.preview || "" })))
|
||||||
if ((Number(data?.novel?.totalChapters || chapters.length || 0)) <= 0) {
|
} else {
|
||||||
|
const chapters = Array.isArray(data?.chaptersPreview) ? data.chaptersPreview : []
|
||||||
|
setPreviewItems(chapters.map((c: any) => ({ bucket: "preview", number: c.number || 0, title: c.title || "", chars: (c.excerpt || "").length, preview: c.excerpt || "" })))
|
||||||
|
}
|
||||||
|
const total = Number(data?.novel?.totalChapters || data?.chapterCount || 0)
|
||||||
|
setChapterCount(total)
|
||||||
|
if (total <= 0) {
|
||||||
setParseError("Không tách được chương từ EPUB này với cấu hình hiện tại. Thử đổi TOC/Regex rồi parse lại.")
|
setParseError("Không tách được chương từ EPUB này với cấu hình hiện tại. Thử đổi TOC/Regex rồi parse lại.")
|
||||||
}
|
}
|
||||||
toast.success("Đã tạo preview chương")
|
toast.success("Đã tạo preview chương")
|
||||||
@@ -311,8 +323,12 @@ export function ImportClient() {
|
|||||||
form.append("splitMode", splitMode)
|
form.append("splitMode", splitMode)
|
||||||
if (splitMode === "regex") form.append("chapterRegex", chapterStartPattern)
|
if (splitMode === "regex") form.append("chapterRegex", chapterStartPattern)
|
||||||
form.append("title", title)
|
form.append("title", title)
|
||||||
|
form.append("originalTitle", originalTitle)
|
||||||
form.append("authorName", author)
|
form.append("authorName", author)
|
||||||
|
form.append("originalAuthorName", originalAuthorName)
|
||||||
|
form.append("status", status)
|
||||||
form.append("description", shortDescription)
|
form.append("description", shortDescription)
|
||||||
|
form.append("genreIds", selectedGenreIds.join(","))
|
||||||
form.append("replaceExisting", String(replaceExisting))
|
form.append("replaceExisting", String(replaceExisting))
|
||||||
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -421,7 +437,18 @@ export function ImportClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Tiêu đề" />
|
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Tiêu đề" />
|
||||||
|
<Input value={originalTitle} onChange={(e) => setOriginalTitle(e.target.value)} placeholder="Tên gốc truyện" />
|
||||||
<Input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Tác giả" />
|
<Input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Tác giả" />
|
||||||
|
<Input value={originalAuthorName} onChange={(e) => setOriginalAuthorName(e.target.value)} placeholder="Tên gốc tác giả" />
|
||||||
|
<select
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => setStatus(e.target.value)}
|
||||||
|
className="h-10 rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="Đang ra">Đang ra</option>
|
||||||
|
<option value="Hoàn thành">Hoàn thành</option>
|
||||||
|
<option value="Tạm ngưng">Tạm ngưng</option>
|
||||||
|
</select>
|
||||||
<Textarea value={shortDescription} onChange={(e) => setShortDescription(e.target.value)} placeholder="Mô tả ngắn" rows={4} />
|
<Textarea value={shortDescription} onChange={(e) => setShortDescription(e.target.value)} placeholder="Mô tả ngắn" rows={4} />
|
||||||
<div className="space-y-2 rounded-md border bg-card p-3">
|
<div className="space-y-2 rounded-md border bg-card p-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -341,15 +341,7 @@ export function NovelClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchSeries = async () => {
|
const fetchSeries = async () => {
|
||||||
try {
|
setSeriesList([])
|
||||||
const res = await fetch("/api/mod/series")
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
setSeriesList(data)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.error("Failed to fetch series")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1250,7 +1242,7 @@ export function NovelClient() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data.error || "Lỗi cập nhật")
|
if (!res.ok) throw new Error(data?.detail || data?.error || "Lỗi cập nhật")
|
||||||
|
|
||||||
toast.success("Cập nhật truyện thành công!")
|
toast.success("Cập nhật truyện thành công!")
|
||||||
setOpenEdit(false)
|
setOpenEdit(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user