Refactor API integration and data fetching for genre, novel, and chapter pages

- Replace Prisma database calls with API fetches from the reader API in GenreDetailPage, GenresPage, SearchPage, ChapterReaderPage, and NovelDetailPage.
- Introduce new utility functions for API requests in server-api.ts, including error handling.
- Update authentication flow in auth.ts to sync Google login with the reader API.
- Modify NextAuth session and JWT types to include additional user information.
- Clean up unused imports and code related to Prisma and MongoDB connections.
- Adjust the configuration in next.config.mjs to remove unnecessary API routes.
This commit is contained in:
2026-03-30 13:54:51 +07:00
parent f9bb247ff1
commit 41aca718c9
12 changed files with 515 additions and 749 deletions
+61 -84
View File
@@ -1,13 +1,39 @@
// Server component instead of client component
import Link from "next/link"
import { Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { NovelCard } from "@/components/novel-card"
import { prisma } from "@/lib/prisma"
import { readerApiFetch } from "@/lib/server-api"
export const dynamic = "force-dynamic"
const PAGE_SIZE = 20
type BrowseNovel = {
id: string
slug: string
title: string
authorName: string
coverColor: string | null
coverUrl: string | null
rating: number
views: number
totalChapters: number
status: string
seriesId?: string | null
}
type BrowseResponse = {
items: BrowseNovel[]
totalCount: number
totalPages: number
currentPage: number
}
type GenreItem = {
id: string
name: string
slug: string
}
function collapseSeriesRows<T extends { id: string; seriesId?: string | null }>(rows: T[]): T[] {
const pickedSeries = new Set<string>()
const output: T[] = []
@@ -26,6 +52,11 @@ function collapseSeriesRows<T extends { id: string; seriesId?: string | null }>(
return output
}
function buildBrowsePath(params: Record<string, string>) {
const search = new URLSearchParams(params)
return `/api/novels/browse?${search.toString()}`
}
export default async function SearchPage({
searchParams,
}: {
@@ -38,99 +69,47 @@ export default async function SearchPage({
const statusFilter = resolvedParams.statusFilter || "all"
const requestedPage = Math.max(1, Number(resolvedParams.page || "1") || 1)
// Build where clause
let where: any = {}
const genres = await readerApiFetch<GenreItem[]>("/api/genres")
if (q) {
where.OR = [
{ title: { contains: q, mode: "insensitive" } },
{ authorName: { contains: q, mode: "insensitive" } },
{ originalAuthorName: { contains: q, mode: "insensitive" } },
{ series: { name: { contains: q, mode: "insensitive" } } },
]
}
if (genreFilter !== "all") {
where.genres = {
some: {
genre: {
slug: genreFilter
}
}
}
}
if (statusFilter !== "all") {
where.status = statusFilter
}
// Build order clause
let orderBy: any = {}
switch (sortBy) {
case "popular":
orderBy = { views: "desc" }
break
case "rating":
orderBy = { rating: "desc" }
break
case "name":
orderBy = { title: "asc" }
break
case "latest":
default:
orderBy = { updatedAt: "desc" }
}
let filteredNovels: any[] = []
let filteredNovels: BrowseNovel[] = []
let totalResults = 0
let totalPages = 1
let currentPage = requestedPage
if (q) {
totalResults = await prisma.novel.count({ where })
totalPages = Math.max(1, Math.ceil(totalResults / PAGE_SIZE))
currentPage = Math.min(currentPage, totalPages)
const browse = await readerApiFetch<BrowseResponse>(
buildBrowsePath({
q,
sort: sortBy,
genre: genreFilter === "all" ? "" : genreFilter,
status: statusFilter === "all" ? "" : statusFilter,
page: String(requestedPage),
limit: String(PAGE_SIZE),
})
)
filteredNovels = await prisma.novel.findMany({
where,
orderBy,
include: {
series: {
select: {
id: true,
name: true,
slug: true,
},
},
},
skip: (currentPage - 1) * PAGE_SIZE,
take: PAGE_SIZE,
})
filteredNovels = browse.items
totalResults = browse.totalCount
totalPages = Math.max(1, browse.totalPages || 1)
currentPage = Math.min(requestedPage, totalPages)
} else {
const filteredNovelsRaw = await prisma.novel.findMany({
where,
orderBy,
include: {
series: {
select: {
id: true,
name: true,
slug: true,
},
},
},
take: 500,
})
const browse = await readerApiFetch<BrowseResponse>(
buildBrowsePath({
sort: sortBy,
genre: genreFilter === "all" ? "" : genreFilter,
status: statusFilter === "all" ? "" : statusFilter,
page: "1",
limit: "500",
})
)
const collapsed = collapseSeriesRows(filteredNovelsRaw)
const collapsed = collapseSeriesRows(browse.items)
totalResults = collapsed.length
totalPages = Math.max(1, Math.ceil(totalResults / PAGE_SIZE))
currentPage = Math.min(currentPage, totalPages)
currentPage = Math.min(requestedPage, totalPages)
filteredNovels = collapsed.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)
}
const genres = await prisma.genre.findMany({ orderBy: { name: "asc" } })
const pageRangeStart = Math.max(1, currentPage - 2)
const pageRangeEnd = Math.min(totalPages, currentPage + 2)
const pageNumbers = Array.from(
@@ -154,7 +133,6 @@ export default async function SearchPage({
<div className="mx-auto max-w-6xl px-4 py-6">
<h1 className="mb-6 text-2xl font-bold text-foreground">Tìm Kiếm Truyện</h1>
{/* Search and Filters - This requires a client component wrapper ideally, but for now we can rely on standard form submissions to update searchParams */}
<form method="GET" action="/tim-kiem">
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -189,11 +167,10 @@ export default async function SearchPage({
<option value="name">Theo tên</option>
</select>
<button type="submit" className="h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium">Lọc</button>
<button type="submit" className="h-9 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground">Lọc</button>
</div>
</form>
{/* Results */}
<p className="mb-4 text-sm text-muted-foreground">
{totalResults} kết quả {totalResults > 0 && `(Trang ${currentPage}/${totalPages})`}
</p>