187 lines
5.1 KiB
TypeScript
Executable File
187 lines
5.1 KiB
TypeScript
Executable File
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;
|
|
},
|
|
);
|