gitscope
← marzneshin / v0.1.2 / app/dashboard/src/components/table/pagination.tsx
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/components/table/pagination.tsx · TypeScript
Blame Raw
  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};