mkcart / frontend /src /components /ItemSection.jsx
Kumar
update
024e9c4
import { useState, useEffect, useRef, useLayoutEffect } from "react";
import { Link } from "react-router-dom";
import { getFullApiUrl } from "../utils/apiConfig";
import { motion } from "framer-motion";
import { Heart, Eye, Star, ShoppingBag, ShoppingCart } from "lucide-react";
import "../styles/ProductList.css";
import {
useCreateWishlistItemMutation,
useDeleteWishlistItemsMutation,
useGetWishlistsQuery,
useGetCartsQuery,
useCreateCartSnapshotMutation,
useUpdateCartSnapshotMutation,
} from "../store/slices/userApiSlice";
import {
notifyCartAction,
notifyWishlistAction,
} from "../utils/notificationService";
import { useSelector } from "react-redux";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export default function ItemSection() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const {
userInfo,
loading: authLoading,
isLoggedIn,
} = useSelector((state) => state.auth);
const navigate = useNavigate();
const [createWishlistItem] = useCreateWishlistItemMutation();
const [deleteWishlistItems] = useDeleteWishlistItemsMutation();
const {
data: wishlistData,
isLoading: wishlistLoading,
refetch: refetchWishlist,
} = useGetWishlistsQuery(undefined, {
skip: !isLoggedIn,
});
const wishlistProductIds = new Set(
(wishlistData?.wishlist || []).map((p) => p.productId),
);
const { data: cartsData, refetch: refetchCarts } = useGetCartsQuery(
undefined,
{
skip: !isLoggedIn,
},
);
const [createCartSnapshot] = useCreateCartSnapshotMutation();
const [updateCartSnapshot] = useUpdateCartSnapshotMutation();
const cartItems = cartsData?.carts?.[0]?.cartItems || [];
useEffect(() => {
fetch(
`${getFullApiUrl()}/products`,
)
.then((res) => res.json())
.then((data) => {
setProducts(data.products);
setLoading(false);
})
.catch(() => {
setLoading(false);
console.error("Failed to fetch products");
});
}, []);
const groupByCategory = () => {
const grouped = {};
if (!Array.isArray(products)) return grouped;
products.forEach((product) => {
if (!grouped[product.category]) {
grouped[product.category] = [];
}
if (grouped[product.category].length < 4) {
grouped[product.category].push(product);
}
});
return grouped;
};
const groupedProducts = groupByCategory();
const toggleWishlist = async (productId, product) => {
if (authLoading) {
toast.info("Checking login status...");
return;
}
if (!isLoggedIn) {
toast.error("You must be logged in to add to wishlist!", {
autoClose: 2000,
});
setTimeout(() => navigate("/login"), 1500);
return;
}
try {
const isWishlisted = wishlistProductIds.has(productId);
if (!isWishlisted) {
await createWishlistItem({ productId }).unwrap();
notifyWishlistAction("add", product);
} else {
await deleteWishlistItems({ productIds: [productId] }).unwrap();
notifyWishlistAction("remove", product);
}
refetchWishlist();
window.dispatchEvent(new Event("wishlist-updated"));
} catch (error) {
console.error("Error toggling wishlist:", error);
notifyWishlistAction("failed", product);
}
};
const isInCart = (productId) => {
return cartItems.some((item) => item.productId === productId);
};
const getCartItemQty = (productId) => {
const item = cartItems.find((item) => item.productId === productId);
return item ? item.qty : 0;
};
const handleAddToCart = async (product) => {
if (authLoading) {
toast.info("Checking login status...");
return;
}
if (!isLoggedIn) {
toast.error("You must be logged in to add to cart!", { autoClose: 2000 });
setTimeout(() => navigate("/login"), 1500);
return;
}
try {
const existingItem = cartItems.find(
(item) => item.productId === product._id,
);
let updatedCartItems;
if (existingItem) {
updatedCartItems = cartItems.map((item) =>
item.productId === product._id
? { ...item, qty: item.qty + 1 }
: item,
);
await updateCartSnapshot({
createdAt: cartsData?.carts?.[0]?.createdAt,
update: {
cartItems: updatedCartItems,
amount: calculateCartAmount(updatedCartItems),
status: "pending",
},
});
notifyCartAction("update", product);
} else if (!cartsData?.carts || cartsData.carts.length === 0) {
updatedCartItems = [
{
productId: product._id,
name: product.name,
price: product.price,
qty: 1,
category: product.category,
seller: product.seller,
stock: product.stock,
image:
product.images && product.images[0]
? product.images[0].image
: "",
ratings: product.ratings,
},
];
await createCartSnapshot({
cart: {
cartItems: updatedCartItems,
amount: calculateCartAmount(updatedCartItems),
status: "pending",
},
});
notifyCartAction("add", product);
} else {
updatedCartItems = [
...cartItems,
{
productId: product._id,
name: product.name,
price: product.price,
qty: 1,
category: product.category,
seller: product.seller,
stock: product.stock,
image:
product.images && product.images[0]
? product.images[0].image
: "",
ratings: product.ratings,
},
];
await updateCartSnapshot({
createdAt: cartsData?.carts?.[0]?.createdAt,
update: {
cartItems: updatedCartItems,
amount: calculateCartAmount(updatedCartItems),
status: "pending",
},
});
notifyCartAction("add", product);
}
refetchCarts();
window.dispatchEvent(new Event("cart-updated"));
} catch (error) {
console.error("Error adding to cart:", error);
notifyCartAction("failed", product);
}
};
function calculateCartAmount(cartItems) {
return cartItems.reduce((acc, item) => acc + item.price * item.qty, 0);
}
const renderStars = (rating) => {
const stars = [];
const fullStars = Math.floor(rating);
const decimalPart = rating - fullStars;
for (let i = 0; i < 5; i++) {
if (i < fullStars) {
stars.push(<Star key={i} size={16} className="star filled" />);
} else if (i === fullStars && decimalPart > 0) {
stars.push(<Star key={i} size={16} className="star half-filled" />);
} else {
stars.push(<Star key={i} size={16} className="star empty" />);
}
}
return stars;
};
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
if (loading) {
return (
<>
<div className="item-section-loading">
<div className="Premium-loading-container">
<div className="cosmic-loader">
<div className="loader-rings">
{[...Array(4)].map((_, i) => (
<motion.div
key={i}
className="loader-ring"
animate={{ rotate: 360 }}
transition={{
duration: 2 - i * 0.2,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
/>
))}
</div>
<motion.div
className="loader-text"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY }}
>
<span>Loading Products...</span>
</motion.div>
</div>
</div>
</div>
</>
);
}
return (
<>
<div className="modern-item-section">
{ }
<motion.div
className="hero-section"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
{ }
<div className="hero-floating-animations">
{[...Array(10)].map((_, i) => (
<motion.div
key={i}
className="hero-float-sparkle"
initial={{ opacity: 0, scale: 0.7, y: 0 }}
animate={{
opacity: [0.2, 1, 0.2],
scale: [0.7, 1.2, 0.7],
y: [0, -30, 0],
rotate: [0, 360],
}}
transition={{
duration:
(isMobile ? 10 : 4) + Math.random() * (isMobile ? 4 : 2),
repeat: Infinity,
delay: i * 0.3,
ease: "easeInOut",
}}
style={{
left: `${10 + Math.random() * 80}%`,
top: `${10 + Math.random() * 60}%`,
}}
>
<span role="img" aria-label="sparkle">
</span>
</motion.div>
))}
</div>
<div className="hero-content">
<motion.h1
className="hero-title gradient-text"
style={{
background: "linear-gradient(to right,#d53369,#cbad6d)",
backgroundImage:
"-webkit-linear-gradient(to right,#d53369,#cbad6d)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
initial={{ opacity: 0, y: 2 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
Discover Amazing Products
</motion.h1>
<motion.p
className="hero-subtitle"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
Explore our curated collection of Premium products
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
<Link to="/productlist" className="hero-cta">
<ShoppingBag size={20} />
<span>View All Products</span>
</Link>
</motion.div>
</div>
</motion.div>
{ }
{Object.keys(groupedProducts).map((category, categoryIndex) => (
<motion.div
key={category}
className="category-section"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: categoryIndex * 0.2 }}
>
<div className="category-header">
<h2
className="category-title gradient-text"
style={{
background: "linear-gradient(to right,#d38312,#a83279)",
backgroundImage:
"-webkit-linear-gradient(to right,#d38312,#a83279)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
transform: "scale(1.1)",
}}
>
{category}
</h2>
<div className="category-line"></div>
</div>
<div className="products-grid grid">
{(groupedProducts[category] || []).map((product) => (
<div className="modern-product-card" key={product._id}>
<div className="product-card-inner">
{ }
<div className="product-image-container">
<Link
to={`/product/${product._id}`}
aria-label={`View details for ${product.name}`}
>
<motion.img
src={product.images?.[0]?.image || "/placeholder.svg"}
alt={product.name || "Product Image"}
className="product-image"
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.3 }}
/>
</Link>
{ }
<motion.button
className={`wishlist-btn ${wishlistProductIds.has(product._id) ? "active" : ""}`}
onClick={() => toggleWishlist(product._id, product)}
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.8 }}
>
<Heart
size={18}
fill={
wishlistProductIds.has(product._id)
? "#ff4757"
: "none"
}
/>
</motion.button>
{ }
<motion.div
className="quick-view-btn"
whileHover={{ scale: 1.1 }}
>
<Link to={`/product/${product._id}`}>
<Eye size={18} />
</Link>
</motion.div>
</div>
{ }
<div className="product-info">
<h1 className="product-title">
<Link to={`/product/${product._id}`}>
{product.name}
</Link>
</h1>
<div className="product-rating">
<div className="stars">
{renderStars(
typeof product.ratings === "number"
? product.ratings
: 0,
)}
</div>
<span className="rating-text">
(
{typeof product.ratings === "number"
? product.ratings.toFixed(1)
: "0.0"}
)
</span>
</div>
<div className="product-price-center">
<span className="currency"></span>
<span
className="amount"
style={{ color: "#00b894", fontWeight: 700 }}
>
{typeof product.price === "number" &&
!isNaN(product.price)
? product.price.toLocaleString()
: "N/A"}
</span>
</div>
<div className="info-row">
<div className="product-description-snippet">
{window.innerWidth <= 600
? product.description.length > 30
? product.description.slice(0, 25) + "..."
: product.description
: product.description.slice(0, 50) +
(product.description.length > 50 ? "..." : "")}
</div>
<motion.div
whileHover={{ scale: 1.08 }}
whileTap={{ scale: 0.96 }}
>
<Link
to={`/product/${product._id}`}
className="view-details-btn-small"
>
<span>View</span>
</Link>
</motion.div>
</div>
{ }
<div className="item-actions">
<motion.button
className={`add-to-cart-btn-premium${isInCart(product._id) ? " in-cart" : ""}`}
onClick={() => handleAddToCart(product)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<ShoppingCart size={18} />
{isInCart(product._id) ? (
<span style={{ marginLeft: 4 }}>
In Cart ({getCartItemQty(product._id)})
</span>
) : (
<span style={{ marginLeft: 4 }}>Add to Cart</span>
)}
<div className="btn-glow"></div>
</motion.button>
</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
))}
</div>
</>
);
}