feat(cart): mockup-style item rows + separate-shipping note
- CartItem: compact row — image + title + رنگ/سایز attribute chips (circular color dot) + a bottom row with the stepper and the effective price plus struck original. Drops the verbose label:value rows and the product-code line. Shared with the desktop detail column (verified). - cart list: add a green «ارسال جداگانه» badge per seller-cart, matching the mockup's per-seller grouping. Keeps the existing list -> detail checkout flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e6574b1e9e
commit
254fed4a99
@ -1,4 +1,4 @@
|
|||||||
import { Check, Trash } from "lucide-react";
|
import { Trash } from "lucide-react";
|
||||||
import { memo, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
import { QuantityControl } from "./QuantityControl";
|
import { QuantityControl } from "./QuantityControl";
|
||||||
import { formatPrice } from "../../utils/helpers";
|
import { formatPrice } from "../../utils/helpers";
|
||||||
@ -34,20 +34,31 @@ export const CartItem = memo(function CartItem({
|
|||||||
);
|
);
|
||||||
}, [item.productVariant?.discountPrice, item.productVariant?.price]);
|
}, [item.productVariant?.discountPrice, item.productVariant?.price]);
|
||||||
|
|
||||||
// Memoize video check
|
|
||||||
const isVideoMedia = useMemo(
|
const isVideoMedia = useMemo(
|
||||||
() => isVideo(item.productVariant?.productImage || ""),
|
() => isVideo(item.productVariant?.productImage || ""),
|
||||||
[item.productVariant?.productImage]
|
[item.productVariant?.productImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const color =
|
||||||
|
item.productVariant?.color?.value !== "UNSELECTED"
|
||||||
|
? item.productVariant?.color?.info?.detail || ""
|
||||||
|
: "";
|
||||||
|
const size =
|
||||||
|
item.productVariant?.size?.value !== "UNSELECTED"
|
||||||
|
? item.productVariant?.size?.info?.detail || ""
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const effectivePrice =
|
||||||
|
item.productVariant?.discountPrice || item.productVariant?.price || "";
|
||||||
|
const inStock =
|
||||||
|
!!item?.productVariant?.stock && item.productVariant.stock > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`flex gap-3 p-4 w-full ${hasBorder && "border-b"}`}>
|
||||||
className={`flex p-4 gap-4 w-full flex-col ${hasBorder && "border-b"}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 w-full items-center">
|
|
||||||
{isVideoMedia ? (
|
{isVideoMedia ? (
|
||||||
<video
|
<video
|
||||||
src={item.productVariant?.productImage}
|
src={item.productVariant?.productImage}
|
||||||
className="rounded-[5px] w-[116px] h-[116px] object-cover"
|
className="rounded-[11px] w-[88px] h-[110px] object-cover shrink-0"
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
autoPlay
|
autoPlay
|
||||||
@ -57,65 +68,39 @@ export const CartItem = memo(function CartItem({
|
|||||||
<img
|
<img
|
||||||
src={item.productVariant?.productImage}
|
src={item.productVariant?.productImage}
|
||||||
alt={item.productVariant?.productTitle || ""}
|
alt={item.productVariant?.productTitle || ""}
|
||||||
className="rounded-[5px] w-[116px] h-[116px] object-cover"
|
className="rounded-[11px] w-[88px] h-[110px] object-cover shrink-0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-B14 font-bold">
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<h4 className="text-B14 font-bold line-clamp-2">
|
||||||
{item.productVariant?.productTitle || ""}
|
{item.productVariant?.productTitle || ""}
|
||||||
</p>
|
</h4>
|
||||||
<p className="text-R12 text-GRAY line-clamp-1">
|
|
||||||
کد کالا: {item.productVariant?.id || ""}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ColorSelector
|
{/* Attribute chips */}
|
||||||
color={
|
{(color || size) && (
|
||||||
item.productVariant?.color?.value !== "UNSELECTED"
|
<div className="flex gap-1.5 flex-wrap mt-2">
|
||||||
? item.productVariant?.color?.info?.detail || ""
|
{color && (
|
||||||
: ""
|
<span className="inline-flex items-center gap-1.5 text-R12 text-BLACK2 bg-WHITE3 rounded-[7px] px-2 py-1">
|
||||||
}
|
<span
|
||||||
/>
|
className="w-3.5 h-3.5 rounded-full border border-WHITE3"
|
||||||
<SizeSelector
|
style={{ backgroundColor: color }}
|
||||||
size={
|
|
||||||
item.productVariant?.size?.value !== "UNSELECTED"
|
|
||||||
? item.productVariant?.size?.info?.detail || ""
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
رنگ
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{size && (
|
||||||
|
<span className="text-R12 text-BLACK2 bg-WHITE3 rounded-[7px] px-2 py-1">
|
||||||
|
سایز {size}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-between">
|
{/* Bottom row: stepper + price */}
|
||||||
<p className="text-B14 font-bold">قیمت واحد :</p>
|
{inStock ? (
|
||||||
<p className="text-B14 font-bold">
|
<div className="flex items-center gap-3 mt-auto pt-3">
|
||||||
{formatPrice(item.productVariant?.price || "")} تومان
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{discountPercent ? (
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-B14 font-bold">مقدار تخفیف :</p>
|
|
||||||
<div className="bg-BLACK rounded-full text-WHITE text-R14 px-2 py-1">
|
|
||||||
{discountPercent}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-B14 font-bold">
|
|
||||||
{formatPrice(
|
|
||||||
(
|
|
||||||
Number(item.productVariant?.price) -
|
|
||||||
Number(item.productVariant?.discountPrice)
|
|
||||||
).toString()
|
|
||||||
)}{" "}
|
|
||||||
تومان
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{item?.productVariant?.stock && item?.productVariant?.stock > 0 ? (
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2"></div>
|
|
||||||
<QuantityControl
|
<QuantityControl
|
||||||
isPending={isLoading}
|
isPending={isLoading}
|
||||||
quantity={item.quantity}
|
quantity={item.quantity}
|
||||||
@ -128,10 +113,23 @@ export const CartItem = memo(function CartItem({
|
|||||||
}
|
}
|
||||||
onRemove={() => onRemoveItem(item.id || "")}
|
onRemove={() => onRemoveItem(item.id || "")}
|
||||||
/>
|
/>
|
||||||
|
<div className="ms-auto text-left">
|
||||||
|
<b className="text-B14 font-bold">
|
||||||
|
{formatPrice(effectivePrice)}
|
||||||
|
<span className="text-R10 font-semibold text-BLACK2"> ت</span>
|
||||||
|
</b>
|
||||||
|
{discountPercent ? (
|
||||||
|
<del className="block text-R10 text-GRAY">
|
||||||
|
{formatPrice(item.productVariant?.price || "")}
|
||||||
|
</del>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between mt-auto pt-3">
|
||||||
<p className="text-B14 font-bold">محصول در حال حاضر موجود نیست</p>
|
<p className="text-R12 font-bold text-RED">
|
||||||
|
در حال حاضر موجود نیست
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="dark"
|
variant="dark"
|
||||||
className="flex items-center gap-2 bg-red-500"
|
className="flex items-center gap-2 bg-red-500"
|
||||||
@ -143,36 +141,6 @@ export const CartItem = memo(function CartItem({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Color Selector Component
|
|
||||||
const ColorSelector = ({ color }: { color: string }) => (
|
|
||||||
<>
|
|
||||||
{color && (
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<p className="text-B14 font-bold">رنگ :</p>
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-[7px] flex items-center justify-center border border-BLACK"
|
|
||||||
style={{
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check size={24} className="text-WHITE" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Size Selector Component
|
|
||||||
const SizeSelector = ({ size }: { size: string }) => (
|
|
||||||
<>
|
|
||||||
{size && (
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<p className="text-B14 font-bold">سایز :</p>
|
|
||||||
<p className="text-BLACK text-R12">{size}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|||||||
@ -116,6 +116,9 @@ export default function CartIndex() {
|
|||||||
<p className="text-B12 font-bold">
|
<p className="text-B12 font-bold">
|
||||||
{cart.seller?.name}
|
{cart.seller?.name}
|
||||||
</p>
|
</p>
|
||||||
|
<span className="ms-auto shrink-0 text-R10 font-semibold text-green-600">
|
||||||
|
ارسال جداگانه
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<p className="text-B12 font-bold line-clamp-1">
|
<p className="text-B12 font-bold line-clamp-1">
|
||||||
{cart.items
|
{cart.items
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user