| 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> |
| </> |
| ); |
| } |
|
|