refactor(web): split TrackListPagination into module (info, nav, utils, skeleton)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7edd6267fc
commit
fa2563558c
10 changed files with 351 additions and 325 deletions
|
|
@ -1,63 +1,69 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TrackListPagination } from './TrackListPagination';
|
||||
import {
|
||||
TrackListPagination,
|
||||
TrackListPaginationSkeleton,
|
||||
} from './track-list-pagination';
|
||||
|
||||
const meta: Meta<typeof TrackListPagination> = {
|
||||
title: 'Components/Features/Tracks/TrackListPagination',
|
||||
component: TrackListPagination,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onPageChange: { action: 'onPageChange' },
|
||||
},
|
||||
title: 'Components/Features/Tracks/TrackListPagination',
|
||||
component: TrackListPagination,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onPageChange: { action: 'onPageChange' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TrackListPagination>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const MiddlePage: Story = {
|
||||
args: {
|
||||
currentPage: 5,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
args: {
|
||||
currentPage: 5,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const LastPage: Story = {
|
||||
args: {
|
||||
currentPage: 10,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
args: {
|
||||
currentPage: 10,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyPages: Story = {
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 50,
|
||||
totalItems: 500,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 50,
|
||||
totalItems: 500,
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
disabled: true,
|
||||
},
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
totalItems: 100,
|
||||
itemsPerPage: 10,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
render: () => <TrackListPaginationSkeleton />,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe('TrackListPagination', () => {
|
|||
);
|
||||
const currentPageButton = screen.getByLabelText('Aller à la page 3');
|
||||
expect(currentPageButton).toHaveAttribute('aria-current', 'page');
|
||||
expect(currentPageButton).toHaveClass('bg-blue-600');
|
||||
expect(currentPageButton).toHaveClass('bg-primary');
|
||||
});
|
||||
|
||||
it('should call onPageChange when page number is clicked', async () => {
|
||||
|
|
|
|||
|
|
@ -1,283 +1,5 @@
|
|||
/**
|
||||
* Composant TrackListPagination
|
||||
* Pagination pour la liste de pistes avec contrôles de navigation
|
||||
*/
|
||||
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TrackListPaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
className?: string;
|
||||
showPageNumbers?: boolean;
|
||||
maxVisiblePages?: number;
|
||||
showItemsInfo?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function TrackListPagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
itemsPerPage,
|
||||
onPageChange,
|
||||
className,
|
||||
showPageNumbers = true,
|
||||
maxVisiblePages = 5,
|
||||
showItemsInfo = true,
|
||||
disabled = false,
|
||||
}: TrackListPaginationProps) {
|
||||
const canGoPrevious = currentPage > 1 && !disabled;
|
||||
const canGoNext = currentPage < totalPages && !disabled;
|
||||
const canGoFirst = currentPage > 1 && !disabled;
|
||||
const canGoLast = currentPage < totalPages && !disabled;
|
||||
|
||||
const handleFirstPage = () => {
|
||||
if (canGoFirst) {
|
||||
onPageChange(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (canGoPrevious) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (canGoNext) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLastPage = () => {
|
||||
if (canGoLast) {
|
||||
onPageChange(totalPages);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageClick = (page: number) => {
|
||||
if (!disabled && page !== currentPage && page >= 1 && page <= totalPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculer les numéros de page à afficher
|
||||
const getVisiblePages = (): number[] => {
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const half = Math.floor(maxVisiblePages / 2);
|
||||
let start = Math.max(1, currentPage - half);
|
||||
const end = Math.min(totalPages, start + maxVisiblePages - 1);
|
||||
|
||||
// Ajuster le début si on est proche de la fin
|
||||
if (end - start < maxVisiblePages - 1) {
|
||||
start = Math.max(1, end - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
};
|
||||
|
||||
const visiblePages = getVisiblePages();
|
||||
const startItem = totalItems > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
if (totalPages <= 1 && !showItemsInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col sm:flex-row items-center justify-between gap-4',
|
||||
'px-4 py-4 border-t border-kodo-steel dark:border-kodo-steel',
|
||||
className,
|
||||
)}
|
||||
role="navigation"
|
||||
aria-label="Pagination de la liste de pistes"
|
||||
>
|
||||
{/* Informations sur les items */}
|
||||
{showItemsInfo && (
|
||||
<div className="text-sm text-kodo-content-dim dark:text-kodo-content-dim">
|
||||
Affichage de{' '}
|
||||
<span className="font-medium text-kodo-text-main dark:text-white">
|
||||
{startItem}
|
||||
</span>{' '}
|
||||
à{' '}
|
||||
<span className="font-medium text-kodo-text-main dark:text-white">
|
||||
{endItem}
|
||||
</span>{' '}
|
||||
sur{' '}
|
||||
<span className="font-medium text-kodo-text-main dark:text-white">
|
||||
{totalItems}
|
||||
</span>{' '}
|
||||
pistes
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contrôles de pagination */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Première page */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFirstPage}
|
||||
disabled={!canGoFirst || disabled}
|
||||
className={cn(
|
||||
'p-2 rounded-md transition-colors',
|
||||
'text-kodo-content-dim dark:text-kodo-content-dim',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
(!canGoFirst || disabled) && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Aller à la première page"
|
||||
>
|
||||
<ChevronsLeft className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Première page</span>
|
||||
</button>
|
||||
|
||||
{/* Page précédente */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={!canGoPrevious || disabled}
|
||||
className={cn(
|
||||
'p-2 rounded-md transition-colors',
|
||||
'text-kodo-content-dim dark:text-kodo-content-dim',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
(!canGoPrevious || disabled) && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Page précédente"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Page précédente</span>
|
||||
</button>
|
||||
|
||||
{/* Numéros de page */}
|
||||
{showPageNumbers && (
|
||||
<div className="flex items-center gap-1">
|
||||
{visiblePages[0] > 1 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePageClick(1)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'px-4 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||
'text-kodo-text-main dark:text-kodo-text-main',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Aller à la page 1"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
{visiblePages[0] > 2 && (
|
||||
<span className="px-2 text-kodo-content-dim dark:text-kodo-content-dim">
|
||||
...
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{visiblePages.map((page) => (
|
||||
<button
|
||||
key={page}
|
||||
type="button"
|
||||
onClick={() => handlePageClick(page)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'px-4 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
page === currentPage
|
||||
? 'bg-kodo-cyan text-white dark:bg-kodo-cyan'
|
||||
: 'text-kodo-text-main dark:text-kodo-text-main hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label={`Aller à la page ${page}`}
|
||||
aria-current={page === currentPage ? 'page' : undefined}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{visiblePages[visiblePages.length - 1] < totalPages && (
|
||||
<>
|
||||
{visiblePages[visiblePages.length - 1] < totalPages - 1 && (
|
||||
<span className="px-2 text-kodo-content-dim dark:text-kodo-content-dim">
|
||||
...
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePageClick(totalPages)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'px-4 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||
'text-kodo-text-main dark:text-kodo-text-main',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label={`Aller à la page ${totalPages}`}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page suivante */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNextPage}
|
||||
disabled={!canGoNext || disabled}
|
||||
className={cn(
|
||||
'p-2 rounded-md transition-colors',
|
||||
'text-kodo-content-dim dark:text-kodo-content-dim',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
(!canGoNext || disabled) && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Page suivante"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Page suivante</span>
|
||||
</button>
|
||||
|
||||
{/* Dernière page */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLastPage}
|
||||
disabled={!canGoLast || disabled}
|
||||
className={cn(
|
||||
'p-2 rounded-md transition-colors',
|
||||
'text-kodo-content-dim dark:text-kodo-content-dim',
|
||||
'hover:bg-kodo-void dark:hover:bg-kodo-graphite',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
(!canGoLast || disabled) && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Aller à la dernière page"
|
||||
>
|
||||
<ChevronsRight className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Dernière page</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackListPagination;
|
||||
export {
|
||||
TrackListPagination,
|
||||
TrackListPaginationSkeleton,
|
||||
} from './track-list-pagination';
|
||||
export type { TrackListPaginationProps } from './track-list-pagination';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { getVisiblePages, getItemsRange } from './utils';
|
||||
import { TrackListPaginationInfo } from './TrackListPaginationInfo';
|
||||
import { TrackListPaginationNav } from './TrackListPaginationNav';
|
||||
import type { TrackListPaginationProps } from './types';
|
||||
|
||||
export function TrackListPagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
itemsPerPage,
|
||||
onPageChange,
|
||||
className,
|
||||
showPageNumbers = true,
|
||||
maxVisiblePages = 5,
|
||||
showItemsInfo = true,
|
||||
disabled = false,
|
||||
}: TrackListPaginationProps) {
|
||||
const visiblePages = getVisiblePages(currentPage, totalPages, maxVisiblePages);
|
||||
const { startItem, endItem } = getItemsRange(
|
||||
currentPage,
|
||||
itemsPerPage,
|
||||
totalItems
|
||||
);
|
||||
|
||||
if (totalPages <= 1 && !showItemsInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
'flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-4 border-t border-border',
|
||||
className
|
||||
)}
|
||||
role="navigation"
|
||||
aria-label="Pagination de la liste de pistes"
|
||||
>
|
||||
{showItemsInfo && (
|
||||
<TrackListPaginationInfo
|
||||
startItem={startItem}
|
||||
endItem={endItem}
|
||||
totalItems={totalItems}
|
||||
/>
|
||||
)}
|
||||
<TrackListPaginationNav
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
visiblePages={visiblePages}
|
||||
maxVisiblePages={maxVisiblePages}
|
||||
disabled={disabled}
|
||||
showPageNumbers={showPageNumbers}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
interface TrackListPaginationInfoProps {
|
||||
startItem: number;
|
||||
endItem: number;
|
||||
totalItems: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function TrackListPaginationInfo({
|
||||
startItem,
|
||||
endItem,
|
||||
totalItems,
|
||||
label = 'pistes',
|
||||
}: TrackListPaginationInfoProps) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Affichage de{' '}
|
||||
<span className="font-medium text-foreground">{startItem}</span> à{' '}
|
||||
<span className="font-medium text-foreground">{endItem}</span> sur{' '}
|
||||
<span className="font-medium text-foreground">{totalItems}</span> {label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TrackListPaginationNavProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
visiblePages: number[];
|
||||
maxVisiblePages: number;
|
||||
disabled?: boolean;
|
||||
showPageNumbers?: boolean;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
const navButtonClass =
|
||||
'p-2 rounded-md transition-colors text-muted-foreground hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const pageButtonClass =
|
||||
'px-4 py-1.5 rounded-md text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
export function TrackListPaginationNav({
|
||||
currentPage,
|
||||
totalPages,
|
||||
visiblePages,
|
||||
disabled = false,
|
||||
showPageNumbers = true,
|
||||
onPageChange,
|
||||
}: TrackListPaginationNavProps) {
|
||||
const canGoPrevious = currentPage > 1 && !disabled;
|
||||
const canGoNext = currentPage < totalPages && !disabled;
|
||||
const canGoFirst = currentPage > 1 && !disabled;
|
||||
const canGoLast = currentPage < totalPages && !disabled;
|
||||
|
||||
const handlePageClick = (page: number) => {
|
||||
if (!disabled && page !== currentPage && page >= 1 && page <= totalPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canGoFirst && onPageChange(1)}
|
||||
disabled={!canGoFirst || disabled}
|
||||
className={cn(navButtonClass, (!canGoFirst || disabled) && 'opacity-50 cursor-not-allowed')}
|
||||
aria-label="Aller à la première page"
|
||||
>
|
||||
<ChevronsLeft className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canGoPrevious && onPageChange(currentPage - 1)}
|
||||
disabled={!canGoPrevious || disabled}
|
||||
className={cn(navButtonClass, (!canGoPrevious || disabled) && 'opacity-50 cursor-not-allowed')}
|
||||
aria-label="Page précédente"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
|
||||
{showPageNumbers && (
|
||||
<div className="flex items-center gap-1">
|
||||
{visiblePages[0] > 1 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePageClick(1)}
|
||||
disabled={disabled}
|
||||
className={cn(pageButtonClass, 'text-foreground hover:bg-muted')}
|
||||
aria-label="Aller à la page 1"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
{visiblePages[0] > 2 && (
|
||||
<span className="px-2 text-muted-foreground">...</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{visiblePages.map((page) => (
|
||||
<button
|
||||
key={page}
|
||||
type="button"
|
||||
onClick={() => handlePageClick(page)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
pageButtonClass,
|
||||
page === currentPage
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
)}
|
||||
aria-label={`Aller à la page ${page}`}
|
||||
aria-current={page === currentPage ? 'page' : undefined}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
{visiblePages[visiblePages.length - 1] < totalPages && (
|
||||
<>
|
||||
{visiblePages[visiblePages.length - 1] < totalPages - 1 && (
|
||||
<span className="px-2 text-muted-foreground">...</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePageClick(totalPages)}
|
||||
disabled={disabled}
|
||||
className={cn(pageButtonClass, 'text-foreground hover:bg-muted')}
|
||||
aria-label={`Aller à la page ${totalPages}`}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canGoNext && onPageChange(currentPage + 1)}
|
||||
disabled={!canGoNext || disabled}
|
||||
className={cn(navButtonClass, (!canGoNext || disabled) && 'opacity-50 cursor-not-allowed')}
|
||||
aria-label="Page suivante"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canGoLast && onPageChange(totalPages)}
|
||||
disabled={!canGoLast || disabled}
|
||||
className={cn(navButtonClass, (!canGoLast || disabled) && 'opacity-50 cursor-not-allowed')}
|
||||
aria-label="Aller à la dernière page"
|
||||
>
|
||||
<ChevronsRight className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TrackListPaginationSkeletonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TrackListPaginationSkeleton({
|
||||
className,
|
||||
}: TrackListPaginationSkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-4 border-t border-border',
|
||||
className
|
||||
)}
|
||||
role="presentation"
|
||||
>
|
||||
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 bg-muted animate-pulse rounded-md" />
|
||||
<div className="h-10 w-10 bg-muted animate-pulse rounded-md" />
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<div key={i} className="h-9 w-10 bg-muted animate-pulse rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
<div className="h-10 w-10 bg-muted animate-pulse rounded-md" />
|
||||
<div className="h-10 w-10 bg-muted animate-pulse rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export { TrackListPagination } from './TrackListPagination';
|
||||
export { TrackListPaginationSkeleton } from './TrackListPaginationSkeleton';
|
||||
export { TrackListPaginationInfo } from './TrackListPaginationInfo';
|
||||
export { TrackListPaginationNav } from './TrackListPaginationNav';
|
||||
export { getVisiblePages, getItemsRange } from './utils';
|
||||
export type { TrackListPaginationProps } from './types';
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export interface TrackListPaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
className?: string;
|
||||
showPageNumbers?: boolean;
|
||||
maxVisiblePages?: number;
|
||||
showItemsInfo?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Retourne les numéros de page visibles autour de la page courante.
|
||||
*/
|
||||
export function getVisiblePages(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
maxVisiblePages: number
|
||||
): number[] {
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
const half = Math.floor(maxVisiblePages / 2);
|
||||
let start = Math.max(1, currentPage - half);
|
||||
const end = Math.min(totalPages, start + maxVisiblePages - 1);
|
||||
if (end - start < maxVisiblePages - 1) {
|
||||
start = Math.max(1, end - maxVisiblePages + 1);
|
||||
}
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
}
|
||||
|
||||
export function getItemsRange(
|
||||
currentPage: number,
|
||||
itemsPerPage: number,
|
||||
totalItems: number
|
||||
): { startItem: number; endItem: number } {
|
||||
const startItem = totalItems > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
return { startItem, endItem };
|
||||
}
|
||||
Loading…
Reference in a new issue