update ai commerce section
This commit is contained in:
437
src/components/navbar/PremiumNavbar.tsx
Normal file
437
src/components/navbar/PremiumNavbar.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
motion,
|
||||
AnimatePresence,
|
||||
useScroll,
|
||||
useMotionValueEvent,
|
||||
} from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
type NavChild = { label: string; href: string; icon: string; desc: string };
|
||||
type NavLink = { label: string; href: string; children?: NavChild[] };
|
||||
|
||||
const SOLUTIONS_CHILDREN: NavChild[] = [
|
||||
{
|
||||
label: "For Business",
|
||||
href: "/businesses",
|
||||
icon: "storefront",
|
||||
desc: "Run your neighborhood shop online.",
|
||||
},
|
||||
{
|
||||
label: "For People",
|
||||
href: "/people",
|
||||
icon: "shopping_bag",
|
||||
desc: "Order from local stores you trust.",
|
||||
},
|
||||
{
|
||||
label: "For D2C Brands",
|
||||
href: "/brands",
|
||||
icon: "auto_awesome",
|
||||
desc: "Connect regional catalogs direct.",
|
||||
},
|
||||
];
|
||||
|
||||
const NAV_LINKS: NavLink[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Why Nearle", href: "/why-nearle" },
|
||||
{ label: "Platform", href: "/platform" },
|
||||
{ label: "Solutions", href: "/businesses", children: SOLUTIONS_CHILDREN },
|
||||
{ label: "About", href: "/about" },
|
||||
];
|
||||
|
||||
const mobileMenuVariants = {
|
||||
hidden: { opacity: 0, height: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
transition: { duration: 0.3, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transition: { duration: 0.2, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
const mobileStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.04, delayChildren: 0.06 } },
|
||||
};
|
||||
|
||||
const mobileItemVariants = {
|
||||
hidden: { opacity: 0, x: -10 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
|
||||
};
|
||||
|
||||
const dropdownVariants = {
|
||||
hidden: { opacity: 0, y: 8, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0.22, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: 6,
|
||||
scale: 0.98,
|
||||
transition: { duration: 0.15, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
const mobileSubmenuVariants = {
|
||||
hidden: { opacity: 0, height: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
transition: { duration: 0.26, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transition: { duration: 0.18, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
export function PremiumNavbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [mobileSolutionsOpen, setMobileSolutionsOpen] = useState(false);
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { scrollY } = useScroll();
|
||||
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (y) => {
|
||||
setScrolled(y > 20);
|
||||
});
|
||||
|
||||
const isActiveLink = (href: string) => {
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const isGroupActive = (link: NavLink) => {
|
||||
if (link.children) {
|
||||
return link.children.some((c) => isActiveLink(c.href));
|
||||
}
|
||||
return isActiveLink(link.href);
|
||||
};
|
||||
|
||||
const openMenu = (label: string) => {
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
setOpenDropdown(label);
|
||||
};
|
||||
|
||||
const scheduleClose = () => {
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
closeTimer.current = setTimeout(() => setOpenDropdown(null), 120);
|
||||
};
|
||||
|
||||
const closeMobile = () => {
|
||||
setMobileOpen(false);
|
||||
setMobileSolutionsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
|
||||
scrolled
|
||||
? "bg-[#83429f]/82 backdrop-blur-lg shadow-[0_6px_32px_rgba(0,0,0,0.35)] border-b border-white/15"
|
||||
: "bg-[#83429f] border-b border-white/10"
|
||||
}`}
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: EASE }}
|
||||
>
|
||||
<div className="relative mx-auto flex h-16 w-full max-w-[1600px] items-center px-6 md:h-20 md:px-10 xl:px-16">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex shrink-0 items-center transition-transform duration-200 hover:scale-[1.02] active:scale-95"
|
||||
aria-label="Nearle Home"
|
||||
>
|
||||
<Image
|
||||
src="/logos/logo-light.png"
|
||||
alt="Nearle"
|
||||
width={190}
|
||||
height={48}
|
||||
className="h-10 w-auto object-contain md:h-12 lg:h-11 min-[1440px]:h-14"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-9">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = isGroupActive(link);
|
||||
|
||||
if (link.children) {
|
||||
const isOpen = openDropdown === link.label;
|
||||
return (
|
||||
<div
|
||||
key={link.label}
|
||||
className="relative py-2"
|
||||
onMouseEnter={() => openMenu(link.label)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
|
||||
isActive ? "text-white" : "text-white/70 hover:text-white"
|
||||
}`}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => setOpenDropdown(isOpen ? null : link.label)}
|
||||
onFocus={() => openMenu(link.label)}
|
||||
>
|
||||
{link.label}
|
||||
<motion.span
|
||||
className="flex"
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
>
|
||||
<MaterialIcon icon="expand_more" size={16} />
|
||||
</motion.span>
|
||||
</button>
|
||||
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="activeNavUnderline"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
variants={dropdownVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="absolute left-1/2 top-full z-50 mt-3 w-100 -translate-x-1/2 overflow-hidden rounded-2xl border border-white/12 bg-gradient-to-b from-[#1C0F4E] to-[#0A0524] p-2 shadow-[0_24px_50px_rgba(0,0,0,0.55)] backdrop-blur-2xl"
|
||||
role="menu"
|
||||
>
|
||||
{link.children.map((child) => {
|
||||
const childActive = isActiveLink(child.href);
|
||||
return (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
role="menuitem"
|
||||
onClick={() => setOpenDropdown(null)}
|
||||
className={`group flex items-start gap-3.5 rounded-xl p-3 transition-all duration-200 ${
|
||||
childActive
|
||||
? "bg-white/12 text-white shadow-sm"
|
||||
: "text-white/70 hover:bg-white/[0.07] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{/* Left — Medallion Icon */}
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-white/10 bg-white/[0.05] text-[#C9A2FF] transition-colors duration-200 group-hover:border-[#C9A2FF]/30 group-hover:bg-[#C9A2FF]/10 group-hover:text-white">
|
||||
<MaterialIcon icon={child.icon} size={18} />
|
||||
</div>
|
||||
{/* Right — Info */}
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-lg font-bold tracking-tight leading-none mb-1 text-white group-hover:text-[#C9A2FF] transition-colors">
|
||||
{child.label}
|
||||
</span>
|
||||
<span className="text-[12px] font-normal leading-normal text-white/55 transition-colors group-hover:text-white/70">
|
||||
{child.desc}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={link.label}
|
||||
className="relative py-2"
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
animate="rest"
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
className={`whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
|
||||
isActive ? "text-white" : "text-white/70 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
{/* Active underline: layoutId lets it slide between links on navigation */}
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="activeNavUnderline"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hover underline: grows from left on inactive links */}
|
||||
{!isActive && (
|
||||
<motion.span
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white/40"
|
||||
style={{ originX: 0 }}
|
||||
variants={{ rest: { scaleX: 0 }, hover: { scaleX: 1 } }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Desktop Action Button */}
|
||||
<div className="hidden lg:block">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center rounded-full bg-white px-7 py-3 text-sm font-black uppercase tracking-wider text-[#6330D6] shadow-[0_4px_20px_rgba(0,0,0,0.15)] hover:bg-[#FAF7FD] hover:text-[#5225b8] hover:scale-[1.03] active:scale-95 transition-all duration-200"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto flex h-10 w-10 items-center justify-center rounded-full text-white transition-all duration-200 hover:bg-white/10 active:scale-90 lg:hidden"
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
>
|
||||
<MaterialIcon icon={mobileOpen ? "close" : "menu"} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu with enter/exit animation */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<motion.div
|
||||
key="mobile-menu"
|
||||
variants={mobileMenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="overflow-hidden border-t border-white/10 bg-[#30226F]/98 backdrop-blur-xl lg:hidden"
|
||||
>
|
||||
<motion.nav
|
||||
className="mx-auto flex w-full max-w-[1600px] flex-col gap-1 px-6 py-5 md:px-10"
|
||||
variants={mobileStagger}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{NAV_LINKS.map((link) => {
|
||||
if (link.children) {
|
||||
const groupActive = isGroupActive(link);
|
||||
return (
|
||||
<motion.div key={link.label} variants={mobileItemVariants}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileSolutionsOpen((v) => !v)}
|
||||
aria-expanded={mobileSolutionsOpen}
|
||||
className={`flex w-full items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
groupActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
<motion.span
|
||||
className="flex"
|
||||
animate={{ rotate: mobileSolutionsOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
>
|
||||
<MaterialIcon icon="expand_more" size={20} />
|
||||
</motion.span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{mobileSolutionsOpen && (
|
||||
<motion.div
|
||||
variants={mobileSubmenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-1 flex flex-col gap-1 pl-3">
|
||||
{link.children.map((child) => {
|
||||
const childActive = isActiveLink(child.href);
|
||||
return (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
onClick={closeMobile}
|
||||
className={`flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
|
||||
childActive
|
||||
? "bg-white/12 text-white"
|
||||
: "text-white/60 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={child.icon} size={18} className="text-[#C9A2FF]" />
|
||||
{child.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = isActiveLink(link.href);
|
||||
return (
|
||||
<motion.div key={link.label} variants={mobileItemVariants}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={closeMobile}
|
||||
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<span className="h-2 w-2 rounded-full bg-white" />
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Mobile Contact CTA */}
|
||||
<motion.div variants={mobileItemVariants} className="mt-4 px-4">
|
||||
<Link
|
||||
href="/contact"
|
||||
onClick={closeMobile}
|
||||
className="flex w-full items-center justify-center rounded-full bg-white py-3.5 text-sm font-black uppercase tracking-wider text-[#6330D6] shadow-md active:scale-[0.98] transition-all hover:bg-[#FAF7FD] hover:text-[#5225b8]"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.nav>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user