From 757f167845d03aad91eea79ff5d0bc0a3b3b0823 Mon Sep 17 00:00:00 2001 From: Aditi Date: Mon, 22 Jun 2026 03:37:55 +0530 Subject: [PATCH] feat: redesign header with premium responsive UI --- FRONTEND/src/components/ui/Navbar.jsx | 667 ++++++++++++++------------ 1 file changed, 361 insertions(+), 306 deletions(-) diff --git a/FRONTEND/src/components/ui/Navbar.jsx b/FRONTEND/src/components/ui/Navbar.jsx index a4b1692..8b8e10a 100644 --- a/FRONTEND/src/components/ui/Navbar.jsx +++ b/FRONTEND/src/components/ui/Navbar.jsx @@ -7,9 +7,9 @@ import { import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { LanguageSwitcher } from '../LanguageSwitcher.jsx'; -import { PlusSquareIcon } from "@chakra-ui/icons" +import { PlusSquareIcon } from "@chakra-ui/icons"; import { IoMoon } from "react-icons/io5"; -import { LuSun, LuShoppingCart, LuHeart } from "react-icons/lu"; +import { LuSun, LuShoppingCart, LuHeart, LuSearch } from "react-icons/lu"; import { useCart } from "../../store/cart"; import { useWishlist } from "../../context/WishlistContext.jsx"; import { useProductStore } from "../../store/product"; @@ -33,32 +33,46 @@ const Navbar = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [isCheckoutLoading, setIsCheckoutLoading] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); const totalItemsCount = cartItems.reduce((acc, item) => acc + item.quantity, 0); - const navBg = useColorModeValue("white", "gray.800"); - const border = useColorModeValue("gray.200", "gray.700"); - const labelColor = useColorModeValue("gray.600", "gray.300"); - const mobileInputBg = useColorModeValue("gray.50", "gray.700"); - const mobileInputBorder = useColorModeValue("gray.200", "gray.600"); - const searchBg = useColorModeValue("gray.50", "gray.700"); - const searchBorder = useColorModeValue("gray.200", "gray.600"); + // --- Colour tokens matched to the site's cyan/blue palette --- + const navBg = useColorModeValue("rgba(255,255,255,0.97)", "rgba(17,24,39,0.97)"); + const border = useColorModeValue("blue.100", "blue.900"); + const labelColor = useColorModeValue("gray.500", "gray.400"); + const iconColor = useColorModeValue("gray.600", "gray.300"); + const iconHoverBg = useColorModeValue("cyan.50", "blue.900"); + const iconHoverColor= useColorModeValue("blue.600", "cyan.300"); + const searchBg = useColorModeValue("gray.50", "gray.800"); + const searchBorder = useColorModeValue("blue.100", "blue.800"); + const dividerColor = useColorModeValue("blue.100", "blue.800"); + const cartItemBorder= useColorModeValue("blue.50", "blue.900"); + const drawerBg = useColorModeValue("white", "gray.900"); + const drawerText = useColorModeValue("gray.900", "white"); + // Announcement bar uses the site's blue.500 → cyan.400 gradient direction + const currencyBg = useColorModeValue("white", "gray.800"); + const currencyColor = useColorModeValue("blue.700", "cyan.300"); + const currencyBorder= useColorModeValue("blue.200", "blue.700"); + const totalColor = useColorModeValue("cyan.500", "cyan.300"); useEffect(() => { setIsLoggedIn(!!localStorage.getItem("authToken")); }, [location]); - // ✅ Wrapped in useCallback so it's stable and safe to use in useEffect deps + useEffect(() => { + const onScroll = () => setIsScrolled(window.scrollY > 8); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); + const handleCartOpen = useCallback(async () => { await fetchProducts(); onOpen(); }, [fetchProducts, onOpen]); - // ✅ handleCartOpen is now stable — no missing-deps warning useEffect(() => { - const handleOpenCart = () => { - handleCartOpen(); - }; + const handleOpenCart = () => handleCartOpen(); window.addEventListener('open-cart', handleOpenCart); return () => window.removeEventListener('open-cart', handleOpenCart); }, [handleCartOpen]); @@ -66,50 +80,28 @@ const Navbar = () => { const handleCheckout = async () => { if (cartItems.length === 0) return; setIsCheckoutLoading(true); - try { const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: cartItems }), }); - if (!res.ok) { - throw new Error(`Server error: ${res.status} ${res.statusText}`); - } + if (!res.ok) throw new Error(`Server error: ${res.status} ${res.statusText}`); const data = await res.json(); - if (!data.success) { - toast({ - title: "Checkout Error", - description: data.message, - status: "error", - duration: 3000, - isClosable: true, - }); + toast({ title: "Checkout Error", description: data.message, status: "error", duration: 3000, isClosable: true }); return; } emptyCart(); onClose(); navigate("/success"); } catch (err) { - console.error("Checkout failed:", err); - - let message; - if (err instanceof TypeError) { - message = "Network error — please check your connection"; - } else if (err.message && err.message.startsWith("Server error:")) { - message = "Something went wrong on our end. Please try again later."; - } else { - message = "Failed to process checkout"; - } - - toast({ - title: "Error", - description: message, - status: "error", - duration: 3000, - isClosable: true, - }); + let message = err instanceof TypeError + ? "Network error — please check your connection" + : err.message?.startsWith("Server error:") + ? "Something went wrong on our end. Please try again later." + : "Failed to process checkout"; + toast({ title: "Error", description: message, status: "error", duration: 3000, isClosable: true }); } finally { setIsCheckoutLoading(false); } @@ -121,10 +113,7 @@ const Navbar = () => { try { await fetch("/api/auth/logout", { method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", - }, + headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, }); } catch (err) { console.error("Failed to call logout API:", err); @@ -137,83 +126,116 @@ const Navbar = () => { navigate("/login"); }; + // Shared icon button — cyan/blue hover to match site palette + const iconBtnStyle = { + size: "sm", + variant: "ghost", + color: iconColor, + borderRadius: "lg", + _hover: { bg: iconHoverBg, color: iconHoverColor }, + _active: { bg: iconHoverBg }, + transition: "all 0.15s ease", + }; + return ( - - - - {/* LEFT SIDE - LOGO */} - - Product Store 🛒 - + + + {/* Main header */} + + + + + {/* ── LOGO — identical to original ── */} + + Product Store 🛒 + - {/* RIGHT SIDE - ICONS + HAMBURGER */} - - {/* Search Box - Desktop only */} - - setSearchQuery(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter' && searchQuery.trim()) { - if (location.pathname !== '/') { - navigate(`/?search=${encodeURIComponent(searchQuery)}`); - } else { - fetchProducts(); + {/* SEARCH — desktop */} + + + + + + setSearchQuery(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && searchQuery.trim()) { + if (location.pathname !== '/') { + navigate(`/?search=${encodeURIComponent(searchQuery)}`); + } else { + fetchProducts(); + } } - } - }} - placeholder={t('common.search')} - aria-label={t('common.search')} - size="sm" - bg={searchBg} - borderColor={searchBorder} - /> + }} + placeholder={t('common.search')} + aria-label={t('common.search')} + size="sm" + pl="32px" + bg={searchBg} + borderColor={searchBorder} + borderRadius="lg" + fontSize="13px" + _placeholder={{ color: labelColor }} + _hover={{ borderColor: "cyan.300" }} + _focus={{ borderColor: "cyan.400", boxShadow: "0 0 0 1px var(--chakra-colors-cyan-400)" }} + transition="all 0.15s" + /> + - {/* Desktop Icons */} - - - - - - - - - + - + {isLoggedIn && } + {isLoggedIn && ( <> - + - )} - {/* Hamburger Button - Mobile only */} - + + + + + {/* MOBILE MENU */} + {isMobileMenuOpen && ( + setIsMobileMenuOpen(!isMobileMenuOpen)} - aria-label="Open menu" - variant="ghost" - fontSize="22px" + spacing={2} pb={4} pt={2} + borderTop="1px solid" + borderColor={border} + w="full" align="stretch" > - ☰ - - - - - {/* Mobile Menu Dropdown */} - {isMobileMenuOpen && ( - - setSearchQuery(e.target.value)} placeholder={t('common.search')} aria-label={t('common.search')} - bg={mobileInputBg} - borderColor={mobileInputBorder} + size="sm" + bg={searchBg} + borderColor={searchBorder} + borderRadius="lg" + _focus={{ borderColor: "cyan.400", boxShadow: "0 0 0 1px var(--chakra-colors-cyan-400)" }} /> - - setIsMobileMenuOpen(false)} style={{ width: '100%' }}> - - - - setIsMobileMenuOpen(false)} style={{ width: '100%' }}> - - - - {isLoggedIn && ( - setIsMobileMenuOpen(false)} style={{ width: '100%' }}> - - - )} - - setIsMobileMenuOpen(false)} style={{ width: '100%' }}> - - + {label} + {badge > 0 && ( + + {badge} + + )} + + + ))} - + )} - + + + )} + + + + {/* CART DRAWER */} + + + + + + {t('cart.title')} + {totalItemsCount > 0 && ( + {totalItemsCount} + )} + + + + {cartItems.length === 0 ? ( + + + {t('cart.empty')} + + ) : ( + + {cartItems.map((item) => { + const latestProduct = products.find((p) => p._id === item._id); + const currentPrice = latestProduct?.price ?? item.price; + return ( + + + {item.name} + + {item.quantity} × {formatPrice(currentPrice, currency, rates)} + + + + + ); + })} + + )} + + + + + {t('cart.total')} + + {formatPrice(totalPrice ?? 0, currency, rates)} + + - - )} - - - - - - {t('cart.title')} - - - {cartItems.length === 0 ? ( - {t('cart.empty')} - ) : ( - - {cartItems.map((item) => { - const latestProduct = products.find((p) => p._id === item._id); - const currentPrice = latestProduct?.price ?? item.price; - - return ( - - - {item.name} - - {t('cart.quantity')}: {item.quantity} × {formatPrice(currentPrice, currency, rates)} - - - - - ); - })} - - )} - - - - - {t('cart.total')}: - - {formatPrice(totalPrice ?? 0, currency, rates)} - - - - - - - + + + ); }; -export default Navbar; +export default Navbar; \ No newline at end of file