"use client"; import type { ColumnFiltersState, ColumnPinningState, Row, RowSelectionState, VisibilityState, } from "@tanstack/react-table"; import { type Column, type ColumnDef, type ColumnSort, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getSortedRowModel, type SortDirection, type SortingState, type Table, useReactTable, } from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { ArrowDown, ArrowDownUp, ArrowUp, ChevronsUpDown, LoaderIcon, MoreHorizontal, Trash2, } from "lucide-react"; import type { CSSProperties } from "react"; import * as React from "react"; import { toast } from "sonner"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { cn } from "@/lib/utils"; const DEFAULT_ROW_HEIGHT = 56; const DEFAULT_OVERSCAN = 8; const DATA_URL = "https://raw.githubusercontent.com/zerostaticthemes/ShadcnBlocks-library-data/refs/heads/main/data/orders-ecommerce-1000.json"; type UseDataTableOptions = { data: Array; columns: Array>; getRowId?: (row: TData) => string; initialSorting?: SortingState; initialGlobalFilter?: string; initialFilters?: ColumnFiltersState; initialVisibility?: VisibilityState; initialSelection?: RowSelectionState; initialColumnPinning?: ColumnPinningState; enableRowSelection?: boolean; rowHeight?: number; overscan?: number; }; type ColumnStyleMeta = { align?: "left" | "center" | "right"; headerClassName?: string; cellClassName?: string; className?: string; width?: number | string; minWidth?: number | string; maxWidth?: number | string; }; type ColumnWithPinning = { getIsPinned?: () => "left" | "right" | false; getIsLastColumn?: (position: "left" | "right") => boolean; getIsFirstColumn?: (position: "left" | "right") => boolean; getStart?: (position: "left" | "right") => number; getAfter?: (position: "left" | "right") => number; getSize?: () => number; }; function getColumnLayoutStyles( meta: ColumnStyleMeta, columnWithPinning: ColumnWithPinning, ) { const baseStyle: CSSProperties = {}; if (meta.width !== undefined) { baseStyle.width = typeof meta.width === "number" ? `${meta.width}px` : meta.width; } if (meta.minWidth !== undefined) { baseStyle.minWidth = typeof meta.minWidth === "number" ? `${meta.minWidth}px` : meta.minWidth; } if (meta.maxWidth !== undefined) { baseStyle.maxWidth = typeof meta.maxWidth === "number" ? `${meta.maxWidth}px` : meta.maxWidth; } if ( baseStyle.width === undefined && typeof columnWithPinning.getSize === "function" ) { const computedWidth = columnWithPinning.getSize(); if (typeof computedWidth === "number" && Number.isFinite(computedWidth)) { baseStyle.width = `${computedWidth}px`; } } const pinned = columnWithPinning.getIsPinned?.(); const pinnedStyle: CSSProperties = {}; let lastPinned: "left" | "right" | undefined; if (pinned) { pinnedStyle.position = "sticky"; pinnedStyle.zIndex = 1; if (pinned === "left") { const start = columnWithPinning.getStart?.("left") ?? 0; pinnedStyle.left = `${start}px`; if (columnWithPinning.getIsLastColumn?.("left")) { lastPinned = "left"; } } if (pinned === "right") { const after = columnWithPinning.getAfter?.("right") ?? 0; pinnedStyle.right = `${after}px`; if (columnWithPinning.getIsFirstColumn?.("right")) { lastPinned = "right"; } } const resolvedWidth = baseStyle.width ?? (typeof columnWithPinning.getSize === "function" ? columnWithPinning.getSize() : undefined); if (resolvedWidth !== undefined) { pinnedStyle.width = typeof resolvedWidth === "number" ? `${resolvedWidth}px` : resolvedWidth; } } return { baseStyle, pinnedStyle, pinned, lastPinned }; } export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialSelection = {}, enableRowSelection = true, initialColumnPinning = {}, rowHeight = DEFAULT_ROW_HEIGHT, overscan = DEFAULT_OVERSCAN, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [rowSelection, setRowSelection] = React.useState(initialSelection); const [columnPinning, setColumnPinning] = React.useState(initialColumnPinning); const tableContainerRef = React.useRef(null); const table = useReactTable({ data, columns, getRowId, initialState: { sorting: initialSorting, }, state: { sorting, columnPinning, rowSelection, }, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), onColumnPinningChange: setColumnPinning, enableRowSelection, }); const rowVirtualizer = useVirtualizer({ count: table.getRowModel().rows.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => rowHeight, overscan, measureElement: typeof window !== "undefined" && navigator.userAgent.indexOf("Firefox") === -1 ? (element) => element?.getBoundingClientRect().height : undefined, }); return { table, sorting, setSorting, rowSelection, setRowSelection, columnPinning, setColumnPinning, tableContainerRef, rowVirtualizer, }; } 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 ( ); }; type DataTableSortMenuProps = { table: Table; }; const SORT_DIRECTIONS: Array<{ label: string; value: SortDirection }> = [ { label: "صعودی", value: "asc" }, { label: "نزولی", value: "desc" }, ]; export function DataTableSortMenu({ table, }: DataTableSortMenuProps) { const [open, setOpen] = React.useState(false); const sorting = table.getState().sorting; const onSortingChange = table.setSorting; const sortableColumns = React.useMemo( () => table.getAllColumns().filter((column) => column.getCanSort()), [table], ); const sortedColumnIds = React.useMemo( () => new Set(sorting.map((sort) => sort.id)), [sorting], ); const availableColumns = React.useMemo( () => sortableColumns.filter((column) => !sortedColumnIds.has(column.id)), [sortableColumns, sortedColumnIds], ); const getColumnLabel = React.useCallback( (id: string) => { const column = sortableColumns.find((col) => col.id === id); if (!column) return id; const metaLabel = (column.columnDef.meta as { label?: string })?.label; if (metaLabel) return metaLabel; const header = column.columnDef.header; if (typeof header === "string") return header; return column.id; }, [sortableColumns], ); const onSortAdd = React.useCallback(() => { const firstAvailable = availableColumns[0]; if (!firstAvailable) return; onSortingChange((prev) => [ ...prev, { id: firstAvailable.id, desc: false }, ]); }, [availableColumns, onSortingChange]); const onSortUpdate = React.useCallback( (sortId: string, updates: Partial) => { onSortingChange((prevSorting) => prevSorting.map((sort) => sort.id === sortId ? { ...sort, ...updates } : sort, ), ); }, [onSortingChange], ); const onSortRemove = React.useCallback( (sortId: string) => { onSortingChange((prevSorting) => prevSorting.filter((sort) => sort.id !== sortId), ); }, [onSortingChange], ); const onSortMove = React.useCallback( (sortId: string, delta: number) => { onSortingChange((prevSorting) => { const index = prevSorting.findIndex((sort) => sort.id === sortId); const targetIndex = index + delta; if ( index === -1 || targetIndex < 0 || targetIndex >= prevSorting.length ) return prevSorting; const next = [...prevSorting]; const [item] = next.splice(index, 1); next.splice(targetIndex, 0, item); return next; }); }, [onSortingChange], ); const onSortingReset = React.useCallback(() => { onSortingChange(table.initialState?.sorting ?? []); }, [onSortingChange, table.initialState?.sorting]); return (
{sorting.length === 0 ? (

مرتب‌سازی اعمال نشده است.

) : ( sorting.map((sort, index) => { const usedByOthers = new Set( sorting.filter((s) => s.id !== sort.id).map((item) => item.id), ); const columnOptions = sortableColumns.filter( (column) => column.id === sort.id || !usedByOthers.has(column.id), ); return (
); }) )}
{sorting.length > 0 ? ( ) : null}
); } interface DataTableRowActionsProps { row: Row; schema: z.ZodSchema; itemName: string; idField?: string; } export function DataTableRowActions({ row, schema, itemName, idField = "id", }: DataTableRowActionsProps) { const item = schema.parse(row.original) as TData; const itemRecord = item as Record; const handleCopy = async () => { const value = itemRecord[idField] ?? itemRecord.id; if (!value) { toast.error("شناسه‌ای برای کپی موجود نیست"); return; } if (!navigator.clipboard?.writeText) { toast.error("کلیپ‌بورد در این مرورگر در دسترس نیست"); return; } try { await navigator.clipboard.writeText(String(value)); toast.success("شناسه در کلیپ‌بورد کپی شد"); } catch (error) { console.error("Error copying to clipboard", error); toast.error("کپی در کلیپ‌بورد انجام نشد"); } }; return ( عملیات کپی شناسه {itemName} ویرایش {itemName} مشاهده جزئیات ); } export const schema = z.object({ id: z.string(), purchased: z.string(), items: z.number(), customer: z.string(), customer_email: z.string(), fulfillment_channel: z.string(), notes: z.string(), }); export type Order = z.infer; const formatNumber = (value: number) => value.toLocaleString("fa-IR"); const fulfillmentLabels: Record = { "3PL": "لجستیک شخص ثالث", Dropship: "دراپ‌شیپ", FBA: "FBA", "Merchant Fulfilled": "ارسال توسط فروشنده", }; const translateLabel = (map: Record, value: string) => map[value] ?? value; export const columns: ColumnDef>[] = [ { 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, meta: { width: 56, minWidth: 48, headerClassName: "px-2", cellClassName: "px-2", align: "center", }, }, { accessorKey: "id", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.id}
), enableSorting: true, enableHiding: false, meta: { width: 120, label: "سفارش", }, }, { accessorKey: "purchased", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.purchased}
), enableSorting: false, enableHiding: false, meta: { width: 280, label: "محصول", }, }, { accessorKey: "customer", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.customer}
), meta: { width: 200, label: "مشتری", }, }, { accessorKey: "customer_email", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.customer_email} ), meta: { width: 240, label: "ایمیل مشتری", }, }, { accessorKey: "fulfillment_channel", header: ({ column }) => ( ), cell: ({ row }) => ( {translateLabel(fulfillmentLabels, row.original.fulfillment_channel)} ), meta: { className: "whitespace-nowrap", width: 160, label: "تحویل", }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "items", header: ({ column }) => ( ), cell: ({ row }) => ( {formatNumber(row.original.items)} ), meta: { align: "center", className: "tabular-nums", width: 100, label: "تعداد", }, }, { accessorKey: "notes", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.notes} ), meta: { width: 260, label: "یادداشت" }, }, { id: "actions", cell: ({ row }) => ( ), meta: { width: 56, minWidth: 48, headerClassName: "px-2", cellClassName: "px-2", align: "center", }, }, ]; export const DataTable32 = ({ className }: { className?: string }) => { const [data, setData] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let active = true; const fetchData = async () => { setError(null); setIsLoading(true); try { const res = await fetch(DATA_URL); if (!res.ok) { const message = `بارگذاری داده سفارش‌ها نام مخاطبوفق بود (${res.status})`; console.error(message, res.statusText); if (active) setError(message); return; } const json = await res.json(); const parsed = schema.array().parse(json); if (active) { setData(parsed); } } catch (error) { console.error("Error fetching orders data", error); if (active) setError("خطا در بارگذاری داده سفارش‌ها"); } finally { if (active) setIsLoading(false); } }; fetchData(); return () => { active = false; }; }, []); const { table, tableContainerRef, rowVirtualizer } = useDataTable({ data, columns, getRowId: (row) => row.id.toString(), initialColumnPinning: { left: ["select"], right: ["actions"] }, }); const visibleColumns = table.getVisibleLeafColumns(); const visibleColumnCount = Math.max(visibleColumns.length, 1); const visibleColumnIds = visibleColumns.map((column) => column.id); const rightPinnedColumnIdSet = new Set( table.getState().columnPinning.right ?? [], ); const firstRightPinnedIndex = visibleColumnIds.findIndex((columnId) => rightPinnedColumnIdSet.has(columnId), ); const rightStickyDividerTargetId = firstRightPinnedIndex > 0 ? visibleColumnIds[firstRightPinnedIndex - 1] : null; const rowModel = table.getRowModel().rows; const virtualRows = rowVirtualizer.getVirtualItems(); const totalSize = rowVirtualizer.getTotalSize(); const paddingTop = virtualRows.length > 0 ? (virtualRows[0]?.start ?? 0) : 0; const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0; const hasRows = rowModel.length > 0; return (

جدول مجازی با مرتب‌سازی چندگانه

از منوی مرتب‌سازی برای چیدن چند ستون، تغییر اولویت و بازنشانی استفاده کنید؛ با Shift/Cmd/Ctrl+کلیک روی سرستون‌ها مرتب‌سازی را اضافه کنید نه جایگزین پیمایش مجازی مجموعه داده بزرگ را روان نگه می‌دارد.

{isLoading ? (
در حال بارگذاری سفارش‌ها…
) : null} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = (header.column.columnDef.meta as ColumnStyleMeta) ?? {}; const columnWithPinning = header.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned } = getColumnLayoutStyles(meta, columnWithPinning); const alignClass = meta.align === "center" ? "text-center" : meta.align === "right" ? "text-end" : meta.align === "left" ? "text-start" : undefined; return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {isLoading ? (
در حال بارگذاری سفارش‌ها…
) : error ? ( {error} ) : hasRows ? ( <> {paddingTop > 0 ? ( ) : null} {virtualRows.map((virtualRow) => { const row = rowModel[virtualRow.index]; if (!row) return null; return ( {row.getVisibleCells().map((cell) => { const meta = (cell.column.columnDef .meta as ColumnStyleMeta) ?? {}; const columnWithPinning = cell.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned, } = getColumnLayoutStyles( meta, columnWithPinning, ); const alignClass = meta.align === "center" ? "text-center" : meta.align === "right" ? "text-end" : meta.align === "left" ? "text-start" : undefined; return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })} {paddingBottom > 0 ? ( ) : null} ) : ( نتیجه‌ای یافت نشد. )}
); };