SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.9 KB · 61 lines tsx
Raw Blame History
1import Link from 'next/link';2import { ChevronLeft, ChevronRight } from 'lucide-react';3import { cn } from '@/lib/format';45/** URL-driven pagination: keeps every other search param. Touch targets ≥ 44 px on mobile. */6export function Pagination({ page, pageSize, total, basePath, params, className }: { page: number; pageSize: number; total: number; basePath: string; params: Record<string, string | undefined>; className?: string }) {7  const pages = Math.max(1, Math.ceil(total / pageSize));8  if (pages <= 1) return null;9  const href = (p: number) => {10    const sp = new URLSearchParams();11    for (const [k, v] of Object.entries(params)) if (v) sp.set(k, v);12    if (p > 1) sp.set('page', String(p));13    else sp.delete('page');14    const qs = sp.toString();15    return qs ? `${basePath}?${qs}` : basePath;16  };17  const from = (page - 1) * pageSize + 1;18  const to = Math.min(total, page * pageSize);19  const btn = 'inline-flex h-10 min-w-[44px] items-center justify-center gap-1 rounded-md border border-border bg-elevated px-3 text-xs font-medium hover:bg-sunken aria-disabled:pointer-events-none aria-disabled:opacity-40 md:h-8 md:min-w-0';20  const around: number[] = [];21  for (let p = Math.max(1, page - 2); p <= Math.min(pages, page + 2); p++) around.push(p);22  return (23    <nav className={cn('mt-4 flex flex-wrap items-center justify-between gap-3 text-xs text-muted', className)} aria-label="Pagination">24      <span className="num">25        {from.toLocaleString('en-US')}–{to.toLocaleString('en-US')} of {total.toLocaleString('en-US')}26      </span>27      <div className="flex items-center gap-1">28        <Link href={href(Math.max(1, page - 1))} aria-disabled={page <= 1} className={btn} aria-label="Previous page">29          <ChevronLeft className="h-3.5 w-3.5" />30          <span className="hidden sm:inline">Prev</span>31        </Link>32        {around[0]! > 1 ? (33          <>34            <Link href={href(1)} className={cn(btn, 'num')}>35              136            </Link>37            {around[0]! > 2 ? <span className="px-1 text-subtle">…</span> : null}38          </>39        ) : null}40        {around.map((p) => (41          <Link key={p} href={href(p)} aria-current={p === page ? 'page' : undefined} className={cn(btn, 'num', p === page && 'border-fg bg-accent text-accent-fg hover:bg-accent')}>42            {p}43          </Link>44        ))}45        {around[around.length - 1]! < pages ? (46          <>47            {around[around.length - 1]! < pages - 1 ? <span className="px-1 text-subtle">…</span> : null}48            <Link href={href(pages)} className={cn(btn, 'num')}>49              {pages.toLocaleString('en-US')}50            </Link>51          </>52        ) : null}53        <Link href={href(Math.min(pages, page + 1))} aria-disabled={page >= pages} className={btn} aria-label="Next page">54          <span className="hidden sm:inline">Next</span>55          <ChevronRight className="h-3.5 w-3.5" />56        </Link>57      </div>58    </nav>59  );60}61