feat: standardize table pagination styling

This commit is contained in:
Esdras Renan 2025-10-20 21:05:50 -03:00
parent 50f6796ffa
commit 2e7f575682
3 changed files with 215 additions and 129 deletions

View file

@ -13,13 +13,7 @@ import {
useReactTable, useReactTable,
SortingState, SortingState,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { import { IconFilter, IconTrash, IconUser } from "@tabler/icons-react"
IconChevronLeft,
IconChevronRight,
IconFilter,
IconTrash,
IconUser,
} from "@tabler/icons-react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -49,6 +43,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { TablePagination } from "@/components/ui/table-pagination"
export type AdminClient = { export type AdminClient = {
id: string id: string
@ -386,29 +381,13 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
</Table> </Table>
</div> </div>
<div className="flex flex-col items-center justify-between gap-3 text-sm text-neutral-600 md:flex-row"> <TablePagination
<div> table={table}
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount() || 1} pageSizeOptions={[10, 20, 30, 40, 50]}
</div> rowsPerPageLabel="Itens por página"
<div className="flex items-center gap-2"> showSelectedRows
<Button selectionLabel={(selected, total) => `${selected} de ${total} selecionados`}
variant="outline" />
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconChevronLeft className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconChevronRight className="size-4" />
</Button>
</div>
</div>
</div> </div>
<Dialog <Dialog

View file

@ -20,20 +20,16 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from "@dnd-kit/sortable" } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities" import { CSS } from "@dnd-kit/utilities"
import { import {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconCircleCheckFilled,
IconChevronRight, IconDotsVertical,
IconChevronsLeft, IconGripVertical,
IconChevronsRight, IconLayoutColumns,
IconCircleCheckFilled, IconLoader,
IconDotsVertical, IconPlus,
IconGripVertical, IconTrendingUp,
IconLayoutColumns, } from "@tabler/icons-react"
IconLoader,
IconPlus,
IconTrendingUp,
} from "@tabler/icons-react"
import { import {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
@ -99,12 +95,13 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "@/components/ui/tabs" } from "@/components/ui/tabs"
import { TablePagination } from "@/components/ui/table-pagination"
export const schema = z.object({ export const schema = z.object({
id: z.number(), id: z.number(),
@ -529,85 +526,18 @@ export function DataTable({
</TableBody> </TableBody>
</Table> </Table>
</DndContext> </DndContext>
</div> </div>
<div className="flex items-center justify-between px-4"> <TablePagination
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex"> table={table}
{table.getFilteredSelectedRowModel().rows.length} of{" "} pageSizeOptions={[10, 20, 30, 40, 50]}
{table.getFilteredRowModel().rows.length} row(s) selected. rowsPerPageLabel="Rows per page"
</div> showSelectedRows
<div className="flex w-full items-center gap-8 lg:w-fit"> selectionLabel={(selected, total) =>
<div className="hidden items-center gap-2 lg:flex"> `${selected} of ${total} row${total === 1 ? "" : "s"} selected.`
<Label htmlFor="rows-per-page" className="text-sm font-medium"> }
Rows per page className="pb-4"
</Label> />
<Select </TabsContent>
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<IconChevronRight />
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent <TabsContent
value="past-performance" value="past-performance"
className="flex flex-col px-4 lg:px-6" className="flex flex-col px-4 lg:px-6"

View file

@ -0,0 +1,177 @@
import { useMemo } from "react"
import type { Table } from "@tanstack/react-table"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { cn } from "@/lib/utils"
type TablePaginationProps<TData> = {
table: Table<TData>
pageSizeOptions?: number[]
rowsPerPageLabel?: string
showSelectedRows?: boolean
selectionLabel?: (selected: number, total: number) => React.ReactNode
className?: string
}
function buildPaginationRange(currentPage: number, pageCount: number) {
if (pageCount <= 7) {
return Array.from({ length: pageCount }, (_, index) => index + 1)
}
const range: Array<number | "ellipsis-left" | "ellipsis-right"> = [1]
const left = Math.max(2, currentPage - 1)
const right = Math.min(pageCount - 1, currentPage + 1)
if (left > 2) {
range.push("ellipsis-left")
}
for (let page = left; page <= right; page += 1) {
range.push(page)
}
if (right < pageCount - 1) {
range.push("ellipsis-right")
}
range.push(pageCount)
return range
}
export function TablePagination<TData>({
table,
pageSizeOptions = [10, 20, 30, 50],
rowsPerPageLabel = "Itens por página",
showSelectedRows = false,
selectionLabel,
className,
}: TablePaginationProps<TData>) {
const pageIndex = table.getState().pagination.pageIndex
const pageSize = table.getState().pagination.pageSize
const pageCount = Math.max(table.getPageCount(), 1)
const currentPage = Math.min(pageIndex + 1, pageCount)
const totalRows = table.getFilteredRowModel().rows.length
const currentRows = table.getRowModel().rows.length
const rangeStart = totalRows === 0 ? 0 : pageIndex * pageSize + 1
const rangeEnd = totalRows === 0 ? 0 : rangeStart + currentRows - 1
const selectedCount = table.getFilteredSelectedRowModel().rows.length
const paginationRange = useMemo(
() => buildPaginationRange(currentPage, pageCount),
[currentPage, pageCount]
)
const handlePageChange = (pageNumber: number) => {
table.setPageIndex(pageNumber - 1)
}
return (
<div
className={cn(
"flex flex-col gap-4 border-t border-slate-200 px-4 pt-4 text-sm text-neutral-600 md:flex-row md:items-center md:justify-between",
className
)}
>
{showSelectedRows ? (
<div className="text-xs text-neutral-500 md:text-sm">
{selectionLabel
? selectionLabel(selectedCount, totalRows)
: `${selectedCount} de ${totalRows} selecionados`}
</div>
) : (
<div className="sr-only">Paginação de tabela</div>
)}
<div className="flex flex-col-reverse items-center gap-3 md:flex-row md:gap-4 md:justify-end md:flex-1">
{pageSizeOptions.length > 0 ? (
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
<span>{rowsPerPageLabel}</span>
<Select
value={`${pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
table.setPageIndex(0)
}}
>
<SelectTrigger className="h-8 w-20">
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent align="end">
{pageSizeOptions.map((option) => (
<SelectItem key={option} value={`${option}`}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="flex flex-col items-center gap-2 md:flex-row md:gap-3">
<span className="text-xs font-medium text-neutral-500 md:text-sm">
{totalRows === 0
? "Nenhum registro"
: `Mostrando ${rangeStart}-${rangeEnd} de ${totalRows}`}
</span>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
/>
</PaginationItem>
{paginationRange.map((item, index) => {
if (typeof item === "number") {
return (
<PaginationItem key={`page-${item}`}>
<PaginationLink
href="#"
isActive={item === currentPage}
onClick={(event) => {
event.preventDefault()
handlePageChange(item)
}}
>
{item}
</PaginationLink>
</PaginationItem>
)
}
return (
<PaginationItem key={`ellipsis-${item}-${index}`}>
<PaginationEllipsis />
</PaginationItem>
)
})}
<PaginationItem>
<PaginationNext
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
</div>
)
}