'use client'
import { useEffect, useState } from 'react'
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from 'lucide-react'
import type { ColumnDef, PaginationState, SortingState } from '@tanstack/react-table'
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table'
import { Badge } from '@/registry/new-york/ui/badge'
import { Button } from '@/registry/new-york/ui/button'
import { Checkbox } from '@/registry/new-york/ui/checkbox'
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem } from '@/registry/new-york/ui/pagination'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/registry/new-york/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/registry/new-york/ui/table'
import { usePagination } from '@/registry/new-york/hooks/use-pagination'
import { cn } from '@/registry/new-york/lib/utils'
type Item = {
product_name: string
price: string
availability: 'In Stock' | 'Out of Stock' | 'Limited'
}
const columns: ColumnDef<Item>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label='Select all'
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)}
aria-label='Select row'
/>
),
size: 28,
enableSorting: false
},
{
header: 'Product Name',
accessorKey: 'product_name',
cell: ({ row }) => <div className='font-medium'>{row.getValue('product_name')}</div>
},
{
header: 'Price',
accessorKey: 'price',
cell: ({ row }) => <div className='font-medium'>{row.getValue('price')}</div>
},
{
header: 'Availability',
accessorKey: 'availability',
cell: ({ row }) => {
const availability = row.getValue('availability') as string
const styles = {
'In Stock':
'bg-green-600/10 text-green-600 focus-visible:ring-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:focus-visible:ring-green-400/40 [a&]:hover:bg-green-600/5 dark:[a&]:hover:bg-green-400/5',
'Out of Stock':
'bg-destructive/10 [a&]:hover:bg-destructive/5 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive',
Limited:
'bg-amber-600/10 text-amber-600 focus-visible:ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:focus-visible:ring-amber-400/40 [a&]:hover:bg-amber-600/5 dark:[a&]:hover:bg-amber-400/5'
}[availability]
return (
<Badge className={(cn('border-none focus-visible:outline-none'), styles)}>{row.getValue('availability')}</Badge>
)
}
}
]
const DataTableWithPaginationDemo = () => {
const pageSize = 5
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: pageSize
})
const [sorting, setSorting] = useState<SortingState>([
{
id: 'product_name',
desc: false
}
])
const [data, setData] = useState<Item[]>([])
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://cdn.jsdelivr.net/gh/themeselection/fy-assets/assets/json/mobile-stock.json')
if (!res.ok) {
throw new Error('Failed to fetch data')
}
const items = await res.json()
const data = await items.data
setData([...data, ...data])
}
fetchPosts()
}, [])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
enableSortingRemoval: false,
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
sorting,
pagination
}
})
const { pages, showLeftEllipsis, showRightEllipsis } = usePagination({
currentPage: table.getState().pagination.pageIndex + 1,
totalPages: table.getPageCount(),
paginationItemsToDisplay: 5
})
return (
<div className='w-full space-y-4'>
<div className='rounded-md border'>
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
{headerGroup.headers.map(header => {
return (
<TableHead key={header.id} style={{ width: `${header.getSize()}px` }} className='h-11'>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<div
className={cn(
header.column.getCanSort() &&
'flex h-full cursor-pointer items-center justify-between gap-2 select-none'
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={e => {
if (header.column.getCanSort() && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault()
header.column.getToggleSortingHandler()?.(e)
}
}}
tabIndex={header.column.getCanSort() ? 0 : undefined}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <ChevronUpIcon className='shrink-0 opacity-60' size={16} aria-hidden='true' />,
desc: <ChevronDownIcon className='shrink-0 opacity-60' size={16} aria-hidden='true' />
}[header.column.getIsSorted() as string] ?? null}
</div>
) : (
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}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className='h-24 text-center'>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className='flex items-center justify-between gap-3 max-sm:flex-col'>
<p className='text-muted-foreground flex-1 text-sm whitespace-nowrap' aria-live='polite'>
Page <span className='text-foreground'>{table.getState().pagination.pageIndex + 1}</span> of{' '}
<span className='text-foreground'>{table.getPageCount()}</span>
</p>
<div className='grow'>
<Pagination>
<PaginationContent>
<PaginationItem>
<Button
size='icon'
variant='outline'
className='disabled:pointer-events-none disabled:opacity-50'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label='Go to previous page'
>
<ChevronLeftIcon aria-hidden='true' />
</Button>
</PaginationItem>
{showLeftEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{pages.map(page => {
const isActive = page === table.getState().pagination.pageIndex + 1
return (
<PaginationItem key={page}>
<Button
size='icon'
variant={`${isActive ? 'outline' : 'ghost'}`}
onClick={() => table.setPageIndex(page - 1)}
aria-current={isActive ? 'page' : undefined}
>
{page}
</Button>
</PaginationItem>
)
})}
{showRightEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
<PaginationItem>
<Button
size='icon'
variant='outline'
className='disabled:pointer-events-none disabled:opacity-50'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label='Go to next page'
>
<ChevronRightIcon aria-hidden='true' />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
<div className='flex flex-1 justify-end'>
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={value => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger id='results-per-page' className='w-fit whitespace-nowrap' aria-label='Results per page'>
<SelectValue placeholder='Select number of results' />
</SelectTrigger>
<SelectContent>
{[5, 10, 25, 50].map(pageSize => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize} / page
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className='text-muted-foreground mt-4 text-center text-sm'>
Data table with pagination{' '}
<a href='https://originui.com/table' className='hover:text-primary underline' target='_blank'>
Origin UI
</a>
</p>
</div>
)
}
export default DataTableWithPaginationDemo