first commit
This commit is contained in:
18
src/components/navbar/BellIcon.jsx
Normal file
18
src/components/navbar/BellIcon.jsx
Normal 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;
|
||||
46
src/components/navbar/CartIcon.jsx
Normal file
46
src/components/navbar/CartIcon.jsx
Normal 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;
|
||||
21
src/components/navbar/Logo.jsx
Normal file
21
src/components/navbar/Logo.jsx
Normal 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;
|
||||
144
src/components/navbar/MenuLinks.jsx
Normal file
144
src/components/navbar/MenuLinks.jsx
Normal 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;
|
||||
132
src/components/navbar/MobileMenu.jsx
Normal file
132
src/components/navbar/MobileMenu.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
145
src/components/navbar/Navbar.jsx
Normal file
145
src/components/navbar/Navbar.jsx
Normal 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;
|
||||
20
src/components/navbar/SearchBar.jsx
Normal file
20
src/components/navbar/SearchBar.jsx
Normal 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;
|
||||
124
src/components/navbar/TopPromoBanner.jsx
Normal file
124
src/components/navbar/TopPromoBanner.jsx
Normal 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;
|
||||
80
src/components/navbar/UserIcon.jsx
Normal file
80
src/components/navbar/UserIcon.jsx
Normal 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;
|
||||
353
src/components/navbar/Wishlist.jsx
Normal file
353
src/components/navbar/Wishlist.jsx
Normal 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;
|
||||
26
src/components/navbar/WishlistEmpty.jsx
Normal file
26
src/components/navbar/WishlistEmpty.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user