first commit

This commit is contained in:
2026-03-10 14:47:34 +05:30
commit c504876102
198 changed files with 17343 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import React from "react";
import { Bell } from "lucide-react";
import { Link } from "react-router-dom";
const BellIcon = ({ notificationCount = 0 }) => {
return (
<Link to="/notifications" className="relative">
<Bell className="w-6 h-6 text-gray-700 hover:text-primary transition" />
{notificationCount > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
{notificationCount}
</span>
)}
</Link>
);
};
export default BellIcon;

View File

@@ -0,0 +1,46 @@
// import React from "react";
// import { ShoppingCart } from "lucide-react";
// import { Link } from "react-router-dom";
// const CartIcon = ({ itemCount = 0 }) => {
// return (
// <Link to="/cart" className="relative">
// <ShoppingCart className="w-6 h-6 text-gray-700 hover:text-primary transition" />
// {itemCount > 0 && (
// <span className="absolute -top-2 -right-2 bg-primary text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
// {itemCount}
// </span>
// )}
// </Link>
// );
// };
// export default CartIcon;
import { ShoppingCart } from "lucide-react";
import { Link } from "react-router-dom";
import { useGetCartItemsQuery } from "../../features/cart/cartAPI";
const CartIcon = ({ className = "" }) => {
const { data } = useGetCartItemsQuery();
const cartCount = data?.data?.items?.length || 0;
return (
<Link to="/cart" className="relative">
<ShoppingCart
className={`w-6 h-6 text-gray-700 hover:text-rose-500 ${className}`}
/>
{cartCount > 0 && (
<span className="absolute -top-2 -right-2 bg-rose-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</Link>
);
};
export default CartIcon;

View File

@@ -0,0 +1,21 @@
import { Link } from "react-router-dom";
const Logo = () => (
<Link to="/">
{/* Desktop Full Logo */}
<img
src="/logo.png"
alt="E-Shop Logo"
className="hidden md:block h-36 w-auto"
/>
{/* Mobile Small Logo */}
<img
src="/logo-small.png"
alt="E-Shop Logo"
className="block md:hidden h-8 w-auto"
/>
</Link>
);
export default Logo;

View File

@@ -0,0 +1,144 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { useGetCategoryTreeQuery } from "../../features/categories/categoryApi";
const navItems = [
{ id: "home", label: "Home", href: "/" },
{ id: "collections", label: "Collections", href: "#", hasDropdown: true },
{ id: "wardrobe", label: "My Wardrobe", href: "/wardrobe" },
{ id: "calendar", label: "Style Calendar", href: "#" },
{ id: "trending", label: "Trending", href: "/trending" },
];
// const megaMenu = {
// men: [
// { title: "Casual Wear", items: ["T-Shirts", "Polos", "Shirts", "Jackets"] },
// { title: "Formal Wear", items: ["Shirts", "Trousers", "Blazers"] },
// {
// title: "Indian & Festive",
// items: ["Kurtas", "Kurta Sets", "Nehru Jackets"],
// },
// { title: "Winterwear", items: ["Sweaters", "Jackets", "Thermals"] },
// ],
// women: [
// { title: "Westernwear", items: ["Dresses", "Tops", "Shrugs", "Jeans"] },
// // { title: "Indian & Fusion", items: ["Kurtas", "Ethnic Skirts", "Sarees"] },
// {
// title: "Indian & Fusion",
// items: [
// { label: "Sarees", href: "/sarees" },
// { label: "Badhni", href: "/badhni" },
// { label: "Patola", href: "/patola" },
// { label: "Lehengas", href: "/lehenga" },
// ],
// },
// { title: "Winterwear", items: ["Sweaters", "Thermals"] },
// { title: "Athleisure", items: ["Sports Bra", "Tights", "Track Pants"] },
// ],
// kids: [
// { title: "Girls", items: ["Dresses", "T-Shirts", "Jeans", "Ethnic Wear"] },
// { title: "Boys", items: ["Shirts", "Shorts", "Jeans", "Winterwear"] },
// ],
// accessories: [
// { title: "Footwear", items: ["Shoes", "Sandals"] },
// { title: "Accessories", items: ["Belts", "Wallets", "Caps"] },
// ],
// };
const MenuLinks = () => {
// const [megaMenu, setMegaMenu] = useState(null);
const [activeDropdown, setActiveDropdown] = useState(null);
// const [activeTab, setActiveTab] = useState("men");
const { data } = useGetCategoryTreeQuery();
const categories = data?.data || [];
const [activeTab, setActiveTab] = useState("men");
// find active category object
const activeCategory = categories.find((cat) => cat.slug === activeTab);
return (
<div className="hidden md:flex items-center space-x-8 font-medium">
{navItems.map((item) => (
<div
key={item.id}
className="relative"
onMouseEnter={() => item.hasDropdown && setActiveDropdown(item.id)}
onMouseLeave={() => item.hasDropdown && setActiveDropdown(null)}
>
<Link
to={item.href}
className="relative text-gray-700 hover:text-primary-default transition font-medium"
>
{item.label}
<span className="absolute left-0 -bottom-1 w-0 h-0.5 bg-primary-default transition-all duration-300 group-hover:w-full"></span>
</Link>
{/* Dropdown */}
{/* Dropdown */}
{item.hasDropdown && activeDropdown === item.id && (
<>
{/* Invisible Hover Bridge (prevents dropdown from closing) */}
<div className="absolute left-0 top-full w-full h-4"></div>
<div
className="
absolute left-0 top-[100%] mt-4 w-[950px]
bg-white/70 backdrop-blur-xl
rounded-2xl border border-gray-200
shadow-[0_12px_40px_rgba(0,0,0,0.12)]
p-6 z-50 animate-slideDownFade
"
>
{/* Tabs */}
<div className="flex space-x-6 border-b pb-3">
{categories.map((cat) => (
<button
key={cat.id}
onMouseEnter={() => setActiveTab(cat.slug)}
className={`capitalize pb-2 px-2 font-semibold text-sm
${
activeTab === cat.slug
? "text-primary-default border-b-2 border-primary-default"
: "text-gray-500 hover:text-primary-default"
}`}
>
{cat.name}
</button>
))}
</div>
{/* Mega Menu Grid */}
<div className="grid grid-cols-4 gap-8 pt-5">
{activeCategory?.children?.map((section) => (
<div key={section.id}>
<h4 className="text-gray-800 font-semibold mb-3 text-sm">
{section.name}
</h4>
<ul className="space-y-2">
{section.children?.map((sub) => (
<li key={sub.id}>
<Link
to={`/product/${activeCategory.slug}/${section.slug}/${sub.slug}`}
className="text-gray-600 hover:text-primary-default text-sm"
>
{sub.name}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</>
)}
</div>
))}
</div>
);
};
export default MenuLinks;

View File

@@ -0,0 +1,132 @@
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { Link } from "react-router-dom";
const MobileMenu = ({ isOpen, closeMenu, navItems, megaMenu }) => {
const [openSection, setOpenSection] = useState(null);
if (!isOpen) return null;
const toggle = (key) => {
setOpenSection(openSection === key ? null : key);
};
return (
<div className="md:hidden bg-white shadow-lg border-t z-50">
<div className="px-4 py-3 space-y-2">
{/* MAIN NAV ITEMS */}
{navItems.map((item) => (
<div key={item.id}>
{/* If dropdown exists */}
{item.hasDropdown ? (
<>
<button
onClick={() => toggle(item.id)}
className="w-full flex justify-between items-center py-3 text-left font-semibold text-gray-900"
>
{item.label}
{openSection === item.id ? (
<ChevronUp size={20} />
) : (
<ChevronDown size={20} />
)}
</button>
{/* DROPDOWN MEGA MENU */}
<div
className={`transition-all overflow-hidden duration-300 ${
openSection === item.id
? "max-h-[700px] opacity-100"
: "max-h-0 opacity-0"
}`}
>
{/* MEN */}
<AccordionBlock
title="Men"
items={megaMenu.men}
closeMenu={closeMenu}
/>
{/* WOMEN */}
<AccordionBlock
title="Women"
items={megaMenu.women}
closeMenu={closeMenu}
/>
{/* KIDS */}
<AccordionBlock
title="Kids"
items={megaMenu.kids}
closeMenu={closeMenu}
/>
{/* ACCESSORIES */}
<AccordionBlock
title="Accessories"
items={megaMenu.accessories}
closeMenu={closeMenu}
/>
</div>
</>
) : (
<Link
to={item.href}
onClick={closeMenu}
className="block py-3 font-semibold text-gray-900 rounded hover:bg-gray-100"
>
{item.label}
</Link>
)}
</div>
))}
</div>
</div>
);
};
export default MobileMenu;
/* ---------------------------------------------
Sub Menu Accordion (MEN / WOMEN / KIDS...)
---------------------------------------------- */
const AccordionBlock = ({ title, items, closeMenu }) => {
const [open, setOpen] = useState(false);
return (
<div className="border-b">
<button
onClick={() => setOpen(!open)}
className="w-full flex justify-between items-center py-2 font-medium text-gray-800"
>
{title}
{open ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
</button>
<div
className={`transition-all duration-300 overflow-hidden ${
open ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
}`}
>
{items.map((block, idx) => (
<div key={idx} className="pl-4 py-2">
<h4 className="font-semibold text-gray-700">{block.title}</h4>
<div className="mt-1 space-y-1">
{block.items.map((item, index) => (
<Link
key={index}
to={item.href}
onClick={closeMenu}
className="block text-sm text-gray-600 hover:text-rose-500"
>
{item.label}
</Link>
))}
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,145 @@
import React, { useState } from "react";
import Logo from "./Logo";
import MenuLinks from "./MenuLinks";
import CartIcon from "./CartIcon";
import UserIcon from "./UserIcon";
import BellIcon from "./BellIcon";
import MobileMenu from "./MobileMenu";
import SearchBar from "./SearchBar";
import TopPromoBanner from "./TopPromoBanner";
import { Menu, X, Heart } from "lucide-react";
import { Link } from "react-router-dom";
import { useGetCategoryTreeQuery } from "../../features/categories/categoryApi";
import { formatMegaMenu } from "../../utils/formatMegaMenu";
// ----------------------------
// NAV ITEMS
// ----------------------------
const navItems = [
{ id: "home", label: "Home", href: "/" },
{ id: "collections", label: "Collections", href: "#", hasDropdown: true },
{ id: "wardrobe", label: "My Wardrobe", href: "/wardrobe" },
{ id: "calendar", label: "Style Calendar", href: "#" },
{ id: "trending", label: "Trending", href: "#" },
];
// ----------------------------
// MEGA MENU DATA
// ----------------------------
// const megaMenu = {
// men: [
// { title: "Casual Wear", items: ["T-Shirts", "Polos", "Shirts", "Jackets"] },
// { title: "Formal Wear", items: ["Shirts", "Trousers", "Blazers"] },
// {
// title: "Indian & Festive",
// items: ["Kurtas", "Kurta Sets", "Nehru Jackets"],
// },
// { title: "Winterwear", items: ["Sweaters", "Jackets", "Thermals"] },
// ],
// women: [
// { title: "Westernwear", items: ["Dresses", "Tops", "Shrugs", "Jeans"] },
// {
// title: "Indian & Fusion",
// items: [
// { label: "Sarees", href: "/sarees" },
// { label: "Badhni", href: "/badhni" },
// { label: "Patola", href: "/patola" },
// { label: "Lehengas", href: "/lehenga" },
// ],
// },
// { title: "Winterwear", items: ["Sweaters", "Thermals"] },
// { title: "Athleisure", items: ["Sports Bra", "Tights", "Track Pants"] },
// ],
// kids: [
// { title: "Girls", items: ["Dresses", "T-Shirts", "Jeans", "Ethnic Wear"] },
// { title: "Boys", items: ["Shirts", "Shorts", "Jeans", "Winterwear"] },
// ],
// accessories: [
// { title: "Footwear", items: ["Shoes", "Sandals"] },
// { title: "Accessories", items: ["Belts", "Wallets", "Caps"] },
// ],
// };
const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false);
const toggleMenu = () => setMenuOpen(!menuOpen);
// const [menuOpen, setMenuOpen] = useState(false);
const { data } = useGetCategoryTreeQuery();
const megaMenu = data ? formatMegaMenu(data.data) : {};
// const toggleMenu = () => setMenuOpen(!menuOpen);
return (
<nav className="bg-white shadow-md fixed w-full z-50">
<TopPromoBanner />
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex-shrink-0">
<Logo />
</div>
{/* Desktop Menu + Search */}
<div className="hidden md:flex flex-1 items-center justify-between mx-4">
<MenuLinks />
<SearchBar />
</div>
{/* Icons */}
<div className="flex items-center space-x-3 md:space-x-4">
{/* Desktop Icons */}
<div className="hidden md:flex items-center space-x-5 h-10">
<Link to="/wishlist">
<Heart className="w-6 h-6 cursor-pointer text-gray-700 hover:text-rose-500" />
</Link>
<BellIcon notificationCount={3} />
<Link to="/cart">
<CartIcon className="w-6 h-6 cursor-pointer text-gray-700 hover:text-rose-500" />
</Link>
<UserIcon />
</div>
{/* Mobile */}
<div className="flex md:hidden items-center space-x-4 h-10">
<Link to="/wishlist">
<Heart className="w-5 h-5 text-gray-700 hover:text-rose-500 cursor-pointer" />
</Link>
<CartIcon />
<UserIcon />
<BellIcon notificationCount={3} />
<button onClick={toggleMenu}>
{menuOpen ? (
<X className="w-6 h-6 text-gray-700" />
) : (
<Menu className="w-6 h-6 text-gray-700" />
)}
</button>
</div>
{/* Mobile Icons */}
</div>
</div>
</div>
{/* Mobile Menu */}
<MobileMenu
isOpen={menuOpen}
closeMenu={() => setMenuOpen(false)}
navItems={navItems}
megaMenu={megaMenu}
/>
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,20 @@
import { Search } from "lucide-react";
const SearchBar = () => {
return (
<div className="flex flex-1 max-w-sm mx-4">
<div className="relative w-full">
<input
type="text"
placeholder="Search products, brands..."
className="w-full h-10 pl-10 pr-4 rounded-full border border-gray-300 text-sm sm:text-base
focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary
transition-shadow duration-300 shadow-sm hover:shadow-md"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
);
};
export default SearchBar;

View File

@@ -0,0 +1,124 @@
// import React, { useState, useEffect } from "react";
// import { Sparkles, Play, HelpCircle, ChevronRight } from "lucide-react";
// const lines = [
// "🎉 Limited Time Offer Extra 15% OFF!",
// "🔥 Shop New Winter Collection Now!",
// "💝 Use Code: FASHION15 & Save More!",
// ];
// const TopPromoBanner = () => {
// const [current, setCurrent] = useState(0);
// useEffect(() => {
// const interval = setInterval(() => {
// setCurrent((prev) => (prev + 1) % lines.length);
// }, 2500);
// return () => clearInterval(interval);
// }, []);
// return (
// <div className="relative w-full bg-gradient-to-r from-pink-600 via-rose-500 to-pink-600 text-white py-2 shadow-md z-50 overflow-hidden">
// {/* Shiny moving light */}
// <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shine" />
// <div className="relative max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between px-4 gap-2">
// {/* LEFT — Rotating Text */}
// <div className="flex items-center gap-2 flex-1 min-w-0">
// <Sparkles className="w-4 h-4 text-yellow-300 animate-pulse flex-shrink-0" />
// <p className="text-xs sm:text-sm md:text-base font-medium truncate">
// {lines[current]}
// </p>
// </div>
// {/* RIGHT — Icons / Menu */}
// <div className="flex flex-wrap sm:flex-nowrap items-center gap-4 text-xs sm:text-sm font-medium justify-center sm:justify-end w-full sm:w-auto">
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// First Citizen Club <ChevronRight size={12} />
// </button>
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// <HelpCircle size={14} />
// Help & Support
// </button>
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// <Play size={14} />
// App Download
// </button>
// </div>
// </div>
// </div>
// );
// };
// export default TopPromoBanner;
import React, { useState, useEffect } from "react";
import { Sparkles, Play, HelpCircle, ChevronRight } from "lucide-react";
const lines = [
"🎉 Limited Time Offer Extra 15% OFF!",
"🔥 Shop New Winter Collection Now!",
"💝 Use Code: FASHION15 & Save More!",
];
const TopPromoBanner = () => {
const [current, setCurrent] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrent((prev) => (prev + 1) % lines.length);
}, 2500);
return () => clearInterval(interval);
}, []);
return (
<div className="relative w-full bg-gradient-to-r from-pink-600 via-rose-500 to-pink-600 text-white py-2 shadow-md z-50 overflow-hidden">
{/* Shiny moving light */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shine" />
<div className="relative max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-center sm:justify-between px-4 gap-2">
{/* LEFT — Rotating Text */}
<div className="flex items-center gap-2 flex-1 min-w-0">
<Sparkles className="w-4 h-4 text-yellow-300 animate-pulse flex-shrink-0" />
<p className="text-xs sm:text-sm md:text-base font-medium truncate">
{lines[current]}
</p>
</div>
{/* RIGHT — MOBILE: ONLY ICONS */}
<div className="flex items-center gap-4 text-xs sm:hidden">
<Sparkles size={16} className="text-yellow-200" />
<HelpCircle size={16} />
<Play size={16} />
</div>
{/* RIGHT — DESKTOP: FULL TEXT MENU */}
<div className="hidden sm:flex flex-wrap sm:flex-nowrap items-center gap-4 text-xs sm:text-sm font-medium justify-end">
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
First Citizen Club <ChevronRight size={12} />
</button>
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
<HelpCircle size={14} />
Help & Support
</button>
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
<Play size={14} />
App Download
</button>
</div>
</div>
</div>
);
};
export default TopPromoBanner;

View File

@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { User } from "lucide-react";
import { useSelector, useDispatch } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { logout } from "../../features/auth/authSlice";
const UserIcon = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { user } = useSelector((state) => state.auth);
const [open, setOpen] = useState(false);
const handleLogout = () => {
dispatch(logout());
localStorage.removeItem("token");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
setOpen(false);
};
const handleUserClick = () => {
if (!user) {
navigate("/login");
return;
}
setOpen(!open);
};
return (
<div className="relative">
{/* CLICKABLE SECTION */}
<div
onClick={handleUserClick}
className="cursor-pointer flex items-center space-x-1"
>
<User className="w-6 h-6 text-gray-700 hover:text-primary" />
{user && (
<span className="text-sm font-medium text-gray-700 hover:text-primary">
{user.name}
</span>
)}
</div>
{/* DROPDOWN */}
{user && open && (
<div className="absolute right-0 mt-3 w-52 bg-white shadow-xl rounded-lg border border-gray-200 z-50 animate-fade-in">
<div className="px-4 py-3 border-b border-gray-200">
<p className="text-gray-900 font-semibold">
{`${user.firstName || ""} ${user.lastName || ""}`.trim()}
</p>
</div>
<Link
to="/profile"
className="block px-4 py-3 text-gray-700 hover:bg-primary-default hover:text-white transition"
>
Profile
</Link>
<Link
to="/my-orders"
className="block px-4 py-3 text-gray-700 hover:bg-primary-default hover:text-white transition"
>
My Order
</Link>
<button
onClick={handleLogout}
className="w-full text-left px-4 py-3 text-gray-700 hover:bg-red-500 hover:text-white transition"
>
Logout
</button>
</div>
)}
</div>
);
};
export default UserIcon;

View File

@@ -0,0 +1,353 @@
// import React, { useState } from "react";
// import {
// useGetWishlistQuery,
// useRemoveFromWishlistMutation,
// } from "../../features/wishlist/wishlistApi";
// import WishlistEmpty from "./WishlistEmpty";
// import AddToCart from "../../pages/Cart/AddToCart";
// import { useAddToCartMutation } from "../../features/cart/cartAPI";
// import { useNavigate } from "react-router-dom";
// const Wishlist = () => {
// const navigate = useNavigate();
// const [addToCart] = useAddToCartMutation();
// const { data, isLoading, isError } = useGetWishlistQuery();
// const [removeFromWishlist] = useRemoveFromWishlistMutation();
// const [confirmModal, setConfirmModal] = useState({
// show: false,
// productId: null,
// productName: "",
// });
// const wishlistItems = data?.data?.wishlist || [];
// // Loading
// if (isLoading)
// return (
// <p className="text-center mt-20 text-xl font-medium text-gray-600">
// Loading your wishlist...
// </p>
// );
// if (isError) return <WishlistEmpty />;
// // Empty
// if (wishlistItems.length === 0) return <WishlistEmpty />;
// const handleRemove = async (productId) => {
// try {
// await removeFromWishlist(productId).unwrap();
// setConfirmModal({ show: false, productId: null, productName: "" });
// } catch (error) {
// console.log("Remove failed", error);
// }
// };
// const fallbackImage = "/default-product.png";
// return (
// <div className="max-w-7xl mx-auto px-4 py-12 mt-24 bg-gradient-to-br from-gray-50 to-gray-100">
// <h1 className="text-2xl font-extrabold mb-10 text-center text-gray-800 tracking-tight">
// My Wishlist
// </h1>
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// {wishlistItems.map((item) => {
// const image =
// item?.product?.images?.gallery?.[0] ||
// item?.product?.variants?.[0]?.images?.[0] ||
// fallbackImage;
// return (
// <div
// key={item.id}
// className="bg-white rounded-2xl shadow-md hover:shadow-lg flex flex-col"
// >
// {/* Product Image */}
// <div className="relative h-44 bg-gray-100 overflow-hidden group">
// <img
// src={image}
// onError={(e) => (e.target.src = fallbackImage)}
// alt={item?.product?.name || "Product"}
// className="object-cover h-full w-full transition-transform duration-500 group-hover:scale-105"
// />
// {/* Remove Button */}
// <button
// onClick={() =>
// setConfirmModal({
// show: true,
// productId: item.productId,
// productName: item?.product?.name || "this product",
// })
// }
// className="absolute top-3 right-3 bg-white shadow p-1.5 rounded-full hover:bg-red-500 hover:text-white transition-all text-sm"
// >
// ✖
// </button>
// </div>
// {/* Product Info */}
// <div className="p-4 flex flex-col flex-1">
// <h2 className="text-md font-semibold text-gray-800 mb-1 truncate">
// {item?.product?.name || "Unknown Product"}
// </h2>
// {/* Price */}
// {item?.product?.basePrice && (
// <p className="text-lg font-bold text-primary-dark mb-1">
// ₹ {item.product.basePrice.toLocaleString()}
// </p>
// )}
// {/* Added Date */}
// <p className="text-gray-500 text-xs mb-3">
// Added on{" "}
// <span className="font-medium">
// {new Date(item.createdAt).toLocaleDateString()}
// </span>
// </p>
// {/* Add To Cart Component */}
// {/* Buttons */}
// <div className="mt-auto flex justify-end w-full">
// <button
// className="text-primary-default font-semibold text-sm underline hover:text-primary-dark transition-colors"
// onClick={async () => {
// const token = localStorage.getItem("token");
// if (!token) {
// alert("Please login first to add product to cart");
// navigate("/login");
// return;
// }
// try {
// // 1⃣ Add to Cart API call
// await addToCart({
// productId: item.productId,
// quantity: 1,
// }).unwrap();
// // 2⃣ Remove from Wishlist API call
// await removeFromWishlist(item.productId).unwrap();
// // 3⃣ Redirect to Cart page
// navigate("/cart");
// } catch (err) {
// console.log("Error:", err);
// }
// }}
// >
// Add To Bag
// </button>
// </div>
// </div>
// </div>
// );
// })}
// </div>
// {/* Confirmation Modal */}
// {confirmModal.show && (
// <div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
// <div className="bg-white rounded-xl shadow-lg p-8 w-96 sm:w-4/5 max-w-md text-center">
// <h3 className="text-base font-semibold mb-6">
// Are you sure you want to delete <br />
// <span className="text-amber-800">
// {confirmModal.productName}
// </span>{" "}
// from your wishlist?
// </h3>
// <div className="flex justify-center gap-6 mt-6">
// <button
// onClick={() => handleRemove(confirmModal.productId)}
// className="px-6 py-3 bg-primary-default text-white rounded-lg font-semibold hover:bg-primary-dark transition"
// >
// Yes
// </button>
// <button
// onClick={() =>
// setConfirmModal({
// show: false,
// productId: null,
// productName: "",
// })
// }
// className="px-6 py-3 bg-gray-300 text-gray-800 rounded-lg font-semibold hover:bg-gray-400 transition"
// >
// No
// </button>
// </div>
// </div>
// </div>
// )}
// </div>
// );
// };
// export default Wishlist;
import React, { useState } from "react";
import {
useGetWishlistQuery,
useRemoveFromWishlistMutation,
} from "../../features/wishlist/wishlistApi";
import { useAddToCartMutation } from "../../features/cart/cartAPI";
import WishlistEmpty from "./WishlistEmpty";
import { useNavigate } from "react-router-dom";
const Wishlist = () => {
const navigate = useNavigate();
const { data, isLoading, isError } = useGetWishlistQuery();
const [addToCart] = useAddToCartMutation();
const [removeFromWishlist] = useRemoveFromWishlistMutation();
const [confirm, setConfirm] = useState(null);
const wishlist = data?.data?.wishlist || [];
const fallbackImage = "/default-product.png";
if (isLoading) {
return (
<div className="mt-32 text-center text-gray-500 text-lg">
Loading wishlist
</div>
);
}
if (isError || wishlist.length === 0) {
return <WishlistEmpty />;
}
const handleAddToCart = async (item) => {
const token = localStorage.getItem("token");
if (!token) {
alert("Please login first to add product to cart");
navigate("/login");
return;
}
try {
await addToCart({
productId: item.productId,
quantity: 1,
}).unwrap();
await removeFromWishlist(item.productId).unwrap();
navigate("/cart");
} catch (err) {
console.error(err);
}
};
return (
<div className="max-w-7xl mx-auto px-4 py-12 mt-32 bg-gradient-to-br from-gray-50 to-gray-100">
<h1 className="text-2xl font-semibold text-gray-600 mb-10">
My Wishlist ({wishlist.length} items)
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{wishlist.map((item) => {
const image =
item?.product?.images?.gallery?.[0] ||
item?.product?.variants?.[0]?.images?.[0] ||
fallbackImage;
return (
<div
key={item.id}
className="border border-gray-200 rounded-lg bg-white"
>
{/* Image */}
<div className="relative h-56 bg-gray-50">
<img
src={image}
onError={(e) => (e.target.src = fallbackImage)}
alt={item?.product?.name}
className="h-full w-full object-contain p-6"
/>
{/* Remove */}
<button
onClick={() => setConfirm(item)}
className="absolute top-3 right-3 text-gray-400
hover:text-gray-700 text-sm"
>
Remove
</button>
</div>
{/* Content */}
<div className="px-5 py-4 space-y-2">
<p className="text-sm text-gray-500">
{item?.product?.category || "Product"}
</p>
<h2 className="text-base font-medium text-gray-900 line-clamp-2">
{item?.product?.name}
</h2>
<div className="flex items-center justify-between pt-3">
<p className="text-lg font-semibold text-gray-900">
{item?.product?.basePrice?.toLocaleString()}
</p>
<button
onClick={() => handleAddToCart(item)}
className="px-4 py-2 text-sm font-medium
border border-gray-900
text-gray-900
hover:bg-gray-900 hover:text-white
transition"
>
Add to Cart
</button>
</div>
</div>
</div>
);
})}
</div>
{/* Confirm Modal */}
{confirm && (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="bg-white w-96 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Remove item?
</h3>
<p className="text-sm text-gray-500 mb-6">
Are you sure you want to remove{" "}
<span className="font-medium text-gray-900">
{confirm.product?.name}
</span>{" "}
from your wishlist?
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setConfirm(null)}
className="px-4 py-2 text-sm border border-gray-300"
>
Cancel
</button>
<button
onClick={async () => {
await removeFromWishlist(confirm.productId).unwrap();
setConfirm(null);
}}
className="px-4 py-2 text-sm bg-gray-900 text-white"
>
Remove
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Wishlist;

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Link } from "react-router-dom";
import Button from "../common/Button";
const WishlistEmpty = () => {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 px-4 py-24 mt-20">
<img
src="/empty-wishlist.png"
alt="Empty Wishlist"
className="w-36 md:w-36 mb-6"
/>
<h1 className="text-3xl font-bold text-gray-800 mb-2 text-center">
Your Wishlist is Empty
</h1>
<p className="text-gray-500 mb-6 text-center max-w-sm">
Tap the heart icon on products to save them for later.
</p>
<Link to="/">
<Button>Shop Now</Button>
</Link>
</div>
);
};
export default WishlistEmpty;