feat(backoffice): add page preview carousel on book detail page
Shows 5 pages at a time in a full-width grid with prev/next navigation. Pages are fetched via the existing proxy route with webp format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { fetchLibraries, getBookCoverUrl, BookDto, apiFetch } from "../../../lib/api";
|
import { fetchLibraries, getBookCoverUrl, BookDto, apiFetch } from "../../../lib/api";
|
||||||
|
import { BookPreview } from "../../components/BookPreview";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -157,6 +158,12 @@ export default async function BookDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{book.page_count && book.page_count > 0 && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<BookPreview bookId={book.id} pageCount={book.page_count} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
60
apps/backoffice/app/components/BookPreview.tsx
Normal file
60
apps/backoffice/app/components/BookPreview.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 5;
|
||||||
|
|
||||||
|
export function BookPreview({ bookId, pageCount }: { bookId: string; pageCount: number }) {
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
const pages = Array.from({ length: PAGE_SIZE }, (_, i) => offset + i + 1).filter(
|
||||||
|
(p) => p <= pageCount
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-xl border border-border p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
|
Preview
|
||||||
|
<span className="ml-2 text-sm font-normal text-muted-foreground">
|
||||||
|
pages {offset + 1}–{Math.min(offset + PAGE_SIZE, pageCount)} / {pageCount}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}
|
||||||
|
disabled={offset === 0}
|
||||||
|
className="px-3 py-1.5 text-sm rounded-lg border border-border bg-muted/50 text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setOffset((o) => Math.min(o + PAGE_SIZE, pageCount - 1))}
|
||||||
|
disabled={offset + PAGE_SIZE >= pageCount}
|
||||||
|
className="px-3 py-1.5 text-sm rounded-lg border border-border bg-muted/50 text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-3">
|
||||||
|
{pages.map((pageNum) => (
|
||||||
|
<div key={pageNum} className="flex flex-col items-center gap-1.5">
|
||||||
|
<div className="relative w-full aspect-[2/3] bg-muted rounded-lg overflow-hidden border border-border">
|
||||||
|
<Image
|
||||||
|
src={`/api/books/${bookId}/pages/${pageNum}?format=webp&width=600&quality=80`}
|
||||||
|
alt={`Page ${pageNum}`}
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">{pageNum}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user