Add moderation APIs and admin UI

Add moderator/admin backend APIs and client features for managing novels and chapters. New endpoints include mod chapter routes (paginated list, single GET, PUT, DELETE, and bulk optimize), mod novel routes (create, GET by id, update, delete), genre CRUD, user bookmarks, novel comments, and rating endpoints. Update EPUB import to use a shared slugify util. Enhance moderator UI: chapter manager gains pagination, bulk optimization preview/apply, edit/delete dialogs; novel client adds genre management and edit/delete flows. Also update Prisma schema, add a DB wipe script, remove unused lib/data.ts, and adjust related types/utils and bookmark context.
This commit is contained in:
2026-03-06 17:30:56 +07:00
parent ce805adb08
commit 75ed8e233b
31 changed files with 1853 additions and 687 deletions
+17 -2
View File
@@ -54,12 +54,22 @@ export function TTSPlayer({ paragraphs, novelSlug, currentChapter, maxChapter, c
const loadVoices = () => {
const available = speechSynthesis.getVoices()
// First try to find any Vietnamese voice ('vi-VN', 'Google Tiếng Việt', etc.)
const viVoices = available.filter(
(v) => v.lang.startsWith("vi") || v.name.toLowerCase().includes("vietnam")
(v) => v.lang.startsWith("vi") || v.name.toLowerCase().includes("vietnam") || v.name.toLowerCase().includes("tiếng việt")
)
const allUsable = viVoices.length > 0 ? viVoices : available.slice(0, 10)
// Filter out overly robotic generic fallbacks if we have good ones
const goodViVoices = viVoices.filter(v => v.name.includes("Google") || v.name.includes("Microsoft") || v.name.includes("Natural"))
const preferredViVoices = goodViVoices.length > 0 ? goodViVoices : viVoices
// If we still have NO vi voices, fallback to ALL voices so the user isn't stuck with an empty list
const allUsable = preferredViVoices.length > 0 ? preferredViVoices : available
setVoices(allUsable)
if (allUsable.length > 0 && !selectedVoiceURI) {
// Automatically default to the first Vietnamese voice if available, otherwise just the first system voice
setSelectedVoiceURI(allUsable[0].voiceURI)
}
}
@@ -170,6 +180,11 @@ export function TTSPlayer({ paragraphs, novelSlug, currentChapter, maxChapter, c
if (e.error !== "canceled" && e.error !== "interrupted") {
setIsPlaying(false)
releaseWakeLock()
if (e.error === "synthesis-failed" || e.error === "network") {
// Toast notification is tricky here without importing sonner, let's just log and stop cleanly without crashing
console.warn("Trình duyệt không hỗ trợ đọc giọng nói này hoặc bị lỗi kết nối.")
}
}
}