Vitron-Front/app/components/cart/CartItem.tsx
Arda Samadi 254fed4a99 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>
2026-06-27 23:16:42 +03:30

147 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Trash } from "lucide-react";
import { memo, useMemo } from "react";
import { QuantityControl } from "./QuantityControl";
import { formatPrice } from "../../utils/helpers";
import { CartItem as CartItemType } from "src/api/types";
import { isVideo } from "../MondrianProductList";
import { Button } from "../ui/button";
interface CartItemProps {
item: CartItemType;
onQuantityChange: (itemId: string, quantity: number) => void;
onRemoveItem: (itemId: string) => void;
isLoading: boolean;
hasBorder: boolean;
}
export const CartItem = memo(function CartItem({
item,
onQuantityChange,
onRemoveItem,
isLoading,
hasBorder,
}: CartItemProps) {
// Memoize discount calculation
const discountPercent = useMemo(() => {
if (!item.productVariant?.discountPrice || !item.productVariant?.price) {
return null;
}
return Math.round(
((Number(item.productVariant.price) -
Number(item.productVariant.discountPrice)) /
Number(item.productVariant.price)) *
100
);
}, [item.productVariant?.discountPrice, item.productVariant?.price]);
const isVideoMedia = useMemo(
() => isVideo(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 (
<div className={`flex gap-3 p-4 w-full ${hasBorder && "border-b"}`}>
{isVideoMedia ? (
<video
src={item.productVariant?.productImage}
className="rounded-[11px] w-[88px] h-[110px] object-cover shrink-0"
muted
loop
autoPlay
playsInline
/>
) : (
<img
src={item.productVariant?.productImage}
alt={item.productVariant?.productTitle || ""}
className="rounded-[11px] w-[88px] h-[110px] object-cover shrink-0"
loading="lazy"
/>
)}
<div className="flex flex-col flex-1 min-w-0">
<h4 className="text-B14 font-bold line-clamp-2">
{item.productVariant?.productTitle || ""}
</h4>
{/* Attribute chips */}
{(color || size) && (
<div className="flex gap-1.5 flex-wrap mt-2">
{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"
style={{ backgroundColor: color }}
/>
رنگ
</span>
)}
{size && (
<span className="text-R12 text-BLACK2 bg-WHITE3 rounded-[7px] px-2 py-1">
سایز {size}
</span>
)}
</div>
)}
{/* Bottom row: stepper + price */}
{inStock ? (
<div className="flex items-center gap-3 mt-auto pt-3">
<QuantityControl
isPending={isLoading}
quantity={item.quantity}
maxQuantity={item.productVariant?.stock || 0}
onIncrease={() =>
onQuantityChange(item.id || "", item.quantity + 1)
}
onDecrease={() =>
onQuantityChange(item.id || "", item.quantity - 1)
}
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 className="flex w-full items-center justify-between mt-auto pt-3">
<p className="text-R12 font-bold text-RED">
در حال حاضر موجود نیست
</p>
<Button
variant="dark"
className="flex items-center gap-2 bg-red-500"
size="sm"
onClick={() => onRemoveItem(item.id || "")}
>
<Trash size={16} />
</Button>
</div>
)}
</div>
</div>
);
});