All files / apps/web/src/components Pagination.jsx

0% Statements 0/35
0% Branches 0/32
0% Functions 0/8
0% Lines 0/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                                                                                                                                                                                                                                             
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'
 
const DOTS = 'dots'
 
function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i)
}
 
// Construit la liste affichée : numéros + ellipses (ex: 1 … 4 5 6 … 20)
function buildPageItems(current, totalPages) {
  if (totalPages <= 7) return range(1, totalPages)
  const first = 1
  const last = totalPages
  const left = Math.max(first + 1, current - 1)
  const right = Math.min(last - 1, current + 1)
  const items = [first]
  if (left > first + 1) items.push(DOTS)
  items.push(...range(left, right))
  if (right < last - 1) items.push(DOTS)
  items.push(last)
  return items
}
 
/**
 * Pagination réutilisable.
 * - `total` connu → pagination numérotée (avec ellipses).
 * - `total` inconnu (ex. recherche) → mode curseur "Page N" + Précédent/Suivant (`hasMore`).
 *
 * @param {number}  page          Page courante (0-based)
 * @param {number}  pageSize      Taille de page
 * @param {?number} total         Nombre total d'éléments (null si inconnu)
 * @param {boolean} hasMore       En mode inconnu : reste-t-il des éléments après la page courante
 * @param {Function} onPageChange Callback(newPage) (0-based)
 * @param {boolean} isLoading     Désactive les contrôles pendant le chargement
 * @param {string}  itemLabel     Libellé pour "X–Y sur Z <itemLabel>"
 */
export default function Pagination({
  page = 0,
  pageSize = 20,
  total = null,
  hasMore = false,
  onPageChange,
  isLoading = false,
  itemLabel = 'éléments',
}) {
  const totalPages = total != null ? Math.max(1, Math.ceil(total / pageSize)) : null
  const current = page + 1
 
  let rangeLabel
  if (total != null) {
    if (total === 0) {
      rangeLabel = `0 ${itemLabel}`
    } else {
      const start = page * pageSize + 1
      const end = Math.min(total, (page + 1) * pageSize)
      rangeLabel = `${start}–${end} sur ${total} ${itemLabel}`
    }
  } else {
    rangeLabel = `Page ${current}`
  }
 
  const canPrev = page > 0 && !isLoading
  const canNext = (totalPages != null ? current < totalPages : hasMore) && !isLoading
  const items = totalPages != null ? buildPageItems(current, totalPages) : []
 
  const btnBase =
    'inline-flex items-center justify-center h-8 min-w-[2rem] px-2 rounded-lg text-sm border transition-colors disabled:opacity-40 disabled:cursor-not-allowed'
 
  return (
    <div className="flex flex-wrap items-center justify-between gap-3 mt-4 pt-4 border-t border-gray-200">
      <span className="text-sm text-gray-500">{rangeLabel}</span>
 
      <div className="flex items-center gap-1">
        <button
          type="button"
          onClick={() => onPageChange(page - 1)}
          disabled={!canPrev}
          className={`${btnBase} border-gray-300 text-gray-700 hover:bg-gray-50`}
          aria-label="Page précédente"
        >
          <ChevronLeft className="w-4 h-4" />
        </button>
 
        {items.map((it, idx) =>
          it === DOTS ? (
            <span key={`dots-${idx}`} className="px-1 text-gray-400">
              <MoreHorizontal className="w-4 h-4" />
            </span>
          ) : (
            <button
              key={it}
              type="button"
              onClick={() => onPageChange(it - 1)}
              disabled={isLoading || it === current}
              className={`${btnBase} ${
                it === current
                  ? 'border-green-500 bg-green-50 text-green-700 font-medium'
                  : 'border-gray-300 text-gray-700 hover:bg-gray-50'
              }`}
            >
              {it}
            </button>
          )
        )}
 
        <button
          type="button"
          onClick={() => onPageChange(page + 1)}
          disabled={!canNext}
          className={`${btnBase} border-gray-300 text-gray-700 hover:bg-gray-50`}
          aria-label="Page suivante"
        >
          <ChevronRight className="w-4 h-4" />
        </button>
      </div>
    </div>
  )
}