- app/components/store/ColorSelector.tsx: renders as a 9-column grid sorted
by info.sortOrder. Compact aspect-square swatches; Persian name lives in
title/aria-label so 81 swatches breathe on mobile. Checkmark ink flips by
swatch luminance so it stays visible on both سفید and مشکی.
- app/components/store/SizeSelector.tsx + ProductVariantDrawer.tsx: dropped
the "بدون مشخص کردن سایز" toggle now that "FreeSize" exists in the
palette. Every product must pick a size (or FreeSize) on save.
- app/routes/store.add.manual.$storeId.tsx:
- two-step category picker (parent → sub) driven by category.parentId,
with edit-mode pre-fill deriving the parent from the sub's parentId.
- getFinalImageUrls now uploads in parallel with concurrency 3, retries
each image once, persists successful uploads to state as they land so
a resubmit only retries the still-failing ones. If every upload fails
we refuse to submit rather than sending an empty gallery.
- only ship discountPrice when it's a REAL discount (finalNum > 0 and
< priceNum) — otherwise pass null so the backend clears any stale
value. Closes the "phantom discount" hole where PricingForm's
"no discount" radio still shipped the previous typed number.
- isStep1Valid requires every variant to have a real (non-"unselected")
size, matching the new mandatory-size rule.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { X } from "lucide-react";
|
||
import { useSizeAttributes } from "~/requestHandler/use-product-hooks";
|
||
|
||
interface SizeSelectorProps {
|
||
selectedSizes: string[];
|
||
onToggleSize: (size: string) => void;
|
||
onRemoveSize: (size: string) => void;
|
||
}
|
||
|
||
export const SizeSelector: React.FC<SizeSelectorProps> = ({
|
||
selectedSizes,
|
||
onToggleSize,
|
||
onRemoveSize,
|
||
}: SizeSelectorProps) => {
|
||
const { data: sizes, isLoading } = useSizeAttributes();
|
||
if (isLoading) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<h4 className="font-medium text-right">انتخاب سایز های موجود:</h4>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{[...Array(7)].map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="py-3 px-4 rounded-lg border-2 border-WHITE2 bg-WHITE3 animate-pulse h-12"
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<h4 className="font-medium text-right">انتخاب سایز های موجود:</h4>
|
||
<p className="text-xs text-GRAY text-right">
|
||
اگر محصول تکسایز است، «فری سایز» را انتخاب کنید.
|
||
</p>
|
||
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{sizes.map((size) => (
|
||
<>
|
||
{size.value !== "UNSELECTED" && (
|
||
<button
|
||
key={size.id}
|
||
onClick={() => onToggleSize(size.value)}
|
||
className={`py-3 px-4 rounded-lg border-2 text-sm font-medium transition-all ${
|
||
selectedSizes.includes(size.value)
|
||
? "border-blue-500 bg-blue-50 text-blue-700"
|
||
: "border-WHITE2 hover:border-GRAY3 hover:bg-WHITE3"
|
||
}`}
|
||
>
|
||
{size.info?.persian || size.value}
|
||
</button>
|
||
)}
|
||
</>
|
||
))}
|
||
</div>
|
||
|
||
{/* Selected Sizes */}
|
||
{selectedSizes.length > 0 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{selectedSizes.map((sizeValue) => {
|
||
const sizeInfo = sizes.find((s) => s.value === sizeValue);
|
||
return (
|
||
<div
|
||
key={sizeValue}
|
||
className="flex items-center gap-1 bg-blue-100 px-3 py-1 rounded-full text-sm text-blue-700"
|
||
>
|
||
<span>{sizeInfo?.info?.persian || sizeValue}</span>
|
||
<X
|
||
onClick={() => onRemoveSize(sizeValue)}
|
||
className="w-4 h-4 cursor-pointer text-red-500"
|
||
/>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|