Initial reader-api backend extracted from reader

This commit is contained in:
2026-03-24 13:55:10 +07:00
parent 56f8f5ccfc
commit 24f070d14e
69 changed files with 12167 additions and 1 deletions
+67
View File
@@ -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 })
}
}
+115
View File
@@ -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 })
}
}