'use client'
import { useEffect, useId, useState } from 'react'
import {
ChevronDownIcon,
ChevronFirstIcon,
ChevronLastIcon,
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 { Label } from '@/registry/new-york/ui/label'
import { Pagination, PaginationContent, 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 { 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 PaginatedDataTableDemo = () => {
const id = useId()
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5
})
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
}
})
return (
<div className='space-y-4 md:w-full'>
<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-8'>
<div className='flex items-center gap-3'>
<Label htmlFor={id} className='max-sm:sr-only'>
Rows per page
</Label>
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={value => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger id={id} className='w-fit whitespace-nowrap'>
<SelectValue placeholder='Select number of results' />
</SelectTrigger>
<SelectContent className='[&_*[role=option]]:pr-8 [&_*[role=option]]:pl-2 [&_*[role=option]>span]:right-2 [&_*[role=option]>span]:left-auto'>
{[5, 10, 25, 50].map(pageSize => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='text-muted-foreground flex grow justify-end text-sm whitespace-nowrap'>
<p className='text-muted-foreground text-sm whitespace-nowrap' aria-live='polite'>
<span className='text-foreground'>
{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}-
{Math.min(
Math.max(
table.getState().pagination.pageIndex * table.getState().pagination.pageSize +
table.getState().pagination.pageSize,
0
),
table.getRowCount()
)}
</span>{' '}
of <span className='text-foreground'>{table.getRowCount().toString()}</span>
</p>
</div>
<div>
<Pagination>
<PaginationContent>
<PaginationItem>
<Button
size='icon'
variant='outline'
className='disabled:pointer-events-none disabled:opacity-50'
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
aria-label='Go to first page'
>
<ChevronFirstIcon aria-hidden='true' />
</Button>
</PaginationItem>
<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>
<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>
<PaginationItem>
<Button
size='icon'
variant='outline'
className='disabled:pointer-events-none disabled:opacity-50'
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
aria-label='Go to last page'
>
<ChevronLastIcon aria-hidden='true' />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
<p className='text-muted-foreground mt-4 text-center text-sm'>Paginated data table </p>
</div>
)
}
export default PaginatedDataTableDemo