This commit is contained in:
2025-11-06 19:17:50 -03:00
parent 14f8b65df5
commit 58e2bf0c32
15 changed files with 559 additions and 382 deletions

View File

@@ -0,0 +1,66 @@
import { type Column } from '@tanstack/react-table'
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { cn } from '@repo/ui/lib/utils'
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn('flex items-center gap-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="data-[state=open]:bg-accent -ml-3 h-8"
>
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDown />
) : column.getIsSorted() === 'asc' ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUp />
Cres.
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDown />
Decr.
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff />
Ocultar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -0,0 +1,181 @@
'use client'
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type ColumnSort,
type SortingState,
type Table,
type VisibilityState
} from '@tanstack/react-table'
import { createContext, useState, type ReactNode } from 'react'
import { useSearchParams } from 'react-router'
import { Card, CardContent } from '@repo/ui/components/ui/card'
import {
Table as Table_,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
import { DataTablePagination } from './pagination'
interface DataTableProps<TData, TValue> {
children?: ReactNode
columns: ColumnDef<TData, TValue>[]
data: TData[]
pageIndex: number
sort: SortingState
pageSize: number
rowCount: number
hiddenColumn?: string[]
}
export const TableContext = createContext<{ table: Table<any> } | null>(null)
export function DataTable<TData, TValue>({
children,
columns,
data,
sort,
pageIndex,
pageSize,
rowCount,
hiddenColumn = []
}: DataTableProps<TData, TValue>) {
const [searchParams, setSearchParams] = useSearchParams()
const hiddenColumn_ = Object.fromEntries(
hiddenColumn.map((column) => [column, false])
)
const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>(hiddenColumn_)
const [rowSelection, setRowSelection] = useState({})
const sortParam = searchParams.get('sort')
const sorting = sortParam
? sortParam.split(',').map((s) => {
const [id, dir] = s.split(':')
return { id, desc: dir === 'desc' }
})
: sort
const setPagination = (updater: any) => {
const newState =
typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater
setSearchParams((searchParams) => {
searchParams.set('p', newState?.pageIndex.toString())
searchParams.set('perPage', newState?.pageSize.toString())
return searchParams
})
}
const setSorting = (updater: any) => {
const newSorting =
typeof updater === 'function' ? updater(sorting) : updater
setSearchParams((searchParams) => {
if (newSorting.length) {
const sort = newSorting
.map((s: ColumnSort) => `${s.id}:${s.desc ? 'desc' : 'asc'}`)
.join(',')
searchParams.set('sort', sort)
}
return searchParams
})
}
const table = useReactTable({
data,
columns,
rowCount,
state: {
sorting,
rowSelection,
columnVisibility,
pagination: {
pageIndex,
pageSize
}
},
manualSorting: true,
manualPagination: true,
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination
})
return (
<TableContext value={{ table }}>
<div className="space-y-2.5 max-md:mb-2">
<Card className="relative w-full overflow-auto">
<CardContent>
{children}
<Table_>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="hover:bg-transparent"
>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="p-2.5">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="p-2.5">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
Nenhum resultado.
</TableCell>
</TableRow>
)}
</TableBody>
</Table_>
</CardContent>
</Card>
<DataTablePagination table={table} />
</div>
</TableContext>
)
}

View File

@@ -0,0 +1,3 @@
export { DataTableColumnHeader } from './column-header'
export { DataTable } from './data-table'
export { DataTableViewOptions } from './view-options'

View File

@@ -0,0 +1,84 @@
import { type Table } from '@tanstack/react-table'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@repo/ui/components/ui/select'
interface DataTablePaginationProps<TData> {
table: Table<TData>
}
export function DataTablePagination<TData>({
table
}: DataTablePaginationProps<TData>) {
const { pageIndex, pageSize } = table.getState().pagination
const rowCount = table.getRowCount()
return (
<div className="flex items-center">
<div className="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} de{' '}
{table.getFilteredRowModel().rows.length} linha(s) selecionada(s).
</div>
<div className="flex items-center gap-3 lg:gap-6">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium hidden lg:block">
Itens por página
</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger>
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[12, 25, 50, 100].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-sm font-medium">
{(pageIndex + 1) * pageSize - pageSize + 1}-
{Math.min((pageIndex + 1) * pageSize, rowCount)}
</div>
<div className="flex items-center gap-2 *:cursor-pointer">
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Ir para a página anterior</span>
<ChevronLeft />
</Button>
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Ir para a próxima página</span>
<ChevronRight />
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,59 @@
'use client'
import { useContext } from 'react'
import { type Table } from '@tanstack/react-table'
import { Columns2Icon } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { cn } from '@repo/ui/lib/utils'
import { TableContext } from './data-table'
export function DataTableViewOptions<TData>({
className
}: {
className: string
}) {
const ctx = useContext(TableContext) as { table: Table<TData> } | null
if (!ctx) {
throw new Error('TableContext is null')
}
const { table } = ctx
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className={cn('cursor-pointer', className)}>
<Columns2Icon /> Colunas
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44 *:cursor-pointer">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== 'undefined' && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.columnDef?.meta?.title ?? column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}