@@ -12,10 +12,11 @@ import {
1212 flexRender ,
1313 getCoreRowModel ,
1414 getFilteredRowModel ,
15+ getPaginationRowModel ,
1516 getSortedRowModel ,
1617 useReactTable ,
1718} from "@tanstack/react-table"
18- import { Search } from "lucide-react"
19+ import { Search , ChevronLeft , ChevronRight } from "lucide-react"
1920import {
2021 Table ,
2122 TableBody ,
@@ -43,6 +44,8 @@ interface DataTableWithDataProps<TData, TValue> {
4344 defaultSorting ?: SortingState
4445 renderExpandedRow ?: ( row : Row < TData > ) => React . ReactNode
4546 getRowClassName ?: ( row : Row < TData > ) => string
47+ /** Number of rows per page. Defaults to 100. Set to 0 to disable pagination. */
48+ pageSize ?: number
4649}
4750
4851type DataTableProps < TData , TValue = unknown > =
@@ -170,11 +173,23 @@ function DataTableWithData<TData, TValue>({
170173 defaultSorting = [ ] ,
171174 renderExpandedRow,
172175 getRowClassName,
176+ pageSize = 100 ,
173177} : DataTableWithDataProps < TData , TValue > ) {
174178 const [ sorting , setSorting ] = React . useState < SortingState > ( defaultSorting )
175179 const [ columnFilters , setColumnFilters ] =
176180 React . useState < ColumnFiltersState > ( [ ] )
177181 const [ globalFilter , setGlobalFilter ] = React . useState ( "" )
182+ const [ pagination , setPagination ] = React . useState ( {
183+ pageIndex : 0 ,
184+ pageSize : pageSize > 0 ? pageSize : data . length ,
185+ } )
186+
187+ // Reset to page 1 when filter changes
188+ React . useEffect ( ( ) => {
189+ setPagination ( ( p ) => ( { ...p , pageIndex : 0 } ) )
190+ } , [ globalFilter ] )
191+
192+ const paginationEnabled = pageSize > 0
178193
179194 const table = useReactTable ( {
180195 data,
@@ -184,18 +199,36 @@ function DataTableWithData<TData, TValue>({
184199 getSortedRowModel : getSortedRowModel ( ) ,
185200 onColumnFiltersChange : setColumnFilters ,
186201 getFilteredRowModel : getFilteredRowModel ( ) ,
202+ ...( paginationEnabled
203+ ? {
204+ getPaginationRowModel : getPaginationRowModel ( ) ,
205+ onPaginationChange : setPagination ,
206+ }
207+ : { } ) ,
187208 onGlobalFilterChange : setGlobalFilter ,
188209 globalFilterFn : "includesString" ,
189210 state : {
190211 sorting,
191212 columnFilters,
192213 globalFilter,
214+ ...( paginationEnabled ? { pagination } : { } ) ,
193215 } ,
194216 } )
195217
218+ const filteredCount = table . getFilteredRowModel ( ) . rows . length
219+ const pageCount = table . getPageCount ( )
220+ const currentPage = table . getState ( ) . pagination . pageIndex + 1
221+ const canPrev = table . getCanPreviousPage ( )
222+ const canNext = table . getCanNextPage ( )
223+
224+ // Page range info: "Showing 1-100 of 500"
225+ const { pageIndex, pageSize : ps } = table . getState ( ) . pagination
226+ const rangeStart = pageIndex * ps + 1
227+ const rangeEnd = Math . min ( ( pageIndex + 1 ) * ps , filteredCount )
228+
196229 return (
197230 < div className = "space-y-4" >
198- { /* Search */ }
231+ { /* Search + row count */ }
199232 < div className = "flex items-center gap-4 pb-4" >
200233 < div className = "relative flex-1 max-w-md" >
201234 < Search className = "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -207,7 +240,9 @@ function DataTableWithData<TData, TValue>({
207240 />
208241 </ div >
209242 < span className = "text-sm text-muted-foreground whitespace-nowrap" >
210- { table . getFilteredRowModel ( ) . rows . length } of { data . length } results
243+ { filteredCount === data . length
244+ ? `${ data . length } results`
245+ : `${ filteredCount } of ${ data . length } results` }
211246 </ span >
212247 </ div >
213248
@@ -217,6 +252,38 @@ function DataTableWithData<TData, TValue>({
217252 renderExpandedRow = { renderExpandedRow }
218253 getRowClassName = { getRowClassName }
219254 />
255+
256+ { /* Pagination controls (only shown when pagination is enabled and there are multiple pages) */ }
257+ { paginationEnabled && pageCount > 1 && (
258+ < div className = "flex items-center justify-between px-1" >
259+ < span className = "text-sm text-muted-foreground" >
260+ Showing { rangeStart } –{ rangeEnd } of { filteredCount }
261+ </ span >
262+ < div className = "flex items-center gap-1" >
263+ < button
264+ onClick = { ( ) => table . previousPage ( ) }
265+ disabled = { ! canPrev }
266+ className = "inline-flex items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground hover:bg-muted/50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
267+ aria-label = "Previous page"
268+ >
269+ < ChevronLeft className = "h-3.5 w-3.5" />
270+ Prev
271+ </ button >
272+ < span className = "px-2 text-xs text-muted-foreground tabular-nums" >
273+ { currentPage } / { pageCount }
274+ </ span >
275+ < button
276+ onClick = { ( ) => table . nextPage ( ) }
277+ disabled = { ! canNext }
278+ className = "inline-flex items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground hover:bg-muted/50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
279+ aria-label = "Next page"
280+ >
281+ Next
282+ < ChevronRight className = "h-3.5 w-3.5" />
283+ </ button >
284+ </ div >
285+ </ div >
286+ ) }
220287 </ div >
221288 )
222289}
0 commit comments