330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import { Plus, X, Trash2, Ruler } from "lucide-react";
|
||
import type { SizeChart } from "../../../src/api/types";
|
||
import { cn } from "../../lib/utils";
|
||
import { FormField } from "../ui/FormField";
|
||
import {
|
||
getDefaultColumns,
|
||
getSizeSuggestions,
|
||
} from "../../lib/sizeChartSuggestions";
|
||
|
||
interface SizeChartSectionProps {
|
||
value: SizeChart | null;
|
||
onChange: (value: SizeChart | null) => void;
|
||
categoryName?: string | null;
|
||
/** Sizes the product currently offers (from its variants), used to seed rows. */
|
||
productSizes?: string[];
|
||
/** Product offers sizes → the chart is expected (visual emphasis + error). */
|
||
required?: boolean;
|
||
/** Show the "must fill" error (set when the seller tried to proceed). */
|
||
showError?: boolean;
|
||
}
|
||
|
||
export function SizeChartSection({
|
||
value,
|
||
onChange,
|
||
categoryName,
|
||
productSizes = [],
|
||
required = false,
|
||
showError = false,
|
||
}: SizeChartSectionProps) {
|
||
const [customColumn, setCustomColumn] = useState("");
|
||
|
||
const columns = value?.columns ?? [];
|
||
const rows = value?.rows ?? [];
|
||
const unit = value?.unit ?? "cm";
|
||
|
||
// Quick-add chips: category suggestions not already used as a column.
|
||
const suggestionChips = useMemo(() => {
|
||
const used = new Set(columns.map((c) => c.trim()));
|
||
return getSizeSuggestions(categoryName).filter((s) => !used.has(s));
|
||
}, [categoryName, columns]);
|
||
|
||
// Product sizes that don't yet have a row (offer to add them).
|
||
const missingSizes = useMemo(() => {
|
||
const present = new Set(rows.map((r) => r.label.trim()));
|
||
return productSizes.filter((s) => s && s.trim() && !present.has(s.trim()));
|
||
}, [productSizes, rows]);
|
||
|
||
// Offered sizes still lacking a row or any filled measurement (blocks publish).
|
||
const incompleteSizes = useMemo(() => {
|
||
return productSizes.filter((s) => {
|
||
const row = rows.find((r) => r.label.trim() === s.trim());
|
||
return !row || !row.values.some((v) => v && v.trim() !== "");
|
||
});
|
||
}, [productSizes, rows]);
|
||
|
||
const emit = (next: Partial<SizeChart>) => {
|
||
onChange({ unit, columns, rows, ...next });
|
||
};
|
||
|
||
const initialize = () => {
|
||
const seededColumns = getDefaultColumns(categoryName);
|
||
const seededRows = (productSizes.length ? productSizes : [""]).map((s) => ({
|
||
label: s,
|
||
values: seededColumns.map(() => ""),
|
||
}));
|
||
onChange({ unit: "cm", columns: seededColumns, rows: seededRows });
|
||
};
|
||
|
||
const addColumn = (name: string) => {
|
||
const trimmed = name.trim();
|
||
if (!trimmed || columns.includes(trimmed)) return;
|
||
emit({
|
||
columns: [...columns, trimmed],
|
||
rows: rows.map((r) => ({ ...r, values: [...r.values, ""] })),
|
||
});
|
||
};
|
||
|
||
const renameColumn = (index: number, name: string) => {
|
||
emit({ columns: columns.map((c, i) => (i === index ? name : c)) });
|
||
};
|
||
|
||
const removeColumn = (index: number) => {
|
||
emit({
|
||
columns: columns.filter((_, i) => i !== index),
|
||
rows: rows.map((r) => ({
|
||
...r,
|
||
values: r.values.filter((_, i) => i !== index),
|
||
})),
|
||
});
|
||
};
|
||
|
||
const addRow = (label = "") => {
|
||
emit({ rows: [...rows, { label, values: columns.map(() => "") }] });
|
||
};
|
||
|
||
const addMissingSizes = () => {
|
||
emit({
|
||
rows: [
|
||
...rows,
|
||
...missingSizes.map((label) => ({
|
||
label,
|
||
values: columns.map(() => ""),
|
||
})),
|
||
],
|
||
});
|
||
};
|
||
|
||
const setRowLabel = (index: number, label: string) => {
|
||
emit({ rows: rows.map((r, i) => (i === index ? { ...r, label } : r)) });
|
||
};
|
||
|
||
const setCell = (rowIndex: number, colIndex: number, val: string) => {
|
||
emit({
|
||
rows: rows.map((r, i) =>
|
||
i === rowIndex
|
||
? {
|
||
...r,
|
||
values: r.values.map((v, j) => (j === colIndex ? val : v)),
|
||
}
|
||
: r
|
||
),
|
||
});
|
||
};
|
||
|
||
const removeRow = (index: number) => {
|
||
emit({ rows: rows.filter((_, i) => i !== index) });
|
||
};
|
||
|
||
const cellInput =
|
||
"w-20 rounded-lg border border-GRAY3 bg-WHITE px-2 py-1.5 text-center text-R12 outline-none focus:border-VITROWN_BLUE";
|
||
|
||
return (
|
||
<FormField label="راهنمای سایز" required={required}>
|
||
<p className="text-R12 text-GRAY -mt-1 mb-2">
|
||
اندازههای هر سایز را وارد کنید تا خریدار بداند مثلاً «لارج» برای این محصول
|
||
چه اندازهای است.
|
||
</p>
|
||
|
||
{value == null ? (
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={initialize}
|
||
className={cn(
|
||
"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed py-4 text-R14 transition-colors cursor-pointer",
|
||
required && showError
|
||
? "border-RED text-RED"
|
||
: "border-GRAY3 text-BLACK2 hover:border-VITROWN_BLUE hover:text-VITROWN_BLUE"
|
||
)}
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
افزودن جدول سایز
|
||
</button>
|
||
{required && showError && (
|
||
<p className="mt-2 text-R12 text-RED">
|
||
این محصول سایز دارد؛ پر کردن راهنمای سایز الزامی است.
|
||
</p>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="flex flex-col gap-3 rounded-xl border border-GRAY3 bg-WHITE p-3">
|
||
{required && showError && incompleteSizes.length > 0 && (
|
||
<p className="rounded-lg bg-RED/10 px-3 py-2 text-R12 text-RED">
|
||
برای این سایزها هنوز اندازهای وارد نشده: {incompleteSizes.join("، ")}
|
||
</p>
|
||
)}
|
||
|
||
<div className="flex items-center justify-end">
|
||
<span className="text-R12 text-GRAY">اندازهها به سانتیمتر</span>
|
||
</div>
|
||
|
||
{/* Grid */}
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full border-separate border-spacing-1">
|
||
<thead>
|
||
<tr>
|
||
<th className="sticky right-0 z-10 bg-WHITE text-R12 text-GRAY font-normal">
|
||
سایز
|
||
</th>
|
||
{columns.map((col, ci) => (
|
||
<th key={ci} className="min-w-[6rem]">
|
||
<div className="flex items-center gap-1">
|
||
<input
|
||
value={col}
|
||
onChange={(e) => renameColumn(ci, e.target.value)}
|
||
placeholder="نام اندازه"
|
||
className="w-24 rounded-lg border border-GRAY3 bg-WHITE3 px-2 py-1.5 text-center text-R12 font-bold outline-none focus:border-VITROWN_BLUE"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeColumn(ci)}
|
||
aria-label={`حذف ستون ${col}`}
|
||
className="text-GRAY hover:text-RED transition-colors cursor-pointer"
|
||
>
|
||
<X className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((row, ri) => (
|
||
<tr key={ri}>
|
||
<td className="sticky right-0 z-10 bg-WHITE">
|
||
<div className="flex items-center gap-1">
|
||
<input
|
||
value={row.label}
|
||
onChange={(e) => setRowLabel(ri, e.target.value)}
|
||
placeholder="سایز"
|
||
className="w-16 rounded-lg border border-GRAY3 bg-WHITE3 px-2 py-1.5 text-center text-R12 font-bold outline-none focus:border-VITROWN_BLUE"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeRow(ri)}
|
||
aria-label={`حذف سایز ${row.label}`}
|
||
className="text-GRAY hover:text-RED transition-colors cursor-pointer"
|
||
>
|
||
<Trash2 className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
{columns.map((_, ci) => (
|
||
<td key={ci} className="text-center">
|
||
<input
|
||
value={row.values[ci] ?? ""}
|
||
onChange={(e) => setCell(ri, ci, e.target.value)}
|
||
inputMode="numeric"
|
||
placeholder="—"
|
||
dir="ltr"
|
||
className={cellInput}
|
||
/>
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Add row / add missing product sizes */}
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => addRow()}
|
||
className="inline-flex items-center gap-1 rounded-lg border border-GRAY3 px-3 py-1.5 text-R12 text-BLACK2 transition-colors hover:border-VITROWN_BLUE hover:text-VITROWN_BLUE cursor-pointer"
|
||
>
|
||
<Plus className="h-3.5 w-3.5" />
|
||
افزودن سایز
|
||
</button>
|
||
{missingSizes.length > 0 && (
|
||
<button
|
||
type="button"
|
||
onClick={addMissingSizes}
|
||
className="inline-flex items-center gap-1 rounded-lg bg-VITROWN_BLUE/10 px-3 py-1.5 text-R12 text-VITROWN_BLUE transition-colors hover:bg-VITROWN_BLUE/20 cursor-pointer"
|
||
>
|
||
<Plus className="h-3.5 w-3.5" />
|
||
افزودن سایزهای محصول ({missingSizes.join("، ")})
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Quick-add measurement columns */}
|
||
{suggestionChips.length > 0 && (
|
||
<div className="flex flex-col gap-1.5">
|
||
<span className="text-R12 text-GRAY">اندازههای پیشنهادی:</span>
|
||
<div className="flex flex-wrap gap-2">
|
||
{suggestionChips.map((chip) => (
|
||
<button
|
||
key={chip}
|
||
type="button"
|
||
onClick={() => addColumn(chip)}
|
||
className="inline-flex items-center gap-1 rounded-full bg-WHITE3 px-3 py-1 text-R12 text-BLACK2 transition-colors hover:bg-VITROWN_BLUE/10 hover:text-VITROWN_BLUE cursor-pointer"
|
||
>
|
||
<Plus className="h-3 w-3" />
|
||
{chip}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Custom column */}
|
||
<div className="flex items-center gap-2">
|
||
<div className="relative flex-1">
|
||
<Ruler className="absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-GRAY" />
|
||
<input
|
||
value={customColumn}
|
||
onChange={(e) => setCustomColumn(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
addColumn(customColumn);
|
||
setCustomColumn("");
|
||
}
|
||
}}
|
||
placeholder="اندازه دلخواه (مثلاً دور یقه)"
|
||
className="w-full rounded-lg border border-GRAY3 bg-WHITE pr-7 pl-2 py-1.5 text-R12 outline-none focus:border-VITROWN_BLUE"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
addColumn(customColumn);
|
||
setCustomColumn("");
|
||
}}
|
||
disabled={!customColumn.trim()}
|
||
className="rounded-lg bg-BLACK px-3 py-1.5 text-R12 text-WHITE transition-colors hover:bg-BLACK2 disabled:opacity-40 cursor-pointer disabled:cursor-default"
|
||
>
|
||
افزودن ستون
|
||
</button>
|
||
</div>
|
||
|
||
{/* Remove whole chart */}
|
||
<button
|
||
type="button"
|
||
onClick={() => onChange(null)}
|
||
className="self-start text-R12 text-RED hover:underline cursor-pointer"
|
||
>
|
||
حذف جدول سایز
|
||
</button>
|
||
</div>
|
||
)}
|
||
</FormField>
|
||
);
|
||
}
|
||
|
||
export default SizeChartSection;
|