SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
yesterdaylast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.9 KB · 72 lines tsx
Raw Blame History
1import Link from "next/link";2import { ChevronLeft, ChevronRight } from "lucide-react";3import { fmtInt } from "@/lib/format";4import { cn } from "./cn";56export interface PaginationProps {7  page: number;8  perPage: number;9  total: number;10  /** returns href for a page number */11  hrefFor: (page: number) => string;12  className?: string;13  label?: string;14}1516/** Compact pager: "1–50 of 1,234" + prev/next + a few numbered links on ≥ sm. */17export function Pagination({ page, perPage, total, hrefFor, className, label = "results" }: PaginationProps) {18  const pages = Math.max(1, Math.ceil(total / Math.max(1, perPage)));19  if (total <= 0) return null;20  const from = (page - 1) * perPage + 1;21  const to = Math.min(total, page * perPage);22  const nums = new Set<number>([1, pages, page, page - 1, page + 1, page - 2, page + 2].filter((n) => n >= 1 && n <= pages));23  const list = Array.from(nums).sort((a, b) => a - b);24  return (25    <nav aria-label="Pagination" className={cn("flex items-center justify-between gap-3 border-t border-hair py-3 text-[12.5px]", className)}>26      <div className="figure text-ink-3">27        {fmtInt(from)}–{fmtInt(to)} of {fmtInt(total)} {label}28      </div>29      <div className="flex items-center gap-1">30        <PageLink href={page > 1 ? hrefFor(page - 1) : null} label="Previous page">31          <ChevronLeft className="size-4" />32        </PageLink>33        <div className="hidden items-center gap-1 sm:flex">34          {list.map((n, i) => {35            const prev = list[i - 1];36            const gap = prev !== undefined && n - prev > 1;37            return (38              <span key={n} className="flex items-center gap-1">39                {gap && <span className="px-1 text-ink-4">…</span>}40                <Link41                  href={hrefFor(n)}42                  aria-current={n === page ? "page" : undefined}43                  scroll={false}44                  className={cn("figure inline-flex h-8 min-w-8 items-center justify-center rounded-sm border px-2", n === page ? "border-ink bg-ink text-paper" : "border-hair text-ink-2 hover:bg-surface-2")}45                >46                  {n}47                </Link>48              </span>49            );50          })}51        </div>52        <span className="figure px-1 text-ink-3 sm:hidden">53          {page} / {pages}54        </span>55        <PageLink href={page < pages ? hrefFor(page + 1) : null} label="Next page">56          <ChevronRight className="size-4" />57        </PageLink>58      </div>59    </nav>60  );61}6263function PageLink({ href, label, children }: { href: string | null; label: string; children: React.ReactNode }) {64  const cls = "inline-flex size-10 items-center justify-center rounded-sm border border-hair text-ink-2 sm:size-9";65  if (!href) return <span className={cn(cls, "opacity-40")}>{children}</span>;66  return (67    <Link href={href} aria-label={label} scroll={false} className={cn(cls, "hover:bg-surface-2")}>68      {children}69    </Link>70  );71}72