mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
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:
@@ -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,8 +58,16 @@ 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>
|
||||||
|
<div className="flex items-center justify-end mt-4 gap-4">
|
||||||
|
{showSizeSelector && (
|
||||||
|
<PaginationSize
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<PaginationNavigation
|
<PaginationNavigation
|
||||||
className="justify-end mt-4"
|
className="justify-end"
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
goToPage={goToPage}
|
goToPage={goToPage}
|
||||||
@@ -57,5 +76,6 @@ export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
|
|||||||
maxVisiblePages={maxVisiblePages}
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user