fix: add page size selector to CardsWithPagination (#253)

* fix: add page size selector to CardsWithPagination

Add an optional pageSizeOptions prop to CardsWithPagination that renders
a "Cards per page" dropdown selector, consistent with the existing
TablePaginationSize pattern used in DataTable.

- New PaginationSize component (standalone, no TanStack Table dependency)
- CardsWithPagination: manage pageSize as state, reset to page 1 on change
- Project databases page: increase default from 6 to 20, add selector [10, 20, 50]
- Projects list page: increase default from 9 to 12, add selector [12, 24, 48]
- Fully backward compatible: without pageSizeOptions, behavior is unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- Validate pageSize in handlePageSizeChange (reject 0/negative/NaN)
- Guard empty pageSizeOptions array from rendering empty selector
- Restore className on PaginationNavigation for backward compatibility
- Add aria-label to SelectTrigger for accessibility on small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(pagination-size): fallback to pageSizeOptions[0] when pageSize not in options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(pagination-size): fix pageSize is not defined (placeholder ref)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roman
2026-04-06 20:16:15 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent db9d23b5ec
commit 47de5373b1
4 changed files with 72 additions and 15 deletions
@@ -102,8 +102,9 @@ export default async function RoutePage(props: PageParams<{
organizationSlug={organization.slug} organizationSlug={organization.slug}
// @ts-ignore // @ts-ignore
cardItem={ProjectDatabaseCard} cardItem={ProjectDatabaseCard}
cardsPerPage={6} cardsPerPage={20}
numberOfColumns={3} numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
extendedProps={proj} extendedProps={proj}
/> />
) : ( ) : (
@@ -66,8 +66,9 @@ export default async function RoutePage(props: PageParams<{}>) {
organizationSlug={organization.slug} organizationSlug={organization.slug}
data={projects} data={projects}
cardItem={ProjectCard} cardItem={ProjectCard}
cardsPerPage={9} cardsPerPage={12}
numberOfColumns={3} numberOfColumns={3}
pageSizeOptions={[12, 24, 48]}
/> />
) : isMember ? ( ) : isMember ? (
<EmptyStatePlaceholder text="No project available"/> <EmptyStatePlaceholder text="No project available"/>
@@ -4,6 +4,7 @@ import React, { ComponentType, useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation"; import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
import { PaginationSize } from "@/components/wrappers/common/pagination/pagination-size";
interface CardsWithPaginationProps<T> { interface CardsWithPaginationProps<T> {
className?: string; className?: string;
@@ -13,19 +14,21 @@ interface CardsWithPaginationProps<T> {
cardsPerPage?: number; cardsPerPage?: number;
numberOfColumns?: number; numberOfColumns?: number;
maxVisiblePages?: number; maxVisiblePages?: number;
pageSizeOptions?: number[];
[key: string]: any; [key: string]: any;
} }
export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) { export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
const { className, organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3, ...rest } = props; const { className, organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3, pageSizeOptions, ...rest } = props;
const CardItem = cardItem; const CardItem = cardItem;
const [pageSize, setPageSize] = useState(cardsPerPage);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(data.length / cardsPerPage); const totalPages = Math.ceil(data.length / pageSize);
const indexOfLastCard = currentPage * cardsPerPage; const indexOfLastCard = currentPage * pageSize;
const indexOfFirstCard = indexOfLastCard - cardsPerPage; const indexOfFirstCard = indexOfLastCard - pageSize;
const currentCards = data.slice(indexOfFirstCard, indexOfLastCard); const currentCards = data.slice(indexOfFirstCard, indexOfLastCard);
const goToPage = (pageNumber: number) => { const goToPage = (pageNumber: number) => {
@@ -40,6 +43,14 @@ export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
goToPage(Math.min(totalPages, currentPage + 1)); goToPage(Math.min(totalPages, currentPage + 1));
}; };
const handlePageSizeChange = (newSize: number) => {
if (!Number.isFinite(newSize) || newSize < 1) return;
setPageSize(newSize);
setCurrentPage(1);
};
const showSizeSelector = pageSizeOptions && pageSizeOptions.length > 0;
return ( return (
<div className={cn("flex flex-col h-full justify-between", className)}> <div className={cn("flex flex-col h-full justify-between", className)}>
<div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}> <div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
@@ -47,15 +58,24 @@ export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
<CardItem key={key} data={card} organizationSlug={organizationSlug} {...rest} /> <CardItem key={key} data={card} organizationSlug={organizationSlug} {...rest} />
))} ))}
</div> </div>
<PaginationNavigation <div className="flex items-center justify-end mt-4 gap-4">
className="justify-end mt-4" {showSizeSelector && (
totalPages={totalPages} <PaginationSize
currentPage={currentPage} pageSize={pageSize}
goToPage={goToPage} onPageSizeChange={handlePageSizeChange}
goToPrevPage={goToPrevPage} pageSizeOptions={pageSizeOptions}
goToNextPage={goToNextPage} />
maxVisiblePages={maxVisiblePages} )}
/> <PaginationNavigation
className="justify-end"
totalPages={totalPages}
currentPage={currentPage}
goToPage={goToPage}
goToPrevPage={goToPrevPage}
goToNextPage={goToNextPage}
maxVisiblePages={maxVisiblePages}
/>
</div>
</div> </div>
); );
} }
@@ -0,0 +1,35 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
export type PaginationSizeProps = {
className?: string;
pageSize: number;
onPageSizeChange: (size: number) => void;
pageSizeOptions?: number[];
};
export const PaginationSize = (props: PaginationSizeProps) => {
const { className, onPageSizeChange, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
const effectivePageSize = pageSizeOptions.includes(props.pageSize) ? props.pageSize : pageSizeOptions[0];
return (
<div className={cn("flex items-center justify-end sm:justify-center space-x-2", className)}>
<p className="whitespace-nowrap text-sm font-medium hidden md:block">Cards per page</p>
<Select
value={`${effectivePageSize}`}
onValueChange={(value) => onPageSizeChange(Number(value))}
>
<SelectTrigger className="h-8 w-[4.5rem]" aria-label="Cards per page">
<SelectValue placeholder={effectivePageSize} />
</SelectTrigger>
<SelectContent side="top">
{pageSizeOptions.map((size) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};