Components
npx shadcn@latest add https://21st.dev/r/originui/tableLoading preview...
"use client";
import { usePagination } from "@/components/hooks/use-pagination";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ColumnDef,
PaginationState,
SortingState,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react";
import { useEffect, useState } from "react";
type Item = {
id: string;
name: string;
email: string;
location: string;
flag: string;
status: "Active" | "Inactive" | "Pending";
balance: number;
};
const columns: ColumnDef<Item>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all rows"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
size: 28,
enableSorting: false,
},
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => <div className="font-medium">{row.getValue("name")}</div>,
size: 180,
},
{
header: "Email",
accessorKey: "email",
size: 200,
},
{
header: "Location",
accessorKey: "location",
cell: ({ row }) => (
<div>
<span className="text-lg leading-none">{row.original.flag}</span> {row.getValue("location")}
</div>
),
size: 180,
},
{
header: "Status",
accessorKey: "status",
cell: ({ row }) => (
<Badge
className={cn(
row.getValue("status") === "Inactive" && "bg-muted-foreground/60 text-primary-foreground",
)}
>
{row.getValue("status")}
</Badge>
),
size: 120,
},
{
header: "Balance",
accessorKey: "balance",
cell: ({ row }) => {
const amount = parseFloat(row.getValue("balance"));
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return formatted;
},
size: 120,
},
];
export default function Component() {
const pageSize = 5;
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: pageSize,
});
const [sorting, setSorting] = useState<SortingState>([
{
id: "name",
desc: false,
},
]);
const [data, setData] = useState<Item[]>([]);
useEffect(() => {
async function fetchPosts() {
const res = await fetch(
"https://res.cloudinary.com/dlzlfasou/raw/upload/users-01_fertyx.json",
);
const data = await res.json();
setData(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="space-y-4">
<div className="overflow-hidden rounded-lg border border-border bg-background">
<Table className="table-fixed">
<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 select-none items-center justify-between gap-2",
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={(e) => {
// Enhanced keyboard handling for sorting
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: (
<ChevronUp
className="shrink-0 opacity-60"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
),
desc: (
<ChevronDown
className="shrink-0 opacity-60"
size={16}
strokeWidth={2}
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>
{/* Pagination */}
<div className="flex items-center justify-between gap-3 max-sm:flex-col">
{/* Page number information */}
<p className="flex-1 whitespace-nowrap text-sm text-muted-foreground" aria-live="polite">
Page <span className="text-foreground">{table.getState().pagination.pageIndex + 1}</span>{" "}
of <span className="text-foreground">{table.getPageCount()}</span>
</p>
{/* Pagination buttons */}
<div className="grow">
<Pagination>
<PaginationContent>
{/* Previous page button */}
<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"
>
<ChevronLeft size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</PaginationItem>
{/* Left ellipsis (...) */}
{showLeftEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{/* Page number buttons */}
{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>
);
})}
{/* Right ellipsis (...) */}
{showRightEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{/* Next page button */}
<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"
>
<ChevronRight size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
{/* Results per page */}
<div className="flex flex-1 justify-end">
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
aria-label="Results per page"
>
<SelectTrigger id="results-per-page" className="w-fit whitespace-nowrap">
<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="mt-4 text-center text-sm text-muted-foreground">
Numeric pagination made with{" "}
<a
className="underline hover:text-foreground"
href="https://tanstack.com/table"
target="_blank"
rel="noopener noreferrer"
>
TanStack Table
</a>
</p>
</div>
);
}
export { Component }Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...