Skip to content

Commit 66df6a3

Browse files
authored
feat: enhance data table pagination with page numbers (#207)
Enhance data table pagination with page numbers and improve layout responsiveness.
1 parent e796db4 commit 66df6a3

4 files changed

Lines changed: 137 additions & 31 deletions

File tree

src/components/layout/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function Main({ fixed, className, fluid, ...props }: MainProps) {
1111
<main
1212
data-layout={fixed ? 'fixed' : 'auto'}
1313
className={cn(
14-
'px-4 py-6',
14+
'@container/main px-4 py-6',
1515

1616
// If layout is fixed, make the main container flex and grow
1717
fixed && 'flex grow flex-col overflow-hidden',

src/features/tasks/components/data-table-pagination.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DoubleArrowRightIcon,
66
} from '@radix-ui/react-icons'
77
import { type Table } from '@tanstack/react-table'
8+
import { cn, getPageNumbers } from '@/lib/utils'
89
import { Button } from '@/components/ui/button'
910
import {
1011
Select,
@@ -21,18 +22,23 @@ type DataTablePaginationProps<TData> = {
2122
export function DataTablePagination<TData>({
2223
table,
2324
}: DataTablePaginationProps<TData>) {
25+
const currentPage = table.getState().pagination.pageIndex + 1
26+
const totalPages = table.getPageCount()
27+
const pageNumbers = getPageNumbers(currentPage, totalPages)
28+
2429
return (
2530
<div
26-
className='flex items-center justify-between overflow-clip px-2'
31+
className={cn(
32+
'flex items-center justify-between overflow-clip px-2',
33+
'@max-2xl/main:flex-col-reverse @max-2xl/main:gap-4'
34+
)}
2735
style={{ overflowClipMargin: 1 }}
2836
>
29-
<div className='text-muted-foreground hidden flex-1 text-sm sm:block'>
30-
{table.getFilteredSelectedRowModel().rows.length} of{' '}
31-
{table.getFilteredRowModel().rows.length} row(s) selected.
32-
</div>
33-
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
34-
<div className='flex items-center space-x-2'>
35-
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
37+
<div className='flex w-full items-center justify-between'>
38+
<div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/main:hidden'>
39+
Page {currentPage} of {totalPages}
40+
</div>
41+
<div className='flex items-center gap-2 @max-2xl/main:flex-row-reverse'>
3642
<Select
3743
value={`${table.getState().pagination.pageSize}`}
3844
onValueChange={(value) => {
@@ -50,15 +56,18 @@ export function DataTablePagination<TData>({
5056
))}
5157
</SelectContent>
5258
</Select>
59+
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
5360
</div>
54-
<div className='flex w-[100px] items-center justify-center text-sm font-medium'>
55-
Page {table.getState().pagination.pageIndex + 1} of{' '}
56-
{table.getPageCount()}
61+
</div>
62+
63+
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
64+
<div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/main:hidden'>
65+
Page {currentPage} of {totalPages}
5766
</div>
5867
<div className='flex items-center space-x-2'>
5968
<Button
6069
variant='outline'
61-
className='hidden h-8 w-8 p-0 lg:flex'
70+
className='size-8 p-0 @max-md/main:hidden'
6271
onClick={() => table.setPageIndex(0)}
6372
disabled={!table.getCanPreviousPage()}
6473
>
@@ -67,16 +76,35 @@ export function DataTablePagination<TData>({
6776
</Button>
6877
<Button
6978
variant='outline'
70-
className='h-8 w-8 p-0'
79+
className='size-8 p-0'
7180
onClick={() => table.previousPage()}
7281
disabled={!table.getCanPreviousPage()}
7382
>
7483
<span className='sr-only'>Go to previous page</span>
7584
<ChevronLeftIcon className='h-4 w-4' />
7685
</Button>
86+
87+
{/* Page number buttons */}
88+
{pageNumbers.map((pageNumber, index) => (
89+
<div key={`${pageNumber}-${index}`} className='flex items-center'>
90+
{pageNumber === '...' ? (
91+
<span className='text-muted-foreground px-1 text-sm'>...</span>
92+
) : (
93+
<Button
94+
variant={currentPage === pageNumber ? 'default' : 'outline'}
95+
className='size-8 p-0'
96+
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
97+
>
98+
<span className='sr-only'>Go to page {pageNumber}</span>
99+
{pageNumber}
100+
</Button>
101+
)}
102+
</div>
103+
))}
104+
77105
<Button
78106
variant='outline'
79-
className='h-8 w-8 p-0'
107+
className='size-8 p-0'
80108
onClick={() => table.nextPage()}
81109
disabled={!table.getCanNextPage()}
82110
>
@@ -85,7 +113,7 @@ export function DataTablePagination<TData>({
85113
</Button>
86114
<Button
87115
variant='outline'
88-
className='hidden h-8 w-8 p-0 lg:flex'
116+
className='size-8 p-0 @max-md/main:hidden'
89117
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90118
disabled={!table.getCanNextPage()}
91119
>

src/features/users/components/data-table-pagination.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DoubleArrowRightIcon,
66
} from '@radix-ui/react-icons'
77
import { type Table } from '@tanstack/react-table'
8+
import { cn, getPageNumbers } from '@/lib/utils'
89
import { Button } from '@/components/ui/button'
910
import {
1011
Select,
@@ -21,18 +22,23 @@ type DataTablePaginationProps<TData> = {
2122
export function DataTablePagination<TData>({
2223
table,
2324
}: DataTablePaginationProps<TData>) {
25+
const currentPage = table.getState().pagination.pageIndex + 1
26+
const totalPages = table.getPageCount()
27+
const pageNumbers = getPageNumbers(currentPage, totalPages)
28+
2429
return (
2530
<div
26-
className='flex items-center justify-between overflow-clip px-2'
31+
className={cn(
32+
'flex items-center justify-between overflow-clip px-2',
33+
'@max-2xl/main:flex-col-reverse @max-2xl/main:gap-4'
34+
)}
2735
style={{ overflowClipMargin: 1 }}
2836
>
29-
<div className='text-muted-foreground hidden flex-1 text-sm sm:block'>
30-
{table.getFilteredSelectedRowModel().rows.length} of{' '}
31-
{table.getFilteredRowModel().rows.length} row(s) selected.
32-
</div>
33-
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
34-
<div className='flex items-center space-x-2'>
35-
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
37+
<div className='flex w-full items-center justify-between'>
38+
<div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/main:hidden'>
39+
Page {currentPage} of {totalPages}
40+
</div>
41+
<div className='flex items-center gap-2 @max-2xl/main:flex-row-reverse'>
3642
<Select
3743
value={`${table.getState().pagination.pageSize}`}
3844
onValueChange={(value) => {
@@ -50,15 +56,18 @@ export function DataTablePagination<TData>({
5056
))}
5157
</SelectContent>
5258
</Select>
59+
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
5360
</div>
54-
<div className='flex w-[100px] items-center justify-center text-sm font-medium'>
55-
Page {table.getState().pagination.pageIndex + 1} of{' '}
56-
{table.getPageCount()}
61+
</div>
62+
63+
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
64+
<div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/main:hidden'>
65+
Page {currentPage} of {totalPages}
5766
</div>
5867
<div className='flex items-center space-x-2'>
5968
<Button
6069
variant='outline'
61-
className='hidden h-8 w-8 p-0 lg:flex'
70+
className='size-8 p-0 @max-md/main:hidden'
6271
onClick={() => table.setPageIndex(0)}
6372
disabled={!table.getCanPreviousPage()}
6473
>
@@ -67,16 +76,35 @@ export function DataTablePagination<TData>({
6776
</Button>
6877
<Button
6978
variant='outline'
70-
className='h-8 w-8 p-0'
79+
className='size-8 p-0'
7180
onClick={() => table.previousPage()}
7281
disabled={!table.getCanPreviousPage()}
7382
>
7483
<span className='sr-only'>Go to previous page</span>
7584
<ChevronLeftIcon className='h-4 w-4' />
7685
</Button>
86+
87+
{/* Page number buttons */}
88+
{pageNumbers.map((pageNumber, index) => (
89+
<div key={`${pageNumber}-${index}`} className='flex items-center'>
90+
{pageNumber === '...' ? (
91+
<span className='text-muted-foreground px-1 text-sm'>...</span>
92+
) : (
93+
<Button
94+
variant={currentPage === pageNumber ? 'default' : 'outline'}
95+
className='size-8 p-0'
96+
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
97+
>
98+
<span className='sr-only'>Go to page {pageNumber}</span>
99+
{pageNumber}
100+
</Button>
101+
)}
102+
</div>
103+
))}
104+
77105
<Button
78106
variant='outline'
79-
className='h-8 w-8 p-0'
107+
className='size-8 p-0'
80108
onClick={() => table.nextPage()}
81109
disabled={!table.getCanNextPage()}
82110
>
@@ -85,7 +113,7 @@ export function DataTablePagination<TData>({
85113
</Button>
86114
<Button
87115
variant='outline'
88-
className='hidden h-8 w-8 p-0 lg:flex'
116+
className='size-8 p-0 @max-md/main:hidden'
89117
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90118
disabled={!table.getCanNextPage()}
91119
>

src/lib/utils.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,53 @@ import { twMerge } from 'tailwind-merge'
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs))
66
}
7+
8+
/**
9+
* Generates page numbers for pagination with ellipsis
10+
* @param currentPage - Current page number (1-based)
11+
* @param totalPages - Total number of pages
12+
* @returns Array of page numbers and ellipsis strings
13+
*
14+
* Examples:
15+
* - Small dataset (≤5 pages): [1, 2, 3, 4, 5]
16+
* - Near beginning: [1, 2, 3, 4, '...', 10]
17+
* - In middle: [1, '...', 4, 5, 6, '...', 10]
18+
* - Near end: [1, '...', 7, 8, 9, 10]
19+
*/
20+
export function getPageNumbers(currentPage: number, totalPages: number) {
21+
const maxVisiblePages = 5 // Maximum number of page buttons to show
22+
const rangeWithDots = []
23+
24+
if (totalPages <= maxVisiblePages) {
25+
// If total pages is 5 or less, show all pages
26+
for (let i = 1; i <= totalPages; i++) {
27+
rangeWithDots.push(i)
28+
}
29+
} else {
30+
// Always show first page
31+
rangeWithDots.push(1)
32+
33+
if (currentPage <= 3) {
34+
// Near the beginning: [1] [2] [3] [4] ... [10]
35+
for (let i = 2; i <= 4; i++) {
36+
rangeWithDots.push(i)
37+
}
38+
rangeWithDots.push('...', totalPages)
39+
} else if (currentPage >= totalPages - 2) {
40+
// Near the end: [1] ... [7] [8] [9] [10]
41+
rangeWithDots.push('...')
42+
for (let i = totalPages - 3; i <= totalPages; i++) {
43+
rangeWithDots.push(i)
44+
}
45+
} else {
46+
// In the middle: [1] ... [4] [5] [6] ... [10]
47+
rangeWithDots.push('...')
48+
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
49+
rangeWithDots.push(i)
50+
}
51+
rangeWithDots.push('...', totalPages)
52+
}
53+
}
54+
55+
return rangeWithDots
56+
}

0 commit comments

Comments
 (0)