Files
reui-blocks/blocks/field-mapping1.tsx
T

467 lines
14 KiB
TypeScript

"use client";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Check, ChevronRight, ChevronsUpDown, Plus } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
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 InputData {
firstName: string;
lastName: string;
email: string;
mobile: string;
country: string;
city: string;
}
const SOURCE_FIELD_LABELS: Record<keyof InputData, string> = {
firstName: "نام",
lastName: "نام خانوادگی",
email: "ایمیل",
mobile: "موبایل",
country: "کشور",
city: "شهر",
};
const DUMMY_INPUT_DATA: InputData[] = [
{
firstName: "علی",
lastName: "محمدی",
email: "ali.mohammadi@acme.co",
mobile: "5551234567",
country: "آلمان",
city: "برلین",
},
{
firstName: "سارا",
lastName: "احمدی",
email: "sara.ahmadi@acme.co",
mobile: "5559876543",
country: "ژاپن",
city: "توکیو",
},
{
firstName: "رضا",
lastName: "کریمی",
email: "reza.karimi@acme.co",
mobile: "5552468135",
country: "برزیل",
city: "سائوپائولو",
},
{
firstName: "مینا",
lastName: "حسینی",
email: "mina.hosseini@acme.co",
mobile: "5551357924",
country: "فرانسه",
city: "پاریس",
},
{
firstName: "امیر",
lastName: "رضایی",
email: "amir.rezaei@acme.co",
mobile: "5558642097",
country: "اسپانیا",
city: "بارسلونا",
},
];
interface Column {
header: string;
accessorKey: string;
}
const DEFAULT_COLUMNS: Column[] = [
{
header: "نام",
accessorKey: "firstName",
},
{
header: "نام خانوادگی",
accessorKey: "lastName",
},
{
header: "ایمیل",
accessorKey: "email",
},
{
header: "تلفن",
accessorKey: "phone",
},
{
header: "کشور",
accessorKey: "country",
},
];
interface FieldComboboxProps {
columns: Column[];
value: string;
onValueChange: (value: string) => void;
onCreateField: (fieldName: string) => void;
onFocus: () => void;
}
const FieldCombobox = ({
columns,
value,
onValueChange,
onCreateField,
onFocus,
}: FieldComboboxProps) => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const handleCreate = () => {
if (
search &&
!columns.find((col) => col.header.toLowerCase() === search.toLowerCase())
) {
onCreateField(search);
onValueChange(search.toLowerCase().replace(/\s+/g, ""));
setOpen(false);
setSearch("");
}
};
const selectedColumn = columns.find((col) => col.accessorKey === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild onClick={onFocus}>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{selectedColumn ? selectedColumn.header : "انتخاب فیلد"}
<ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput
placeholder="جستجو یا افزودن فیلد..."
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>
<Button
variant="ghost"
className="w-full justify-start"
onClick={handleCreate}
>
<Plus className="size-4" />
افزودن «{search}»
</Button>
</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.accessorKey}
value={column.accessorKey}
onSelect={(currentValue) => {
onValueChange(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
<Check
className={cn(
"size-4",
value !== column.accessorKey && "opacity-0",
)}
/>
{column.header}
</CommandItem>
))}
</CommandGroup>
{search &&
!columns.find(
(col) => col.header.toLowerCase() === search.toLowerCase(),
) && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem onSelect={handleCreate}>
<Plus className="size-4" />
افزودن «{search}»
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
interface DataPreviewListProps {
currentFocusedField: string | null;
}
const DataPreviewList = ({ currentFocusedField }: DataPreviewListProps) => {
if (!currentFocusedField) return null;
return (
<div className="space-y-2 border bg-background p-4 shadow-xs">
<p className="text-sm font-semibold">
نمونه مقادیر برای{" "}
{SOURCE_FIELD_LABELS[currentFocusedField as keyof InputData] ??
currentFocusedField}
</p>
<ul>
{DUMMY_INPUT_DATA.map((item) => {
return (
<li
key={item.email}
className="border-b py-2 text-sm first:border-t last:border-b-0"
>
{item[currentFocusedField as keyof typeof item]}
</li>
);
})}
</ul>
</div>
);
};
interface FieldMapping1Props {
className?: string;
}
const FieldMapping1 = ({ className }: FieldMapping1Props) => {
const [data, setData] = useState<Record<string, string>[]>([]);
const [columns, setColumns] = useState<Column[]>(DEFAULT_COLUMNS);
const [open, setOpen] = useState(false);
const [currentFocusedField, setCurrentFocusedField] = useState<string | null>(
null,
);
const [mappings, setMappings] = useState<Record<string, string>>({});
const handleCreateField = (fieldName: string) => {
const accessorKey = fieldName.toLowerCase().replace(/\s+/g, "");
if (!columns.find((col) => col.accessorKey === accessorKey)) {
setColumns([...columns, { header: fieldName, accessorKey }]);
}
};
const handleMappingChange = (incomingField: string, destField: string) => {
setMappings((prev) => ({
...prev,
[incomingField]: destField,
}));
};
const handleSave = () => {
const transformedData = DUMMY_INPUT_DATA.map((item) => {
const newRow: Record<string, string> = {};
Object.entries(mappings).forEach(([incomingField, destField]) => {
if (destField) {
newRow[destField] = item[incomingField as keyof InputData];
}
});
return newRow;
});
setData(transformedData);
setOpen(false);
};
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<section className="py-32">
<div className="relative container">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length === 0
? Array.from({ length: 10 }).map((_, i) => (
<TableRow key={`empty-${i}`}>
{columns.map((column) => (
<TableCell
key={`empty-${i}-${column.accessorKey}`}
className="h-9"
/>
))}
</TableRow>
))
: table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{data.length === 0 && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-background/50 backdrop-blur-[1px]">
<h3 className="text-center text-lg font-semibold md:text-xl">
فایل را اینجا رها کنید یا برای شروع مرور کنید
</h3>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>مرور فایل‌ها</Button>
</DialogTrigger>
<DialogContent
className={cn(
"overflow-hidden p-0 md:!max-w-3xl lg:!max-w-6xl",
className,
)}
showCloseButton={false}
>
<DialogHeader className="sr-only">
<DialogTitle>پیکربندی نگاشت فیلد</DialogTitle>
<DialogDescription>
ستون‌های داده را با فیلدهای مقصد تطبیق دهید
</DialogDescription>
</DialogHeader>
<div>
<div className="flex items-center justify-between gap-4 border-b px-6 py-4">
<h3 className="text-lg font-semibold md:text-xl">
پیکربندی نگاشت
</h3>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
خروج
</Button>
<Button
disabled={Object.keys(mappings).length === 0}
onClick={handleSave}
>
ذخیره
</Button>
</div>
</div>
<div className="grid gap-4 md:grid-cols-9">
<div className="col-span-5 space-y-4 p-6">
<p className="text-sm font-semibold md:text-base">
ستون‌های مبدأ را با فیلدهای مقصد تطبیق دهید
</p>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-xs font-semibold text-muted-foreground">
<p>ستون‌های مبدأ</p>
<p>فیلدهای مقصد</p>
</div>
<ul className="space-y-6">
{Object.keys(DUMMY_INPUT_DATA[0]).map((key) => {
return (
<li
key={`incoming-field-${key}`}
className="grid grid-cols-2 gap-4"
>
<div className="flex items-center justify-between">
<Badge variant="secondary">
{
SOURCE_FIELD_LABELS[
key as keyof InputData
]
}
</Badge>
<ChevronRight className="size-4 text-muted-foreground" />
</div>
<FieldCombobox
columns={columns}
value={mappings[key] || ""}
onValueChange={(value) =>
handleMappingChange(key, value)
}
onCreateField={handleCreateField}
onFocus={() => setCurrentFocusedField(key)}
/>
</li>
);
})}
</ul>
</div>
</div>
<div className="col-span-4 hidden bg-muted p-8 md:block">
<DataPreviewList
currentFocusedField={currentFocusedField}
/>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)}
</div>
</section>
);
};
export { FieldMapping1 };