186
common/resources/client/ui/navigation/pagination-controls.tsx
Executable file
186
common/resources/client/ui/navigation/pagination-controls.tsx
Executable file
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
LengthAwarePaginationResponse,
|
||||
PaginationResponse,
|
||||
SimplePaginationResponse,
|
||||
} from '@common/http/backend-response/pagination-response';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import memoize from 'nano-memoize';
|
||||
import {Link} from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {KeyboardArrowRightIcon} from '@common/icons/material/KeyboardArrowRight';
|
||||
import {KeyboardArrowLeftIcon} from '@common/icons/material/KeyboardArrowLeft';
|
||||
import {scrollToTop} from '@common/ui/navigation/use-scroll-to-top';
|
||||
import {useRef} from 'react';
|
||||
import {FirstPageIcon} from '@common/icons/material/FirstPage';
|
||||
|
||||
export type PaginationControlsType = 'simple' | 'lengthAware';
|
||||
|
||||
interface Props {
|
||||
pagination: PaginationResponse<unknown> | undefined;
|
||||
className?: string;
|
||||
type?: PaginationControlsType;
|
||||
scrollToTop?: boolean;
|
||||
}
|
||||
export function PaginationControls({
|
||||
pagination,
|
||||
className,
|
||||
type,
|
||||
scrollToTop,
|
||||
}: Props) {
|
||||
if (
|
||||
!pagination?.data?.length ||
|
||||
(!hasNextPage(pagination) && !hasPreviousPage(pagination))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLengthAware =
|
||||
(!type || type === 'lengthAware') &&
|
||||
'total' in pagination &&
|
||||
pagination.total != null;
|
||||
|
||||
if (isLengthAware) {
|
||||
return (
|
||||
<LengthAwarePagination
|
||||
data={pagination as LengthAwarePaginationResponse}
|
||||
className={className}
|
||||
scrollToTop={scrollToTop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SimplePagination
|
||||
data={pagination as SimplePaginationResponse}
|
||||
className={className}
|
||||
scrollToTop={scrollToTop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface LengthAwarePaginationProps {
|
||||
data: LengthAwarePaginationResponse;
|
||||
className?: string;
|
||||
scrollToTop?: boolean;
|
||||
}
|
||||
function LengthAwarePagination({
|
||||
data,
|
||||
className,
|
||||
scrollToTop: shouldScrollToTop,
|
||||
}: LengthAwarePaginationProps) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const currentPage = data.current_page;
|
||||
const total = data.total;
|
||||
const perPage = data.per_page;
|
||||
|
||||
const range = generatePaginationRangeWithDots(currentPage, total, perPage);
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={ref}
|
||||
className={clsx('flex flex-wrap items-center justify-center', className)}
|
||||
>
|
||||
<ul className="flex items-center gap-4">
|
||||
{range.map((item, index) => {
|
||||
const isCurrentPage = item === currentPage;
|
||||
return (
|
||||
<li key={item === '...' ? `...-${index}` : item}>
|
||||
<Button
|
||||
elementType={isCurrentPage ? undefined : Link}
|
||||
to={!isCurrentPage ? `?page=${item}` : undefined}
|
||||
variant={isCurrentPage ? 'outline' : undefined}
|
||||
disabled={isCurrentPage || item === '...'}
|
||||
onClick={shouldScrollToTop ? () => scrollToTop(ref) : undefined}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
interface SimplePaginationProps {
|
||||
data: SimplePaginationResponse<unknown>;
|
||||
className?: string;
|
||||
scrollToTop?: boolean;
|
||||
}
|
||||
function SimplePagination({
|
||||
data,
|
||||
className,
|
||||
scrollToTop: shouldScrollToTop,
|
||||
}: SimplePaginationProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const currentPage = data.current_page;
|
||||
const isLastPage = !hasNextPage(data);
|
||||
return (
|
||||
<div ref={ref} className={clsx('flex items-center gap-12', className)}>
|
||||
{currentPage > 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
elementType={Link}
|
||||
className="min-w-110"
|
||||
to="?page=1"
|
||||
startIcon={<FirstPageIcon />}
|
||||
onClick={shouldScrollToTop ? () => scrollToTop(ref) : undefined}
|
||||
size="xs"
|
||||
>
|
||||
<Trans message="First" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
elementType={currentPage == 1 ? undefined : Link}
|
||||
disabled={currentPage == 1}
|
||||
className="mr-auto min-w-110"
|
||||
to={currentPage == 1 ? undefined : `?page=${currentPage - 1}`}
|
||||
startIcon={<KeyboardArrowLeftIcon />}
|
||||
onClick={shouldScrollToTop ? () => scrollToTop(ref) : undefined}
|
||||
size="xs"
|
||||
>
|
||||
<Trans message="Previous" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
elementType={isLastPage ? undefined : Link}
|
||||
disabled={isLastPage}
|
||||
className="min-w-110"
|
||||
to={isLastPage ? undefined : `?page=${currentPage + 1}`}
|
||||
endIcon={<KeyboardArrowRightIcon />}
|
||||
onClick={shouldScrollToTop ? () => scrollToTop(ref) : undefined}
|
||||
size="xs"
|
||||
>
|
||||
<Trans message="Next" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const generatePaginationRangeWithDots = memoize(
|
||||
(currentPage: number, total: number, perPage: number) => {
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
const delta = 3;
|
||||
const range = [];
|
||||
for (
|
||||
let i = Math.max(2, currentPage - delta);
|
||||
i <= Math.min(totalPages - 1, currentPage + delta);
|
||||
i++
|
||||
) {
|
||||
range.push(i);
|
||||
}
|
||||
if (currentPage - delta > 2) {
|
||||
range.unshift('...');
|
||||
}
|
||||
if (currentPage + delta < totalPages - 1) {
|
||||
range.push('...');
|
||||
}
|
||||
range.unshift(1);
|
||||
range.push(totalPages);
|
||||
return range;
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user