"use client"; import type { ColumnFiltersState, ColumnPinningState, Row, RowSelectionState, VisibilityState, } from "@tanstack/react-table"; import { type Column, type ColumnDef, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getSortedRowModel, type SortingState, useReactTable, } from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { ArrowDown, ArrowUp, CheckCheckIcon, CheckCircle, ChevronsUpDown, Clock, LoaderIcon, MoreHorizontal, Package, PieChart, RefreshCcw, } 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 { 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; }; 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, 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 ( ); }; 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 ( typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function" ) { toast.error("کلیپ‌بورد در این مرورگر در دسترس نیست"); return; } try { await navigator.clipboard.writeText(String(value)); toast.success("شناسه در کلیپ‌بورد کپی شد"); } catch (error) { console.error("Failed to copy ID", 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(), customer_segment: z.string(), fulfillment_channel: z.string(), shipping_service: z.string(), warehouse: z.string(), order_status: z.string(), payment_status: z.string(), payment_method: z.string(), tracking_number: z.string(), assigned_to: z.string(), return_status: z.string(), coupon_code: z.string().nullable(), gift: z.boolean(), notes: z.string(), }); export type Order = z.infer; export type StatusOption = { label: string; value: string; icon?: React.ComponentType<{ className?: string }>; }; export const orderStatuses: StatusOption[] = [ { label: "تحویل‌شده", value: "Fulfilled", icon: CheckCircle }, { label: "آماده تحویل", value: "Ready for pickup", icon: Package }, { label: "تحویل‌نشده", value: "Unfulfilled", icon: Clock }, ]; export const paymentStatuses: StatusOption[] = [ { label: "پرداخت‌شده", value: "Paid", icon: CheckCheckIcon }, { label: "در انتظار پیگیری", value: "Pending", icon: LoaderIcon }, { label: "مسترد شده", value: "Refunded", icon: RefreshCcw }, { label: "مسترد جزئی", value: "Partially refunded", icon: PieChart, }, ]; const formatNumber = (value: number) => value.toLocaleString("fa-IR"); const segmentLabels: Record = { Enterprise: "سازمانی", Marketplace: "بازارچه", Retail: "خرده‌فروشی", VIP: "ویژه", Wholesale: "عمده‌فروشی", }; const fulfillmentLabels: Record = { "3PL": "لجستیک شخص ثالث", Dropship: "دراپ‌شیپ", FBA: "FBA", "Merchant Fulfilled": "ارسال توسط فروشنده", }; const shippingLabels: Record = { "Expedited Air": "هوایی سریع", "Prime 1-Day": "پرایم ۱ روزه", "Prime 2-Day": "پرایم ۲ روزه", "Same-Day Local": "تحویل همان‌روز", "Standard Ground": "ارسال عادی", }; const returnStatusLabels: Record = { Authorized: "تأیید‌شده", None: "ندارد", Received: "دریافت‌شده", Requested: "درخواست‌شده", }; const translateLabel = (map: Record, value: string) => map[value] ?? value; const createStatusLookup = (options: StatusOption[]) => options.reduce>((acc, option) => { acc[option.value] = option; return acc; }, {}); const orderStatusLookup = createStatusLookup(orderStatuses); const paymentStatusLookup = createStatusLookup(paymentStatuses); export const getOrderStatusMeta = (status: string) => orderStatusLookup[status]; export const getPaymentStatusMeta = (status: string) => paymentStatusLookup[status]; export const getStatusVariant = (status: string) => { switch (status) { case "Fulfilled": return "default"; case "Ready for pickup": return "secondary"; case "Unfulfilled": return "outline"; default: return "outline"; } }; export const getPaymentVariant = (status: string) => { switch (status) { case "Paid": return "default"; case "Pending": return "secondary"; case "Refunded": return "outline"; case "Partially refunded": return "outline"; default: return "outline"; } }; 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, }, }, { accessorKey: "purchased", header: ({ column }) => ( ), cell: ({ row }) => { const items = row.original.items; return (
{row.original.purchased}
{items ? ( {formatNumber(items)} ) : null}
); }, enableSorting: false, enableHiding: false, meta: { width: 280, }, }, { accessorKey: "customer", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.customer}
), meta: { width: 200, }, }, { accessorKey: "customer_email", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.customer_email} ), meta: { width: 240, }, }, { accessorKey: "customer_segment", header: ({ column }) => ( ), cell: ({ row }) => ( {translateLabel(segmentLabels, row.original.customer_segment)} ), meta: { className: "whitespace-nowrap", width: 160 }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "fulfillment_channel", header: ({ column }) => ( ), cell: ({ row }) => ( {translateLabel(fulfillmentLabels, row.original.fulfillment_channel)} ), meta: { className: "whitespace-nowrap", width: 160 }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "shipping_service", header: ({ column }) => ( ), cell: ({ row }) => ( {translateLabel(shippingLabels, row.original.shipping_service)} ), meta: { width: 220 }, }, { accessorKey: "warehouse", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.warehouse} ), meta: { className: "whitespace-nowrap", width: 140 }, }, { accessorKey: "order_status", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.order_status; const statusMeta = getOrderStatusMeta(status); const StatusIcon = statusMeta?.icon; return (
{StatusIcon ? : null} {statusMeta?.label ?? status}
); }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.includes(rowValue); }, enableSorting: false, meta: { width: 200 }, }, { accessorKey: "payment_status", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.payment_status; const statusMeta = getPaymentStatusMeta(status); const StatusIcon = statusMeta?.icon; return (
{StatusIcon ? : null} {statusMeta?.label ?? status}
); }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.includes(rowValue); }, enableSorting: false, meta: { width: 200 }, }, { accessorKey: "payment_method", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.payment_method}
), meta: { width: 180 }, }, { accessorKey: "items", header: ({ column }) => ( ), cell: ({ row }) => ( {formatNumber(row.original.items)} ), meta: { align: "center", className: "tabular-nums", width: 100 }, }, { accessorKey: "tracking_number", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.tracking_number} ), meta: { className: "whitespace-nowrap", width: 200 }, }, { accessorKey: "assigned_to", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.assigned_to} ), meta: { className: "whitespace-nowrap", width: 180 }, }, { accessorKey: "return_status", header: ({ column }) => ( ), cell: ({ row }) => ( {translateLabel(returnStatusLabels, row.original.return_status)} ), filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, meta: { width: 180 }, }, { accessorKey: "coupon_code", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.coupon_code ?? ""} ), meta: { width: 140 }, }, { accessorKey: "gift", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.gift ? "بله" : "خیر"} ), meta: { width: 120 }, }, { accessorKey: "notes", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.notes} ), meta: { width: 260 }, }, { id: "actions", cell: ({ row }) => ( ), meta: { width: 56, minWidth: 48, headerClassName: "px-2", cellClassName: "px-2", align: "center", }, }, ]; export const DataTable27 = ({ 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; const lastLoggedRowIndexRef = React.useRef(-1); // Demo-only logging: surface which virtualized rows get rendered as you scroll. // Remove this if you don't need console noise in production. React.useEffect(() => { if (!hasRows || virtualRows.length === 0) return; const currentMaxIndex = virtualRows[virtualRows.length - 1]?.index ?? -1; if (currentMaxIndex > lastLoggedRowIndexRef.current) { const start = Math.max(lastLoggedRowIndexRef.current + 1, 0); const end = Math.min(currentMaxIndex, rowModel.length - 1); const newlyLoadedRows = rowModel .slice(start, end + 1) .map((row) => row.original); console.log( `[DataTable27] Loaded virtualized rows ${start}–${end}`, newlyLoadedRows, ); lastLoggedRowIndexRef.current = currentMaxIndex; } }, [hasRows, rowModel, virtualRows]); return (

جدول داده با مجازی‌سازی

جدول داده با رکورد‌های مجازی برای نمایش مجموعه داده بزرگ. با پیمایش، رکورد‌های بیشتری بارگذاری می‌شوند؛ کنسول مرورگر نشان می‌دهد کدام رکورد‌ها رندر شده‌اند.

{isLoading ? (
در حال بارگذاری سفارش‌ها…
) : null} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = (header.column.columnDef.meta as { align?: "left" | "center" | "right"; headerClassName?: string; className?: string; width?: number | string; minWidth?: number | string; maxWidth?: number | string; }) ?? {}; const columnWithPinning = header.column as unknown as { 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; }; 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; } } 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 { align?: "left" | "center" | "right"; cellClassName?: string; className?: string; width?: number | string; minWidth?: number | string; maxWidth?: number | string; }) ?? {}; const columnWithPinning = cell.column as unknown as { 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; }; 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; } } 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} ) : ( نتیجه‌ای یافت نشد. )}
); };