327 lines
11 KiB
TypeScript
327 lines
11 KiB
TypeScript
import {
|
|
json,
|
|
type LoaderFunctionArgs,
|
|
type MetaFunction,
|
|
} from "@remix-run/node";
|
|
import { useParams, useSearchParams } from "@remix-run/react";
|
|
import { useToast } from "../hooks/use-toast";
|
|
import {
|
|
useGetCartDetails,
|
|
useUpdateCartItemQuantity,
|
|
useRemoveFromCart,
|
|
} from "../requestHandler/use-cart-hooks";
|
|
import { useCreateShippingInfo } from "../requestHandler/use-shipping-hooks";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { CartHeader } from "../components/cart/CartHeader";
|
|
import { CartItem } from "../components/cart/CartItem";
|
|
import { CartSummary } from "../components/cart/CartSummary";
|
|
import { ShippingInformation } from "../components/cart/ShippingInformation";
|
|
import { ReceiverInformation } from "../components/cart/ReceiverInformation";
|
|
import { useCashOutCart } from "../requestHandler/use-order-hooks";
|
|
import {
|
|
Address,
|
|
CartDetail as CartDetailType,
|
|
CartItem as CartItemType,
|
|
SellerCourierList,
|
|
} from "src/api/types";
|
|
import UiProvider from "~/components/UiProvider";
|
|
|
|
export const meta: MetaFunction = () => {
|
|
return [
|
|
{ title: "سبد خرید - ویترون" },
|
|
{
|
|
name: "description",
|
|
content:
|
|
"مدیریت سبد خرید شما در ویترون. بررسی محصولات، انتخاب روش ارسال و تکمیل فرآیند خرید.",
|
|
},
|
|
{
|
|
name: "keywords",
|
|
content: "سبد خرید، خرید آنلاین، پرداخت، ارسال، ویترون",
|
|
},
|
|
{ name: "robots", content: "noindex, follow" },
|
|
{ property: "og:title", content: "سبد خرید - ویترون" },
|
|
{
|
|
property: "og:description",
|
|
content:
|
|
"مدیریت سبد خرید شما در ویترون. بررسی محصولات، انتخاب روش ارسال و تکمیل فرآیند خرید.",
|
|
},
|
|
{ property: "og:type", content: "website" },
|
|
{ name: "twitter:card", content: "summary" },
|
|
{ name: "twitter:title", content: "سبد خرید - ویترون" },
|
|
{
|
|
name: "twitter:description",
|
|
content:
|
|
"مدیریت سبد خرید شما در ویترون. بررسی محصولات، انتخاب روش ارسال و تکمیل فرآیند خرید.",
|
|
},
|
|
];
|
|
};
|
|
|
|
// Server-side loader to fetch initial cart data
|
|
export async function loader({ params }: LoaderFunctionArgs) {
|
|
const { cartId } = params;
|
|
if (!cartId) {
|
|
throw new Response("Missing cart ID", { status: 400 });
|
|
}
|
|
return json({ cartId });
|
|
}
|
|
|
|
// Main Cart Component
|
|
export default function CartDetail() {
|
|
const { cartId } = useParams();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const {
|
|
data: cart,
|
|
isLoading: isLoadingCart,
|
|
refetch,
|
|
isError,
|
|
} = useGetCartDetails(cartId || "");
|
|
const updateMutation = useUpdateCartItemQuantity();
|
|
const removeMutation = useRemoveFromCart();
|
|
const { toast } = useToast();
|
|
const [step, setStep] = useState<number>(0);
|
|
const createShippingInfo = useCreateShippingInfo();
|
|
const cashOutMutation = useCashOutCart(cartId || "");
|
|
|
|
// Calculate total price of all items in the cart
|
|
const calculateTotal = useCallback(
|
|
(
|
|
cart: CartDetailType | null | undefined,
|
|
deliveryPrice?: number | null,
|
|
discountValue?: number | null
|
|
) => {
|
|
if (!cart || !cart.items || cart.items.length === 0) return "0";
|
|
|
|
let total = cart.items.reduce((sum: number, item: CartItemType) => {
|
|
if (item.productVariant?.stock && item.productVariant?.stock > 0) {
|
|
const price =
|
|
Number(
|
|
item.productVariant?.discountPrice || item.productVariant?.price
|
|
) * item.quantity;
|
|
return sum + price;
|
|
}
|
|
return sum;
|
|
}, 0);
|
|
if (deliveryPrice) {
|
|
total += deliveryPrice;
|
|
}
|
|
if (discountValue) {
|
|
total -= discountValue;
|
|
}
|
|
|
|
return total.toString();
|
|
},
|
|
[]
|
|
);
|
|
// Handle quantity change
|
|
const handleQuantityChange = async (itemId: string, newQuantity: number) => {
|
|
if (newQuantity < 1) return;
|
|
|
|
try {
|
|
await updateMutation.mutateAsync({ itemId, quantity: newQuantity });
|
|
} catch (error) {
|
|
console.error("Error updating item quantity:", error);
|
|
toast({
|
|
title: "خطا",
|
|
description: "بروزرسانی تعداد محصول با مشکل مواجه شد",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle item removal
|
|
const handleRemoveItem = async (itemId: string) => {
|
|
try {
|
|
await removeMutation.mutateAsync(itemId);
|
|
|
|
toast({
|
|
title: "سبد خرید",
|
|
description: "محصول با موفقیت از سبد خرید حذف شد",
|
|
variant: "default",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error removing item:", error);
|
|
toast({
|
|
title: "خطا",
|
|
description: "حذف محصول از سبد خرید با مشکل مواجه شد",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
const [selectedAddressInfo, setSelectedAddressInfo] =
|
|
useState<Address | null>(null);
|
|
const [selectedCourierInfo, setSelectedCourierInfo] =
|
|
useState<SellerCourierList | null>(null);
|
|
const [shippingInfoId, setShippingInfoId] = useState<string>("");
|
|
const [isCreatingOrder, setIsCreatingOrder] = useState<boolean>(false);
|
|
const [shippingInfoIsLoading, setShippingInfoIsLoading] =
|
|
useState<boolean>(false);
|
|
|
|
// Initialize step from URL query parameter when component mounts
|
|
useEffect(() => {
|
|
const stepParam = searchParams.get("step");
|
|
if (stepParam) {
|
|
const newStep = parseInt(stepParam, 10);
|
|
if (!isNaN(newStep) && newStep >= 0 && newStep <= 2) {
|
|
setStep(newStep);
|
|
}
|
|
// Clean up the URL by removing the step parameter
|
|
const newSearchParams = new URLSearchParams(searchParams);
|
|
newSearchParams.delete("step");
|
|
setSearchParams(newSearchParams, { replace: true });
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []); // Only run once on mount
|
|
|
|
useEffect(() => {
|
|
console.log("selectedCourierInfo", selectedCourierInfo);
|
|
}, [selectedCourierInfo]);
|
|
const continueHandler = async () => {
|
|
if (step === 0) {
|
|
setStep(1);
|
|
} else if (step === 1) {
|
|
const isFaceToFace = selectedCourierInfo?.courierIsFaceToFace;
|
|
if (!isFaceToFace && (!selectedAddressInfo || !selectedAddressInfo?.id)) {
|
|
toast({
|
|
title: "خطا",
|
|
description: "لطفا آدرس را انتخاب کنید",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!selectedCourierInfo || !selectedCourierInfo?.id) {
|
|
toast({
|
|
title: "خطا",
|
|
description: "لطفا روش ارسال را انتخاب کنید",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await createShippingInfo.mutateAsync({
|
|
address: isFaceToFace ? undefined : selectedAddressInfo?.id,
|
|
sellerCourier: selectedCourierInfo.id,
|
|
});
|
|
setShippingInfoId(response.id || "");
|
|
setStep(2);
|
|
} catch (error) {
|
|
console.error("Error creating shipping info:", error);
|
|
toast({
|
|
title: "خطا",
|
|
description: "ثبت اطلاعات ارسال با مشکل مواجه شد",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
} else if (step === 2) {
|
|
if (!shippingInfoId) {
|
|
toast({
|
|
title: "Error",
|
|
description: "Shipping information ID is missing.",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
setIsCreatingOrder(true);
|
|
try {
|
|
console.log(
|
|
`Initiating cash out for cart ${cartId} with shipping info ID: ${shippingInfoId}`
|
|
);
|
|
const response = await cashOutMutation.mutateAsync({
|
|
shippingInfoId: shippingInfoId,
|
|
});
|
|
console.log("Cash out mutation initiated.");
|
|
window.location.href = response.redirect;
|
|
} catch (error) {
|
|
console.error(
|
|
"Error caught in continueHandler during cash out:",
|
|
error
|
|
);
|
|
toast({
|
|
title: "عدم وجود محصول",
|
|
description:
|
|
"موجودی این محصول کمتر از تعداد درخواستی شما است. لطفا تعداد محصولات موجود را چک کنید.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setIsCreatingOrder(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-background pb-14">
|
|
<CartHeader cart={cart} step={step} setStep={setStep} />
|
|
|
|
{/* Cart Content */}
|
|
<div className=" gap-1 flex flex-col">
|
|
<UiProvider
|
|
isLoading={isLoadingCart}
|
|
isEmpty={!cart || cart.items?.length === 0}
|
|
type="cart"
|
|
isError={isError}
|
|
onRetry={refetch}
|
|
>
|
|
<>
|
|
{step === 0 && (
|
|
<div className="flex flex-col w-full">
|
|
{cart?.items?.map((item, index) => (
|
|
<CartItem
|
|
key={item.id}
|
|
item={item}
|
|
onQuantityChange={handleQuantityChange}
|
|
onRemoveItem={handleRemoveItem}
|
|
isLoading={removeMutation.isPending}
|
|
hasBorder={
|
|
!!(cart?.items && index !== cart?.items?.length - 1)
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
{step === 1 && (
|
|
<ShippingInformation
|
|
sellerId={cart?.seller?.username || ""}
|
|
total={calculateTotal(cart)}
|
|
setSelectedAddressInfo={setSelectedAddressInfo}
|
|
setSelectedCourierInfo={setSelectedCourierInfo}
|
|
setShippingInfoIsLoading={setShippingInfoIsLoading}
|
|
selectedAddressInfo={selectedAddressInfo}
|
|
cartId={cartId}
|
|
/>
|
|
)}
|
|
{step === 2 && (
|
|
<ReceiverInformation
|
|
discountValue={Number(cart?.discountValue) || null}
|
|
cartItems={cart?.items || []}
|
|
selectedAddressInfo={selectedAddressInfo}
|
|
selectedCourierInfo={selectedCourierInfo}
|
|
total={calculateTotal(
|
|
cart,
|
|
selectedCourierInfo?.courierIsFreightCollect ? 0 : selectedCourierInfo?.deliveryPrice,
|
|
Number(cart?.discountValue)
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<CartSummary
|
|
total={calculateTotal(
|
|
cart,
|
|
step === 2 && !selectedCourierInfo?.courierIsFreightCollect ? selectedCourierInfo?.deliveryPrice : 0,
|
|
step === 2 ? Number(cart?.discountValue) : 0
|
|
)}
|
|
onContinue={() => continueHandler()}
|
|
step={step}
|
|
isLoading={
|
|
isCreatingOrder ||
|
|
cashOutMutation.isPending ||
|
|
shippingInfoIsLoading
|
|
}
|
|
/>
|
|
</>
|
|
</UiProvider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|