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 { LoginRequiredDialog } from "../LoginRequiredDialog";
|
||||
import AIPromoteModal from "../AIPromoteModal";
|
||||
import { SizeGuideDialog } from "./SizeGuideDialog";
|
||||
|
||||
interface ProductDetailViewProps {
|
||||
product: ProductDetailType;
|
||||
@ -259,12 +258,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
||||
const [qty, setQty] = useState(1);
|
||||
const [isAIOpen, setIsAIOpen] = 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: commentsData } = useServiceGetComments(1, product.id || "");
|
||||
@ -579,11 +572,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
||||
sizes={inStockSizes.map((size) => size.value || "")}
|
||||
selectedSize={selectedSize}
|
||||
setSelectedSize={setSelectedSize}
|
||||
onSizeGuide={
|
||||
hasSizeChart
|
||||
? () => setIsSizeGuideOpen(true)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -770,13 +758,6 @@ export const ProductDetailView = memo(function ProductDetailView({
|
||||
onClose={() => setIsAIOpen(false)}
|
||||
productId={product.id || ""}
|
||||
/>
|
||||
{hasSizeChart && product.sizeChart && (
|
||||
<SizeGuideDialog
|
||||
isOpen={isSizeGuideOpen}
|
||||
onOpenChange={setIsSizeGuideOpen}
|
||||
sizeChart={product.sizeChart}
|
||||
/>
|
||||
)}
|
||||
</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 {
|
||||
sizes: string[] | undefined;
|
||||
selectedSize: string | null;
|
||||
setSelectedSize: (size: string) => void;
|
||||
/** When provided, shows a "راهنمای سایز" link that opens the size guide. */
|
||||
onSizeGuide?: () => void;
|
||||
}
|
||||
|
||||
export const SizeSelector = ({
|
||||
sizes,
|
||||
selectedSize,
|
||||
setSelectedSize,
|
||||
onSizeGuide,
|
||||
}: SizeSelectorProps) => (
|
||||
<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>
|
||||
{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>
|
||||
<p className="text-B18 font-bold">انتخاب سایز</p>
|
||||
<div className="gap-4 flex">
|
||||
{sizes?.map((size: string, index: number) => (
|
||||
<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,14 +61,12 @@ export default function CollectionDetail() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
|
||||
{/* Loading Skeleton */}
|
||||
@ -96,14 +94,12 @@ export default function CollectionDetail() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
@ -134,14 +130,12 @@ export default function CollectionDetail() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
|
||||
{/* Header (mobile) */}
|
||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">جزئیات کالکشن</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@ -118,14 +118,12 @@ export default function Collections() {
|
||||
return (
|
||||
<div className="flex flex-col bg-WHITE pb-20">
|
||||
{/* Header (mobile) */}
|
||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">کالکشنها</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">کالکشنها</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@ -25,14 +25,12 @@ export default function Discounts() {
|
||||
return (
|
||||
<div className="flex flex-col bg-WHITE pb-20">
|
||||
{/* Header (mobile) */}
|
||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">تخفیفها</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">تخفیفها</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@ -199,7 +199,7 @@ export default function Profile() {
|
||||
{/* Mobile: account hub */}
|
||||
<div className="flex flex-col gap-0 lg:hidden">
|
||||
{/* 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" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-B16 font-bold truncate">
|
||||
|
||||
@ -62,14 +62,12 @@ export default function Sellers() {
|
||||
return (
|
||||
<div className="flex flex-col bg-WHITE pb-20">
|
||||
{/* Header (mobile) */}
|
||||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||||
<div className="relative flex flex-row h-[65px] w-full items-center justify-center border-b border-inner-border">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">فروشگاهها</p>
|
||||
</div>
|
||||
<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">
|
||||
<ArrowRight
|
||||
onClick={handleBack}
|
||||
className="absolute top-5 right-5 cursor-pointer"
|
||||
/>
|
||||
<p className="text-B16 font-bold">فروشگاهها</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@ -558,7 +558,7 @@ function SellerDetailHeader({
|
||||
};
|
||||
|
||||
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)} />
|
||||
<div className="flex flex-col gap-2 py-4 items-center">
|
||||
<SellerLogo src={sellerData?.storeLogo} alt="Seller Logo" />
|
||||
|
||||
@ -136,29 +136,27 @@ export default function Transactions() {
|
||||
return (
|
||||
<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) */}
|
||||
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)] lg:static lg:pt-0">
|
||||
<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" />
|
||||
<p className={"text-B16 lg:text-[28px] font-bold lg:font-extrabold"}>لیست تراکنشها</p>
|
||||
<div
|
||||
onClick={() => {
|
||||
<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">
|
||||
<ArrowRight onClick={handleBack} className="absolute top-5 right-5 lg:hidden" />
|
||||
<p className={"text-B16 lg:text-[28px] font-bold lg:font-extrabold"}>لیست تراکنشها</p>
|
||||
<div
|
||||
onClick={() => {
|
||||
filterIconClicked();
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
filterIconClicked();
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
filterIconClicked();
|
||||
}
|
||||
}}
|
||||
className="absolute top-4 h-8 w-8 left-5 rounded-full bg-WHITE shadow-xl flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={theme === "dark" ? filterIconDark : filterIcon}
|
||||
alt="filter"
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
className="absolute top-4 h-8 w-8 left-5 rounded-full bg-WHITE shadow-xl flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={theme === "dark" ? filterIconDark : filterIcon}
|
||||
alt="filter"
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
|
||||
@ -11,7 +11,6 @@ import { ImageEditor } from "~/components/store/ImageEditor";
|
||||
import { ImageUploadSection } from "~/components/store/ImageUploadSection";
|
||||
import { ProductDetailsForm } from "~/components/store/ProductDetailsForm";
|
||||
import { ProductVariantsSection } from "~/components/store/ProductVariantsSection";
|
||||
import { SizeChartSection } from "~/components/store/SizeChartSection";
|
||||
import { PricingForm } from "~/components/store/PricingForm";
|
||||
import {
|
||||
useCreateFullProduct,
|
||||
@ -31,7 +30,6 @@ import {
|
||||
ProductVariant as ApiProductVariant,
|
||||
ProductAttributeItem,
|
||||
MediaUploadImageUsageEnum,
|
||||
SizeChart,
|
||||
} from "../../src/api/types";
|
||||
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
|
||||
import { useNavigate, useParams } from "@remix-run/react";
|
||||
@ -152,7 +150,6 @@ export default function AddProduct() {
|
||||
setProductTitle(existingProduct.title || "");
|
||||
setProductDescription(existingProduct.description || "");
|
||||
setSelectedCategory(existingProduct.category || "");
|
||||
setSizeChart(existingProduct.sizeChart || null);
|
||||
setProductPrice(
|
||||
existingProduct.basePrice
|
||||
? parseInt(existingProduct.basePrice).toString()
|
||||
@ -415,29 +412,6 @@ export default function AddProduct() {
|
||||
}>
|
||||
>([]);
|
||||
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
|
||||
const updateVariantQuantity = (index: number, newQuantity: number) => {
|
||||
@ -455,8 +429,7 @@ export default function AddProduct() {
|
||||
productTitle.trim() !== "" &&
|
||||
productDescription.trim() !== "" &&
|
||||
selectedCategory !== "" &&
|
||||
productVariants.length > 0 &&
|
||||
isSizeChartComplete()
|
||||
productVariants.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
@ -602,11 +575,6 @@ export default function AddProduct() {
|
||||
// Get final image URLs (only upload edited/new images)
|
||||
const imageUrls = await getFinalImageUrls();
|
||||
|
||||
const sizeChartPayload =
|
||||
sizeChart && sizeChart.columns.length > 0 && sizeChart.rows.length > 0
|
||||
? sizeChart
|
||||
: null;
|
||||
|
||||
if (isEditMode && editProductId) {
|
||||
// Update existing product
|
||||
const updateData = {
|
||||
@ -617,7 +585,6 @@ export default function AddProduct() {
|
||||
category: selectedCategory,
|
||||
attributes: prepareProductAttributes(productVariants),
|
||||
variants: prepareProductVariants(productVariants),
|
||||
sizeChart: sizeChartPayload,
|
||||
};
|
||||
await updateProductMutation.mutateAsync({
|
||||
productId: editProductId,
|
||||
@ -635,7 +602,6 @@ export default function AddProduct() {
|
||||
attributes: prepareProductAttributes(productVariants),
|
||||
variants: prepareProductVariants(productVariants),
|
||||
instagramPostId: postId ? parseInt(postId) : undefined,
|
||||
sizeChart: sizeChartPayload,
|
||||
};
|
||||
|
||||
const product = await createProductMutation.mutateAsync(productData);
|
||||
@ -848,18 +814,6 @@ export default function AddProduct() {
|
||||
productVariants={productVariants}
|
||||
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 && (
|
||||
<div>•حداقل یک ویژگی اضافه کنید</div>
|
||||
)}
|
||||
{hasSizes && !isSizeChartComplete() && (
|
||||
<div>•راهنمای سایز را برای همه سایزها کامل کنید</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1727,17 +1727,11 @@ export interface FullProductCreate {
|
||||
*/
|
||||
variants?: Array<ProductVariant>;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof FullProductCreate
|
||||
*/
|
||||
instagramPostId?: number;
|
||||
/**
|
||||
* Seller-filled size guide (راهنمای سایز).
|
||||
* @type {SizeChart}
|
||||
* @memberof FullProductCreate
|
||||
*/
|
||||
sizeChart?: SizeChart | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -1788,17 +1782,11 @@ export interface FullProductUpdate {
|
||||
*/
|
||||
attributes?: Array<ProductAttributeName>;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @type {Array<ProductVariant>}
|
||||
* @memberof FullProductUpdate
|
||||
*/
|
||||
variants?: Array<ProductVariant>;
|
||||
/**
|
||||
* Seller-filled size guide (راهنمای سایز).
|
||||
* @type {SizeChart}
|
||||
* @memberof FullProductUpdate
|
||||
*/
|
||||
sizeChart?: SizeChart | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -3745,66 +3733,14 @@ export interface ProductDetail {
|
||||
*/
|
||||
readonly averageRating?: string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ProductDetail
|
||||
*/
|
||||
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>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @interface ProductGenerationResponse
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user