454 lines
12 KiB
TypeScript
454 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
type Column,
|
||
type ColumnDef,
|
||
flexRender,
|
||
getCoreRowModel,
|
||
getPaginationRowModel,
|
||
getSortedRowModel,
|
||
type SortingState,
|
||
type Table as TableType,
|
||
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);
|
||
|
||
const formatNumber = (value: number) =>
|
||
value.toLocaleString("fa-IR");
|
||
|
||
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 },
|
||
onSortingChange: setSorting,
|
||
getCoreRowModel: getCoreRowModel(),
|
||
getSortedRowModel: getSortedRowModel(),
|
||
getPaginationRowModel: getPaginationRowModel(),
|
||
});
|
||
|
||
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 text-sm font-medium text-foreground">
|
||
{title}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="flex h-8 items-center gap-2 px-0 text-sm font-medium text-foreground"
|
||
onClick={() => column.toggleSorting(sorted === "asc")}
|
||
>
|
||
<span>{title}</span>
|
||
{sorted === "desc" ? (
|
||
<ArrowDown className="h-4 w-4" />
|
||
) : sorted === "asc" ? (
|
||
<ArrowUp className="h-4 w-4" />
|
||
) : (
|
||
<ChevronsUpDown className="h-4 w-4 opacity-50" />
|
||
)}
|
||
</Button>
|
||
);
|
||
};
|
||
|
||
type DataTablePaginationProps<TData> = {
|
||
table: TableType<TData>;
|
||
};
|
||
|
||
export const DataTablePagination = <TData,>({
|
||
table,
|
||
}: DataTablePaginationProps<TData>) => {
|
||
const currentPage = table.getState().pagination.pageIndex;
|
||
const pageSize = table.getState().pagination.pageSize;
|
||
const totalRows = table.getRowCount();
|
||
|
||
const startRow = currentPage * pageSize + 1;
|
||
const endRow = Math.min((currentPage + 1) * pageSize, totalRows);
|
||
|
||
return (
|
||
<div className="flex items-center justify-end gap-2 py-4">
|
||
<div className="flex-1 text-sm text-muted-foreground">
|
||
<span>
|
||
{formatNumber(startRow)}–{formatNumber(endRow)}
|
||
</span>{" "}
|
||
از <span>{formatNumber(totalRows)}</span>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => table.previousPage()}
|
||
disabled={!table.getCanPreviousPage()}
|
||
>
|
||
قبلی
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => table.nextPage()}
|
||
disabled={!table.getCanNextPage()}
|
||
>
|
||
بعدی
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
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: ["فروشگاه"],
|
||
},
|
||
{
|
||
id: "prod-011",
|
||
item: "بوت روزمره",
|
||
type: "کفش",
|
||
stock: true,
|
||
sku: "CB-011",
|
||
price: 276.15,
|
||
availability: ["فروشگاه", "آنلاین"],
|
||
},
|
||
{
|
||
id: "prod-012",
|
||
item: "کفش پاشنهبلند",
|
||
type: "پوشاک",
|
||
stock: true,
|
||
sku: "HH-012",
|
||
price: 85.47,
|
||
availability: ["فروشگاه"],
|
||
},
|
||
{
|
||
id: "prod-013",
|
||
item: "صندل ساحلی",
|
||
type: "کفش",
|
||
stock: true,
|
||
sku: "BS-013",
|
||
price: 41.25,
|
||
availability: ["فروشگاه", "آنلاین"],
|
||
},
|
||
{
|
||
id: "prod-014",
|
||
item: "گردنبند طلا",
|
||
type: "لوازم جانبی",
|
||
stock: true,
|
||
sku: "GN-014",
|
||
price: 201.47,
|
||
availability: ["فروشگاه", "آنلاین"],
|
||
},
|
||
{
|
||
id: "prod-015",
|
||
item: "اسپیکر بلوتوث",
|
||
type: "الکترونیک",
|
||
stock: true,
|
||
sku: "BS-015",
|
||
price: 297.37,
|
||
availability: ["فروشگاه", "آنلاین"],
|
||
},
|
||
];
|
||
|
||
export const validatedData = schema.array().parse(data);
|
||
|
||
export const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||
{
|
||
accessorKey: "sku",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="کد کالا" />
|
||
),
|
||
enableSorting: true,
|
||
enableHiding: false,
|
||
},
|
||
{
|
||
accessorKey: "item",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="محصول" />
|
||
),
|
||
cell: ({ row }) => (
|
||
<div className="font-medium">{row.getValue("item")}</div>
|
||
),
|
||
enableSorting: true,
|
||
enableHiding: false,
|
||
},
|
||
{
|
||
accessorKey: "type",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="دستهبندی" />
|
||
),
|
||
enableSorting: true,
|
||
enableHiding: false,
|
||
},
|
||
{
|
||
accessorKey: "stock",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="موجود در انبار" />
|
||
),
|
||
cell: ({ row }) => {
|
||
const inStock: boolean = row.getValue("stock");
|
||
return inStock ? "بله" : "خیر";
|
||
},
|
||
enableSorting: false,
|
||
enableHiding: false,
|
||
},
|
||
{
|
||
accessorKey: "price",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="قیمت" />
|
||
),
|
||
cell: ({ row }) => {
|
||
const price = parseFloat(row.getValue("price"));
|
||
return <div className="font-medium">{formatPrice(price)}</div>;
|
||
},
|
||
enableSorting: true,
|
||
enableHiding: false,
|
||
},
|
||
{
|
||
accessorKey: "availability",
|
||
header: ({ column }) => (
|
||
<DataTableColumnHeader column={column} title="دسترسپذیر در" />
|
||
),
|
||
cell: ({ row }) => {
|
||
const availability: ("فروشگاه" | "آنلاین")[] =
|
||
row.getValue("availability");
|
||
return (
|
||
<div className="flex gap-2">
|
||
{availability.map((location) => (
|
||
<Badge key={location} variant="secondary">
|
||
{location}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
);
|
||
},
|
||
enableSorting: false,
|
||
enableHiding: false,
|
||
},
|
||
];
|
||
|
||
export const DataTable6 = ({ 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">
|
||
<Table>
|
||
<TableHeader>
|
||
{table.getHeaderGroups().map((headerGroup) => (
|
||
<TableRow key={headerGroup.id}>
|
||
{headerGroup.headers.map((header) => {
|
||
return (
|
||
<TableHead key={header.id} className="px-0">
|
||
{header.isPlaceholder
|
||
? null
|
||
: flexRender(
|
||
header.column.columnDef.header,
|
||
header.getContext(),
|
||
)}
|
||
</TableHead>
|
||
);
|
||
})}
|
||
</TableRow>
|
||
))}
|
||
</TableHeader>
|
||
<TableBody>
|
||
{table.getRowModel().rows?.length ? (
|
||
table.getRowModel().rows.map((row) => (
|
||
<TableRow
|
||
key={row.id}
|
||
data-state={row.getIsSelected() && "selected"}
|
||
>
|
||
{row.getVisibleCells().map((cell) => (
|
||
<TableCell key={cell.id}>
|
||
{flexRender(
|
||
cell.column.columnDef.cell,
|
||
cell.getContext(),
|
||
)}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))
|
||
) : (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={columns.length}
|
||
className="h-24 text-center"
|
||
>
|
||
نتیجهای یافت نشد.
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
<DataTablePagination table={table} />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
};
|