feat(ui): Add pagination to books pages and improve spacing

- Added CursorPagination component with page size selector (20/50/100)
- Updated /books page with pagination support
- Updated /libraries/[id]/books with pagination
- Improved layout margins (added pb-16 and responsive px)
- Series page uses improved layout spacing
This commit is contained in:
2026-03-06 14:50:27 +01:00
parent c421f427b0
commit fa574586ed
9 changed files with 368 additions and 24 deletions

View File

@@ -0,0 +1,225 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { Button } from "./Button";
interface CursorPaginationProps {
hasNextPage: boolean;
hasPrevPage: boolean;
pageSize: number;
currentCount: number;
pageSizeOptions?: number[];
nextCursor?: string | null;
}
export function CursorPagination({
hasNextPage,
hasPrevPage,
pageSize,
currentCount,
pageSizeOptions = [20, 50, 100],
nextCursor,
}: CursorPaginationProps) {
const router = useRouter();
const searchParams = useSearchParams();
const goToNext = () => {
if (!nextCursor) return;
const params = new URLSearchParams(searchParams);
params.set("cursor", nextCursor);
router.push(`?${params.toString()}`);
};
const goToFirst = () => {
const params = new URLSearchParams(searchParams);
params.delete("cursor");
router.push(`?${params.toString()}`);
};
const changePageSize = (size: number) => {
const params = new URLSearchParams(searchParams);
params.set("limit", size.toString());
params.delete("cursor");
router.push(`?${params.toString()}`);
};
return (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-6 border-t border-line">
{/* Page size selector */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted">Show</span>
<select
value={pageSize.toString()}
onChange={(e) => changePageSize(Number(e.target.value))}
className="w-20 px-2 py-1.5 text-sm border border-line rounded-lg bg-background text-foreground"
>
{pageSizeOptions.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
<span className="text-sm text-muted">per page</span>
</div>
{/* Count info */}
<div className="text-sm text-muted">
Showing {currentCount} items
</div>
{/* Navigation */}
<div className="flex items-center gap-2">
{hasPrevPage && (
<Button
variant="secondary"
size="sm"
onClick={goToFirst}
>
First
</Button>
)}
<Button
variant="secondary"
size="sm"
onClick={goToNext}
disabled={!hasNextPage}
>
Next
</Button>
</div>
</div>
);
}
interface OffsetPaginationProps {
currentPage: number;
totalPages: number;
pageSize: number;
totalItems: number;
pageSizeOptions?: number[];
}
export function OffsetPagination({
currentPage,
totalPages,
pageSize,
totalItems,
pageSizeOptions = [20, 50, 100],
}: OffsetPaginationProps) {
const router = useRouter();
const searchParams = useSearchParams();
const goToPage = (page: number) => {
const params = new URLSearchParams(searchParams);
params.set("page", page.toString());
router.push(`?${params.toString()}`);
};
const changePageSize = (size: number) => {
const params = new URLSearchParams(searchParams);
params.set("limit", size.toString());
params.set("page", "1");
router.push(`?${params.toString()}`);
};
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems);
const getPageNumbers = () => {
const pages: (number | string)[] = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (currentPage <= 3) {
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push("...");
pages.push(totalPages);
} else if (currentPage >= totalPages - 2) {
pages.push(1);
pages.push("...");
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push("...");
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
pages.push(i);
}
pages.push("...");
pages.push(totalPages);
}
}
return pages;
};
return (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-6 border-t border-line">
{/* Page size selector */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted">Show</span>
<select
value={pageSize.toString()}
onChange={(e) => changePageSize(Number(e.target.value))}
className="w-20 px-2 py-1.5 text-sm border border-line rounded-lg bg-background text-foreground"
>
{pageSizeOptions.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
<span className="text-sm text-muted">per page</span>
</div>
{/* Page info */}
<div className="text-sm text-muted">
{startItem}-{endItem} of {totalItems}
</div>
{/* Page navigation */}
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1}
>
</Button>
{getPageNumbers().map((page, index) => (
<span key={index}>
{page === "..." ? (
<span className="px-3 py-2 text-sm text-muted">...</span>
) : (
<Button
variant={currentPage === page ? "primary" : "ghost"}
size="sm"
onClick={() => goToPage(page as number)}
>
{page}
</Button>
)}
</span>
))}
<Button
variant="ghost"
size="sm"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage >= totalPages}
>
</Button>
</div>
</div>
);
}