Compare commits
No commits in common. "d694040bf782e0df9c708496d377af0d3fc0e481" and "05cbbd8d5efdaf5b7e4d902367adad61d0309776" have entirely different histories.
d694040bf7
...
05cbbd8d5e
@ -43,7 +43,6 @@ import { useToast } from "~/hooks/use-toast";
|
|||||||
import { useRootData } from "~/hooks/use-root-data";
|
import { useRootData } from "~/hooks/use-root-data";
|
||||||
import { LoginRequiredDialog } from "../LoginRequiredDialog";
|
import { LoginRequiredDialog } from "../LoginRequiredDialog";
|
||||||
import AIPromoteModal from "../AIPromoteModal";
|
import AIPromoteModal from "../AIPromoteModal";
|
||||||
import { SizeGuideDialog } from "./SizeGuideDialog";
|
|
||||||
|
|
||||||
interface ProductDetailViewProps {
|
interface ProductDetailViewProps {
|
||||||
product: ProductDetailType;
|
product: ProductDetailType;
|
||||||
@ -259,12 +258,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
const [qty, setQty] = useState(1);
|
const [qty, setQty] = useState(1);
|
||||||
const [isAIOpen, setIsAIOpen] = useState(false);
|
const [isAIOpen, setIsAIOpen] = useState(false);
|
||||||
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
||||||
const [isSizeGuideOpen, setIsSizeGuideOpen] = useState(false);
|
|
||||||
const hasSizeChart = !!(
|
|
||||||
product.sizeChart &&
|
|
||||||
product.sizeChart.columns?.length &&
|
|
||||||
product.sizeChart.rows?.length
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: ratingData } = useServiceGetProductRating(product.id || "");
|
const { data: ratingData } = useServiceGetProductRating(product.id || "");
|
||||||
const { data: commentsData } = useServiceGetComments(1, product.id || "");
|
const { data: commentsData } = useServiceGetComments(1, product.id || "");
|
||||||
@ -579,11 +572,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
sizes={inStockSizes.map((size) => size.value || "")}
|
sizes={inStockSizes.map((size) => size.value || "")}
|
||||||
selectedSize={selectedSize}
|
selectedSize={selectedSize}
|
||||||
setSelectedSize={setSelectedSize}
|
setSelectedSize={setSelectedSize}
|
||||||
onSizeGuide={
|
|
||||||
hasSizeChart
|
|
||||||
? () => setIsSizeGuideOpen(true)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -770,13 +758,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
onClose={() => setIsAIOpen(false)}
|
onClose={() => setIsAIOpen(false)}
|
||||||
productId={product.id || ""}
|
productId={product.id || ""}
|
||||||
/>
|
/>
|
||||||
{hasSizeChart && product.sizeChart && (
|
|
||||||
<SizeGuideDialog
|
|
||||||
isOpen={isSizeGuideOpen}
|
|
||||||
onOpenChange={setIsSizeGuideOpen}
|
|
||||||
sizeChart={product.sizeChart}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
import { Dialog, DialogContent, DialogOverlay } from "../ui/dialog";
|
|
||||||
import type { SizeChart } from "../../../src/api/types";
|
|
||||||
|
|
||||||
interface SizeGuideDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
sizeChart: SizeChart;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNIT_LABELS: Record<string, string> = {
|
|
||||||
cm: "سانتیمتر",
|
|
||||||
inch: "اینچ",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SizeGuideDialog({
|
|
||||||
isOpen,
|
|
||||||
onOpenChange,
|
|
||||||
sizeChart,
|
|
||||||
}: SizeGuideDialogProps) {
|
|
||||||
const { columns = [], rows = [], unit = "cm" } = sizeChart;
|
|
||||||
const unitLabel = UNIT_LABELS[unit] || unit;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogContent className="p-4 bg-WHITE rounded-[15px] w-[calc(100%-40px)] max-w-lg max-h-[85vh] overflow-y-auto">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-B18 font-bold">راهنمای سایز</h2>
|
|
||||||
<span className="text-R12 text-GRAY">اندازهها به {unitLabel}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full border-collapse text-center">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="sticky right-0 z-10 bg-WHITE2 border border-GRAY3 px-3 py-2 text-R12 font-bold text-BLACK2">
|
|
||||||
سایز
|
|
||||||
</th>
|
|
||||||
{columns.map((col, ci) => (
|
|
||||||
<th
|
|
||||||
key={ci}
|
|
||||||
className="border border-GRAY3 bg-WHITE2 px-3 py-2 text-R12 font-bold text-BLACK2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{col}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((row, ri) => (
|
|
||||||
<tr key={ri}>
|
|
||||||
<td className="sticky right-0 z-10 bg-WHITE border border-GRAY3 px-3 py-2 text-R12 font-bold">
|
|
||||||
{row.label}
|
|
||||||
</td>
|
|
||||||
{columns.map((_, ci) => (
|
|
||||||
<td
|
|
||||||
key={ci}
|
|
||||||
className="border border-GRAY3 px-3 py-2 text-R12 text-BLACK2"
|
|
||||||
dir="ltr"
|
|
||||||
>
|
|
||||||
{row.values[ci]?.trim() ? row.values[ci] : "—"}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-R12 text-GRAY leading-6">
|
|
||||||
اندازهها ممکن است بسته به روش اندازهگیری کمی متفاوت باشند.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SizeGuideDialog;
|
|
||||||
@ -1,33 +1,16 @@
|
|||||||
import { Ruler } from "lucide-react";
|
|
||||||
|
|
||||||
interface SizeSelectorProps {
|
interface SizeSelectorProps {
|
||||||
sizes: string[] | undefined;
|
sizes: string[] | undefined;
|
||||||
selectedSize: string | null;
|
selectedSize: string | null;
|
||||||
setSelectedSize: (size: string) => void;
|
setSelectedSize: (size: string) => void;
|
||||||
/** When provided, shows a "راهنمای سایز" link that opens the size guide. */
|
|
||||||
onSizeGuide?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SizeSelector = ({
|
export const SizeSelector = ({
|
||||||
sizes,
|
sizes,
|
||||||
selectedSize,
|
selectedSize,
|
||||||
setSelectedSize,
|
setSelectedSize,
|
||||||
onSizeGuide,
|
|
||||||
}: SizeSelectorProps) => (
|
}: SizeSelectorProps) => (
|
||||||
<div className="flex flex-col gap-4 mt-10 px-4">
|
<div className="flex flex-col gap-4 mt-10 px-4">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-B18 font-bold">انتخاب سایز</p>
|
<p className="text-B18 font-bold">انتخاب سایز</p>
|
||||||
{onSizeGuide && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onSizeGuide}
|
|
||||||
className="flex items-center gap-1 text-R12 text-VITROWN_BLUE hover:underline cursor-pointer"
|
|
||||||
>
|
|
||||||
<Ruler className="h-3.5 w-3.5" />
|
|
||||||
راهنمای سایز
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="gap-4 flex">
|
<div className="gap-4 flex">
|
||||||
{sizes?.map((size: string, index: number) => (
|
{sizes?.map((size: string, index: number) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,352 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNITS: { value: string; label: string }[] = [
|
|
||||||
{ value: "cm", label: "سانتیمتر" },
|
|
||||||
{ value: "inch", label: "اینچ" },
|
|
||||||
];
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Unit toggle */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-R12 text-GRAY">واحد اندازهگیری</span>
|
|
||||||
<div className="flex overflow-hidden rounded-lg border border-GRAY3">
|
|
||||||
{UNITS.map((u) => (
|
|
||||||
<button
|
|
||||||
key={u.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => emit({ unit: u.value })}
|
|
||||||
className={cn(
|
|
||||||
"px-3 py-1 text-R12 transition-colors cursor-pointer",
|
|
||||||
unit === u.value
|
|
||||||
? "bg-BLACK text-WHITE"
|
|
||||||
: "bg-WHITE text-BLACK2 hover:bg-WHITE3"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{u.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</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;
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Category-aware measurement suggestions for the product size chart (راهنمای سایز).
|
|
||||||
*
|
|
||||||
* Sellers can always type their own columns; these just power the quick-add
|
|
||||||
* chips and seed a sensible default column set per garment type. Matching is by
|
|
||||||
* Persian keyword substring against the product's category name, most-specific
|
|
||||||
* groups first, falling back to a general garment set.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface SuggestionGroup {
|
|
||||||
keys: string[];
|
|
||||||
suggestions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order matters: the first group whose keyword appears in the category name wins.
|
|
||||||
const GROUPS: SuggestionGroup[] = [
|
|
||||||
// ---- bottoms ----
|
|
||||||
{ keys: ["شلوار", "شلوارک"], suggestions: ["دور کمر", "دور باسن", "فاق", "دور ران", "قد", "دمپا"] },
|
|
||||||
{ keys: ["دامن"], suggestions: ["دور کمر", "دور باسن", "قد", "دمپا"] },
|
|
||||||
{ keys: ["مایو"], suggestions: ["دور سینه", "دور کمر", "دور باسن", "قد"] },
|
|
||||||
|
|
||||||
// ---- footwear ----
|
|
||||||
{ keys: ["کفش", "صندل", "دمپایی", "پاشنه"], suggestions: ["سایز اروپایی", "طول کف پا"] },
|
|
||||||
|
|
||||||
// ---- small accessories with their own sizing ----
|
|
||||||
{ keys: ["جوراب"], suggestions: ["سایز", "طول ساق"] },
|
|
||||||
{ keys: ["کلاه"], suggestions: ["دور سر"] },
|
|
||||||
{ keys: ["کمربند"], suggestions: ["طول", "عرض"] },
|
|
||||||
{ keys: ["دستکش"], suggestions: ["دور مچ", "طول دست"] },
|
|
||||||
{ keys: ["انگشتر"], suggestions: ["سایز انگشتر", "دور انگشت"] },
|
|
||||||
{ keys: ["دستبند", "پابند"], suggestions: ["دور مچ", "طول"] },
|
|
||||||
{ keys: ["گردنبند", "آویز"], suggestions: ["طول زنجیر"] },
|
|
||||||
{ keys: ["گوشواره", "گل سینه", "زیورآلات", "سردست"], suggestions: ["طول", "عرض"] },
|
|
||||||
{ keys: ["شال", "روسری", "دستمال"], suggestions: ["طول", "عرض"] },
|
|
||||||
{ keys: ["عینک"], suggestions: ["عرض فریم", "طول دسته"] },
|
|
||||||
|
|
||||||
// ---- bags ----
|
|
||||||
{ keys: ["کیف", "کوله"], suggestions: ["طول", "عرض", "ارتفاع", "طول بند"] },
|
|
||||||
|
|
||||||
// ---- one-piece / manteau ----
|
|
||||||
{ keys: ["مانتو", "کیمونو"], suggestions: ["دور سینه", "قد", "سرشانه", "آستین", "دور کمر"] },
|
|
||||||
{ keys: ["اورال", "پیراهن و اورال"], suggestions: ["دور سینه", "دور کمر", "دور باسن", "قد", "آستین"] },
|
|
||||||
|
|
||||||
// ---- tops & outerwear (broad garment fallback) ----
|
|
||||||
{
|
|
||||||
keys: [
|
|
||||||
"کاپشن", "بارانی", "پالتو", "کت", "ژاکت", "بامبر", "جلیقه", "دورس",
|
|
||||||
"سویشرت", "هودی", "پلیور", "بلوز", "شومیز", "تیشرت", "تاپ", "پلوشرت", "پیراهن",
|
|
||||||
],
|
|
||||||
suggestions: ["دور سینه", "قد", "سرشانه", "آستین", "عرض سینه", "دور کمر"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generic fallback for anything unmatched (also the base defaults the seller named).
|
|
||||||
const DEFAULT_SUGGESTIONS = ["عرض سینه", "قد", "دور کمر", "دور باسن", "سرشانه", "آستین"];
|
|
||||||
|
|
||||||
/** All measurement suggestions for a category (used for the quick-add chips). */
|
|
||||||
export function getSizeSuggestions(categoryName?: string | null): string[] {
|
|
||||||
const name = (categoryName || "").trim();
|
|
||||||
if (name) {
|
|
||||||
for (const group of GROUPS) {
|
|
||||||
if (group.keys.some((k) => name.includes(k))) return group.suggestions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DEFAULT_SUGGESTIONS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The columns to seed a brand-new (empty) chart with for this category. */
|
|
||||||
export function getDefaultColumns(categoryName?: string | null): string[] {
|
|
||||||
return getSizeSuggestions(categoryName).slice(0, 2);
|
|
||||||
}
|
|
||||||
@ -61,15 +61,13 @@ export default function CollectionDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Loading Skeleton */}
|
{/* Loading Skeleton */}
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
@ -96,15 +94,13 @@ export default function CollectionDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error State */}
|
{/* Error State */}
|
||||||
<div className="flex flex-col items-center justify-center py-20 px-4">
|
<div className="flex flex-col items-center justify-center py-20 px-4">
|
||||||
@ -134,15 +130,13 @@ export default function CollectionDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||||
{/* Header (mobile) */}
|
{/* Header (mobile) */}
|
||||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:pt-6">
|
<div className="flex flex-col lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:pt-6">
|
||||||
|
|||||||
@ -118,15 +118,13 @@ export default function Collections() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-WHITE pb-20">
|
<div className="flex flex-col bg-WHITE pb-20">
|
||||||
{/* Header (mobile) */}
|
{/* Header (mobile) */}
|
||||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">کالکشنها</p>
|
<p className="text-B16 font-bold">کالکشنها</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
||||||
|
|||||||
@ -25,15 +25,13 @@ export default function Discounts() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-WHITE pb-20">
|
<div className="flex flex-col bg-WHITE pb-20">
|
||||||
{/* Header (mobile) */}
|
{/* Header (mobile) */}
|
||||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">تخفیفها</p>
|
<p className="text-B16 font-bold">تخفیفها</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
||||||
|
|||||||
@ -199,7 +199,7 @@ export default function Profile() {
|
|||||||
{/* Mobile: account hub */}
|
{/* Mobile: account hub */}
|
||||||
<div className="flex flex-col gap-0 lg:hidden">
|
<div className="flex flex-col gap-0 lg:hidden">
|
||||||
{/* Header: avatar + name + phone + edit */}
|
{/* Header: avatar + name + phone + edit */}
|
||||||
<div className="flex items-center gap-3.5 px-4 pt-[calc(1rem_+_env(safe-area-inset-top))] pb-3">
|
<div className="flex items-center gap-3.5 px-4 pt-4 pb-3">
|
||||||
<CircleUserRound size={56} className="text-GRAY shrink-0" />
|
<CircleUserRound size={56} className="text-GRAY shrink-0" />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h2 className="text-B16 font-bold truncate">
|
<h2 className="text-B16 font-bold truncate">
|
||||||
|
|||||||
@ -62,15 +62,13 @@ export default function Sellers() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-WHITE pb-20">
|
<div className="flex flex-col bg-WHITE pb-20">
|
||||||
{/* Header (mobile) */}
|
{/* Header (mobile) */}
|
||||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="absolute top-5 right-5 cursor-pointer"
|
className="absolute top-5 right-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="text-B16 font-bold">فروشگاهها</p>
|
<p className="text-B16 font-bold">فروشگاهها</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
||||||
|
|||||||
@ -558,7 +558,7 @@ function SellerDetailHeader({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex px-4 w-full justify-between items-center sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
<div className="flex px-4 w-full justify-between items-center sticky top-0 z-30 bg-WHITE">
|
||||||
<ArrowRight size={24} onClick={() => safeGoBack(navigate)} />
|
<ArrowRight size={24} onClick={() => safeGoBack(navigate)} />
|
||||||
<div className="flex flex-col gap-2 py-4 items-center">
|
<div className="flex flex-col gap-2 py-4 items-center">
|
||||||
<SellerLogo src={sellerData?.storeLogo} alt="Seller Logo" />
|
<SellerLogo src={sellerData?.storeLogo} alt="Seller Logo" />
|
||||||
|
|||||||
@ -136,8 +136,7 @@ export default function Transactions() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-WHITE lg:max-w-[900px] lg:mx-auto lg:w-full lg:pt-6">
|
<div className="bg-WHITE lg:max-w-[900px] lg:mx-auto lg:w-full lg:pt-6">
|
||||||
{/* Header (keeps the filter button; just constrained on desktop) */}
|
{/* Header (keeps the filter button; just constrained on desktop) */}
|
||||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)] lg:static lg:pt-0">
|
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border lg:static lg:justify-start lg:gap-3">
|
||||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border lg:justify-start lg:gap-3">
|
|
||||||
<ArrowRight onClick={handleBack} className="absolute top-5 right-5 lg:hidden" />
|
<ArrowRight onClick={handleBack} className="absolute top-5 right-5 lg:hidden" />
|
||||||
<p className={"text-B16 lg:text-[28px] font-bold lg:font-extrabold"}>لیست تراکنشها</p>
|
<p className={"text-B16 lg:text-[28px] font-bold lg:font-extrabold"}>لیست تراکنشها</p>
|
||||||
<div
|
<div
|
||||||
@ -160,7 +159,6 @@ export default function Transactions() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { ImageEditor } from "~/components/store/ImageEditor";
|
|||||||
import { ImageUploadSection } from "~/components/store/ImageUploadSection";
|
import { ImageUploadSection } from "~/components/store/ImageUploadSection";
|
||||||
import { ProductDetailsForm } from "~/components/store/ProductDetailsForm";
|
import { ProductDetailsForm } from "~/components/store/ProductDetailsForm";
|
||||||
import { ProductVariantsSection } from "~/components/store/ProductVariantsSection";
|
import { ProductVariantsSection } from "~/components/store/ProductVariantsSection";
|
||||||
import { SizeChartSection } from "~/components/store/SizeChartSection";
|
|
||||||
import { PricingForm } from "~/components/store/PricingForm";
|
import { PricingForm } from "~/components/store/PricingForm";
|
||||||
import {
|
import {
|
||||||
useCreateFullProduct,
|
useCreateFullProduct,
|
||||||
@ -31,7 +30,6 @@ import {
|
|||||||
ProductVariant as ApiProductVariant,
|
ProductVariant as ApiProductVariant,
|
||||||
ProductAttributeItem,
|
ProductAttributeItem,
|
||||||
MediaUploadImageUsageEnum,
|
MediaUploadImageUsageEnum,
|
||||||
SizeChart,
|
|
||||||
} from "../../src/api/types";
|
} from "../../src/api/types";
|
||||||
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
|
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
|
||||||
import { useNavigate, useParams } from "@remix-run/react";
|
import { useNavigate, useParams } from "@remix-run/react";
|
||||||
@ -152,7 +150,6 @@ export default function AddProduct() {
|
|||||||
setProductTitle(existingProduct.title || "");
|
setProductTitle(existingProduct.title || "");
|
||||||
setProductDescription(existingProduct.description || "");
|
setProductDescription(existingProduct.description || "");
|
||||||
setSelectedCategory(existingProduct.category || "");
|
setSelectedCategory(existingProduct.category || "");
|
||||||
setSizeChart(existingProduct.sizeChart || null);
|
|
||||||
setProductPrice(
|
setProductPrice(
|
||||||
existingProduct.basePrice
|
existingProduct.basePrice
|
||||||
? parseInt(existingProduct.basePrice).toString()
|
? parseInt(existingProduct.basePrice).toString()
|
||||||
@ -415,29 +412,6 @@ export default function AddProduct() {
|
|||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
const [productVariants, setProductVariants] = useState<ProductVariant[]>([]);
|
const [productVariants, setProductVariants] = useState<ProductVariant[]>([]);
|
||||||
const [sizeChart, setSizeChart] = useState<SizeChart | null>(null);
|
|
||||||
|
|
||||||
// Distinct sizes this product offers (drives the size-chart rows & requirement).
|
|
||||||
const productSizes = [
|
|
||||||
...new Set(
|
|
||||||
productVariants
|
|
||||||
.map((v) => v.size)
|
|
||||||
.filter((s) => s && s !== "unselected" && s.trim() !== "")
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const hasSizes = productSizes.length > 0;
|
|
||||||
const selectedCategoryName =
|
|
||||||
categoryOptions.find((o) => o.value === selectedCategory)?.label || "";
|
|
||||||
|
|
||||||
// Complete = has columns and every offered size has a row with ≥1 filled value.
|
|
||||||
const isSizeChartComplete = () => {
|
|
||||||
if (!hasSizes) return true; // only required when the product has sizes
|
|
||||||
if (!sizeChart || sizeChart.columns.length === 0) return false;
|
|
||||||
return productSizes.every((s) => {
|
|
||||||
const row = sizeChart.rows.find((r) => r.label.trim() === s.trim());
|
|
||||||
return !!row && row.values.some((v) => v && v.trim() !== "");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to update variant quantity
|
// Function to update variant quantity
|
||||||
const updateVariantQuantity = (index: number, newQuantity: number) => {
|
const updateVariantQuantity = (index: number, newQuantity: number) => {
|
||||||
@ -455,8 +429,7 @@ export default function AddProduct() {
|
|||||||
productTitle.trim() !== "" &&
|
productTitle.trim() !== "" &&
|
||||||
productDescription.trim() !== "" &&
|
productDescription.trim() !== "" &&
|
||||||
selectedCategory !== "" &&
|
selectedCategory !== "" &&
|
||||||
productVariants.length > 0 &&
|
productVariants.length > 0
|
||||||
isSizeChartComplete()
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -602,11 +575,6 @@ export default function AddProduct() {
|
|||||||
// Get final image URLs (only upload edited/new images)
|
// Get final image URLs (only upload edited/new images)
|
||||||
const imageUrls = await getFinalImageUrls();
|
const imageUrls = await getFinalImageUrls();
|
||||||
|
|
||||||
const sizeChartPayload =
|
|
||||||
sizeChart && sizeChart.columns.length > 0 && sizeChart.rows.length > 0
|
|
||||||
? sizeChart
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (isEditMode && editProductId) {
|
if (isEditMode && editProductId) {
|
||||||
// Update existing product
|
// Update existing product
|
||||||
const updateData = {
|
const updateData = {
|
||||||
@ -617,7 +585,6 @@ export default function AddProduct() {
|
|||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
attributes: prepareProductAttributes(productVariants),
|
attributes: prepareProductAttributes(productVariants),
|
||||||
variants: prepareProductVariants(productVariants),
|
variants: prepareProductVariants(productVariants),
|
||||||
sizeChart: sizeChartPayload,
|
|
||||||
};
|
};
|
||||||
await updateProductMutation.mutateAsync({
|
await updateProductMutation.mutateAsync({
|
||||||
productId: editProductId,
|
productId: editProductId,
|
||||||
@ -635,7 +602,6 @@ export default function AddProduct() {
|
|||||||
attributes: prepareProductAttributes(productVariants),
|
attributes: prepareProductAttributes(productVariants),
|
||||||
variants: prepareProductVariants(productVariants),
|
variants: prepareProductVariants(productVariants),
|
||||||
instagramPostId: postId ? parseInt(postId) : undefined,
|
instagramPostId: postId ? parseInt(postId) : undefined,
|
||||||
sizeChart: sizeChartPayload,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const product = await createProductMutation.mutateAsync(productData);
|
const product = await createProductMutation.mutateAsync(productData);
|
||||||
@ -848,18 +814,6 @@ export default function AddProduct() {
|
|||||||
productVariants={productVariants}
|
productVariants={productVariants}
|
||||||
onUpdateVariantQuantity={updateVariantQuantity}
|
onUpdateVariantQuantity={updateVariantQuantity}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Size Chart (راهنمای سایز) */}
|
|
||||||
<div className="px-4 mt-4">
|
|
||||||
<SizeChartSection
|
|
||||||
value={sizeChart}
|
|
||||||
onChange={setSizeChart}
|
|
||||||
categoryName={selectedCategoryName}
|
|
||||||
productSizes={productSizes}
|
|
||||||
required={hasSizes}
|
|
||||||
showError={hasSizes && !isSizeChartComplete()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -908,9 +862,6 @@ export default function AddProduct() {
|
|||||||
{productVariants.length === 0 && (
|
{productVariants.length === 0 && (
|
||||||
<div>•حداقل یک ویژگی اضافه کنید</div>
|
<div>•حداقل یک ویژگی اضافه کنید</div>
|
||||||
)}
|
)}
|
||||||
{hasSizes && !isSizeChartComplete() && (
|
|
||||||
<div>•راهنمای سایز را برای همه سایزها کامل کنید</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1732,12 +1732,6 @@ export interface FullProductCreate {
|
|||||||
* @memberof FullProductCreate
|
* @memberof FullProductCreate
|
||||||
*/
|
*/
|
||||||
instagramPostId?: number;
|
instagramPostId?: number;
|
||||||
/**
|
|
||||||
* Seller-filled size guide (راهنمای سایز).
|
|
||||||
* @type {SizeChart}
|
|
||||||
* @memberof FullProductCreate
|
|
||||||
*/
|
|
||||||
sizeChart?: SizeChart | null;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -1793,12 +1787,6 @@ export interface FullProductUpdate {
|
|||||||
* @memberof FullProductUpdate
|
* @memberof FullProductUpdate
|
||||||
*/
|
*/
|
||||||
variants?: Array<ProductVariant>;
|
variants?: Array<ProductVariant>;
|
||||||
/**
|
|
||||||
* Seller-filled size guide (راهنمای سایز).
|
|
||||||
* @type {SizeChart}
|
|
||||||
* @memberof FullProductUpdate
|
|
||||||
*/
|
|
||||||
sizeChart?: SizeChart | null;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -3750,58 +3738,6 @@ export interface ProductDetail {
|
|||||||
* @memberof ProductDetail
|
* @memberof ProductDetail
|
||||||
*/
|
*/
|
||||||
readonly rateCount?: string;
|
readonly rateCount?: string;
|
||||||
/**
|
|
||||||
* Seller-filled size guide (راهنمای سایز).
|
|
||||||
* @type {SizeChart}
|
|
||||||
* @memberof ProductDetail
|
|
||||||
*/
|
|
||||||
sizeChart?: SizeChart | null;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* One row of a size chart — a size label plus measurement values aligned by
|
|
||||||
* index to the chart's `columns`.
|
|
||||||
* @export
|
|
||||||
* @interface SizeChartRow
|
|
||||||
*/
|
|
||||||
export interface SizeChartRow {
|
|
||||||
/**
|
|
||||||
* Size label, e.g. "L" or "42".
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SizeChartRow
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Measurement values, aligned by index to SizeChart.columns.
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof SizeChartRow
|
|
||||||
*/
|
|
||||||
values: Array<string>;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A seller-filled size guide. `columns` are measurement names (عرض سینه، قد،
|
|
||||||
* plus any custom ones); each row aligns its `values` to those columns.
|
|
||||||
* @export
|
|
||||||
* @interface SizeChart
|
|
||||||
*/
|
|
||||||
export interface SizeChart {
|
|
||||||
/**
|
|
||||||
* Measurement unit, e.g. "cm".
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SizeChart
|
|
||||||
*/
|
|
||||||
unit?: string;
|
|
||||||
/**
|
|
||||||
* Ordered measurement column names.
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof SizeChart
|
|
||||||
*/
|
|
||||||
columns: Array<string>;
|
|
||||||
/**
|
|
||||||
* Size rows.
|
|
||||||
* @type {Array<SizeChartRow>}
|
|
||||||
* @memberof SizeChart
|
|
||||||
*/
|
|
||||||
rows: Array<SizeChartRow>;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user