Files
mm.com.tm-frontend/src/components/ProductCard/index.jsx
@jcarymuhammedow 6ef7aa3c47 fixed some bugs
2026-04-17 18:38:16 +05:00

341 lines
10 KiB
JavaScript
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 { useState, useEffect } from "react";
import styles from "./ProductCard.module.scss";
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
import { FaShoppingCart } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import { debounce } from "lodash";
import {
useAddFavoriteMutation,
useRemoveFavoriteMutation,
} from "../../app/api/favoritesApi";
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
import {
useAddToCartMutation,
useUpdateCartItemMutation,
useRemoveFromCartMutation,
useGetCartQuery,
} from "../../app/api/cartApi";
import { Modal } from "antd";
import { useTranslation } from "react-i18next";
import { DecreaseIcon, IncreaseIcon } from "../Icons";
import ImageCarousel from "./imageCarousel/index";
// Helper function to strip HTML tags and truncate text
const truncateDescription = (htmlString, maxLength = 80) => {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlString;
const textContent = tempDiv.textContent || tempDiv.innerText || "";
const truncatedText =
textContent.length > maxLength
? textContent.substring(0, maxLength).trim() + "..."
: textContent;
return truncatedText;
};
import { useCart } from "../../app/api/useCart";
const ProductCard = ({
product,
showAddToCart = true,
showFavoriteButton = true,
onAddToCart,
onToggleFavorite,
isFavorite = false,
descriptionMaxLength = 85,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
const [addFavorite] = useAddFavoriteMutation();
const [removeFavorite] = useRemoveFavoriteMutation();
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
const [isLoading, setIsLoading] = useState(false);
const [localIsFavorite, setLocalIsFavorite] = useState(
favoriteProducts.some((fav) => fav.product?.id === product.id),
);
// const [isHovered, setIsHovered] = useState(false);
const truncatedDesc = truncateDescription(
product.description,
descriptionMaxLength,
);
const { getCartItem } = useCart();
const [addToCart] = useAddToCartMutation();
const [updateCartItem] = useUpdateCartItemMutation();
const [removeFromCart] = useRemoveFromCartMutation();
const cartItem = getCartItem(product.id);
const [localQuantity, setLocalQuantity] = useState(0);
const [pendingQuantity, setPendingQuantity] = useState(0);
// ✅ Cart item değiştiğinde local state'i güncelle
useEffect(() => {
const qty = parseInt(
cartItem?.quantity || cartItem?.product_quantity || 0,
10,
);
setLocalQuantity(qty);
setPendingQuantity(qty);
}, [cartItem]);
// ✅ Favorite state'i güncelle
useEffect(() => {
if (Array.isArray(favoriteProducts)) {
const isFav = favoriteProducts.some(
(fav) => fav.product?.id === product.id,
);
setLocalIsFavorite(isFav);
}
}, [favoriteProducts, product.id]);
const handleAddToCart = async (event) => {
event.preventDefault();
event.stopPropagation();
if (product.stock <= 0) {
setStockErrorModalVisible(true);
return;
}
// ✅ Optimistic update
setLocalQuantity((prev) => prev + 1);
setPendingQuantity((prev) => prev + 1);
try {
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
// ✅ Başarılı - RTK Query otomatik cache'i güncelleyecek
} catch (error) {
console.error("Failed to add to cart:", error);
// ✅ Hata varsa geri al
setLocalQuantity((prev) => prev - 1);
setPendingQuantity((prev) => prev - 1);
}
};
// ✅ Debounced update - sadece mutation, refetch yok
useEffect(() => {
const serverQty = parseInt(
cartItem?.quantity || cartItem?.product_quantity || 0,
10,
);
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
return;
}
const handler = setTimeout(async () => {
try {
setIsLoading(true);
await updateCartItem({
productId: product.id,
quantity: pendingQuantity,
}).unwrap();
} catch (error) {
console.error("Failed to update cart item:", error);
setLocalQuantity(serverQty);
setPendingQuantity(serverQty);
} finally {
setIsLoading(false);
}
}, 500);
return () => clearTimeout(handler);
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
const handleQuantityIncrease = (event) => {
event.preventDefault();
event.stopPropagation();
if (isLoading) return;
if (localQuantity >= product.stock) {
setStockErrorModalVisible(true);
return;
}
setLocalQuantity((prev) => prev + 1);
setPendingQuantity((prev) => prev + 1);
};
const handleQuantityDecrease = (event) => {
event.preventDefault();
event.stopPropagation();
if (isLoading) return;
if (pendingQuantity <= 1) {
// ✅ Sıfıra düşünce direkt sil
setPendingQuantity(0);
setLocalQuantity(0);
setIsLoading(true);
removeFromCart({ productId: product.id })
.unwrap()
.then(() => {
// ✅ Başarılı - RTK Query cache'i güncelleyecek
})
.catch(() => {
// ✅ Hata varsa geri al
setLocalQuantity(1);
setPendingQuantity(1);
})
.finally(() => {
setIsLoading(false);
});
} else {
setLocalQuantity((prev) => prev - 1);
setPendingQuantity((prev) => prev - 1);
}
};
const handleToggleFavorite = async (event) => {
event.preventDefault();
event.stopPropagation();
if (isLoading) return;
setIsLoading(true);
// ✅ Optimistic update
setLocalIsFavorite(!localIsFavorite);
try {
if (localIsFavorite) {
const result = await removeFavorite(product.id).unwrap();
// ✅ Başarılı - RTK Query otomatik güncelleyecek
} else {
const result = await addFavorite(product.id).unwrap();
// ✅ Başarılı - RTK Query otomatik güncelleyecek
}
} catch (error) {
console.error("Failed to toggle favorite:", error);
// ✅ Hata varsa geri al
setLocalIsFavorite(localIsFavorite);
} finally {
setIsLoading(false);
}
};
const handleCardClick = () => {
navigate(`/product/${product.id}`);
};
const [isHovered, setIsHovered] = useState(false);
const { name, price_amount, old_price_amount, media = [], reviews } = product;
// Hesaplanmış indirim oranı
let calculatedDiscount = null;
if (!product.discount && old_price_amount && price_amount && old_price_amount > price_amount) {
calculatedDiscount = Math.round(((old_price_amount - price_amount) / old_price_amount) * 100);
}
return (
<>
<div
className={styles.productCard}
onClick={handleCardClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className={styles.imageContainer}>
{(product.discount || calculatedDiscount) && (
<span className={styles.discountBadge}>
-{product.discount ? product.discount : calculatedDiscount}%
</span>
)}
{product.stock === 0 && (
<span className={`${styles.discountBadge} ${styles.outOfStock}`}>
{t("common.out_of_stock")}
</span>
)}
<ImageCarousel images={media} altText={name} isHovered={isHovered}/>
</div>
<div className={styles.productInfo}>
<h3 className={styles.productName}>{name}</h3>
<p className={styles.productDescription}>{truncatedDesc}</p>
<div className={styles.priceContainer}>
<div>
<span className={styles.currentPrice}>{price_amount} m.</span>
{old_price_amount && (
<span className={styles.oldPrice}>{old_price_amount} m.</span>
)}
</div>
</div>
</div>
<div className={styles.actions}>
{showFavoriteButton && (
<button
className={styles.favoriteButton}
onClick={handleToggleFavorite}
disabled={isLoading}
>
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
</button>
)}
{showAddToCart && (
<>
{localQuantity > 0 ? (
<div className={styles.quantityControls}>
<button
onClick={handleQuantityDecrease}
className={styles.quantityBtn}
disabled={isLoading}
>
<DecreaseIcon />
</button>
<span>{localQuantity}</span>
<button
onClick={handleQuantityIncrease}
className={styles.quantityBtn}
disabled={isLoading}
>
<IncreaseIcon />
</button>
</div>
) : (
<button
className={styles.addToCartButton}
onClick={handleAddToCart}
disabled={isLoading || product.stock === 0}
>
<FaShoppingCart />
</button>
)}
</>
)}
</div>
</div>
<Modal
title={t("common.warning")}
open={stockErrorModalVisible}
onOk={() => setStockErrorModalVisible(false)}
onCancel={() => setStockErrorModalVisible(false)}
okText={t("common.ok")}
footer={[
<button
key="ok"
onClick={() => setStockErrorModalVisible(false)}
className={styles.modalButton}
>
{t("common.ok")}
</button>,
]}
>
<p>
{t("common.not_enough_stock", {
available: product.stock,
requested: localQuantity + 1,
})}
</p>
</Modal>
</>
);
};
export default ProductCard;