feat: standardize table pagination styling
This commit is contained in:
parent
50f6796ffa
commit
2e7f575682
3 changed files with 215 additions and 129 deletions
|
|
@ -13,13 +13,7 @@ import {
|
|||
useReactTable,
|
||||
SortingState,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconFilter,
|
||||
IconTrash,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react"
|
||||
import { IconFilter, IconTrash, IconUser } from "@tabler/icons-react"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
|
@ -49,6 +43,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { TablePagination } from "@/components/ui/table-pagination"
|
||||
|
||||
export type AdminClient = {
|
||||
id: string
|
||||
|
|
@ -386,29 +381,13 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
|
|||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-between gap-3 text-sm text-neutral-600 md:flex-row">
|
||||
<div>
|
||||
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount() || 1}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
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>
|
||||
<TablePagination
|
||||
table={table}
|
||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||
rowsPerPageLabel="Itens por página"
|
||||
showSelectedRows
|
||||
selectionLabel={(selected, total) => `${selected} de ${total} selecionados`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
|
|
|
|||
|
|
@ -20,20 +20,16 @@ import {
|
|||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronsLeft,
|
||||
IconChevronsRight,
|
||||
IconCircleCheckFilled,
|
||||
IconDotsVertical,
|
||||
IconGripVertical,
|
||||
IconLayoutColumns,
|
||||
IconLoader,
|
||||
IconPlus,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconCircleCheckFilled,
|
||||
IconDotsVertical,
|
||||
IconGripVertical,
|
||||
IconLayoutColumns,
|
||||
IconLoader,
|
||||
IconPlus,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
|
|
@ -99,12 +95,13 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs"
|
||||
import { TablePagination } from "@/components/ui/table-pagination"
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
|
|
@ -529,85 +526,18 @@ export function DataTable({
|
|||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
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>
|
||||
</div>
|
||||
<TablePagination
|
||||
table={table}
|
||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||
rowsPerPageLabel="Rows per page"
|
||||
showSelectedRows
|
||||
selectionLabel={(selected, total) =>
|
||||
`${selected} of ${total} row${total === 1 ? "" : "s"} selected.`
|
||||
}
|
||||
className="pb-4"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="past-performance"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
|
|
|
|||
177
src/components/ui/table-pagination.tsx
Normal file
177
src/components/ui/table-pagination.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue