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:
+57
-41
@@ -19,62 +19,78 @@ export function BookmarkProvider({ children }: { children: ReactNode }) {
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
const stored = localStorage.getItem(`truyen-chu-bookmarks-${user.id}`)
|
||||
if (stored) {
|
||||
try {
|
||||
setBookmarks(JSON.parse(stored))
|
||||
} catch {
|
||||
setBookmarks([])
|
||||
}
|
||||
} else {
|
||||
let mounted = true
|
||||
const fetchBookmarks = async () => {
|
||||
if (!user) {
|
||||
setBookmarks([])
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/user/bookmarks")
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (mounted) setBookmarks(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch bookmarks", e)
|
||||
}
|
||||
} else {
|
||||
setBookmarks([])
|
||||
}
|
||||
fetchBookmarks()
|
||||
return () => { mounted = false }
|
||||
}, [user])
|
||||
|
||||
const persist = useCallback((newBookmarks: Bookmark[]) => {
|
||||
if (user) {
|
||||
localStorage.setItem(`truyen-chu-bookmarks-${user.id}`, JSON.stringify(newBookmarks))
|
||||
}
|
||||
}, [user])
|
||||
const toggleBookmark = useCallback(async (novelId: string) => {
|
||||
if (!user) return
|
||||
|
||||
const isBookmarked = useCallback((novelId: string) => {
|
||||
return bookmarks.some((b) => b.novelId === novelId)
|
||||
}, [bookmarks])
|
||||
|
||||
const toggleBookmark = useCallback((novelId: string) => {
|
||||
// Optimistic update
|
||||
setBookmarks((prev) => {
|
||||
const exists = prev.find((b) => b.novelId === novelId)
|
||||
const next = exists
|
||||
? prev.filter((b) => b.novelId !== novelId)
|
||||
: [...prev, { novelId, addedAt: new Date().toISOString() }]
|
||||
persist(next)
|
||||
return next
|
||||
})
|
||||
}, [persist])
|
||||
|
||||
const updateProgress = useCallback((novelId: string, chapterId: string, chapterNumber: number) => {
|
||||
setBookmarks((prev) => {
|
||||
const idx = prev.findIndex((b) => b.novelId === novelId)
|
||||
let next: Bookmark[]
|
||||
if (idx >= 0) {
|
||||
next = [...prev]
|
||||
next[idx] = { ...next[idx], lastChapterId: chapterId, lastChapterNumber: chapterNumber }
|
||||
} else {
|
||||
next = [...prev, { novelId, lastChapterId: chapterId, lastChapterNumber: chapterNumber, addedAt: new Date().toISOString() }]
|
||||
if (exists) {
|
||||
return prev.filter((b) => b.novelId !== novelId)
|
||||
}
|
||||
persist(next)
|
||||
return next
|
||||
return [...prev, { novelId, addedAt: new Date().toISOString() } as any]
|
||||
})
|
||||
}, [persist])
|
||||
|
||||
try {
|
||||
await fetch("/api/user/bookmarks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "toggle", novelId })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const updateProgress = useCallback(async (novelId: string, chapterId: string, chapterNumber: number) => {
|
||||
if (!user) return
|
||||
|
||||
// Optimistic update
|
||||
setBookmarks((prev) => {
|
||||
const exists = prev.find((b) => b.novelId === novelId)
|
||||
if (exists) {
|
||||
return prev.map(b => b.novelId === novelId ? { ...b, lastChapterId: chapterId, lastChapterNumber: chapterNumber } : b)
|
||||
}
|
||||
return [...prev, { novelId, lastChapterId: chapterId, lastChapterNumber: chapterNumber, addedAt: new Date().toISOString() } as any]
|
||||
})
|
||||
|
||||
try {
|
||||
await fetch("/api/user/bookmarks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "updateProgress", novelId, lastChapterId: chapterId, lastChapterNumber: chapterNumber })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const getProgress = useCallback((novelId: string) => {
|
||||
return bookmarks.find((b) => b.novelId === novelId)
|
||||
}, [bookmarks])
|
||||
|
||||
const isBookmarked = useCallback((novelId: string) => {
|
||||
return bookmarks.some((b) => b.novelId === novelId)
|
||||
}, [bookmarks])
|
||||
|
||||
return (
|
||||
<BookmarkContext.Provider value={{ bookmarks, isBookmarked, toggleBookmark, updateProgress, getProgress }}>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user