feat(api+backoffice): pagination par page/offset + filtres séries
API: - Remplace cursor par page (1-indexé) + OFFSET sur GET /books et GET /libraries/:id/series - BooksPage et SeriesPage retournent total, page, limit - GET /libraries/:id/series supporte ?q pour filtrer par nom (ILIKE) Backoffice: - Remplace CursorPagination par OffsetPagination sur les 3 pages de liste - Adapte fetchBooks et fetchSeries (cursor → page) - Met à jour les types BooksPageDto, SeriesPageDto, SeriesDto, BookDto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { fetchBooks, searchBooks, fetchLibraries, BookDto, LibraryDto, getBookCoverUrl } from "../../lib/api";
|
||||
import { BooksGrid, EmptyState } from "../components/BookCard";
|
||||
import { Card, CardContent, Button, FormField, FormInput, FormSelect, FormRow, CursorPagination } from "../components/ui";
|
||||
import { Card, CardContent, Button, FormField, FormInput, FormSelect, FormRow, OffsetPagination } from "../components/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -13,15 +13,15 @@ export default async function BooksPage({
|
||||
const searchParamsAwaited = await searchParams;
|
||||
const libraryId = typeof searchParamsAwaited.library === "string" ? searchParamsAwaited.library : undefined;
|
||||
const searchQuery = typeof searchParamsAwaited.q === "string" ? searchParamsAwaited.q : "";
|
||||
const cursor = typeof searchParamsAwaited.cursor === "string" ? searchParamsAwaited.cursor : undefined;
|
||||
const page = typeof searchParamsAwaited.page === "string" ? parseInt(searchParamsAwaited.page) : 1;
|
||||
const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
|
||||
|
||||
|
||||
const [libraries] = await Promise.all([
|
||||
fetchLibraries().catch(() => [] as LibraryDto[])
|
||||
]);
|
||||
|
||||
let books: BookDto[] = [];
|
||||
let nextCursor: string | null = null;
|
||||
let total = 0;
|
||||
let searchResults: BookDto[] | null = null;
|
||||
let totalHits: number | null = null;
|
||||
|
||||
@@ -41,18 +41,22 @@ export default async function BooksPage({
|
||||
file_path: null,
|
||||
file_format: null,
|
||||
file_parse_status: null,
|
||||
updated_at: ""
|
||||
updated_at: "",
|
||||
reading_status: "unread" as const,
|
||||
reading_current_page: null,
|
||||
reading_last_read_at: null,
|
||||
}));
|
||||
totalHits = searchResponse.estimated_total_hits;
|
||||
}
|
||||
} else {
|
||||
const booksPage = await fetchBooks(libraryId, undefined, cursor, limit).catch(() => ({
|
||||
items: [] as BookDto[],
|
||||
next_cursor: null,
|
||||
prev_cursor: null
|
||||
const booksPage = await fetchBooks(libraryId, undefined, page, limit).catch(() => ({
|
||||
items: [] as BookDto[],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit,
|
||||
}));
|
||||
books = booksPage.items;
|
||||
nextCursor = booksPage.next_cursor;
|
||||
total = booksPage.total;
|
||||
}
|
||||
|
||||
const displayBooks = (searchResults || books).map(book => ({
|
||||
@@ -60,8 +64,7 @@ export default async function BooksPage({
|
||||
coverUrl: getBookCoverUrl(book.id)
|
||||
}));
|
||||
|
||||
const hasNextPage = !!nextCursor;
|
||||
const hasPrevPage = !!cursor;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -142,12 +145,11 @@ export default async function BooksPage({
|
||||
<BooksGrid books={displayBooks} />
|
||||
|
||||
{!searchQuery && (
|
||||
<CursorPagination
|
||||
hasNextPage={hasNextPage}
|
||||
hasPrevPage={hasPrevPage}
|
||||
<OffsetPagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
pageSize={limit}
|
||||
currentCount={displayBooks.length}
|
||||
nextCursor={nextCursor}
|
||||
totalItems={total}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fetchLibraries, fetchBooks, getBookCoverUrl, LibraryDto, BookDto } from "../../../../lib/api";
|
||||
import { BooksGrid, EmptyState } from "../../../components/BookCard";
|
||||
import { LibrarySubPageHeader } from "../../../components/LibrarySubPageHeader";
|
||||
import { CursorPagination } from "../../../components/ui";
|
||||
import { OffsetPagination } from "../../../components/ui";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -15,15 +15,17 @@ export default async function LibraryBooksPage({
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const searchParamsAwaited = await searchParams;
|
||||
const cursor = typeof searchParamsAwaited.cursor === "string" ? searchParamsAwaited.cursor : undefined;
|
||||
const page = typeof searchParamsAwaited.page === "string" ? parseInt(searchParamsAwaited.page) : 1;
|
||||
const series = typeof searchParamsAwaited.series === "string" ? searchParamsAwaited.series : undefined;
|
||||
const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
|
||||
|
||||
|
||||
const [library, booksPage] = await Promise.all([
|
||||
fetchLibraries().then(libs => libs.find(l => l.id === id)),
|
||||
fetchBooks(id, series, cursor, limit).catch(() => ({
|
||||
items: [] as BookDto[],
|
||||
next_cursor: null
|
||||
fetchBooks(id, series, page, limit).catch(() => ({
|
||||
items: [] as BookDto[],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit,
|
||||
}))
|
||||
]);
|
||||
|
||||
@@ -35,11 +37,9 @@ export default async function LibraryBooksPage({
|
||||
...book,
|
||||
coverUrl: getBookCoverUrl(book.id)
|
||||
}));
|
||||
const nextCursor = booksPage.next_cursor;
|
||||
|
||||
|
||||
const seriesDisplayName = series === "unclassified" ? "Unclassified" : series;
|
||||
const hasNextPage = !!nextCursor;
|
||||
const hasPrevPage = !!cursor;
|
||||
const totalPages = Math.ceil(booksPage.total / limit);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -63,12 +63,11 @@ export default async function LibraryBooksPage({
|
||||
<>
|
||||
<BooksGrid books={books} />
|
||||
|
||||
<CursorPagination
|
||||
hasNextPage={hasNextPage}
|
||||
hasPrevPage={hasPrevPage}
|
||||
<OffsetPagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
pageSize={limit}
|
||||
currentCount={books.length}
|
||||
nextCursor={nextCursor}
|
||||
totalItems={booksPage.total}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fetchLibraries, fetchSeries, getBookCoverUrl, LibraryDto, SeriesDto, SeriesPageDto } from "../../../../lib/api";
|
||||
import { CursorPagination } from "../../../components/ui";
|
||||
import { OffsetPagination } from "../../../components/ui";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
@@ -16,12 +16,12 @@ export default async function LibrarySeriesPage({
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const searchParamsAwaited = await searchParams;
|
||||
const cursor = typeof searchParamsAwaited.cursor === "string" ? searchParamsAwaited.cursor : undefined;
|
||||
const page = typeof searchParamsAwaited.page === "string" ? parseInt(searchParamsAwaited.page) : 1;
|
||||
const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
|
||||
|
||||
|
||||
const [library, seriesPage] = await Promise.all([
|
||||
fetchLibraries().then(libs => libs.find(l => l.id === id)),
|
||||
fetchSeries(id, cursor, limit).catch(() => ({ items: [] as SeriesDto[], next_cursor: null }) as SeriesPageDto)
|
||||
fetchSeries(id, page, limit).catch(() => ({ items: [] as SeriesDto[], total: 0, page: 1, limit }) as SeriesPageDto)
|
||||
]);
|
||||
|
||||
if (!library) {
|
||||
@@ -29,9 +29,7 @@ export default async function LibrarySeriesPage({
|
||||
}
|
||||
|
||||
const series = seriesPage.items;
|
||||
const nextCursor = seriesPage.next_cursor;
|
||||
const hasNextPage = !!nextCursor;
|
||||
const hasPrevPage = !!cursor;
|
||||
const totalPages = Math.ceil(seriesPage.total / limit);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -78,12 +76,11 @@ export default async function LibrarySeriesPage({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<CursorPagination
|
||||
hasNextPage={hasNextPage}
|
||||
hasPrevPage={hasPrevPage}
|
||||
<OffsetPagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
pageSize={limit}
|
||||
currentCount={series.length}
|
||||
nextCursor={nextCursor}
|
||||
totalItems={seriesPage.total}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user