Components
npx shadcn@latest add https://21st.dev/r/originui/tableLoading preview...
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Column,
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { ArrowLeftToLine, ArrowRightToLine, Ellipsis, PinOff } from "lucide-react";
import { CSSProperties, useEffect, useState } from "react";
type Item = {
id: string;
name: string;
email: string;
location: string;
flag: string;
status: "Active" | "Inactive" | "Pending";
balance: number;
department: string;
role: string;
joinDate: string;
lastActive: string;
performance: "Good" | "Very Good" | "Excellent" | "Outstanding";
};
// Helper function to compute pinning styles for columns
const getPinningStyles = (column: Column<Item>): CSSProperties => {
const isPinned = column.getIsPinned();
return {
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
position: isPinned ? "sticky" : "relative",
width: column.getSize(),
zIndex: isPinned ? 1 : 0,
};
};
const columns: ColumnDef<Item>[] = [
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => <div className="truncate font-medium">{row.getValue("name")}</div>,
},
{
header: "Email",
accessorKey: "email",
},
{
header: "Location",
accessorKey: "location",
cell: ({ row }) => (
<div className="truncate">
<span className="text-lg leading-none">{row.original.flag}</span> {row.getValue("location")}
</div>
),
},
{
header: "Status",
accessorKey: "status",
},
{
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;
},
},
{
header: "Department",
accessorKey: "department",
},
{
header: "Role",
accessorKey: "role",
},
{
header: "Join Date",
accessorKey: "joinDate",
},
{
header: "Last Active",
accessorKey: "lastActive",
},
{
header: "Performance",
accessorKey: "performance",
},
];
export default function Component() {
const [data, setData] = useState<Item[]>([]);
const [sorting, setSorting] = useState<SortingState>([]);
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.slice(0, 5)); // Limit to 5 items
}
fetchPosts();
}, []);
const table = useReactTable({
data,
columns,
columnResizeMode: "onChange",
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
state: {
sorting,
},
enableSortingRemoval: false,
});
return (
<div className="bg-background max-w-[1000px]">
<Table
className="table-fixed border-separate border-spacing-0 [&_td]:border-border [&_tfoot_td]:border-t [&_th]:border-b [&_th]:border-border [&_tr:not(:last-child)_td]:border-b [&_tr]:border-none"
style={{
width: table.getTotalSize(),
}}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-muted/50">
{headerGroup.headers.map((header) => {
const { column } = header;
const isPinned = column.getIsPinned();
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinned = isPinned === "right" && column.getIsFirstColumn("right");
return (
<TableHead
key={header.id}
className="relative h-10 truncate border-t [&:not([data-pinned]):has(+[data-pinned])_div.cursor-col-resize:last-child]:opacity-0 [&[data-last-col=left]_div.cursor-col-resize:last-child]:opacity-0 [&[data-pinned=left][data-last-col=left]]:border-r [&[data-pinned=right]:last-child_div.cursor-col-resize:last-child]:opacity-0 [&[data-pinned=right][data-last-col=right]]:border-l [&[data-pinned][data-last-col]]:border-border [&[data-pinned]]:bg-muted/90 [&[data-pinned]]:backdrop-blur-sm"
colSpan={header.colSpan}
style={{ ...getPinningStyles(column) }}
data-pinned={isPinned || undefined}
data-last-col={
isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined
}
>
<div className="flex items-center justify-between gap-2">
<span className="truncate">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</span>
{/* Pin/Unpin column controls with enhanced accessibility */}
{!header.isPlaceholder &&
header.column.getCanPin() &&
(header.column.getIsPinned() ? (
<Button
size="icon"
variant="ghost"
className="-mr-1 size-7 shadow-none"
onClick={() => header.column.pin(false)}
aria-label={`Unpin ${header.column.columnDef.header as string} column`}
title={`Unpin ${header.column.columnDef.header as string} column`}
>
<PinOff
className="opacity-60"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className="-mr-1 size-7 shadow-none"
aria-label={`Pin options for ${header.column.columnDef.header as string} column`}
title={`Pin options for ${header.column.columnDef.header as string} column`}
>
<Ellipsis
className="opacity-60"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => header.column.pin("left")}>
<ArrowLeftToLine
size={16}
strokeWidth={2}
className="opacity-60"
aria-hidden="true"
/>
Stick to left
</DropdownMenuItem>
<DropdownMenuItem onClick={() => header.column.pin("right")}>
<ArrowRightToLine
size={16}
strokeWidth={2}
className="opacity-60"
aria-hidden="true"
/>
Stick to right
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
))}
{header.column.getCanResize() && (
<div
{...{
onDoubleClick: () => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className:
"absolute top-0 h-full w-4 cursor-col-resize user-select-none touch-none -right-2 z-10 flex justify-center before:absolute before:w-px before:inset-y-0 before:bg-border before:-translate-x-px",
}}
/>
)}
</div>
</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) => {
const { column } = cell;
const isPinned = column.getIsPinned();
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinned =
isPinned === "right" && column.getIsFirstColumn("right");
return (
<TableCell
key={cell.id}
className="truncate [&[data-pinned=left][data-last-col=left]]:border-r [&[data-pinned=right][data-last-col=right]]:border-l [&[data-pinned][data-last-col]]:border-border [&[data-pinned]]:bg-background/90 [&[data-pinned]]:backdrop-blur-sm"
style={{ ...getPinningStyles(column) }}
data-pinned={isPinned || undefined}
data-last-col={
isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined
}
>
{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>
<p className="mt-4 text-center text-sm text-muted-foreground">
Pinnable columns 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...