"use client"; import type { DragEndEvent, DraggableAttributes, DraggableSyntheticListeners, } from "@dnd-kit/core"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { Column, ColumnDef, Row, RowSelectionState, SortingState, } from "@tanstack/react-table"; import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowDown, ArrowUp, ChevronsUpDown, Footprints, GripVertical, PackageIcon, Shirt, ShoppingBag, Smartphone, } 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 { Checkbox } from "@/components/ui/checkbox"; 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 = { data: Array; columns: Array>; getRowId?: (row: TData) => string; initialSorting?: SortingState; initialGlobalFilter?: string; initialSelection?: RowSelectionState; enableRowSelection?: boolean; }; export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialGlobalFilter = "", initialSelection = {}, enableRowSelection = true, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [globalFilter, setGlobalFilter] = React.useState(initialGlobalFilter); const [rowSelection, setRowSelection] = React.useState(initialSelection); const table = useReactTable({ data, columns, getRowId, state: { sorting, globalFilter, rowSelection, }, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), enableRowSelection, }); return { table, sorting, setSorting, globalFilter, setGlobalFilter, rowSelection, setRowSelection, }; } type DataTableColumnHeaderProps = { column: Column; title: string; }; export const DataTableColumnHeader = ({ column, title, }: DataTableColumnHeaderProps) => { const canSort = column.getCanSort(); const sorted = column.getIsSorted(); if (!canSort) { return ( {title} ); } return ( ); }; 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(["فروشگاه", "آنلاین"])), }); type Product = z.infer; export 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); export const productTypes = [ { value: "الکترونیک", label: "الکترونیک", icon: Smartphone, }, { value: "لوازم جانبی", label: "لوازم جانبی", icon: ShoppingBag, }, { value: "پوشاک", label: "پوشاک", icon: Shirt, }, { value: "کفش", label: "کفش", icon: Footprints, }, { value: "کفش ورزشی", label: "کفش ورزشی", icon: PackageIcon, }, ]; export const columns: ColumnDef[] = [ { id: "drag", header: () => مرتب‌سازی, cell: () => null, enableSorting: false, enableHiding: false, }, { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="انتخاب همه" className="translate-y-[2px]" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="انتخاب رکورد" className="translate-y-[2px]" /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "sku", header: ({ column }) => ( ), cell: ({ row }) =>
{row.original.sku}
, enableSorting: false, enableHiding: false, }, { accessorKey: "item", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.item}
), enableSorting: false, enableHiding: false, }, { accessorKey: "type", header: ({ column }) => ( ), cell: ({ row }) => { const productType = productTypes.find( (type) => type.value === row.original.type, ); if (!productType) { return null; } return (
{productType.icon && ( )} {productType.label}
); }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.some((type) => String(type) === rowValue); }, }, { accessorKey: "stock", header: ({ column }) => ( ), cell: ({ row }) => { const inStock: boolean = row.getValue("stock"); return inStock ? "بله" : "خیر"; }, enableSorting: false, }, { accessorKey: "price", header: ({ column }) => ( ), cell: ({ row }) => { const price = parseFloat(row.getValue("price")); return
{formatPrice(price)}
; }, }, { accessorKey: "availability", header: ({ column }) => ( ), cell: ({ row }) => { const availability: ("فروشگاه" | "آنلاین")[] = row.getValue("availability"); return (
{availability.map((location) => ( {location} ))}
); }, enableSorting: false, }, ]; type DragHandleProps = { attributes: DraggableAttributes; listeners?: DraggableSyntheticListeners; interactive: boolean; }; const DragHandle = React.forwardRef( ({ attributes, listeners, interactive }, ref) => ( ), ); DragHandle.displayName = "DragHandle"; function SortableProductRow({ row, interactive, }: { row: Row; interactive: boolean; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: row.original.id, }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; return ( {row.getVisibleCells().map((cell) => { const isDragCell = cell.column.id === "drag"; return ( {isDragCell ? ( ) : ( flexRender(cell.column.columnDef.cell, cell.getContext()) )} ); })} ); } export function DataTable17({ className }: { className?: string }) { const [tableData, setTableData] = React.useState(validatedData); const [isMounted, setIsMounted] = React.useState(false); React.useEffect(() => { setIsMounted(true); }, []); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 5, }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const handleDragEnd = React.useCallback( ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) { return; } setTableData((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); if (oldIndex === -1 || newIndex === -1) { return items; } return arrayMove(items, oldIndex, newIndex); }); }, [setTableData], ); const { table } = useDataTable({ data: tableData, columns, getRowId: (row) => row.id.toString(), }); const rows = table.getRowModel().rows; return (

جدول با رکورد‌های قابل جابجایی

جدول داده با قابلیت کشیدن و رها کردن رکورد‌ها با dnd-kit و TanStack Table.

{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {rows.length ? ( row.original.id)} strategy={verticalListSortingStrategy} > {rows.map((row) => ( ))} ) : ( نتیجه‌ای یافت نشد )}
); }