import React from "react"; import { View } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useColorScheme } from "nativewind"; import { cn } from "../utils/cn"; import { Button } from "./button"; import { MutedText } from "./text"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu"; export interface PaginationProps { /** Current page, 1-based. */ page: number; /** Number of rows per page. Used to derive total pages from `totalItems`. */ pageSize: number; /** Total number of items across all pages. */ totalItems: number; /** Called with the next page (1-based) when prev/next is pressed. */ onPageChange: (page: number) => void; /** * Page-size choices. When provided (and `onPageSizeChange` is set) a * size selector renders, built on the existing `DropdownMenu` primitive. */ pageSizeOptions?: number[]; onPageSizeChange?: (pageSize: number) => void; className?: string; } export interface PaginationState { totalPages: number; /** Clamped current page (never below 1, never above totalPages). */ currentPage: number; canPrev: boolean; canNext: boolean; /** 1-based index of the first item on the current page (0 when empty). */ rangeStart: number; /** 1-based index of the last item on the current page (0 when empty). */ rangeEnd: number; } /** * Pure pagination maths, separated from the view so it can be unit-tested * without a render host. Tolerates degenerate inputs (zero items, negative * page, oversized page) by clamping. */ export function getPaginationState( page: number, pageSize: number, totalItems: number ): PaginationState { const safeSize = Math.max(1, Math.floor(pageSize)); const safeTotal = Math.max(0, Math.floor(totalItems)); const totalPages = Math.max(1, Math.ceil(safeTotal / safeSize)); const currentPage = Math.min(Math.max(1, Math.floor(page) || 1), totalPages); const rangeStart = safeTotal === 0 ? 0 : (currentPage - 1) * safeSize + 1; const rangeEnd = Math.min(currentPage * safeSize, safeTotal); return { totalPages, currentPage, canPrev: currentPage > 1, canNext: currentPage < totalPages, rangeStart, rangeEnd, }; } /** * Page controls for paginated lists/tables: prev/next, a "X–Y of Z" summary, * and an optional page-size selector. Disabled states are derived from the * clamped pagination state and exposed to assistive tech via * `accessibilityState`. */ export function Pagination({ page, pageSize, totalItems, onPageChange, pageSizeOptions, onPageSizeChange, className, }: PaginationProps) { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const iconColor = isDark ? "#FFFFFF" : "#000000"; const { totalPages, currentPage, canPrev, canNext, rangeStart, rangeEnd } = getPaginationState( page, pageSize, totalItems ); const showSizeSelector = !!pageSizeOptions && pageSizeOptions.length > 0 && !!onPageSizeChange; return ( {totalItems === 0 ? "No items" : `${rangeStart}–${rangeEnd} of ${totalItems}`} {showSizeSelector ? ( {pageSizeOptions.map((size) => ( onPageSizeChange(size)}> {`${size} / page`} ))} ) : null} {`Page ${currentPage} of ${totalPages}`} ); }