1import {
2 Box,
3 Button,
4 ButtonGroup,
5 chakra,
6 HStack,
7 Select,
8 Text,
9} from '@chakra-ui/react';
10import {
11 ArrowLongLeftIcon,
12 ArrowLongRightIcon,
13} from '@heroicons/react/24/outline';
14import { ChangeEvent, FC } from 'react';
15import { useTranslation } from 'react-i18next';
16import { FilterType } from 'types';
17import { PageSizeManager } from 'utils/userPreferenceStorage';
18
19const PrevIcon = chakra(ArrowLongLeftIcon, {
20 baseStyle: {
21 w: 4,
22 h: 4,
23 },
24});
25const NextIcon = chakra(ArrowLongRightIcon, {
26 baseStyle: {
27 w: 4,
28 h: 4,
29 },
30});
31
32export type PaginationType = {
33 count: number;
34 perPage: number;
35 page: number;
36 onChange?: (page: number) => void;
37};
38
39const MINIMAL_PAGE_ITEM_COUNT = 5;
40
41/**
42 * Generate numeric page items around current page.
43 * - Always include first and last page
44 * - Add ellipsis if needed
45 */
46interface PageItem {
47 value: string | number;
48 isPlaceholder?: boolean;
49}
50
51function validateWidth(width: number): void {
52 if (width < MINIMAL_PAGE_ITEM_COUNT || width % 2 === 0) {
53 throw new Error(`Must allow at least ${MINIMAL_PAGE_ITEM_COUNT} odd page items`);
54 }
55}
56
57function generatePageItems(total: number, current: number, width: number): PageItem[] {
58 validateWidth(width);
59
60 if (total < width) {
61 return Array.from({ length: total }, (_, index) => ({ value: index }));
62 }
63
64 const left = Math.max(0, Math.min(total - width, current - Math.floor(width / 2)));
65 const right = Math.min(left + width, total);
66
67 const items: PageItem[] = [];
68
69 if (left > 0) {
70 items.push({ value: 0 }, { value: 'prev-more', isPlaceholder: true });
71 }
72
73 for (let i = left; i < right; i++) {
74 items.push({ value: i });
75 }
76
77 if (right < total) {
78 items.push({ value: 'next-more', isPlaceholder: true }, { value: total - 1 });
79 }
80
81 return items;
82}
83
84type PaginationProps = {
85 total: number;
86 onFilterChange: (filters: Partial<FilterType>) => void;
87 filters: FilterType;
88 pageSizeManager: PageSizeManager;
89 pageListsNumbers?: number[];
90}
91
92export const Pagination: FC<PaginationProps> = ({
93 filters,
94 total,
95 onFilterChange,
96 pageSizeManager,
97 pageListsNumbers = [10, 20, 30],
98}) => {
99 const { limit: perPage, offset } = filters;
100
101 const page = (offset || 0) / (perPage || 1);
102 const noPages = Math.ceil(total / (perPage || 1));
103 const pages = generatePageItems(noPages, page, 7);
104
105 const changePage = (page: number) => {
106 onFilterChange({
107 ...filters,
108 offset: page * (perPage as number),
109 });
110 };
111
112 const handlePageSizeChange = (e: ChangeEvent<HTMLSelectElement>) => {
113 onFilterChange({
114 ...filters,
115 limit: parseInt(e.target.value),
116 });
117 pageSizeManager.setPageSize(e.target.value);
118 };
119
120 const { t } = useTranslation();
121
122 return (
123 <HStack
124 justifyContent="space-between"
125 mt={4}
126 w="full"
127 display="flex"
128 columnGap={{ lg: 4, md: 0 }}
129 rowGap={{ md: 0, base: 4 }}
130 flexDirection={{ md: 'row', base: 'column' }}
131 >
132 <Box order={{ base: 2, md: 1 }}>
133 <HStack>
134 <Select
135 minW="60px"
136 value={perPage}
137 onChange={handlePageSizeChange}
138 size="sm"
139 rounded="md"
140 >
141 {pageListsNumbers.map((list, i) => {
142 return (<option key={i}>{list}</option>)
143 })}
144 </Select>
145 <Text whiteSpace={'nowrap'} fontSize="sm">
146 {t('itemsPerPage')}
147 </Text>
148 </HStack>
149 </Box>
150
151 <ButtonGroup size="sm" isAttached variant="outline" order={{ base: 1, md: 2 }}>
152 <Button
153 leftIcon={<PrevIcon />}
154 onClick={changePage.bind(null, page - 1)}
155 isDisabled={page === 0 || noPages === 0}
156 >
157 {t('previous')}
158 </Button>
159 {pages.map((pageItem) => (
160 <Button
161 key={String(pageItem.value)}
162 variant={String(pageItem.value) === String(page) ? 'solid' : 'outline'}
163 onClick={() =>
164 typeof pageItem.value === 'number' && changePage(pageItem.value)
165 }
166 >
167 {String(pageItem.value) === 'prev-more' || String(pageItem.value) === 'next-more'
168 ? '...'
169 : (pageItem.value as number) + 1}
170 </Button>
171 ))}
172 <Button
173 rightIcon={<NextIcon />}
174 onClick={changePage.bind(null, page + 1)}
175 isDisabled={page + 1 === noPages || noPages === 0}
176 >
177 {t('next')}
178 </Button>
179 </ButtonGroup>
180 </HStack>
181 );
182};