Initial reader-api backend extracted from reader
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Support both id and slug
|
||||
const novel = await prisma.novel.findFirst({
|
||||
where: { OR: [{ id }, { slug: id }] },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
originalTitle: true,
|
||||
authorName: true,
|
||||
originalAuthorName: true,
|
||||
description: true,
|
||||
coverUrl: true,
|
||||
coverColor: true,
|
||||
status: true,
|
||||
totalChapters: true,
|
||||
views: true,
|
||||
rating: true,
|
||||
ratingCount: true,
|
||||
bookmarkCount: true,
|
||||
seriesId: true,
|
||||
series: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
novels: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
totalChapters: true,
|
||||
status: true,
|
||||
coverUrl: true,
|
||||
},
|
||||
orderBy: { title: "asc" },
|
||||
},
|
||||
},
|
||||
},
|
||||
genres: { select: { genre: { select: { id: true, name: true, slug: true } } } },
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Novel not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...novel,
|
||||
genres: novel.genres.map((g) => g.genre),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Novel detail error:", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
|
||||
type SortKey = "latest" | "popular" | "rating" | "name"
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const q = searchParams.get("q")?.trim() || ""
|
||||
const genre = searchParams.get("genre") || ""
|
||||
const status = searchParams.get("status") || ""
|
||||
const sort: SortKey = (searchParams.get("sort") as SortKey) || "latest"
|
||||
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10))
|
||||
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "20", 10)))
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build where clause
|
||||
const where: Record<string, any> = {}
|
||||
if (status) where.status = status
|
||||
if (genre) {
|
||||
where.genres = { some: { genre: { slug: genre } } }
|
||||
}
|
||||
if (q) {
|
||||
where.OR = [
|
||||
{ title: { contains: q, mode: "insensitive" } },
|
||||
{ originalTitle: { contains: q, mode: "insensitive" } },
|
||||
{ authorName: { contains: q, mode: "insensitive" } },
|
||||
{ originalAuthorName: { contains: q, mode: "insensitive" } },
|
||||
{ series: { name: { contains: q, mode: "insensitive" } } },
|
||||
]
|
||||
}
|
||||
|
||||
// Build orderBy
|
||||
const orderBy: Record<string, any> =
|
||||
sort === "popular"
|
||||
? { views: "desc" }
|
||||
: sort === "rating"
|
||||
? { rating: "desc" }
|
||||
: sort === "name"
|
||||
? { title: "asc" }
|
||||
: { updatedAt: "desc" }
|
||||
|
||||
const [novels, totalCount] = await Promise.all([
|
||||
prisma.novel.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip,
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
originalTitle: true,
|
||||
authorName: true,
|
||||
coverUrl: true,
|
||||
coverColor: true,
|
||||
status: true,
|
||||
totalChapters: true,
|
||||
views: true,
|
||||
rating: true,
|
||||
ratingCount: true,
|
||||
bookmarkCount: true,
|
||||
seriesId: true,
|
||||
series: { select: { id: true, name: true, slug: true } },
|
||||
genres: { select: { genre: { select: { id: true, name: true, slug: true } } } },
|
||||
updatedAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.novel.count({ where }),
|
||||
])
|
||||
|
||||
// Attach latest chapter info
|
||||
await connectToMongoDB()
|
||||
const novelIds = novels.map((n) => n.id)
|
||||
const latestChapters = await Chapter.aggregate([
|
||||
{ $match: { novelId: { $in: novelIds } } },
|
||||
{ $sort: { novelId: 1, number: -1 } },
|
||||
{
|
||||
$group: {
|
||||
_id: "$novelId",
|
||||
latestChapterNumber: { $first: "$number" },
|
||||
latestChapterTitle: { $first: "$title" },
|
||||
latestChapterAt: { $first: "$createdAt" },
|
||||
},
|
||||
},
|
||||
])
|
||||
const chapterMap = Object.fromEntries(
|
||||
latestChapters.map((c) => [c._id, c])
|
||||
)
|
||||
|
||||
const items = novels.map((n) => ({
|
||||
...n,
|
||||
genres: n.genres.map((g) => g.genre),
|
||||
latestChapter: chapterMap[n.id]
|
||||
? {
|
||||
number: chapterMap[n.id].latestChapterNumber,
|
||||
title: chapterMap[n.id].latestChapterTitle,
|
||||
createdAt: chapterMap[n.id].latestChapterAt,
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
items,
|
||||
totalCount,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
currentPage: page,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Browse novels error:", error)
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user