Files
mtdb_movie/common/resources/client/ui/navigation/pagination-controls.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

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;
},
);