Files
reui-blocks/blocks/data-table20.tsx
T

437 lines
13 KiB
TypeScript

"use client";
import {
type Column,
type ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
const formatPrice = (value: number) =>
new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
}).format(value);
type UseDataTableOptions<TData> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, unknown>>;
getRowId?: (row: TData) => string;
initialSorting?: SortingState;
};
export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
const { data, columns, getRowId, initialSorting = [] } = options;
const [sorting, setSorting] = React.useState<SortingState>(initialSorting);
const table = useReactTable({
data,
columns,
getRowId,
state: { sorting },
columnResizeMode: "onChange",
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
defaultColumn: {
size: 150,
minSize: 50,
maxSize: 500,
},
});
return {
table,
sorting,
setSorting,
};
}
type DataTableColumnHeaderProps<TData, TValue> = {
column: Column<TData, TValue>;
title: string;
};
export const DataTableColumnHeader = <TData, TValue>({
column,
title,
}: DataTableColumnHeaderProps<TData, TValue>) => {
const canSort = column.getCanSort();
const sorted = column.getIsSorted();
if (!canSort) {
return (
<span className="flex h-8 items-center pe-4 text-sm font-medium text-foreground">
{title}
</span>
);
}
return (
<Button
variant="ghost"
size="sm"
className="flex h-8 w-full items-center justify-between gap-2 px-0 pe-4 text-sm font-medium text-foreground hover:bg-transparent"
onClick={() => column.toggleSorting(sorted === "asc")}
>
<span className="truncate">{title}</span>
{sorted === "desc" ? (
<ArrowDown className="h-4 w-4 shrink-0" />
) : sorted === "asc" ? (
<ArrowUp className="h-4 w-4 shrink-0" />
) : (
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
)}
</Button>
);
};
export const schema = z.object({
id: z.string(),
item: z.string(),
type: z.string(),
stock: z.boolean(),
sku: z.string(),
price: z.number(),
availability: z.array(z.enum(["فروشگاه", "آنلاین"])),
});
const data = [
{
id: "prod-001",
item: "قاب تبلت",
type: "الکترونیک",
stock: true,
sku: "TC-001",
price: 83.24,
availability: ["فروشگاه", "آنلاین"],
},
{
id: "prod-002",
item: "ساعت هوشمند",
type: "الکترونیک",
stock: true,
sku: "SW-002",
price: 246.27,
availability: ["فروشگاه", "آنلاین"],
},
{
id: "prod-003",
item: "پلیور پشمی",
type: "لوازم جانبی",
stock: true,
sku: "WS-003",
price: 168.27,
availability: ["فروشگاه"],
},
{
id: "prod-004",
item: "هدفون بی‌سیم",
type: "الکترونیک",
stock: true,
sku: "WE-004",
price: 107.75,
availability: ["فروشگاه", "آنلاین"],
},
{
id: "prod-005",
item: "کاور لپ‌تاپ",
type: "الکترونیک",
stock: true,
sku: "LS-005",
price: 248.02,
availability: ["فروشگاه", "آنلاین"],
},
{
id: "prod-006",
item: "کفش دویدن",
type: "کفش",
stock: true,
sku: "RS-006",
price: 208.26,
availability: ["فروشگاه"],
},
{
id: "prod-007",
item: "کاپشن زمستانی",
type: "پوشاک",
stock: true,
sku: "WJ-007",
price: 148.06,
availability: ["فروشگاه"],
},
{
id: "prod-008",
item: "قاب گوشی",
type: "لوازم جانبی",
stock: true,
sku: "PC-008",
price: 298.08,
availability: ["فروشگاه", "آنلاین"],
},
{
id: "prod-009",
item: "ردیاب تناسب اندام",
type: "پوشاک",
stock: true,
sku: "FT-009",
price: 222.09,
availability: ["فروشگاه"],
},
{
id: "prod-010",
item: "عینک آفتابی",
type: "لوازم جانبی",
stock: true,
sku: "SG-010",
price: 60.17,
availability: ["فروشگاه"],
},
];
export const validatedData = schema.array().parse(data);
const COLUMN_SIZES = {
sku: { default: 100, min: 80, max: 150 },
item: { default: 200, min: 100, max: 400 },
type: { default: 120, min: 80, max: 200 },
stock: { default: 100, min: 80, max: 150 },
price: { default: 120, min: 100, max: 200 },
availability: { default: 180, min: 120, max: 300 },
} as const;
export const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
accessorKey: "sku",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="کد کالا" />
),
size: COLUMN_SIZES.sku.default,
minSize: COLUMN_SIZES.sku.min,
maxSize: COLUMN_SIZES.sku.max,
enableSorting: true,
enableHiding: false,
},
{
accessorKey: "item",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="محصول" />
),
cell: ({ row }) => (
<div className="truncate font-medium">{row.getValue("item")}</div>
),
size: COLUMN_SIZES.item.default,
minSize: COLUMN_SIZES.item.min,
maxSize: COLUMN_SIZES.item.max,
enableSorting: true,
enableHiding: false,
},
{
accessorKey: "type",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="دسته‌بندی" />
),
size: COLUMN_SIZES.type.default,
minSize: COLUMN_SIZES.type.min,
maxSize: COLUMN_SIZES.type.max,
enableSorting: true,
enableHiding: false,
},
{
accessorKey: "stock",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="موجود در انبار" />
),
cell: ({ row }) => {
const inStock = row.getValue("stock") as boolean;
return inStock ? "بله" : "خیر";
},
size: COLUMN_SIZES.stock.default,
minSize: COLUMN_SIZES.stock.min,
maxSize: COLUMN_SIZES.stock.max,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "price",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="قیمت" />
),
cell: ({ row }) => {
const price = row.getValue("price") as number;
return (
<div className="truncate font-medium tabular-nums">
{formatPrice(price)}
</div>
);
},
size: COLUMN_SIZES.price.default,
minSize: COLUMN_SIZES.price.min,
maxSize: COLUMN_SIZES.price.max,
enableSorting: true,
enableHiding: false,
},
{
accessorKey: "availability",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="دسترس‌پذیر در" />
),
cell: ({ row }) => {
const availability = row.getValue("availability") as (
| "فروشگاه"
| "آنلاین"
)[];
return (
<div className="flex gap-2">
{availability.map((location) => (
<Badge key={location} variant="secondary" className="shrink-0">
{location}
</Badge>
))}
</div>
);
},
size: COLUMN_SIZES.availability.default,
minSize: COLUMN_SIZES.availability.min,
maxSize: COLUMN_SIZES.availability.max,
enableSorting: false,
enableHiding: false,
},
];
export const DataTable20 = ({ className }: { className?: string }) => {
const { table } = useDataTable({
data: validatedData,
columns,
getRowId: (row) => row.id.toString(),
});
return (
<section dir="rtl" lang="fa" className={cn("py-32", className)}>
<div className="container">
<div className="w-full">
<div className="mb-8 text-start">
<h2 className="text-xl font-bold tracking-tight">
جدول با ستون‌های قابل تغییر اندازه
</h2>
<p className="mt-2 text-muted-foreground">
جدول داده ساده با مرتب‌سازی و امکان تغییر عرض ستون‌ها.
</p>
</div>
<div className="overflow-hidden rounded-md border">
<div className="overflow-x-auto">
<Table
className="relative w-full table-fixed"
style={{
minWidth: 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 px-2 select-none"
style={{
width: `${header.getSize()}px`,
}}
aria-sort={
header.column.getIsSorted() === "asc"
? "ascending"
: header.column.getIsSorted() === "desc"
? "descending"
: "none"
}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{header.column.getCanResize() && (
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className="group/resize absolute top-0 -end-2 z-10 flex h-full w-4 cursor-col-resize touch-none justify-center select-none before:absolute before:inset-y-0 before:w-px before:bg-border before:transition-colors hover:before:bg-primary active:before:bg-primary"
role="separator"
aria-label="تغییر اندازه ستون"
/>
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="transition-colors hover:bg-muted/50"
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="truncate"
style={{
width: `${cell.column.getSize()}px`,
maxWidth: `${cell.column.getSize()}px`,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
<div className="flex flex-col items-center justify-center gap-2">
<p className="text-muted-foreground">
محصولی یافت نشد
</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</div>
</section>
);
};