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>
)
}
|