Add EPUB upload + DB integration
Add server-side EPUB import and integrate Prisma + Mongo for novels/chapters. Introduces a new moderator API route (app/api/mod/epub/route.ts) that parses .epub files, creates a novel record in Prisma, and inserts chapter documents into MongoDB via the Chapter Mongoose model. Frontend: novel management UI now supports EPUB upload (app/mod/truyen/novel-client.tsx) with progress/toasts and preserves the manual 'Add novel' dialog. Convert app pages to fetch real data from Prisma and Mongo (app/page.tsx, app/truyen/[slug]/page.tsx, app/truyen/[slug]/[chapterId]/page.tsx), adapt types/props to use authorName, and adjust chapter/comment IDs to use Mongo _id strings. Minor fixes: TTS player logs playback errors, UI text fixes (e.g. "Chương"), and novel-card/other components updated for authorName. package.json updated with epub2, html-to-text and types; pnpm lock updated. Adds tsconfig.tsbuildinfo.
This commit is contained in:
+57
-41
@@ -1,7 +1,8 @@
|
||||
import Link from "next/link"
|
||||
import { ArrowRight, BookOpen, Sparkles, Flame, Heart, Swords, Building2, Rocket, Crown, Laugh, Search, Shield } from "lucide-react"
|
||||
import { NovelCard } from "@/components/novel-card"
|
||||
import { genres, getPopularNovels, getLatestNovels, getTopRatedNovels, novels } from "@/lib/data"
|
||||
import { genres } from "@/lib/data"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
Sparkles: <Sparkles className="h-5 w-5" />,
|
||||
@@ -16,43 +17,58 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
Shield: <Shield className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const popularNovels = getPopularNovels(6)
|
||||
const latestNovels = getLatestNovels(6)
|
||||
const topRated = getTopRatedNovels(4)
|
||||
const featured = novels[3] // Anh Hung Xa Dieu
|
||||
export default async function HomePage() {
|
||||
const popularNovels = await prisma.novel.findMany({
|
||||
take: 6,
|
||||
orderBy: { views: "desc" },
|
||||
})
|
||||
|
||||
const latestNovels = await prisma.novel.findMany({
|
||||
take: 6,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
})
|
||||
|
||||
const topRated = await prisma.novel.findMany({
|
||||
take: 4,
|
||||
orderBy: { rating: "desc" },
|
||||
})
|
||||
|
||||
// get the most popular as featured (can be empty if DB is new)
|
||||
const featured = popularNovels[0]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-6">
|
||||
{/* Hero / Featured Novel */}
|
||||
<section className="mb-10">
|
||||
<Link
|
||||
href={`/truyen/${featured.slug}`}
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-border bg-card md:flex-row"
|
||||
>
|
||||
<div className={`flex h-48 items-center justify-center bg-gradient-to-br ${featured.coverColor} md:h-auto md:w-72`}>
|
||||
<BookOpen className="h-16 w-16 text-background/80" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-center gap-3 p-6">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-primary">Truyện Nổi Bật</span>
|
||||
<h1 className="text-2xl font-bold text-foreground group-hover:text-primary transition-colors text-balance md:text-3xl">
|
||||
{featured.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Tác giả: {featured.author}</p>
|
||||
<p className="line-clamp-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{featured.totalChapters} chương</span>
|
||||
<span>{featured.status}</span>
|
||||
<span className="flex items-center gap-1 text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{featured.rating}
|
||||
</span>
|
||||
{featured && (
|
||||
<section className="mb-10">
|
||||
<Link
|
||||
href={`/truyen/${featured.slug}`}
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-border bg-card md:flex-row"
|
||||
>
|
||||
<div className={`flex h-48 items-center justify-center bg-gradient-to-br ${featured.coverColor || "from-slate-700 to-slate-800"} md:h-auto md:w-72`}>
|
||||
<BookOpen className="h-16 w-16 text-background/80" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
<div className="flex flex-1 flex-col justify-center gap-3 p-6">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-primary">Truyện Nổi Bật</span>
|
||||
<h1 className="text-2xl font-bold text-foreground group-hover:text-primary transition-colors text-balance md:text-3xl">
|
||||
{featured.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Tác giả: {featured.authorName}</p>
|
||||
<p className="line-clamp-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{featured.totalChapters} chương</span>
|
||||
<span>{featured.status}</span>
|
||||
<span className="flex items-center gap-1 text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{featured.rating}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Popular Novels */}
|
||||
<section className="mb-10">
|
||||
@@ -63,9 +79,9 @@ export default function HomePage() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{popularNovels.map((novel) => (
|
||||
{popularNovels.length > 0 ? popularNovels.map((novel) => (
|
||||
<NovelCard key={novel.id} novel={novel} />
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground col-span-full">Chưa có truyện nào trong hệ thống.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -78,9 +94,9 @@ export default function HomePage() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{latestNovels.map((novel) => (
|
||||
{latestNovels.length > 0 ? latestNovels.map((novel) => (
|
||||
<NovelCard key={novel.id} novel={novel} variant="compact" />
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground col-span-full">Chưa có truyện nào được cập nhật.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,7 +106,7 @@ export default function HomePage() {
|
||||
<section>
|
||||
<h2 className="mb-4 text-xl font-bold text-foreground">Đánh Giá Cao</h2>
|
||||
<div className="flex flex-col gap-3">
|
||||
{topRated.map((novel, idx) => (
|
||||
{topRated.length > 0 ? topRated.map((novel, idx) => (
|
||||
<Link
|
||||
key={novel.id}
|
||||
href={`/truyen/${novel.slug}`}
|
||||
@@ -99,19 +115,19 @@ export default function HomePage() {
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div className={`flex h-12 w-9 shrink-0 items-center justify-center rounded bg-gradient-to-br ${novel.coverColor}`}>
|
||||
<div className={`flex h-12 w-9 shrink-0 items-center justify-center rounded bg-gradient-to-br ${novel.coverColor || "from-slate-700 to-slate-800"}`}>
|
||||
<BookOpen className="h-4 w-4 text-background/80" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-sm font-semibold text-foreground group-hover:text-primary transition-colors">{novel.title}</h3>
|
||||
<p className="text-xs text-muted-foreground">{novel.author} - Ch. {novel.totalChapters}</p>
|
||||
<p className="text-xs text-muted-foreground">{novel.authorName} - Ch. {novel.totalChapters}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{novel.rating}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground">Chưa có đánh giá.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user