Components
Loading preview...
Shadcn table with enhanced appearance
npx shadcn@latest add https://21st.dev/r/originui/table"use client";
import { cn } from "@/lib/utils";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown, 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;
department: string;
role: string;
joinDate: string;
lastActive: string;
performance: "Excellent" | "Good" | "Average" | "Poor";
};
const columns: ColumnDef<Item>[] = [
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => <div className="truncate font-medium">{row.getValue("name")}</div>,
sortUndefined: "last",
sortDescFirst: false,
},
{
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",
},
];
function Component() {
const [data, setData] = useState<Item[]>([]);
const [sorting, setSorting] = useState<SortingState>([
{
id: "name",
desc: false,
},
]);
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"
style={{
width: table.getCenterTotalSize(),
}}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-muted/50">
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className="relative h-10 select-none border-t [&>.cursor-col-resize]:last:opacity-0"
aria-sort={
header.column.getIsSorted() === "asc"
? "ascending"
: header.column.getIsSorted() === "desc"
? "descending"
: "none"
}
{...{
colSpan: header.colSpan,
style: {
width: header.getSize(),
},
}}
>
{header.isPlaceholder ? null : (
<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}
>
<span className="truncate">
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
{{
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>
)}
{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",
}}
/>
)}
</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="truncate">
{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">
Resizable and sortable 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 }