+ {Array(3)
+ .fill(0)
+ .map((_, index) => (
+
= index ? "bg-BLACK" : "bg-GRAY2"
+ }`}
+ />
+ ))}
+
+);
diff --git a/app/components/cart/CartItem.tsx b/app/components/cart/CartItem.tsx
new file mode 100644
index 0000000..ea5273d
--- /dev/null
+++ b/app/components/cart/CartItem.tsx
@@ -0,0 +1,178 @@
+import { Check, 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]);
+
+ // Memoize video check
+ const isVideoMedia = useMemo(
+ () => isVideo(item.productVariant?.productImage || ""),
+ [item.productVariant?.productImage]
+ );
+ return (
+
+
+ {isVideoMedia ? (
+
+ ) : (
+

+ )}
+
+
+ {item.productVariant?.productTitle || ""}
+
+
+ کد کالا: {item.productVariant?.id || ""}
+
+
+
+
+
+
+
+
+
قیمت واحد :
+
+ {formatPrice(item.productVariant?.price || "")} تومان
+
+
+
+ {discountPercent ? (
+
+
+
مقدار تخفیف :
+
+ {discountPercent}%
+
+
+
+ {formatPrice(
+ (
+ Number(item.productVariant?.price) -
+ Number(item.productVariant?.discountPrice)
+ ).toString()
+ )}{" "}
+ تومان
+
+
+ ) : null}
+
+ {item?.productVariant?.stock && item?.productVariant?.stock > 0 ? (
+
+
+
+ onQuantityChange(item.id || "", item.quantity + 1)
+ }
+ onDecrease={() =>
+ onQuantityChange(item.id || "", item.quantity - 1)
+ }
+ onRemove={() => onRemoveItem(item.id || "")}
+ />
+
+ ) : (
+
+
محصول در حال حاضر موجود نیست
+
+
+ )}
+
+ );
+});
+
+// Color Selector Component
+const ColorSelector = ({ color }: { color: string }) => (
+ <>
+ {color && (
+
+ )}
+ >
+);
+
+// Size Selector Component
+const SizeSelector = ({ size }: { size: string }) => (
+ <>
+ {size && (
+
+ )}
+ >
+);
diff --git a/app/components/cart/CartSummary.tsx b/app/components/cart/CartSummary.tsx
new file mode 100644
index 0000000..9dc4b0d
--- /dev/null
+++ b/app/components/cart/CartSummary.tsx
@@ -0,0 +1,37 @@
+import { Loader2 } from "lucide-react";
+import { formatPrice } from "../../utils/helpers";
+import { Button } from "../ui/button";
+
+interface CartSummaryProps {
+ total: string;
+ onContinue: () => void;
+ step: number;
+ isLoading: boolean;
+}
+
+export const CartSummary = ({
+ total,
+ onContinue,
+ step,
+ isLoading,
+}: CartSummaryProps) => (
+
+
+
+
مبلغ کل فاکتور:
+
{formatPrice(total)} تومان
+ {step === 2 && (
+
۱۰٪ مالیات به مبلغ نهایی اضافه میشود
+ )}
+
+
+);
diff --git a/app/components/cart/QuantityControl.tsx b/app/components/cart/QuantityControl.tsx
new file mode 100644
index 0000000..ebfa97a
--- /dev/null
+++ b/app/components/cart/QuantityControl.tsx
@@ -0,0 +1,190 @@
+import { useState } from "react";
+import { Plus, Minus, Trash2 } from "lucide-react";
+import { Dialog, DialogContent, DialogOverlay } from "../ui/dialog";
+import { Button } from "../ui/button";
+
+interface QuantityControlProps {
+ quantity: number;
+ maxQuantity: number;
+ onIncrease: () => void;
+ onDecrease: () => void;
+ onRemove?: () => void;
+ isPending: boolean;
+ allowManualEdit?: boolean;
+ onQuantityChange?: (quantity: number) => void;
+}
+
+export const QuantityControl = ({
+ quantity,
+ maxQuantity,
+ onIncrease,
+ onDecrease,
+ onRemove,
+ isPending,
+ allowManualEdit = false,
+ onQuantityChange,
+}: QuantityControlProps) => {
+ const [openDeleteModal, setOpenDeleteModal] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [editValue, setEditValue] = useState(quantity.toString());
+
+ const handleEditStart = () => {
+ if (allowManualEdit) {
+ setIsEditing(true);
+ setEditValue(quantity.toString());
+ }
+ };
+
+ const handleEditConfirm = () => {
+ const newQuantity = parseInt(editValue);
+ if (!isNaN(newQuantity) && newQuantity > 0 && newQuantity <= maxQuantity) {
+ onQuantityChange?.(newQuantity);
+ }
+ setIsEditing(false);
+ };
+
+ const handleEditCancel = () => {
+ setIsEditing(false);
+ setEditValue(quantity.toString());
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ handleEditConfirm();
+ } else if (e.key === "Escape") {
+ handleEditCancel();
+ }
+ };
+
+ return (
+
+ );
+};
+
+interface RemoveCartModalProps {
+ isPending: boolean;
+ onRemove: () => void;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+const RemoveCartModal = ({
+ isPending,
+ onRemove,
+ open,
+ onOpenChange,
+}: RemoveCartModalProps) => {
+ return (
+
+ );
+};
diff --git a/app/components/cart/ReceiverInformation.tsx b/app/components/cart/ReceiverInformation.tsx
new file mode 100644
index 0000000..beb9c99
--- /dev/null
+++ b/app/components/cart/ReceiverInformation.tsx
@@ -0,0 +1,228 @@
+import { useState } from "react";
+import AppInput from "../../components/AppInput";
+import { Button } from "../../components/ui/button";
+import { FileText, User, Loader2 } from "lucide-react";
+import { formatPrice } from "../../utils/helpers";
+import { formatDateRange } from "./ShippingInformation";
+import { useParams } from "@remix-run/react";
+import {
+ Address,
+ CartItem,
+ CartItem as CartItemType,
+ SellerCourierList,
+} from "src/api/types";
+import { useVerifyDiscountCode } from "../../requestHandler/use-discount-hooks";
+
+interface ReceiverInformationProps {
+ selectedAddressInfo: Address | null;
+ selectedCourierInfo: SellerCourierList | null;
+ total: string;
+ cartItems: CartItemType[];
+ discountValue: number | null;
+}
+
+export const ReceiverInformation = ({
+ selectedAddressInfo,
+ selectedCourierInfo,
+ total,
+ cartItems,
+ discountValue,
+}: ReceiverInformationProps) => {
+ const [discountCode, setDiscountCode] = useState
("");
+ const { cartId } = useParams();
+ const isFaceToFace = selectedCourierInfo?.courierIsFaceToFace;
+ // Use the verify discount code hook
+ const { mutate: verifyDiscountCode, isPending: isLoading } =
+ useVerifyDiscountCode();
+
+ const sendDiscountCode = async () => {
+ if (!cartId || !discountCode) return;
+
+ verifyDiscountCode(
+ {
+ code: parseInt(discountCode),
+ cartId: cartId,
+ },
+ {
+ onSuccess: () => {
+ setDiscountCode("");
+ },
+ }
+ );
+ };
+
+ return (
+
+ {!isFaceToFace && (
+
+
+
+
{}}
+ disabled={true}
+ />
+ {}}
+ />
+
+
) => {
+ const value = e.target.value;
+ setDiscountCode(value);
+ }}
+ />
+
+
+
+ )}
+
+
+ );
+};
+
+interface InvoiceContentProps {
+ selectedCourierInfo: SellerCourierList | null;
+ selectedAddressInfo: Address | null;
+ cartItems: CartItem[];
+ total: string;
+ discountValue: number | null;
+}
+export const InvoiceContent = ({
+ selectedCourierInfo,
+ selectedAddressInfo,
+ cartItems,
+ total,
+ discountValue,
+}: InvoiceContentProps) => {
+ const isFaceToFace = selectedCourierInfo?.courierIsFaceToFace;
+ return (
+
+
+ {!selectedCourierInfo ? (
+
+ ) : (
+ <>
+ {!isFaceToFace && (
+
+
+
کد پستی
+ {!selectedAddressInfo ? (
+
+ ) : (
+
{selectedAddressInfo?.postalCode}
+ )}
+
+
+
زمان تحویل
+
+ {formatDateRange(selectedCourierInfo?.estimatedDays)}
+
+
+
+ {!selectedAddressInfo ? (
+
+ ) : (
+
+ آدرس:{selectedAddressInfo?.mainAddress}
+
+ )}
+
+
+ )}
+
+ {cartItems?.map((item, index) => (
+
+
{item.productVariant?.productTitle}
+
+ {formatPrice(item.productVariant?.discountPrice || "0")}x
+ {item.quantity} تومان
+
+
+ ))}
+
+
مقدار تخفیف
+ {!selectedCourierInfo ? (
+
+ ) : (
+
+ {formatPrice(discountValue?.toString() || "0")} تومان
+
+ )}
+
+
+
هزینه ارسال
+ {!selectedCourierInfo ? (
+
+ ) : (
+
+ {selectedCourierInfo?.courierIsFreightCollect
+ ? "پس پرداخت"
+ : `${formatPrice(
+ selectedCourierInfo?.deliveryPrice?.toString() || "0"
+ )} تومان`}
+
+ )}
+
+
+
مبلغ کل فاکتور
+ {!selectedCourierInfo ? (
+
+ ) : (
+
+ {formatPrice(parseFloat(total).toString())} تومان
+
+ )}
+
+
+ >
+ )}
+
+ );
+};
diff --git a/app/components/cart/ShippingInformation.tsx b/app/components/cart/ShippingInformation.tsx
new file mode 100644
index 0000000..039025b
--- /dev/null
+++ b/app/components/cart/ShippingInformation.tsx
@@ -0,0 +1,440 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import {
+ Plus,
+ Menu,
+ Truck,
+ X,
+ Ellipsis,
+ CalendarDays,
+ MapPin,
+} from "lucide-react";
+import { Link } from "@remix-run/react";
+import { Button } from "../ui/button";
+import { useServiceGetAddresses } from "../../requestHandler/use-address-hooks";
+import { useServiceGetCouriers } from "../../requestHandler/use-courier-hooks";
+import { useEffect, useState } from "react";
+import { DrawerContent, Drawer, DrawerHeader } from "../ui/drawer";
+import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
+import { formatPrice } from "../../utils/helpers";
+import { useToast } from "../../hooks/use-toast";
+import { AdderssSettingDrawer } from "../../routes/profile.location._index";
+import jMoment from "moment-jalaali";
+import { Address, SellerCourierList } from "src/api/types";
+
+export const ShippingInformation = ({
+ sellerId,
+ total,
+ setSelectedAddressInfo,
+ setSelectedCourierInfo,
+ setShippingInfoIsLoading,
+ selectedAddressInfo,
+ cartId,
+}: {
+ sellerId: string;
+ total: string;
+ setSelectedAddressInfo: (address: Address) => void;
+ setSelectedCourierInfo: (courierId: SellerCourierList) => void;
+ setShippingInfoIsLoading: (isLoading: boolean) => void;
+ selectedAddressInfo?: Address | null;
+ cartId?: string;
+}) => {
+ const [selectedCourier, setSelectedCourier] =
+ useState(null);
+
+ const handleCourierSelection = (courier: SellerCourierList) => {
+ console.log("sss");
+ setSelectedCourier(courier);
+ setSelectedCourierInfo(courier);
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
+
+const ShippingInformationSelection = ({
+ sellerId,
+ setSelectedCourierInfo,
+ setShippingInfoIsLoading,
+}: {
+ sellerId: string;
+ setSelectedCourierInfo: (courierId: SellerCourierList) => void;
+ setShippingInfoIsLoading: (isLoading: boolean) => void;
+}) => {
+ const { data: couriersData, isLoading } = useServiceGetCouriers(sellerId);
+ useEffect(() => {
+ setShippingInfoIsLoading(isLoading);
+ }, [isLoading]);
+ const [selectedCourierIndex, setSelectedCourierIndex] = useState("");
+ const selectedCourier = couriersData?.filter(
+ (courier: SellerCourierList) => courier.id === selectedCourierIndex
+ )?.[0];
+ const isFaceToFace = selectedCourier?.courierIsFaceToFace;
+
+ useEffect(() => {
+ if (couriersData && couriersData.length > 0) {
+ setSelectedCourierIndex(couriersData[0].id || "");
+ }
+ }, [couriersData]);
+ useEffect(() => {
+ setSelectedCourierInfo(
+ couriersData?.find(
+ (courier: SellerCourierList) => courier.id === selectedCourierIndex
+ ) as SellerCourierList
+ );
+ }, [selectedCourierIndex, couriersData]);
+
+ return (
+ <>
+
+
+
+
+
+
انتخاب شیوه ارسال:
+
+ {isLoading ? (
+
در حال بارگذاری...
+ ) : couriersData ? (
+ <>
+
setSelectedCourierIndex(value)}
+ value={selectedCourierIndex}
+ className="flex gap-4 px-4"
+ dir="rtl"
+ >
+ {couriersData.map((courier: SellerCourierList) => (
+
+
+
+
+ ))}
+
+
+ {isFaceToFace ? (
+
+ روش ارسال حضوری است و در همان محل تحویل خواهد شد
+
+ ) : (
+ <>
+
+
+
+
+
+ زمان تحویل سفارش:
+
+
+ {formatDateRange(selectedCourier?.estimatedDays || 0)}
+
+
+
+
+
+
+
+ هزینه ارسال:
+
+
+ {selectedCourier?.courierIsFreightCollect
+ ? "پس پرداخت"
+ : `${formatPrice(
+ JSON.stringify(selectedCourier?.deliveryPrice)
+ )} تومان`}
+
+
+ >
+ )}
+
+ >
+ ) : (
+
هیچ روش ارسالی یافت نشد
+ )}
+
+ >
+ );
+};
+
+const AddressSelection = ({
+ total,
+ setSelectedAddressInfo,
+ setShippingInfoIsLoading,
+ selectedAddressInfo,
+ selectedCourier,
+ cartId,
+}: {
+ total: string;
+ setSelectedAddressInfo: (addressInfo: Address) => void;
+ setShippingInfoIsLoading: (isLoading: boolean) => void;
+ selectedAddressInfo?: Address | null;
+ selectedCourier?: SellerCourierList | null;
+ cartId?: string;
+}) => {
+ const { data, isLoading } = useServiceGetAddresses();
+ const [selectedAddress, setSelectedAddress] = useState(
+ selectedAddressInfo || null
+ );
+
+ useEffect(() => {
+ setShippingInfoIsLoading(isLoading);
+ }, [isLoading]);
+
+ const defaultAddress = data?.find((address: Address) => address.isDefault);
+
+ useEffect(() => {
+ if (!selectedAddress && defaultAddress) {
+ setSelectedAddress(defaultAddress);
+ setSelectedAddressInfo(defaultAddress);
+ }
+ }, [defaultAddress]);
+
+ const handleAddressChange = (address: Address) => {
+ setSelectedAddress(address);
+ setSelectedAddressInfo(address);
+ };
+
+ const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
+ const [selectedAddressForSettings, setSelectedAddressForSettings] =
+ useState(null);
+
+ const openSettingsDrawer = (address: Address) => {
+ setSelectedAddressForSettings(address);
+ setSettingsDrawerOpen(true);
+ };
+
+ const isFaceToFace = selectedCourier?.courierIsFaceToFace;
+ return (
+ <>
+
+
+
+
+
+
آدرس دریافت سفارش
+
+
{}}
+ role="button"
+ tabIndex={0}
+ className={`w-full flex gap-2 items-center py-4`}
+ >
+
+ {selectedAddress || defaultAddress ? (
+ <>
+
+ {selectedAddress?.title || defaultAddress?.title}
+
+
+ {selectedAddress?.mainAddress || defaultAddress?.mainAddress}
+
+ >
+ ) : (
+ <>
+ {isFaceToFace ? (
+ <>
+
+ برای دریافت حضوری نیازی به آدرس نیست
+
+ >
+ ) : (
+ <>
+
+ آدرسی اضافه نشده است
+
+
+ برای ادامه خرید، لطفا یک آدرس جدید اضافه کنید
+
+ >
+ )}
+ >
+ )}
+
+ {(selectedAddress || defaultAddress) && (
+
+ )}
+
+
+ {!isFaceToFace && (
+
+
+
+ )}
+
+
+ >
+ );
+};
+const ChangeDefaulAddressDrawer = ({
+ total,
+ onOpenSettings,
+ selectedAddress,
+ onAddressChange,
+}: {
+ total: string;
+ onOpenSettings: (address: Address) => void;
+ selectedAddress: Address | null;
+ onAddressChange: (address: Address) => void;
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const { data: dataAddress } = useServiceGetAddresses();
+ const [address, setAddress] = useState(selectedAddress?.id);
+ const { toast } = useToast();
+
+ useEffect(() => {
+ if (selectedAddress) {
+ setAddress(selectedAddress.id);
+ }
+ }, [selectedAddress]);
+
+ const changeShippingAddress = () => {
+ const newAddress = dataAddress?.find(
+ (addr: Address) => addr.id === address
+ );
+ if (newAddress) {
+ onAddressChange(newAddress);
+ setIsOpen(false);
+ toast({
+ title: "آدرس ارسال با موفقیت تغییر کرد",
+ });
+ }
+ };
+
+ return (
+ <>
+