541 lines
16 KiB
TypeScript
541 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import type {
|
|
Column,
|
|
ColumnPinningState,
|
|
RowSelectionState,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
type ColumnDef,
|
|
type ColumnFiltersState,
|
|
type FilterFn,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getSortedRowModel,
|
|
type SortingState,
|
|
useReactTable,
|
|
type VisibilityState,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
ArrowDown,
|
|
ArrowUp,
|
|
ArrowUpDown,
|
|
Mail,
|
|
Plus,
|
|
SlidersHorizontal,
|
|
X,
|
|
} from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
import { FaEyeSlash } from "react-icons/fa";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Company {
|
|
name: string;
|
|
logo: string;
|
|
email: string;
|
|
url: string;
|
|
typeOfPartner: string;
|
|
tier: string;
|
|
mainContact: string;
|
|
employees: number;
|
|
revenue: string;
|
|
industry: string;
|
|
}
|
|
|
|
const defaultCompanies: Company[] = [
|
|
{
|
|
name: "Figma",
|
|
logo: "/block-media/shadcnblocks/block/logos/figma-icon.svg",
|
|
email: "partners@figma.com",
|
|
url: "https://figma.com",
|
|
typeOfPartner: "فناوری",
|
|
tier: "پلاتین",
|
|
mainContact: "سارا محمدی",
|
|
employees: 1200,
|
|
revenue: "بیش از ۵۰۰ میلیون ریال",
|
|
industry: "ابزار طراحی",
|
|
},
|
|
{
|
|
name: "Vercel",
|
|
logo: "/block-media/shadcnblocks/block/logos/vercel-icon.svg",
|
|
email: "partners@vercel.com",
|
|
url: "https://vercel.com",
|
|
typeOfPartner: "یکپارچهسازی",
|
|
tier: "طلایی",
|
|
mainContact: "محمد رضایی",
|
|
employees: 450,
|
|
revenue: "بیش از ۱۰۰ میلیون ریال",
|
|
industry: "زیرساخت ابری",
|
|
},
|
|
{
|
|
name: "Supabase",
|
|
logo: "/block-media/shadcnblocks/block/logos/supabase.svg",
|
|
email: "partners@supabase.com",
|
|
url: "https://supabase.com",
|
|
typeOfPartner: "فناوری",
|
|
tier: "نقرهای",
|
|
mainContact: "مریم کریمی",
|
|
employees: 180,
|
|
revenue: "بیش از ۵۰ میلیون ریال",
|
|
industry: "پایگاه داده",
|
|
},
|
|
{
|
|
name: "GitHub",
|
|
logo: "/block-media/shadcnblocks/block/logos/github-icon.svg",
|
|
email: "partners@github.com",
|
|
url: "https://github.com",
|
|
typeOfPartner: "استراتژیک",
|
|
tier: "پلاتین",
|
|
mainContact: "علی احمدی",
|
|
employees: 3000,
|
|
revenue: "بیش از ۱ میلیارد ریال",
|
|
industry: "ابزار توسعه",
|
|
},
|
|
{
|
|
name: "Notion",
|
|
logo: "/block-media/shadcnblocks/block/logos/notion-icon.svg",
|
|
email: "partners@notion.so",
|
|
url: "https://notion.so",
|
|
typeOfPartner: "فروشنده",
|
|
tier: "طلایی",
|
|
mainContact: "نرگس حسینی",
|
|
employees: 500,
|
|
revenue: "بیش از ۲۵۰ میلیون ریال",
|
|
industry: "بهرهوری",
|
|
},
|
|
];
|
|
|
|
interface HeaderWithSortingProps {
|
|
column: Column<Company>;
|
|
label: string;
|
|
}
|
|
|
|
const HeaderWithSorting = ({ column, label }: HeaderWithSortingProps) => {
|
|
const sorted = column.getIsSorted();
|
|
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => column.toggleSorting(sorted === "asc")}
|
|
className="w-full justify-between !px-0"
|
|
>
|
|
{label}
|
|
{sorted === "asc" ? (
|
|
<ArrowUp />
|
|
) : sorted === "desc" ? (
|
|
<ArrowDown />
|
|
) : (
|
|
<ArrowUpDown />
|
|
)}
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
interface FilterCheckboxListProps {
|
|
options: string[];
|
|
selectedValues: string[];
|
|
header: string;
|
|
onFilterChange: (updatedValues: string[] | undefined) => void;
|
|
labels?: Record<string, string>;
|
|
}
|
|
|
|
const FilterCheckboxList = ({
|
|
options,
|
|
selectedValues,
|
|
header,
|
|
onFilterChange,
|
|
labels,
|
|
}: FilterCheckboxListProps) => {
|
|
return (
|
|
<div className="space-y-2">
|
|
{options.map((option) => (
|
|
<div
|
|
key={option}
|
|
className="flex max-w-32 items-center gap-2 sm:max-w-full"
|
|
>
|
|
<Checkbox
|
|
id={`${header}-${option}`}
|
|
checked={selectedValues.includes(option)}
|
|
onCheckedChange={(checked) => {
|
|
const updated = checked
|
|
? [...selectedValues, option]
|
|
: selectedValues.filter((v) => v !== option);
|
|
onFilterChange(updated.length ? updated : undefined);
|
|
}}
|
|
/>
|
|
<Label htmlFor={`${header}-${option}`}>
|
|
{labels?.[option] ?? option}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const stringIncludesFilter: FilterFn<Company> = (
|
|
row,
|
|
columnId,
|
|
filterValue,
|
|
) => {
|
|
const value = row.getValue<string>(columnId);
|
|
const filterArray = filterValue as string[];
|
|
if (!filterArray || filterArray.length === 0) return true;
|
|
return filterArray.includes(value);
|
|
};
|
|
|
|
const columns: ColumnDef<Company>[] = [
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => (
|
|
<Checkbox
|
|
checked={
|
|
table.getIsAllPageRowsSelected() ||
|
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
|
}
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
aria-label="انتخاب همه"
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
aria-label="انتخاب ردیف"
|
|
/>
|
|
),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "name",
|
|
header: ({ column }) => (
|
|
<HeaderWithSorting column={column} label="شرکت" />
|
|
),
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center gap-1">
|
|
<img
|
|
src={row.original.logo}
|
|
alt={row.original.name}
|
|
className="size-4 rounded-md object-contain dark:invert"
|
|
/>
|
|
<span className="font-medium">{row.original.name}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "email",
|
|
header: "ایمیل",
|
|
cell: ({ row }) => (
|
|
<a href={`mailto:${row.original.email}`} className="text-sm underline">
|
|
{row.original.email}
|
|
</a>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "url",
|
|
header: ({ column }) => <HeaderWithSorting column={column} label="URL" />,
|
|
cell: ({ row }) => (
|
|
<a
|
|
href={row.original.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm underline"
|
|
>
|
|
{row.original.url}
|
|
</a>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "typeOfPartner",
|
|
header: "نوع شریک",
|
|
cell: ({ row }) => (
|
|
<Badge variant="secondary">{row.original.typeOfPartner}</Badge>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "tier",
|
|
header: ({ column }) => <HeaderWithSorting column={column} label="سطح همکاری" />,
|
|
cell: ({ row }) => {
|
|
const tier = row.original.tier;
|
|
const variant =
|
|
tier === "پلاتین"
|
|
? "default"
|
|
: tier === "طلایی"
|
|
? "secondary"
|
|
: "outline";
|
|
return <Badge variant={variant}>{tier}</Badge>;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "mainContact",
|
|
header: "تماس اصلی",
|
|
},
|
|
{
|
|
accessorKey: "employees",
|
|
header: ({ column }) => (
|
|
<HeaderWithSorting column={column} label="کارکنان" />
|
|
),
|
|
cell: ({ row }) => (
|
|
<span>{row.original.employees.toLocaleString("fa-IR")}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "revenue",
|
|
header: "درآمد",
|
|
cell: ({ row }) => <Badge variant="outline">{row.original.revenue}</Badge>,
|
|
},
|
|
{
|
|
accessorKey: "industry",
|
|
header: "صنعت",
|
|
filterFn: stringIncludesFilter,
|
|
},
|
|
];
|
|
|
|
interface CompaniesList2Props {
|
|
className?: string;
|
|
title?: string;
|
|
data?: Company[];
|
|
}
|
|
|
|
const CompaniesList2 = ({
|
|
className,
|
|
title = "فهرست شرکتها",
|
|
data = defaultCompanies,
|
|
}: CompaniesList2Props) => {
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
const [globalFilter, setGlobalFilter] = useState<string>("");
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
|
|
left: ["select"],
|
|
});
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
|
|
const uniqueIndustries = useMemo(() => {
|
|
const industries = new Set<string>();
|
|
data.forEach((company) => {
|
|
industries.add(company.industry);
|
|
});
|
|
return Array.from(industries).sort();
|
|
}, [data]);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
onColumnFiltersChange: setColumnFilters,
|
|
onColumnPinningChange: setColumnPinning,
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
onRowSelectionChange: setRowSelection,
|
|
onSortingChange: setSorting,
|
|
state: {
|
|
columnFilters,
|
|
columnPinning,
|
|
columnVisibility,
|
|
globalFilter,
|
|
sorting,
|
|
rowSelection,
|
|
},
|
|
});
|
|
|
|
const industryFilter =
|
|
(table.getColumn("industry")?.getFilterValue() as string[]) ?? [];
|
|
|
|
const columnOptions = table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide())
|
|
.map((column) => column.id);
|
|
const selectedColumnOptions = table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide() && column.getIsVisible())
|
|
.map((column) => column.id);
|
|
|
|
return (
|
|
<section className={cn("py-32", className)}>
|
|
<div className="container space-y-8">
|
|
<h3 className="text-xl font-bold">{title}</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Button variant="outline">
|
|
<Plus /> ثبت شرکت
|
|
</Button>
|
|
<Button variant="outline">
|
|
<Mail /> ارسال ایمیل گروهی
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{sorting.length > 0 && (
|
|
<Button variant="ghost" onClick={() => setSorting([])}>
|
|
<X />
|
|
حذف مرتبسازی
|
|
</Button>
|
|
)}
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" className="gap-2" size="icon">
|
|
<FaEyeSlash />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="end" className="w-full space-y-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">
|
|
مدیریت نمایش ستونها
|
|
</Label>
|
|
<FilterCheckboxList
|
|
options={columnOptions}
|
|
selectedValues={selectedColumnOptions}
|
|
header="columns"
|
|
onFilterChange={(values) => {
|
|
const hideableColumns = table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide());
|
|
hideableColumns.forEach((column) => {
|
|
column.toggleVisibility(
|
|
values?.includes(column.id) ?? false,
|
|
);
|
|
});
|
|
}}
|
|
labels={{
|
|
name: "شرکت",
|
|
email: "ایمیل",
|
|
url: "URL",
|
|
typeOfPartner: "نوع شریک",
|
|
tier: "سطح",
|
|
mainContact: "تماس اصلی",
|
|
employees: "کارکنان",
|
|
revenue: "درآمد",
|
|
industry: "صنعت",
|
|
}}
|
|
/>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" className="gap-2" size="icon">
|
|
<SlidersHorizontal className="size-4" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="start" className="w-full space-y-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">صنعت</Label>
|
|
<FilterCheckboxList
|
|
options={uniqueIndustries}
|
|
selectedValues={industryFilter}
|
|
header="industry"
|
|
onFilterChange={(values) =>
|
|
table.getColumn("industry")?.setFilterValue(values)
|
|
}
|
|
/>
|
|
</div>
|
|
{industryFilter.length > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setColumnFilters([])}
|
|
>
|
|
پاک کردن همه فیلترها
|
|
</Button>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
<Input
|
|
type="text"
|
|
placeholder="جستجو بر اساس نام شرکت..."
|
|
value={globalFilter}
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
className="w-full md:w-56"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-hidden border-y">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<TableHead
|
|
key={header.id}
|
|
className={cn(
|
|
"border-r last:border-r-0",
|
|
header.column.getIsPinned() &&
|
|
"sticky start-0 z-10 bg-background !px-2",
|
|
)}
|
|
>
|
|
{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}
|
|
className={cn(
|
|
"border-r last:border-r-0",
|
|
cell.column.getIsPinned() &&
|
|
"sticky start-0 z-10 bg-background",
|
|
)}
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={columns.length} className="h-24 px-5">
|
|
هیچ شرکتی با این معیارها یافت نشد.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { CompaniesList2 };
|