changed order

This commit is contained in:
Jelaletdin12
2025-12-23 22:55:19 +05:00
parent db68bf9c3d
commit 9d95438ab2
13 changed files with 541 additions and 339 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
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";
@@ -17,11 +17,9 @@ import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
import { debounce } from "lodash";
import Loader from "../../components/Loader/index";
// New component for truncated text
const TruncatedDescription = ({ description, maxLength = 100 }) => {
const [isExpanded, setIsExpanded] = useState(false);
// Strip HTML tags for character count
const stripHtml = (html) => {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.textContent || "";
@@ -48,14 +46,25 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
const CartPage = () => {
const {
data: response = {},
refetch,
error,
isError,
isLoading,
} = useGetCartQuery();
const cartItems = isError ? [] : response.data || [];
} = useGetCartQuery(undefined, {
refetchOnMountOrArgChange: 30, // ✅ Sadece 30 saniye sonra mount'ta refetch
refetchOnFocus: false,
refetchOnReconnect: false,
});
// Handle the new data structure - data is now an object grouped by store
const cartData = isError ? {} : (response.data || {});
// Convert object of arrays to flat array for backward compatibility
const cartItems = useMemo(() => {
return Object.values(cartData).flat();
}, [cartData]);
const { t, i18n } = useTranslation();
const [isCheckout, setIsCheckout] = useState(false);
const [checkoutStores, setCheckoutStores] = useState({});
const [addToCart] = useAddToCartMutation();
const [removeFromCart] = useRemoveFromCartMutation();
const [updateCartItem] = useUpdateCartItemMutation();
@@ -69,6 +78,8 @@ const CartPage = () => {
const [localQuantities, setLocalQuantities] = useState({});
const [pendingQuantities, setPendingQuantities] = useState({});
const [loadingItems, setLoadingItems] = useState({});
const checkoutRefs = useRef({});
const modalProps = {
centered: true,
className: styles.cartDeleteModal,
@@ -76,10 +87,25 @@ const CartPage = () => {
width: 400,
};
const handleCheckout = () => setIsCheckout(true);
const handleBackToCart = () => setIsCheckout(false);
const checkoutRef = useRef({ current: null });
// Convert grouped data to stores array
const stores = useMemo(() => {
return Object.entries(cartData).map(([storeSlug, items]) => {
if (!items || !items.length) return null;
// Get store info from first item
const storeInfo = items[0]?.product?.channel?.[0];
return {
id: storeInfo?.id || storeSlug,
name: storeInfo?.name || storeSlug,
slug: storeSlug,
shipping_price: storeInfo?.shipping_price,
items: items
};
}).filter(Boolean);
}, [cartData]);
// ✅ Initialize local quantities from cart items
useEffect(() => {
const newLocalQuantities = {};
const newPendingQuantities = {};
@@ -95,17 +121,17 @@ const CartPage = () => {
setPendingQuantities(newPendingQuantities);
}, [cartItems]);
// ✅ Debounced update - tek bir useEffect
useEffect(() => {
const debouncedUpdates = {};
const updateItem = async (productId) => {
const serverQuantity =
cartItems.find((item) => item.product.id === productId)
?.product_quantity || 0;
const serverItem = cartItems.find((item) => item.product.id === productId);
const serverQuantity = serverItem ? parseInt(serverItem.product_quantity, 10) : 0;
const pendingQuantity = pendingQuantities[productId];
if (
pendingQuantity === undefined ||
pendingQuantity === parseInt(serverQuantity, 10)
) {
// ✅ Eğer değişiklik yoksa, güncelleme yapma
if (pendingQuantity === undefined || pendingQuantity === serverQuantity) {
return;
}
@@ -120,11 +146,12 @@ const CartPage = () => {
quantity: pendingQuantity,
}).unwrap();
}
// ✅ RTK Query otomatik cache'i güncelleyecek, refetch'e gerek yok
refetch();
} catch (error) {
console.error("Failed to update cart:", error);
// ✅ Hata durumunda geri al
const originalItem = cartItems.find(
(item) => item.product.id === productId
);
@@ -144,12 +171,12 @@ const CartPage = () => {
}
};
const debouncedUpdates = {};
// ✅ Her productId için debounced update oluştur
Object.keys(pendingQuantities).forEach((productId) => {
if (!debouncedUpdates[productId]) {
debouncedUpdates[productId] = debounce(
() => updateItem(productId),
300
500 // ✅ 500ms debounce (daha stabil)
);
}
debouncedUpdates[productId]();
@@ -160,7 +187,7 @@ const CartPage = () => {
debouncedFn.cancel()
);
};
}, [pendingQuantities, cartItems, updateCartItem, removeFromCart, refetch]);
}, [pendingQuantities, cartItems, updateCartItem, removeFromCart]);
const handleQuantityIncrease = (productId) => (event) => {
event.preventDefault();
@@ -171,9 +198,11 @@ const CartPage = () => {
const item = cartItems.find((item) => item.product.id === productId);
if (!item) return;
// ✅ Stock kontrolü
if (localQuantities[productId] >= item.product.stock) {
return;
}
const newQuantity = (localQuantities[productId] || 0) + 1;
setLocalQuantities((prev) => ({
...prev,
@@ -185,18 +214,6 @@ const CartPage = () => {
}));
};
const handleOrderSubmit = async () => {
if (isCheckout && checkoutRef.current) {
const success = await checkoutRef.current();
if (success) {
refetch();
setIsCheckout(false);
}
} else {
setIsCheckout(true);
}
};
const handleQuantityDecrease = (productId) => (event) => {
event.preventDefault();
event.stopPropagation();
@@ -205,10 +222,12 @@ const CartPage = () => {
const currentQuantity = localQuantities[productId] || 0;
// ✅ 1'den azsa modal göster
if (currentQuantity <= 1) {
showDeleteConfirm(productId);
return;
}
const newQuantity = currentQuantity - 1;
setLocalQuantities((prev) => ({
...prev,
@@ -220,14 +239,42 @@ const CartPage = () => {
}));
};
const calculateTotal = () => {
return cartItems.reduce((sum, item) => {
const calculateStoreTotal = (storeItems) => {
return storeItems.reduce((sum, item) => {
const itemPrice = parseFloat(item.product.price_amount) || 0;
const itemQuantity = parseInt(item.product_quantity, 10) || 0;
return sum + itemPrice * itemQuantity;
}, 0);
};
const getStoreShippingPrice = (store) => {
return store.shipping_price !== null && store.shipping_price !== undefined
? parseFloat(store.shipping_price)
: 20;
};
const handleCheckout = (storeId) => {
setCheckoutStores(prev => ({ ...prev, [storeId]: true }));
};
const handleBackToCart = (storeId) => {
setCheckoutStores(prev => ({ ...prev, [storeId]: false }));
};
const handleOrderSubmit = async (storeId, storeItems) => {
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
const success = await checkoutRefs.current[storeId]();
if (success) {
// ✅ RTK Query otomatik güncelleyecek, refetch'e gerek yok
setCheckoutStores(prev => ({ ...prev, [storeId]: false }));
}
} else {
handleCheckout(storeId);
}
};
useEffect(() => {
const handleClickOutside = (event) => {
if (expandedRef.current && !expandedRef.current.contains(event.target)) {
@@ -246,8 +293,24 @@ const CartPage = () => {
const handleDeleteConfirm = async () => {
if (itemToDelete) {
await removeFromCart({ productId: itemToDelete }).unwrap();
refetch();
try {
await removeFromCart({ productId: itemToDelete }).unwrap();
// ✅ RTK Query otomatik cache'i güncelleyecek
// ✅ Local state'i de temizle
setLocalQuantities((prev) => {
const newState = { ...prev };
delete newState[itemToDelete];
return newState;
});
setPendingQuantities((prev) => {
const newState = { ...prev };
delete newState[itemToDelete];
return newState;
});
} catch (error) {
console.error("Failed to remove item:", error);
}
}
setDeleteModalVisible(false);
setItemToDelete(null);
@@ -258,17 +321,25 @@ const CartPage = () => {
};
const handleEmptyCartConfirm = async () => {
await cleanCart().unwrap();
refetch();
try {
await cleanCart().unwrap();
// ✅ RTK Query otomatik cache'i güncelleyecek
// ✅ Local state'i temizle
setLocalQuantities({});
setPendingQuantities({});
setCheckoutStores({});
} catch (error) {
console.error("Failed to clean cart:", error);
}
setEmptyCartModalVisible(false);
};
const handleButtonClick = () => {
if (isCheckout) {
handleOrderSubmit();
} else {
handleCheckout();
}
const getTotalItemCount = () => {
return cartItems.reduce(
(sum, item) => sum + parseInt(item.product_quantity, 10),
0
);
};
return (
@@ -296,6 +367,7 @@ const CartPage = () => {
>
<p>{t("common.Are_you_sure_you_want_to_empty_the_cart")}</p>
</Modal>
{isLoading ? (
<Loader />
) : cartItems.length === 0 ? (
@@ -303,118 +375,132 @@ const CartPage = () => {
) : (
<div className={styles.cartItems}>
<div className={styles.cartProducts}>
{isCheckout ? (
<Checkout
cartItems={cartItems}
onBackToCart={handleBackToCart}
onPlaceOrder={checkoutRef}
/>
) : (
<div className={styles.cartItemContainer}>
<div className={styles.cartHeader}>
<h2>
{t("cart.basket")} (
{cartItems.reduce(
(sum, item) => sum + parseInt(item.product_quantity, 10),
0
)}
)
</h2>
<div>
<button
className={styles.deleteBtn}
style={{ padding: "4px 12px" }}
onClick={showEmptyCartConfirm}
>
<FaTrashAlt /> {t("cart.clearCart")}
</button>
</div>
<div className={styles.storesContainer}>
<div className={styles.cartHeader}>
<h2>
{t("cart.basket")} ({getTotalItemCount()})
</h2>
<div>
<button
className={styles.deleteBtn}
style={{ padding: "4px 12px" }}
onClick={showEmptyCartConfirm}
>
<FaTrashAlt /> {t("cart.clearCart")}
</button>
</div>
{cartItems.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>
{stores.map((store) => {
const shippingPrice = getStoreShippingPrice(store);
const storeTotal = calculateStoreTotal(store.items);
const totalWithShipping = storeTotal + shippingPrice;
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={(placeOrderFn) => {
checkoutRefs.current[store.id] = placeOrderFn;
}}
/>
</div>
<div className={styles.itemInfo}>
<div style={{ flex: "1" }}>
<h3>{item.product.name}</h3>
{/* Replace the original description with the TruncatedDescription component */}
<TruncatedDescription
description={item.product.description}
maxLength={150}
/>
</div>
<div className={styles.priceQuantity}>
<span className={styles.price}>
{(parseFloat(item.product.price_amount) || 0).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 style={{background:"white"}}>
<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}>
{(parseFloat(item.product.price_amount) || 0).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>
<div className={styles.deleteBtnContainer}>
<button
className={styles.deleteBtn}
onClick={() => showDeleteConfirm(item.product.id)}
>
<FaTrashAlt />
</button>
)}
<div className={styles.storeSummary}>
<div className={styles.cartContent}>
<h3>{store.name} - {t("cart.basket")}:</h3>
<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, store.items)}
className={styles.checkoutBtn}
>
{checkoutStores[store.id] ? t("cart.order") : t("cart.prepareOrders")}
</button>
</div>
</div>
))}
</div>
)}
<div className={styles.cartSummary}>
<div className={styles.cartContent}>
<h3> {t("cart.basket")}:</h3>
<div className={styles.summaryRow}>
<span> {t("cart.price")}:</span>
<span>{calculateTotal().toFixed(2)} m.</span>
</div>
<div className={styles.summaryRow}>
<span> {t("cart.delivery")} :</span>
<span>0.00 m.</span>
</div>
<div className={styles.summaryRow}>
<span> {t("cart.total")}:</span>
<span>{calculateTotal().toFixed(2)} m.</span>
</div>
</div>
<button
onClick={handleButtonClick}
className={styles.checkoutBtn}
>
{isCheckout ? t("cart.order") : t("cart.prepareOrders")}
</button>
);
})}
</div>
<div className={styles.container}>
{/* Mobile sticky summary */}
{/* <div className={styles.container}>
<div className={styles.summaryCard} ref={expandedRef}>
<div
className={`${styles.expandedContent} ${
@@ -423,14 +509,16 @@ const CartPage = () => {
>
<div className={styles.details}>
<div className={styles.row}>
<span> {t("cart.price")}:</span>
<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}>0.00 m.</span>
<span>{t("cart.delivery")}:</span>
<span className={styles.amount}>
{stores.reduce((sum, store) => sum + getStoreShippingPrice(store), 0).toFixed(2)} m.
</span>
</div>
</div>
</div>
@@ -451,20 +539,12 @@ const CartPage = () => {
{t("cart.total")}:
</span>
<span className={styles.amount}>
{calculateTotal().toFixed(2)} m.
{(calculateTotal() + stores.reduce((sum, store) => sum + getStoreShippingPrice(store), 0)).toFixed(2)} m.
</span>
</div>
<div className={styles.actionWrapper}>
<button
onClick={handleButtonClick}
className={styles.button}
>
{isCheckout ? t("cart.order") : t("cart.prepareOrders")}
</button>
</div>
</div>
</div>
</div>
</div> */}
</div>
</div>
)}
@@ -472,4 +552,4 @@ const CartPage = () => {
);
};
export default CartPage;
export default CartPage;