Images now load direct from images.04080616.xyz. Removes read-side signing (signUrl/signCoverUrls + callers), unlocking browser and edge caching since URLs are stable. Presigned upload kept for /api/upload. PageReader retries failed loads via onError as a safety net. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import { prisma } from "@/lib/db";
|
|
import { parseGenres } from "@/lib/genres";
|
|
import { ChapterList } from "@/components/ChapterList";
|
|
import { ReadingProgressButton } from "@/components/ReadingProgressButton";
|
|
import type { Metadata } from "next";
|
|
|
|
type Props = {
|
|
params: Promise<{ slug: string }>;
|
|
};
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const manga = await prisma.manga.findUnique({ where: { slug } });
|
|
if (!manga) return { title: "Not Found" };
|
|
return {
|
|
title: manga.title,
|
|
description: manga.description,
|
|
openGraph: {
|
|
title: manga.title,
|
|
description: manga.description,
|
|
images: [manga.coverUrl],
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function MangaDetailPage({ params }: Props) {
|
|
const { slug } = await params;
|
|
|
|
const manga = await prisma.manga.findUnique({
|
|
where: { slug },
|
|
include: {
|
|
chapters: {
|
|
orderBy: { number: "asc" },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!manga) notFound();
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto px-4 py-6">
|
|
{/* Hero section */}
|
|
<div className="flex gap-4 mb-6">
|
|
<div className="w-28 sm:w-36 shrink-0">
|
|
<div className="aspect-[3/4] rounded-xl overflow-hidden bg-card">
|
|
<img
|
|
src={manga.coverUrl}
|
|
alt={manga.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0 py-1">
|
|
<h1 className="text-xl sm:text-2xl font-bold leading-tight mb-2">
|
|
{manga.title}
|
|
</h1>
|
|
<div className="flex flex-wrap items-center gap-2 mb-3">
|
|
{parseGenres(manga.genre).map((g) => (
|
|
<span
|
|
key={g}
|
|
className="px-2 py-0.5 text-[11px] font-semibold bg-accent/20 text-accent rounded-full"
|
|
>
|
|
{g}
|
|
</span>
|
|
))}
|
|
<span className="px-2 py-0.5 text-[11px] font-semibold bg-surface text-muted rounded-full border border-border">
|
|
{manga.status}
|
|
</span>
|
|
<span className="text-xs text-muted">
|
|
{manga.chapters.length} chapter
|
|
{manga.chapters.length !== 1 ? "s" : ""}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-muted leading-relaxed line-clamp-4 sm:line-clamp-none">
|
|
{manga.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{manga.chapters.length > 0 && (
|
|
<ReadingProgressButton
|
|
mangaSlug={manga.slug}
|
|
chapters={manga.chapters.map((c) => ({
|
|
number: c.number,
|
|
title: c.title,
|
|
}))}
|
|
/>
|
|
)}
|
|
|
|
{/* Chapters */}
|
|
<div>
|
|
<h2 className="text-lg font-bold mb-3">Chapters</h2>
|
|
<ChapterList chapters={manga.chapters} mangaSlug={manga.slug} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|