476 lines
18 KiB
JavaScript
476 lines
18 KiB
JavaScript
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||
import styles from "./CartPage.module.scss";
|
||
import { FaTrashAlt } from "react-icons/fa";
|
||
import Checkout from "../../components/Checkout";
|
||
import { Modal } from "antd";
|
||
import { useTranslation } from "react-i18next";
|
||
import EmptyCartState from "./emptyCart";
|
||
import {
|
||
useRemoveFromCartMutation,
|
||
useUpdateCartItemMutation,
|
||
useCleanCartMutation,
|
||
} from "../../app/api/cartApi";
|
||
import { useCart } from "../../app/api/useCart";
|
||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||
import Loader from "../../components/Loader/index";
|
||
|
||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||
|
||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||
const stripHtml = (html) => {
|
||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||
return doc.body.textContent || "";
|
||
};
|
||
|
||
const plainText = stripHtml(description);
|
||
const shouldTruncate = plainText.length > maxLength;
|
||
|
||
return (
|
||
<div className={styles.truncatedDescription}>
|
||
<div
|
||
dangerouslySetInnerHTML={{
|
||
__html: shouldTruncate
|
||
? description.substring(0, maxLength) + "..."
|
||
: description,
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const CartPage = () => {
|
||
const { cartData, cartItems, isLoading } = useCart();
|
||
const { t } = useTranslation();
|
||
|
||
const [checkoutStores, setCheckoutStores] = useState({});
|
||
const [removeFromCart] = useRemoveFromCartMutation();
|
||
const [updateCartItem] = useUpdateCartItemMutation();
|
||
const [cleanCart] = useCleanCartMutation();
|
||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
||
const [itemToDelete, setItemToDelete] = useState(null);
|
||
const [localQuantities, setLocalQuantities] = useState({});
|
||
const [pendingQuantities, setPendingQuantities] = useState({});
|
||
const [loadingItems, setLoadingItems] = useState({});
|
||
const checkoutRefs = useRef({});
|
||
|
||
const modalProps = {
|
||
centered: true,
|
||
className: styles.cartDeleteModal,
|
||
maskClosable: false,
|
||
width: 400,
|
||
};
|
||
|
||
const stores = useMemo(() => {
|
||
return Object.entries(cartData)
|
||
.map(([storeSlug, items]) => {
|
||
if (!items?.length) return null;
|
||
const storeInfo = items[0]?.product?.channel?.[0];
|
||
return {
|
||
id: storeInfo?.id || storeSlug,
|
||
name: storeInfo?.name || storeSlug,
|
||
slug: storeSlug,
|
||
shipping_price: storeInfo?.shipping_price,
|
||
items,
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
}, [cartData]);
|
||
|
||
useEffect(() => {
|
||
const newLocal = {};
|
||
const newPending = {};
|
||
cartItems.forEach((item) => {
|
||
const id = item.product.id;
|
||
const qty = parseInt(item.product_quantity, 10) || 0;
|
||
newLocal[id] = qty;
|
||
newPending[id] = qty;
|
||
});
|
||
setLocalQuantities(newLocal);
|
||
setPendingQuantities(newPending);
|
||
}, [cartItems]);
|
||
|
||
useEffect(() => {
|
||
const timers = {};
|
||
|
||
Object.keys(pendingQuantities).forEach((productId) => {
|
||
const serverItem = cartItems.find(
|
||
(item) => String(item.product.id) === String(productId),
|
||
);
|
||
const serverQty = serverItem
|
||
? parseInt(serverItem.product_quantity, 10)
|
||
: 0;
|
||
const pendingQty = pendingQuantities[productId];
|
||
|
||
if (
|
||
pendingQty === undefined ||
|
||
pendingQty === serverQty ||
|
||
pendingQty <= 0
|
||
)
|
||
return;
|
||
|
||
timers[productId] = setTimeout(async () => {
|
||
try {
|
||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
||
await updateCartItem({ productId, quantity: pendingQty }).unwrap();
|
||
} catch {
|
||
setLocalQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||
setPendingQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||
} finally {
|
||
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||
}
|
||
}, 500);
|
||
});
|
||
|
||
return () => Object.values(timers).forEach(clearTimeout);
|
||
}, [pendingQuantities, cartItems, updateCartItem]);
|
||
|
||
const handleQuantityIncrease = (productId) => (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (loadingItems[productId]) return;
|
||
|
||
const item = cartItems.find((i) => i.product.id === productId);
|
||
if (!item || localQuantities[productId] >= item.product.stock) return;
|
||
|
||
const newQty = (localQuantities[productId] || 0) + 1;
|
||
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||
};
|
||
|
||
const handleQuantityDecrease = (productId) => (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (loadingItems[productId]) return;
|
||
|
||
const currentQty = localQuantities[productId] || 0;
|
||
if (currentQty <= 1) {
|
||
showDeleteConfirm(productId);
|
||
return;
|
||
}
|
||
|
||
const newQty = currentQty - 1;
|
||
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||
};
|
||
|
||
const getStoreShippingPrice = (store) =>
|
||
store.shipping_price != null ? parseFloat(store.shipping_price) : 20;
|
||
|
||
// Store içinde fiyatsız ürün var mı?
|
||
const storeHasZeroPriceItem = (storeItems) =>
|
||
storeItems.some((item) => isPriceZero(item.product.price_amount));
|
||
|
||
const calculateStoreTotal = (storeItems) =>
|
||
storeItems.reduce((sum, item) => {
|
||
return (
|
||
sum +
|
||
(parseFloat(item.product.price_amount) || 0) *
|
||
(parseInt(item.product_quantity, 10) || 0)
|
||
);
|
||
}, 0);
|
||
|
||
const handleCheckout = (storeId) =>
|
||
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
||
|
||
const handleBackToCart = (storeId) =>
|
||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||
|
||
const handleOrderSubmit = async (storeId) => {
|
||
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
||
const success = await checkoutRefs.current[storeId]();
|
||
if (success) setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||
} else {
|
||
handleCheckout(storeId);
|
||
}
|
||
};
|
||
|
||
const showDeleteConfirm = (productId) => {
|
||
setItemToDelete(productId);
|
||
setDeleteModalVisible(true);
|
||
};
|
||
|
||
const handleDeleteConfirm = async () => {
|
||
if (itemToDelete) {
|
||
try {
|
||
await removeFromCart({ productId: itemToDelete }).unwrap();
|
||
setLocalQuantities((prev) => {
|
||
const s = { ...prev };
|
||
delete s[itemToDelete];
|
||
return s;
|
||
});
|
||
setPendingQuantities((prev) => {
|
||
const s = { ...prev };
|
||
delete s[itemToDelete];
|
||
return s;
|
||
});
|
||
} catch (e) {
|
||
console.error("Failed to remove item:", e);
|
||
}
|
||
}
|
||
setDeleteModalVisible(false);
|
||
setItemToDelete(null);
|
||
};
|
||
|
||
const handleEmptyCartConfirm = async () => {
|
||
try {
|
||
await cleanCart().unwrap();
|
||
setLocalQuantities({});
|
||
setPendingQuantities({});
|
||
setCheckoutStores({});
|
||
} catch (e) {
|
||
console.error("Failed to clean cart:", e);
|
||
}
|
||
setEmptyCartModalVisible(false);
|
||
};
|
||
|
||
const getTotalItemCount = () =>
|
||
cartItems.reduce(
|
||
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
||
0,
|
||
);
|
||
|
||
return (
|
||
<div className={styles.cartContainer}>
|
||
<Modal
|
||
{...modalProps}
|
||
title={t("common.confirm")}
|
||
open={deleteModalVisible}
|
||
onOk={handleDeleteConfirm}
|
||
onCancel={() => setDeleteModalVisible(false)}
|
||
okText={t("common.yes")}
|
||
cancelText={t("common.no")}
|
||
>
|
||
<p>{t("common.Do_you_really_want_to_remove_the_item_from_the_cart")}</p>
|
||
</Modal>
|
||
|
||
<Modal
|
||
{...modalProps}
|
||
title={t("common.confirm")}
|
||
open={emptyCartModalVisible}
|
||
onOk={handleEmptyCartConfirm}
|
||
onCancel={() => setEmptyCartModalVisible(false)}
|
||
okText={t("common.yes")}
|
||
cancelText={t("common.no")}
|
||
>
|
||
<p>{t("common.Are_you_sure_you_want_to_empty_the_cart")}</p>
|
||
</Modal>
|
||
|
||
{isLoading ? (
|
||
<Loader />
|
||
) : cartItems.length === 0 ? (
|
||
<EmptyCartState />
|
||
) : (
|
||
<div className={styles.cartItems}>
|
||
<div className={styles.cartProducts}>
|
||
<div className={styles.storesContainer}>
|
||
<div className={styles.cartHeader}>
|
||
<h2>
|
||
{t("cart.basket")} ({getTotalItemCount()})
|
||
</h2>
|
||
<button
|
||
className={styles.deleteBtn}
|
||
style={{ padding: "4px 12px" }}
|
||
onClick={() => setEmptyCartModalVisible(true)}
|
||
>
|
||
<FaTrashAlt /> {t("cart.clearCart")}
|
||
</button>
|
||
</div>
|
||
|
||
{stores.map((store) => {
|
||
const shippingPrice = getStoreShippingPrice(store);
|
||
const storeTotal = calculateStoreTotal(store.items);
|
||
const totalWithShipping = storeTotal + shippingPrice;
|
||
const hasZeroPrice = storeHasZeroPriceItem(store.items);
|
||
|
||
return (
|
||
<div key={store.id} className={styles.storeSection}>
|
||
{checkoutStores[store.id] ? (
|
||
<Checkout
|
||
cartItems={store.items}
|
||
shippingPrice={shippingPrice}
|
||
productIds={store.items.map((item) => item.product.id)}
|
||
onBackToCart={() => handleBackToCart(store.id)}
|
||
onPlaceOrder={(fn) => {
|
||
checkoutRefs.current[store.id] = fn;
|
||
}}
|
||
/>
|
||
) : (
|
||
<div style={{ background: "white", width: "100%" }}>
|
||
<div className={styles.storeHeader}>
|
||
<h3>{store.name}</h3>
|
||
</div>
|
||
<div className={styles.cartItemContainer}>
|
||
{store.items.map((item) => (
|
||
<div key={item.id} className={styles.cartItem}>
|
||
<div className={styles.itemImage}>
|
||
<img
|
||
src={item.product.media[0]?.images_400x400}
|
||
alt={item.product.name}
|
||
/>
|
||
</div>
|
||
<div className={styles.itemInfo}>
|
||
<div style={{ flex: "1" }}>
|
||
<h3>{item.product.name}</h3>
|
||
<TruncatedDescription
|
||
description={item.product.description}
|
||
maxLength={150}
|
||
/>
|
||
</div>
|
||
<div className={styles.priceQuantity}>
|
||
<span className={styles.price}>
|
||
{isPriceZero(item.product.price_amount)
|
||
? "Bahasyny anyklamaly"
|
||
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
|
||
</span>
|
||
<div className={styles.quantityControls}>
|
||
<button
|
||
onClick={handleQuantityDecrease(
|
||
item.product.id,
|
||
)}
|
||
className={styles.quantityBtn}
|
||
disabled={loadingItems[item.product.id]}
|
||
>
|
||
<DecreaseIcon />
|
||
</button>
|
||
<span>
|
||
{localQuantities[item.product.id] !==
|
||
undefined
|
||
? localQuantities[item.product.id]
|
||
: parseInt(item.product_quantity, 10) ||
|
||
0}
|
||
</span>
|
||
<button
|
||
onClick={handleQuantityIncrease(
|
||
item.product.id,
|
||
)}
|
||
className={styles.quantityBtn}
|
||
disabled={loadingItems[item.product.id]}
|
||
>
|
||
<IncreaseIcon />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className={styles.deleteBtnContainer}>
|
||
<button
|
||
className={styles.deleteBtn}
|
||
onClick={() =>
|
||
showDeleteConfirm(item.product.id)
|
||
}
|
||
>
|
||
<FaTrashAlt />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ✅ Store Summary - fiyatsız ürün varsa "Baha anyklamak" */}
|
||
<div className={styles.storeSummary}>
|
||
<div className={styles.cartContent}>
|
||
<h3>
|
||
{store.name} - {t("cart.basket")}:
|
||
</h3>
|
||
{hasZeroPrice ? (
|
||
<>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.price")}:</span>
|
||
<span>Bahasyny anyklamaly</span>
|
||
</div>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.delivery")}:</span>
|
||
<span>Bahasyny anyklamaly</span>
|
||
</div>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.total")}:</span>
|
||
<span>Bahasyny anyklamaly</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.price")}:</span>
|
||
<span>{storeTotal.toFixed(2)} m.</span>
|
||
</div>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.delivery")}:</span>
|
||
<span>{shippingPrice.toFixed(2)} m.</span>
|
||
</div>
|
||
<div className={styles.summaryRow}>
|
||
<span>{t("cart.total")}:</span>
|
||
<span>{totalWithShipping.toFixed(2)} m.</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => handleOrderSubmit(store.id)}
|
||
className={styles.checkoutBtn}
|
||
>
|
||
{checkoutStores[store.id]
|
||
? t("cart.order")
|
||
: t("cart.prepareOrders")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
{/* Mobile sticky summary */}
|
||
{/* <div className={styles.container}>
|
||
<div className={styles.summaryCard} ref={expandedRef}>
|
||
<div
|
||
className={`${styles.expandedContent} ${
|
||
isExpanded ? styles.visible : ""
|
||
}`}
|
||
>
|
||
<div className={styles.details}>
|
||
<div className={styles.row}>
|
||
<span>{t("cart.price")}:</span>
|
||
<span className={styles.amount}>
|
||
{calculateTotal().toFixed(2)} m.
|
||
</span>
|
||
</div>
|
||
<div className={styles.row}>
|
||
<span>{t("cart.delivery")}:</span>
|
||
<span className={styles.amount}>
|
||
{stores.reduce((sum, store) => sum + getStoreShippingPrice(store), 0).toFixed(2)} m.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.header}>
|
||
<div
|
||
className={styles.titleWrapper}
|
||
onClick={(e) => {
|
||
setIsExpanded(!isExpanded);
|
||
e.target.style.outline = "none";
|
||
}}
|
||
>
|
||
<span>
|
||
{isExpanded ? (
|
||
<ChevronUp size={20} />
|
||
) : (
|
||
<ChevronDown size={20} />
|
||
)}
|
||
{t("cart.total")}:
|
||
</span>
|
||
<span className={styles.amount}>
|
||
{(calculateTotal() + stores.reduce((sum, store) => sum + getStoreShippingPrice(store), 0)).toFixed(2)} m.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> */}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CartPage;
|