Source: utils/pagination.js

/**
 * @module utils-pagination
 *
 * @description Utilities for dealing with pagination.
 */

import * as v from 'valibot'
const { min, max } = Math
const range = (lo, hi) => Array.from({ length: hi - lo }, (_, i) => i + lo)

/**
 * Generate pagination numbers with ellipsis for long ranges
 * @param {number} count - Total number of pages
 * @param {number} current - Current page number
 * @param {string} [ellipsis='...'] - Ellipsis character(s)
 * @returns {Array<number|string>} Array of page numbers and ellipsis
 */
export const pagination = (count, current, ellipsis = '...') => {
  if (count <= 5) {
    return range(1, count + 1)
  }
  const adjacent = 1
  const left = current === count ? current - 2 * adjacent : max(1, current - adjacent)
  const right = current === 1 ? 1 + adjacent * 2 : min(count, current + adjacent)
  const middle = range(left, right + 1)
  let leftEllipsis = left > 1
  let rightEllipsis = right < count

  if (leftEllipsis && middle[0] === 2) {
    leftEllipsis = false
    middle.unshift(1)
  }

  if (rightEllipsis && middle[middle.length - 1] === count - 1) {
    rightEllipsis = false
    middle.push(count)
  }

  const result = [
    ...(leftEllipsis ? [1, ellipsis] : middle),
    ...(leftEllipsis && rightEllipsis ? middle : []),
    ...(rightEllipsis ? [ellipsis, count] : middle)
  ]
  return result
}

/**
 *
 * Turn items in array returned by {@link pagination} into items
 * conforming to the PaginationItem schema.
 *
 * @param {number|string} paginationItem
 * @param {Object} options options
 * @param {number} options.pageNumber
 * @param {Function} options.href pagination link factory function paginationItem => string
 * @returns {Object}
 */
export const makeItem = (paginationItem, options) => {
  const { pageNumber, href } = v.parse(v.object({ pageNumber: v.pipe(v.number(), v.integer()), href: v.function() }), options)

  if (typeof paginationItem === 'number') {
    return {
      type: 'number',
      number: paginationItem,
      href: href(paginationItem),
      current: paginationItem === pageNumber
    }
  } else {
    return {
      type: 'ellipsis',
      ellipsis: true,
      href: '#'
    }
  }
}

/**
 *
 * @param {Object} options
 * @param {number} options.pageNumber
 * @param {string} options.baseSubpath
 * @param {Object} options.dataRange
 * @param {Object} options.dataRange.maxPageNumber
 * @returns {Object} pagination object for use in templates
 */
export const createPaginationTemplateParamsObject = (options) => {
  const { pageNumber, baseSubpath, dataRange } = v.parse(v.object({
    pageNumber: v.pipe(v.number(), v.integer()),
    baseSubpath: v.string(),
    dataRange: v.object({ maxPageNumber: v.pipe(v.number(), v.integer(), v.minValue(1)) })
  }), options)

  /**
   * @typedef {Object} PaginationItem
   * @property {'ellipsis'|'number'} type - Type of pagination item
   * @property {number} [number] - Page number (for number type)
   * @property {string} href - Link URL
   * @property {boolean} [ellipsis] - Whether this is an ellipsis item
   * @property {boolean} [current] - Whether this is the current page
   */

  /**
   * @typedef {Object} PaginationNav
   * @property {Object} [previous] - Previous page link
   * @property {string} previous.href - Previous page URL
   * @property {Object} [next] - Next page link
   * @property {string} next.href - Next page URL
   * @property {PaginationItem[]} items - Pagination items
   */

  /** @type {PaginationNav} */
  const paginationObj = {
    previous: undefined,
    next: undefined,
    items: []
  }

  if (pageNumber > 1) {
    paginationObj.previous = {
      href: `${baseSubpath}/${pageNumber - 1}`
    }
  }
  if (pageNumber < dataRange.maxPageNumber) {
    paginationObj.next = {
      href: `${baseSubpath}/${pageNumber + 1}`
    }
  }

  for (const item of pagination(dataRange.maxPageNumber, Math.min(pageNumber, dataRange.maxPageNumber))) {
    const itemObj = makeItem(item, { pageNumber, href: (pitem) => `${baseSubpath}/${pitem}` })
    paginationObj.items.push(itemObj)
  }

  return paginationObj
}