Przeglądaj źródła

feat: enhance data table pagination with page numbers (#207)

Enhance data table pagination with page numbers and improve layout responsiveness.
Sat Naing 10 miesięcy temu
rodzic
commit
66df6a34c0

+ 1 - 1
src/components/layout/main.tsx

@@ -11,7 +11,7 @@ export function Main({ fixed, className, fluid, ...props }: MainProps) {
     <main
       data-layout={fixed ? 'fixed' : 'auto'}
       className={cn(
-        'px-4 py-6',
+        '@container/main px-4 py-6',
 
         // If layout is fixed, make the main container flex and grow
         fixed && 'flex grow flex-col overflow-hidden',

+ 43 - 15
src/features/tasks/components/data-table-pagination.tsx

@@ -5,6 +5,7 @@ import {
   DoubleArrowRightIcon,
 } from '@radix-ui/react-icons'
 import { type Table } from '@tanstack/react-table'
+import { cn, getPageNumbers } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import {
   Select,
@@ -21,18 +22,23 @@ type DataTablePaginationProps<TData> = {
 export function DataTablePagination<TData>({
   table,
 }: DataTablePaginationProps<TData>) {
+  const currentPage = table.getState().pagination.pageIndex + 1
+  const totalPages = table.getPageCount()
+  const pageNumbers = getPageNumbers(currentPage, totalPages)
+
   return (
     <div
-      className='flex items-center justify-between overflow-clip px-2'
+      className={cn(
+        'flex items-center justify-between overflow-clip px-2',
+        '@max-2xl/main:flex-col-reverse @max-2xl/main:gap-4'
+      )}
       style={{ overflowClipMargin: 1 }}
     >
-      <div className='text-muted-foreground hidden flex-1 text-sm sm:block'>
-        {table.getFilteredSelectedRowModel().rows.length} of{' '}
-        {table.getFilteredRowModel().rows.length} row(s) selected.
-      </div>
-      <div className='flex items-center sm:space-x-6 lg:space-x-8'>
-        <div className='flex items-center space-x-2'>
-          <p className='hidden text-sm font-medium sm:block'>Rows per page</p>
+      <div className='flex w-full items-center justify-between'>
+        <div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/main:hidden'>
+          Page {currentPage} of {totalPages}
+        </div>
+        <div className='flex items-center gap-2 @max-2xl/main:flex-row-reverse'>
           <Select
             value={`${table.getState().pagination.pageSize}`}
             onValueChange={(value) => {
@@ -50,15 +56,18 @@ export function DataTablePagination<TData>({
               ))}
             </SelectContent>
           </Select>
+          <p className='hidden text-sm font-medium sm:block'>Rows per page</p>
         </div>
-        <div className='flex w-[100px] items-center justify-center text-sm font-medium'>
-          Page {table.getState().pagination.pageIndex + 1} of{' '}
-          {table.getPageCount()}
+      </div>
+
+      <div className='flex items-center sm:space-x-6 lg:space-x-8'>
+        <div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/main:hidden'>
+          Page {currentPage} of {totalPages}
         </div>
         <div className='flex items-center space-x-2'>
           <Button
             variant='outline'
-            className='hidden h-8 w-8 p-0 lg:flex'
+            className='size-8 p-0 @max-md/main:hidden'
             onClick={() => table.setPageIndex(0)}
             disabled={!table.getCanPreviousPage()}
           >
@@ -67,16 +76,35 @@ export function DataTablePagination<TData>({
           </Button>
           <Button
             variant='outline'
-            className='h-8 w-8 p-0'
+            className='size-8 p-0'
             onClick={() => table.previousPage()}
             disabled={!table.getCanPreviousPage()}
           >
             <span className='sr-only'>Go to previous page</span>
             <ChevronLeftIcon className='h-4 w-4' />
           </Button>
+
+          {/* Page number buttons */}
+          {pageNumbers.map((pageNumber, index) => (
+            <div key={`${pageNumber}-${index}`} className='flex items-center'>
+              {pageNumber === '...' ? (
+                <span className='text-muted-foreground px-1 text-sm'>...</span>
+              ) : (
+                <Button
+                  variant={currentPage === pageNumber ? 'default' : 'outline'}
+                  className='size-8 p-0'
+                  onClick={() => table.setPageIndex((pageNumber as number) - 1)}
+                >
+                  <span className='sr-only'>Go to page {pageNumber}</span>
+                  {pageNumber}
+                </Button>
+              )}
+            </div>
+          ))}
+
           <Button
             variant='outline'
-            className='h-8 w-8 p-0'
+            className='size-8 p-0'
             onClick={() => table.nextPage()}
             disabled={!table.getCanNextPage()}
           >
@@ -85,7 +113,7 @@ export function DataTablePagination<TData>({
           </Button>
           <Button
             variant='outline'
-            className='hidden h-8 w-8 p-0 lg:flex'
+            className='size-8 p-0 @max-md/main:hidden'
             onClick={() => table.setPageIndex(table.getPageCount() - 1)}
             disabled={!table.getCanNextPage()}
           >

+ 43 - 15
src/features/users/components/data-table-pagination.tsx

@@ -5,6 +5,7 @@ import {
   DoubleArrowRightIcon,
 } from '@radix-ui/react-icons'
 import { type Table } from '@tanstack/react-table'
+import { cn, getPageNumbers } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import {
   Select,
@@ -21,18 +22,23 @@ type DataTablePaginationProps<TData> = {
 export function DataTablePagination<TData>({
   table,
 }: DataTablePaginationProps<TData>) {
+  const currentPage = table.getState().pagination.pageIndex + 1
+  const totalPages = table.getPageCount()
+  const pageNumbers = getPageNumbers(currentPage, totalPages)
+
   return (
     <div
-      className='flex items-center justify-between overflow-clip px-2'
+      className={cn(
+        'flex items-center justify-between overflow-clip px-2',
+        '@max-2xl/main:flex-col-reverse @max-2xl/main:gap-4'
+      )}
       style={{ overflowClipMargin: 1 }}
     >
-      <div className='text-muted-foreground hidden flex-1 text-sm sm:block'>
-        {table.getFilteredSelectedRowModel().rows.length} of{' '}
-        {table.getFilteredRowModel().rows.length} row(s) selected.
-      </div>
-      <div className='flex items-center sm:space-x-6 lg:space-x-8'>
-        <div className='flex items-center space-x-2'>
-          <p className='hidden text-sm font-medium sm:block'>Rows per page</p>
+      <div className='flex w-full items-center justify-between'>
+        <div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/main:hidden'>
+          Page {currentPage} of {totalPages}
+        </div>
+        <div className='flex items-center gap-2 @max-2xl/main:flex-row-reverse'>
           <Select
             value={`${table.getState().pagination.pageSize}`}
             onValueChange={(value) => {
@@ -50,15 +56,18 @@ export function DataTablePagination<TData>({
               ))}
             </SelectContent>
           </Select>
+          <p className='hidden text-sm font-medium sm:block'>Rows per page</p>
         </div>
-        <div className='flex w-[100px] items-center justify-center text-sm font-medium'>
-          Page {table.getState().pagination.pageIndex + 1} of{' '}
-          {table.getPageCount()}
+      </div>
+
+      <div className='flex items-center sm:space-x-6 lg:space-x-8'>
+        <div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/main:hidden'>
+          Page {currentPage} of {totalPages}
         </div>
         <div className='flex items-center space-x-2'>
           <Button
             variant='outline'
-            className='hidden h-8 w-8 p-0 lg:flex'
+            className='size-8 p-0 @max-md/main:hidden'
             onClick={() => table.setPageIndex(0)}
             disabled={!table.getCanPreviousPage()}
           >
@@ -67,16 +76,35 @@ export function DataTablePagination<TData>({
           </Button>
           <Button
             variant='outline'
-            className='h-8 w-8 p-0'
+            className='size-8 p-0'
             onClick={() => table.previousPage()}
             disabled={!table.getCanPreviousPage()}
           >
             <span className='sr-only'>Go to previous page</span>
             <ChevronLeftIcon className='h-4 w-4' />
           </Button>
+
+          {/* Page number buttons */}
+          {pageNumbers.map((pageNumber, index) => (
+            <div key={`${pageNumber}-${index}`} className='flex items-center'>
+              {pageNumber === '...' ? (
+                <span className='text-muted-foreground px-1 text-sm'>...</span>
+              ) : (
+                <Button
+                  variant={currentPage === pageNumber ? 'default' : 'outline'}
+                  className='size-8 p-0'
+                  onClick={() => table.setPageIndex((pageNumber as number) - 1)}
+                >
+                  <span className='sr-only'>Go to page {pageNumber}</span>
+                  {pageNumber}
+                </Button>
+              )}
+            </div>
+          ))}
+
           <Button
             variant='outline'
-            className='h-8 w-8 p-0'
+            className='size-8 p-0'
             onClick={() => table.nextPage()}
             disabled={!table.getCanNextPage()}
           >
@@ -85,7 +113,7 @@ export function DataTablePagination<TData>({
           </Button>
           <Button
             variant='outline'
-            className='hidden h-8 w-8 p-0 lg:flex'
+            className='size-8 p-0 @max-md/main:hidden'
             onClick={() => table.setPageIndex(table.getPageCount() - 1)}
             disabled={!table.getCanNextPage()}
           >

+ 50 - 0
src/lib/utils.ts

@@ -4,3 +4,53 @@ import { twMerge } from 'tailwind-merge'
 export function cn(...inputs: ClassValue[]) {
   return twMerge(clsx(inputs))
 }
+
+/**
+ * Generates page numbers for pagination with ellipsis
+ * @param currentPage - Current page number (1-based)
+ * @param totalPages - Total number of pages
+ * @returns Array of page numbers and ellipsis strings
+ *
+ * Examples:
+ * - Small dataset (≤5 pages): [1, 2, 3, 4, 5]
+ * - Near beginning: [1, 2, 3, 4, '...', 10]
+ * - In middle: [1, '...', 4, 5, 6, '...', 10]
+ * - Near end: [1, '...', 7, 8, 9, 10]
+ */
+export function getPageNumbers(currentPage: number, totalPages: number) {
+  const maxVisiblePages = 5 // Maximum number of page buttons to show
+  const rangeWithDots = []
+
+  if (totalPages <= maxVisiblePages) {
+    // If total pages is 5 or less, show all pages
+    for (let i = 1; i <= totalPages; i++) {
+      rangeWithDots.push(i)
+    }
+  } else {
+    // Always show first page
+    rangeWithDots.push(1)
+
+    if (currentPage <= 3) {
+      // Near the beginning: [1] [2] [3] [4] ... [10]
+      for (let i = 2; i <= 4; i++) {
+        rangeWithDots.push(i)
+      }
+      rangeWithDots.push('...', totalPages)
+    } else if (currentPage >= totalPages - 2) {
+      // Near the end: [1] ... [7] [8] [9] [10]
+      rangeWithDots.push('...')
+      for (let i = totalPages - 3; i <= totalPages; i++) {
+        rangeWithDots.push(i)
+      }
+    } else {
+      // In the middle: [1] ... [4] [5] [6] ... [10]
+      rangeWithDots.push('...')
+      for (let i = currentPage - 1; i <= currentPage + 1; i++) {
+        rangeWithDots.push(i)
+      }
+      rangeWithDots.push('...', totalPages)
+    }
+  }
+
+  return rangeWithDots
+}