EscolaLMS/Front

View on GitHub
src/components/Common/Pagination/index.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React from "react";
import styled from "styled-components";
import { Text } from "@escolalms/components/lib/components/atoms/Typography/Text";
import { Link } from "react-router-dom";

const StyledPagination = styled.div`
  display: flex;
  justify-content: flex-start;
  align-items: center;
  a {
    text-decoration: none !important;
  }
  .arrow {
    width: 10px;
    height: 10px;
    border-left: 2px solid;
    border-bottom: 2px solid;
    border-color: ${({ theme }) => theme.primaryColor};
    margin: 0 10px;
    &--left {
      transform: rotate(45deg);
    }
    &--right {
      transform: rotate(-135deg);
    }
  }
  .number {
    margin: 0 8px;
    p {
      opacity: 0.5;
      &.current {
        color: ${({ theme }) => theme.black}!important;

        opacity: 1;
      }
    }
  }
`;

const LEFT_PAGE = "LEFT";
const RIGHT_PAGE = "RIGHT";

/**
 * Helper method for creating a range of numbers
 * range(1, 5) => [1, 2, 3, 4, 5]
 */
const range = (from: number, to: number, step = 1) => {
  let i = from;
  const range = [];

  while (i <= to) {
    range.push(i);
    i += step;
  }

  return range;
};

const Pagination: React.FC<{
  currentPage: number;
  total: number;
  perPage: number;
  onPage: (page: number) => void;
  pageNeighbours?: number;
}> = ({ currentPage, total, perPage, onPage, pageNeighbours = 2 }) => {
  const lastPage = Math.ceil(total / perPage);

  const fetchPageNumbers = () => {
    /**
     * totalNumbers: the total page numbers to show on the control
     * totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
     */
    const totalNumbers = pageNeighbours * 2 + 3;
    const totalBlocks = totalNumbers + 2;

    if (lastPage > totalBlocks) {
      const startPage = Math.max(2, currentPage - pageNeighbours);
      const endPage = Math.min(lastPage - 1, currentPage + pageNeighbours);
      let pages: (string | number)[] = range(startPage, endPage);

      /**
       * hasLeftSpill: has hidden pages to the left
       * hasRightSpill: has hidden pages to the right
       * spillOffset: number of hidden pages either to the left or to the right
       */
      const hasLeftSpill = startPage > 2;
      const hasRightSpill = lastPage - endPage > 1;
      const spillOffset = totalNumbers - (pages.length + 1);

      switch (true) {
        // handle: (1) < {5 6} [7] {8 9} (10)
        case hasLeftSpill && !hasRightSpill: {
          const extraPages = range(startPage - spillOffset, startPage - 1);
          pages = [LEFT_PAGE, ...extraPages, ...pages];
          break;
        }

        // handle: (1) {2 3} [4] {5 6} > (10)
        case !hasLeftSpill && hasRightSpill: {
          const extraPages = range(endPage + 1, endPage + spillOffset);
          pages = [...pages, ...extraPages, RIGHT_PAGE];
          break;
        }

        // handle: (1) < {4 5} [6] {7 8} > (10)
        case hasLeftSpill && hasRightSpill:
        default: {
          pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
          break;
        }
      }

      return [1, ...pages, lastPage];
    } else if (currentPage < lastPage && currentPage === 1) {
      return [...range(1, lastPage), RIGHT_PAGE];
    } else if (currentPage > 1 && currentPage !== lastPage) {
      return [LEFT_PAGE, ...range(1, lastPage), RIGHT_PAGE];
    } else if (currentPage > 1 && currentPage === lastPage) {
      return [LEFT_PAGE, ...range(1, lastPage)];
    }

    return range(1, lastPage);
  };

  return (
    <StyledPagination>
      {fetchPageNumbers().map((page) => {
        if (page === LEFT_PAGE) {
          return (
            <a
              href="#!prev_page"
              className="prev page-numbers"
              onClick={(e) => {
                e.preventDefault();
                onPage(currentPage <= 1 ? 1 : currentPage - 1);
              }}
            >
              <div className="arrow arrow--left" />
            </a>
          );
        }
        if (page === RIGHT_PAGE) {
          return (
            <a
              href="#!next_page"
              className="next page-numbers"
              onClick={(e) => {
                e.preventDefault();
                onPage(currentPage === lastPage ? lastPage : currentPage + 1);
              }}
            >
              <div className="arrow arrow--right" />
            </a>
          );
        }
        return (
          <Link
            onClick={(e) => {
              e.preventDefault();
              onPage(Number(page));
            }}
            to={`#!page-${Number(page)}`}
            key={`#${Number(page)}`}
            className="number"
          >
            <Text
              className={Number(page) === currentPage ? "current" : ""}
              size="16"
              aria-current="page"
            >
              {Number(page)}
            </Text>
          </Link>
        );
      })}
    </StyledPagination>
  );
};

export default Pagination;