"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 NON_NAVIGABLE_COLUMN_IDS = ["select", "actions"]; const DATA_URL = "https://raw.githubusercontent.com/zerostaticthemes/ShadcnBlocks-library-data/refs/heads/main/data/orders-ecommerce-1000.json"; type ColumnMeta = { 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; }; type ComputedColumnStyles = { baseStyle: CSSProperties; pinnedStyle: CSSProperties; pinned: "left" | "right" | false; lastPinned?: "left" | "right"; alignClass?: string; }; function computeColumnStyles( meta: ColumnMeta, columnWithPinning: ColumnWithPinning, ): ComputedColumnStyles { 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?.() ?? false; const pinnedStyle: CSSProperties = {}; let lastPinned: "left" | "right" | undefined; if (pinned) { pinnedStyle.position = "sticky"; pinnedStyle.zIndex = 20; 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 { baseStyle, pinnedStyle, pinned, lastPinned, alignClass, }; } type CellPosition = { rowIndex: number; columnId: string; }; type SelectionRange = { start: CellPosition; end: CellPosition; }; type SelectionState = { selectedCells: Set; selectionRange: SelectionRange | null; }; 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 [focusedCell, setFocusedCell] = React.useState( null, ); const [selectionState, setSelectionState] = React.useState({ selectedCells: new Set(), selectionRange: 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, }); const columnIds = React.useMemo(() => { return columns .map((column) => { if (column.id) return column.id; if ("accessorKey" in column && column.accessorKey) { return column.accessorKey.toString(); } return undefined; }) .filter((id): id is string => Boolean(id)); }, [columns]); const navigableColumnIds = React.useMemo(() => { return columnIds.filter((id) => !NON_NAVIGABLE_COLUMN_IDS.includes(id)); }, [columnIds]); const getCellKey = React.useCallback( (rowIndex: number, columnId: string) => `${rowIndex}:${columnId}`, [], ); const clearSelection = React.useCallback(() => { setSelectionState({ selectedCells: new Set(), selectionRange: null }); }, []); const selectSingleCell = React.useCallback( (position: CellPosition) => { const cellKey = getCellKey(position.rowIndex, position.columnId); setSelectionState({ selectedCells: new Set([cellKey]), selectionRange: { start: { ...position }, end: { ...position }, }, }); }, [getCellKey], ); const selectRange = React.useCallback( (start: CellPosition, end: CellPosition) => { const startIndex = columnIds.indexOf(start.columnId); const endIndex = columnIds.indexOf(end.columnId); if (startIndex === -1 || endIndex === -1) return; const minRow = Math.min(start.rowIndex, end.rowIndex); const maxRow = Math.max(start.rowIndex, end.rowIndex); const minCol = Math.min(startIndex, endIndex); const maxCol = Math.max(startIndex, endIndex); const selected = new Set(); for (let row = minRow; row <= maxRow; row++) { for (let col = minCol; col <= maxCol; col++) { const columnId = columnIds[col]; if (!columnId) continue; selected.add(getCellKey(row, columnId)); } } setSelectionState({ selectedCells: selected, selectionRange: { start: { ...start }, end: { ...end } }, }); }, [columnIds, getCellKey], ); const toggleCellSelection = React.useCallback( (position: CellPosition) => { const cellKey = getCellKey(position.rowIndex, position.columnId); setSelectionState((prev) => { const next = new Set(prev.selectedCells); if (next.has(cellKey)) { next.delete(cellKey); } else { next.add(cellKey); } return { selectedCells: next, selectionRange: { start: { ...position }, end: { ...position }, }, }; }); }, [getCellKey], ); const focusCell = React.useCallback( (rowIndex: number, columnId: string) => { setFocusedCell({ rowIndex, columnId }); rowVirtualizer.scrollToIndex(rowIndex, { align: "auto" }); }, [rowVirtualizer], ); const isCellSelected = React.useCallback( (rowIndex: number, columnId: string) => { return selectionState.selectedCells.has(getCellKey(rowIndex, columnId)); }, [selectionState.selectedCells, getCellKey], ); const ensureContainerFocus = React.useCallback(() => { const el = tableContainerRef.current; if (el && document.activeElement !== el) { el.focus(); } }, []); const escapeCsvValue = React.useCallback((value: unknown) => { if (value === null || value === undefined) return ""; const str = typeof value === "string" ? value : String(value); if (/[",\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"`; } return str; }, []); const copySelectionToClipboard = React.useCallback(async () => { const selectedKeys = Array.from(selectionState.selectedCells); if (selectedKeys.length === 0) { toast("یک یا چند سلول انتخاب کنید تا کپی شود."); return; } const parsed = selectedKeys .map((key) => { const [rowIndexStr, columnId] = key.split(":"); const rowIndex = Number.parseInt(rowIndexStr, 10); return Number.isFinite(rowIndex) && columnId ? { rowIndex, columnId } : null; }) .filter((entry): entry is { rowIndex: number; columnId: string } => Boolean(entry), ); if (parsed.length === 0) { toast("یک یا چند سلول انتخاب کنید تا کپی شود."); return; } const rowModel = table.getRowModel().rows; const selectedRowIndexes = Array.from( new Set(parsed.map((item) => item.rowIndex)), ).sort((a, b) => a - b); const selectedColumnIds = columnIds.filter((id) => parsed.some((item) => item.columnId === id), ); if (selectedRowIndexes.length === 0 || selectedColumnIds.length === 0) { toast("یک یا چند سلول انتخاب کنید تا کپی شود."); return; } const headerRow = selectedColumnIds.join(","); const dataRows = selectedRowIndexes .map((rowIndex) => { const row = rowModel[rowIndex]; if (!row) return null; const values = selectedColumnIds.map((columnId) => { const value = row.getValue(columnId); return escapeCsvValue(value); }); return values.join(","); }) .filter((row): row is string => Boolean(row)); const جدول = [headerRow, ...dataRows].join("\n"); try { if ( typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function" ) { toast.error("کلیپ‌بورد در این مرورگر در دسترس نیست"); return; } await navigator.clipboard.writeText(جدول); toast.success( `${selectedKeys.length.toLocaleString("fa-IR")} سلول انتخاب‌شده به‌صورت CSV کپی شد`, ); } catch (error) { console.error("Failed to copy selection", error); toast.error("کپی سلول‌ها در کلیپ‌بورد انجام نشد"); } }, [selectionState.selectedCells, table, columnIds, escapeCsvValue]); const handleCellClick = React.useCallback( ( rowIndex: number, columnId: string, event?: React.MouseEvent, ) => { const position = { rowIndex, columnId }; ensureContainerFocus(); focusCell(position.rowIndex, position.columnId); if (event?.shiftKey) { const start = selectionState.selectionRange?.start || focusedCell || { ...position }; selectRange(start, position); return; } if (event?.metaKey || event?.ctrlKey) { toggleCellSelection(position); return; } selectSingleCell(position); }, [ ensureContainerFocus, focusCell, selectionState.selectionRange, focusedCell, selectRange, toggleCellSelection, selectSingleCell, ], ); const navigateCell = React.useCallback( (direction: "up" | "down" | "left" | "right" | "home" | "end") => { if (!focusedCell) return null; const rowCount = table.getRowModel().rows.length; const currentColIndex = navigableColumnIds.indexOf(focusedCell.columnId); let nextRow = focusedCell.rowIndex; let nextColId = focusedCell.columnId; switch (direction) { case "up": nextRow = Math.max(0, focusedCell.rowIndex - 1); break; case "down": nextRow = Math.min(rowCount - 1, focusedCell.rowIndex + 1); break; case "left": if (currentColIndex > 0) { nextColId = navigableColumnIds[currentColIndex - 1] ?? nextColId; } break; case "right": if (currentColIndex < navigableColumnIds.length - 1) { nextColId = navigableColumnIds[currentColIndex + 1] ?? nextColId; } break; case "home": if (navigableColumnIds.length > 0) { nextColId = navigableColumnIds[0]!; } break; case "end": if (navigableColumnIds.length > 0) { nextColId = navigableColumnIds[navigableColumnIds.length - 1]!; } break; } return { rowIndex: nextRow, columnId: nextColId }; }, [focusedCell, navigableColumnIds, table], ); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (event.key.toLowerCase() === "c" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); copySelectionToClipboard(); return; } if (!focusedCell) return; const { key, shiftKey } = event; let direction: Parameters[0] | null = null; switch (key) { case "ArrowUp": direction = "up"; break; case "ArrowDown": direction = "down"; break; case "ArrowLeft": direction = "left"; break; case "ArrowRight": direction = "right"; break; case "Home": direction = "home"; break; case "End": direction = "end"; break; case "Escape": clearSelection(); return; default: return; } if (!direction) return; const next = navigateCell(direction); if (!next) return; event.preventDefault(); focusCell(next.rowIndex, next.columnId); if (shiftKey) { const start = selectionState.selectionRange?.start || focusedCell || next; selectRange(start, next); } else { selectSingleCell(next); } }, [ copySelectionToClipboard, focusedCell, navigateCell, focusCell, selectRange, selectionState.selectionRange, selectSingleCell, clearSelection, ], ); React.useEffect(() => { if ( !focusedCell && table.getRowModel().rows.length > 0 && navigableColumnIds.length > 0 ) { const firstColumnId = navigableColumnIds[0]; focusCell(0, firstColumnId); selectSingleCell({ rowIndex: 0, columnId: firstColumnId }); } }, [focusedCell, table, navigableColumnIds, focusCell, selectSingleCell]); return { table, sorting, setSorting, rowSelection, setRowSelection, columnPinning, setColumnPinning, tableContainerRef, rowVirtualizer, focusedCell, handleCellClick, handleKeyDown, isCellSelected, }; } 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("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(), 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 DataTable28 = ({ 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, focusedCell, handleCellClick, handleKeyDown, isCellSelected, } = 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( `[DataTable28] Loaded virtualized rows ${start}–${end}`, newlyLoadedRows, ); lastLoggedRowIndexRef.current = currentMaxIndex; } }, [hasRows, rowModel, virtualRows]); return (

جدول مجازی با پیمایش شبکه‌ای از طریق صفحه‌کلید

روی هر سلول کلیک کنید (یا Tab بزنید تا شبکه فوکوس بگیرد)، سپس با کلیدهای جهت‌دار، Home/End و Shift+جهت‌دار فوکوس را جابه‌جا کنید و انتخاب چندسلولی بسازید. Cmd/Ctrl+کلیک سلول‌ها را تغییر می‌دهد، Cmd/Ctrl+C انتخاب فعلی را به‌صورت جدول در کلیپ‌بورد کپی می‌کند (با اعلان)، و Escape انتخاب را پاک می‌کند.

{isLoading ? (
در حال بارگذاری سفارش‌ها…
) : null} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = (header.column.columnDef.meta as ColumnMeta) ?? {}; const columnWithPinning = header.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned, alignClass, } = computeColumnStyles(meta, columnWithPinning); 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 ColumnMeta) ?? {}; const columnWithPinning = cell.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned, alignClass, } = computeColumnStyles(meta, columnWithPinning); const isFocused = focusedCell?.rowIndex === virtualRow.index && focusedCell?.columnId === cell.column.id; const selected = isCellSelected( virtualRow.index, cell.column.id, ); const isNavigable = !NON_NAVIGABLE_COLUMN_IDS.includes( cell.column.id, ); return ( handleCellClick( virtualRow.index, cell.column.id, event, ) } > {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })} {paddingBottom > 0 ? ( ) : null} ) : ( نتیجه‌ای یافت نشد. )}
); };