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:
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
req: Request,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
await connectToMongoDB()
|
||||
// console.log("Fetching chapter with ID:", id)
|
||||
|
||||
const chapter = await Chapter.findById(id)
|
||||
|
||||
if (!chapter) {
|
||||
// console.log("Chapter not found in DB")
|
||||
return NextResponse.json({ error: "Chapter not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Verify the moderator owns the related novel
|
||||
const novel = await prisma.novel.findFirst({
|
||||
where: {
|
||||
id: chapter.novelId,
|
||||
uploaderId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Unauthorized access to this chapter" }, { status: 403 })
|
||||
}
|
||||
|
||||
return NextResponse.json(chapter)
|
||||
} catch (error) {
|
||||
console.error("GET Chapter error:", error)
|
||||
return NextResponse.json({ error: "Failed to fetch chapter details" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { novelId, updates } = body
|
||||
|
||||
if (!novelId || !updates || !Array.isArray(updates)) {
|
||||
return NextResponse.json({ error: "Tham số không hợp lệ" }, { status: 400 })
|
||||
}
|
||||
|
||||
await connectToMongoDB()
|
||||
|
||||
// Prepare bulk operations for mongoose
|
||||
const bulkOps = updates.map((update: any) => ({
|
||||
updateOne: {
|
||||
filter: { _id: update.id, novelId: novelId },
|
||||
update: {
|
||||
$set: {
|
||||
number: update.number,
|
||||
title: update.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if (bulkOps.length === 0) {
|
||||
return NextResponse.json({ message: "Không có thay đổi nào" }, { status: 200 })
|
||||
}
|
||||
|
||||
const result = await Chapter.bulkWrite(bulkOps)
|
||||
|
||||
return NextResponse.json({
|
||||
message: "Cập nhật thành công",
|
||||
modifiedCount: result.modifiedCount
|
||||
}, { status: 200 })
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Bulk optimize error:", error)
|
||||
return NextResponse.json({ error: "Lỗi cập nhật hàng loạt", details: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
+103
-2
@@ -8,6 +8,8 @@ import { prisma } from "@/lib/prisma"
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const novelId = searchParams.get("novelId")
|
||||
const page = parseInt(searchParams.get("page") || "1")
|
||||
const limit = parseInt(searchParams.get("limit") || "20")
|
||||
|
||||
if (!novelId) {
|
||||
return NextResponse.json({ error: "novelId is required" }, { status: 400 })
|
||||
@@ -15,8 +17,23 @@ export async function GET(req: Request) {
|
||||
|
||||
try {
|
||||
await connectToMongoDB()
|
||||
const chapters = await Chapter.find({ novelId }).sort({ number: 1 }).select("-content")
|
||||
return NextResponse.json(chapters)
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const [chapters, totalChapters] = await Promise.all([
|
||||
Chapter.find({ novelId })
|
||||
.sort({ number: 1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.select("-content"),
|
||||
Chapter.countDocuments({ novelId })
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
chapters,
|
||||
totalChapters,
|
||||
totalPages: Math.ceil(totalChapters / limit),
|
||||
currentPage: page
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("GET Chapter Error:", error)
|
||||
return NextResponse.json({ error: "Failed to fetch chapters" }, { status: 500 })
|
||||
@@ -70,3 +87,87 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ error: "Failed to create chapter" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { id, novelId, number, title, content } = data
|
||||
|
||||
// Xác minh truyện thuộc về Mod này
|
||||
const novel = await prisma.novel.findFirst({
|
||||
where: { id: novelId, uploaderId: session.user.id },
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
|
||||
}
|
||||
|
||||
await connectToMongoDB()
|
||||
|
||||
const updatedChapter = await Chapter.findOneAndUpdate(
|
||||
{ _id: id, novelId },
|
||||
{ number, title, content },
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
if (!updatedChapter) {
|
||||
return NextResponse.json({ error: "Không tìm thấy chương" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(updatedChapter)
|
||||
} catch (error) {
|
||||
console.error("PUT Chapter Error:", error)
|
||||
return NextResponse.json({ error: "Failed to update chapter" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const id = url.searchParams.get("id")
|
||||
const novelId = url.searchParams.get("novelId")
|
||||
|
||||
if (!id || !novelId) {
|
||||
return NextResponse.json({ error: "Thiếu ID chương hoặc ID truyện" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Xác minh truyện thuộc về Mod này
|
||||
const novel = await prisma.novel.findFirst({
|
||||
where: { id: novelId, uploaderId: session.user.id },
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
|
||||
}
|
||||
|
||||
await connectToMongoDB()
|
||||
|
||||
const deletedChapter = await Chapter.findOneAndDelete({ _id: id, novelId })
|
||||
|
||||
if (!deletedChapter) {
|
||||
return NextResponse.json({ error: "Không tìm thấy chương" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Cập nhật lại số lượng chương trong Postgres
|
||||
const totalChapters = await Chapter.countDocuments({ novelId })
|
||||
await prisma.novel.update({
|
||||
where: { id: novelId },
|
||||
data: { totalChapters },
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: "Đã xóa chương thành công" })
|
||||
} catch (error) {
|
||||
console.error("DELETE Chapter Error:", error)
|
||||
return NextResponse.json({ error: "Failed to delete chapter" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import path from "path"
|
||||
import os from "os"
|
||||
import { promises as fs } from "fs"
|
||||
import { convert } from "html-to-text"
|
||||
import { slugify } from "@/lib/utils"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
@@ -71,12 +72,7 @@ export async function POST(req: Request) {
|
||||
let novelDesc = metadata.description || "Chưa có giới thiệu"
|
||||
|
||||
// Generate base slug
|
||||
const baseSlug = novelTitle
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "")
|
||||
const baseSlug = slugify(novelTitle)
|
||||
|
||||
let slug = baseSlug
|
||||
let slugCounter = 1
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { slugify } from "@/lib/utils"
|
||||
|
||||
// Get all genres
|
||||
export async function GET() {
|
||||
try {
|
||||
const genres = await prisma.genre.findMany({
|
||||
orderBy: { name: "asc" }
|
||||
})
|
||||
return NextResponse.json(genres)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to fetch genres" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// Admins/Mods can add new genres
|
||||
export async function POST(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { name, description } = data
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: "Genre name is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const slug = slugify(name)
|
||||
|
||||
const newGenre = await prisma.genre.create({
|
||||
data: { name, slug, description }
|
||||
})
|
||||
|
||||
return NextResponse.json(newGenre, { status: 201 })
|
||||
} catch (error: any) {
|
||||
if (error.code === 'P2002') {
|
||||
return NextResponse.json({ error: "Thể loại này đã tồn tại" }, { status: 400 })
|
||||
}
|
||||
return NextResponse.json({ error: "Failed to create genre" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const id = url.searchParams.get("id")
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Thiếu ID thể loại" }, { status: 400 })
|
||||
}
|
||||
|
||||
await prisma.genre.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: "Đã xóa thể loại thành công" })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Lỗi khi xóa thể loại" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
req: Request,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: {
|
||||
id,
|
||||
uploaderId: session.user.id,
|
||||
},
|
||||
include: {
|
||||
genres: {
|
||||
include: {
|
||||
genre: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Novel not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(novel)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to fetch novel details" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { slugify } from "@/lib/utils"
|
||||
|
||||
export async function GET() {
|
||||
const session = await getServerSession(authOptions)
|
||||
@@ -28,21 +29,22 @@ export async function POST(req: Request) {
|
||||
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { title, authorName, description, genreIds = [] } = data
|
||||
// Tạo slug từ title
|
||||
const slug = data.title
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "")
|
||||
const slug = slugify(title)
|
||||
|
||||
const newNovel = await prisma.novel.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
title,
|
||||
slug: slug,
|
||||
authorName: data.authorName,
|
||||
description: data.description,
|
||||
authorName,
|
||||
description,
|
||||
uploaderId: session.user.id,
|
||||
genres: {
|
||||
create: genreIds.map((id: string) => ({
|
||||
genre: { connect: { id } }
|
||||
}))
|
||||
}
|
||||
},
|
||||
})
|
||||
return NextResponse.json(newNovel, { status: 201 })
|
||||
@@ -50,3 +52,62 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ error: "Failed to create novel" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { id, title, authorName, description, status, genreIds } = data
|
||||
|
||||
// Update basic info and recreate genre relations
|
||||
const updatedNovel = await prisma.novel.update({
|
||||
where: { id: id, uploaderId: session.user.id }, // Make sure they own it
|
||||
data: {
|
||||
title,
|
||||
authorName,
|
||||
description,
|
||||
status,
|
||||
// Replace all existing genres if genreIds is provided
|
||||
...(genreIds !== undefined && {
|
||||
genres: {
|
||||
deleteMany: {},
|
||||
create: genreIds.map((gId: string) => ({
|
||||
genre: { connect: { id: gId } }
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
return NextResponse.json(updatedNovel)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to update novel" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const id = url.searchParams.get("id")
|
||||
|
||||
if (!id) return NextResponse.json({ error: "Thiếu ID truyện" }, { status: 400 })
|
||||
|
||||
// Xóa truyện. (Chapters trong MongoDB nên được xóa bằng một cron job hoặc API khác để tránh block UI quá lâu,
|
||||
// ở đây chúng ta chỉ xóa record của Postgres để ẩn truyện).
|
||||
await prisma.novel.delete({
|
||||
where: { id: id, uploaderId: session.user.id },
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: "Đã xóa truyện thành công" })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to delete novel" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: novelId } = await params
|
||||
const body = await req.json()
|
||||
const { content, chapterId } = body
|
||||
|
||||
if (!content || typeof content !== "string") {
|
||||
return NextResponse.json({ error: "Content is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const newComment = await prisma.comment.create({
|
||||
data: {
|
||||
content: content.trim(),
|
||||
userId: session.user.id,
|
||||
novelId,
|
||||
chapterId: chapterId || null
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
id: newComment.id,
|
||||
userId: newComment.user.id,
|
||||
username: newComment.user.name || "User",
|
||||
avatarColor: newComment.user.image || "bg-primary",
|
||||
novelId: newComment.novelId,
|
||||
chapterId: newComment.chapterId,
|
||||
content: newComment.content,
|
||||
createdAt: newComment.createdAt.toISOString().split("T")[0]
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("POST Comment Error", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await req.json()
|
||||
const { score } = body
|
||||
|
||||
if (typeof score !== 'number' || score < 1 || score > 5) {
|
||||
return NextResponse.json({ error: "Invalid score" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Fetch current rating
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: { id },
|
||||
select: { rating: true, ratingCount: true }
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Novel not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const { rating, ratingCount } = novel
|
||||
const newRatingCount = ratingCount + 1
|
||||
const newRating = ((rating * ratingCount) + score) / newRatingCount
|
||||
|
||||
const updatedNovel = await prisma.novel.update({
|
||||
where: { id },
|
||||
data: {
|
||||
rating: newRating,
|
||||
ratingCount: newRatingCount
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
rating: updatedNovel.rating,
|
||||
ratingCount: updatedNovel.ratingCount
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Rating Error", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
// Lấy danh sách bookmark
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const bookmarks = await prisma.bookmark.findMany({
|
||||
where: { userId: session.user.id },
|
||||
include: { novel: true },
|
||||
orderBy: { createdAt: "desc" }
|
||||
})
|
||||
|
||||
return NextResponse.json(bookmarks)
|
||||
} catch (error) {
|
||||
console.error("GET Bookmarks Error", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// Thêm, cập nhật hoặc xóa bookmark
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { action, novelId, lastChapterId, lastChapterNumber } = body
|
||||
|
||||
if (!novelId || !action) {
|
||||
return NextResponse.json({ error: "Bad Request" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (action === "toggle") {
|
||||
const existing = await prisma.bookmark.findUnique({
|
||||
where: {
|
||||
userId_novelId: {
|
||||
userId: session.user.id,
|
||||
novelId,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
// Xoá
|
||||
await prisma.$transaction([
|
||||
prisma.bookmark.delete({ where: { id: existing.id } }),
|
||||
prisma.novel.update({ where: { id: novelId }, data: { bookmarkCount: { decrement: 1 } } })
|
||||
])
|
||||
return NextResponse.json({ status: "removed" })
|
||||
} else {
|
||||
// Thêm mới
|
||||
const newBookmark = await prisma.$transaction(async (tx) => {
|
||||
const b = await tx.bookmark.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
novelId,
|
||||
lastChapterId,
|
||||
lastChapterNumber
|
||||
}
|
||||
})
|
||||
await tx.novel.update({ where: { id: novelId }, data: { bookmarkCount: { increment: 1 } } })
|
||||
return b
|
||||
})
|
||||
return NextResponse.json({ status: "added", bookmark: newBookmark })
|
||||
}
|
||||
} else if (action === "updateProgress") {
|
||||
// Cập nhật tiến độ lưu trang
|
||||
if (!lastChapterId || !lastChapterNumber) {
|
||||
return NextResponse.json({ error: "Missing chapter info" }, { status: 400 })
|
||||
}
|
||||
|
||||
const bookmark = await prisma.bookmark.upsert({
|
||||
where: {
|
||||
userId_novelId: {
|
||||
userId: session.user.id,
|
||||
novelId,
|
||||
}
|
||||
},
|
||||
update: {
|
||||
lastChapterId,
|
||||
lastChapterNumber
|
||||
},
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
novelId,
|
||||
lastChapterId,
|
||||
lastChapterNumber
|
||||
}
|
||||
})
|
||||
return NextResponse.json({ status: "updated", bookmark })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Invalid action" }, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error("POST Bookmarks Error", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user