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,
|
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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
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