ifixkart-storefront/layout/Navbar.tsx

386 lines
13 KiB
TypeScript

'use client';
import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
import Link from 'next/link';
import { ChevronDown } from 'lucide-react';
import { usePathname } from 'next/navigation';
import {
TopDealsMegaMenu,
isTopDealsNavLabel,
} from '@/components/TopDealsMegaMenu';
import {
ProductsMegaMenu,
isProductsNavLabel,
} from '@/components/ProductsMegaMenu';
import { ShopMegaMenu, isShopNavLabel } from '@/components/ShopMegaMenu';
import { FaqMenu, isFaqNavLink } from '@/components/faq/FaqMenu';
import {
getCategoryMenuItems,
// CategorySidebar is kept in the file but the current mega sidebar is commented out below.
} from '@/components/CategorySidebar';
import { catalogService } from '@/services/api/catalogService';
import {
STOREFRONT_STICKY_NAV_GAP_PX,
STOREFRONT_STICKY_SCROLL_Y,
} from '@/layout/Header';
import { parseMetadata, resolveIsActive } from '@/lib/layoutConfig';
import { DEFAULT_NAV_LINKS } from '@/lib/homepageDefaults';
import {
storefrontService,
type AllMegaMenus,
} from '@/services/api/storefrontService';
interface NavLink {
label: string;
url: string;
hasDropdown: boolean;
}
function isContactNavLink(label: string, url?: string): boolean {
const text = String(label || '').toLowerCase();
const path = String(url || '').split('?')[0];
return text.includes('contact') || /^\/contact\/?$/i.test(path);
}
/** Contact Us and FAQs stay in the top announcement bar, not the main nav. */
function withoutUtilityNavLinks(links: NavLink[]): NavLink[] {
return links.filter(
(l) => !isContactNavLink(l.label, l.url) && !isFaqNavLink(l.label, l.url)
);
}
type MegaKind = 'deals' | 'products' | 'shop' | 'faq' | null;
/** Pick which mega panel to open when admin enables “dropdown on hover”. */
function resolveMegaKind(link: NavLink): MegaKind {
const url = link.url || '';
if (isFaqNavLink(link.label, url)) return 'faq';
if (!link.hasDropdown) return null;
if (isTopDealsNavLabel(link.label) || /[?&]tag=deals/i.test(url)) return 'deals';
if (isProductsNavLabel(link.label) || /[?&]featured=/i.test(url)) return 'products';
if (isShopNavLabel(link.label) || url.startsWith('/shop')) return 'shop';
// Checkbox label in admin: “Show categories dropdown on hover”
return 'shop';
}
function mapNavLinks(raw: unknown[]): NavLink[] {
return raw
.filter((l: any) => l && l.label && l.url)
.map((l: any) => ({
label: String(l.label).trim(),
url: String(l.url).trim() || '/',
// Strict: only admin “Show categories dropdown on hover”
hasDropdown: Boolean(l.hasDropdown),
}));
}
function linksFromConfig(config: any): NavLink[] | null {
const meta = parseMetadata(config);
const raw = meta.links;
if (Array.isArray(raw) && raw.length > 0) return mapNavLinks(raw);
return null;
}
interface NavbarProps {
config?: any;
/** When false, hide configurable nav links (All Categories chrome stays). */
isActive?: boolean;
}
export const Navbar: React.FC<NavbarProps> = ({
config,
isActive = true,
}) => {
const pathname = usePathname();
const [isSticky, setIsSticky] = useState(false);
const [headerHeight, setHeaderHeight] = useState(0);
const [navLinks, setNavLinks] = useState<NavLink[]>(() =>
mapNavLinks(DEFAULT_NAV_LINKS as unknown[])
);
const [megaOpen, setMegaOpen] = useState<MegaKind>(null);
const [categoriesOpen, setCategoriesOpen] = useState(false);
const [categoryMenu, setCategoryMenu] = useState(() => getCategoryMenuItems([]));
const [megaMenus, setMegaMenus] = useState<AllMegaMenus>({});
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const categoriesRef = useRef<HTMLDivElement | null>(null);
const openMega = (kind: MegaKind) => {
if (!kind) return;
if (closeTimer.current) clearTimeout(closeTimer.current);
setCategoriesOpen(false);
setMegaOpen(kind);
};
const scheduleCloseMega = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => setMegaOpen(null), 120);
};
const toggleCategories = () => {
setMegaOpen(null);
setCategoriesOpen((open) => !open);
};
useEffect(() => {
let cancelled = false;
catalogService.getCategories().then((res) => {
if (!cancelled) setCategoryMenu(getCategoryMenuItems(res || []));
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
storefrontService.getMegaMenu().then((menus) => {
if (!cancelled) setMegaMenus(menus || {});
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const defaults = mapNavLinks(DEFAULT_NAV_LINKS as unknown[]);
// Section Hidden → keep a usable default nav (do not blank the bar)
if (!isActive) {
setNavLinks(defaults);
setMegaOpen(null);
return;
}
if (config) {
// Prefer CMS links whenever the section is Visible
const fromCms = linksFromConfig(config);
setNavLinks(fromCms ?? defaults);
return;
}
fetch('/api/v1/storefront/layout/home?region=nav_links', {
cache: 'no-store',
})
.then((r) => (r.ok ? r.json() : null))
.then((data: any) => {
const rows = Array.isArray(data) ? data : data?.items || [];
if (rows.length > 0) {
const section = rows[0];
if (resolveIsActive(section) === false) {
setNavLinks(defaults);
return;
}
const fromCms = linksFromConfig(section);
setNavLinks(fromCms ?? defaults);
return;
}
setNavLinks(defaults);
})
.catch(() => setNavLinks(defaults));
}, [config, isActive]);
useEffect(() => {
const syncSticky = () => {
const next = window.scrollY > STOREFRONT_STICKY_SCROLL_Y;
setIsSticky(next);
if (next) {
setMegaOpen(null);
setCategoriesOpen(false);
}
};
syncSticky();
window.addEventListener('scroll', syncSticky, { passive: true });
return () => {
window.removeEventListener('scroll', syncSticky);
if (closeTimer.current) clearTimeout(closeTimer.current);
};
}, []);
useLayoutEffect(() => {
const measure = () => {
const header = document.querySelector<HTMLElement>(
'[data-storefront-header]'
);
setHeaderHeight(header?.offsetHeight || 0);
};
measure();
window.addEventListener('resize', measure, { passive: true });
return () => window.removeEventListener('resize', measure);
}, [isSticky]);
useEffect(() => {
setCategoriesOpen(false);
setMegaOpen(null);
}, [pathname]);
useEffect(() => {
if (!categoriesOpen) return;
const onPointerDown = (event: MouseEvent) => {
const root = categoriesRef.current;
if (root && !root.contains(event.target as Node)) {
setCategoriesOpen(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setCategoriesOpen(false);
};
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('keydown', onKeyDown);
};
}, [categoriesOpen]);
return (
<nav
data-storefront-nav
className={`bg-white hidden xl:block z-[100] ${
isSticky
? 'fixed left-0 right-0 shadow-md py-0 border-b border-gray-100'
: 'relative pt-0'
}`}
style={
isSticky
? { top: headerHeight, paddingTop: STOREFRONT_STICKY_NAV_GAP_PX }
: undefined
}
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
<div
className="relative w-full min-w-0"
onMouseLeave={scheduleCloseMega}
>
<div className="flex items-stretch w-full min-w-0">
<div
ref={categoriesRef}
className="relative w-[280px] shrink-0 self-start z-[130]"
>
<button
type="button"
onClick={toggleCategories}
aria-haspopup="true"
aria-expanded={categoriesOpen}
aria-label="All Categories menu"
className="w-full bg-primary text-white py-4 px-6 font-bold text-[13px] flex items-center gap-3 rounded-t-lg select-none cursor-pointer hover:bg-primary-hover transition-colors"
>
<span className="flex flex-col gap-1 w-4" aria-hidden="true">
<span className="h-[2px] bg-white w-full rounded" />
<span className="h-[2px] bg-white w-4/5 rounded" />
<span className="h-[2px] bg-white w-full rounded" />
</span>
All Categories
</button>
{categoriesOpen && (
<div className="absolute left-0 top-full z-[140] w-[280px] bg-white border border-gray-200 rounded-b-lg shadow-[0_12px_32px_rgba(15,23,42,0.12)] overflow-hidden">
{/*
Current mega CategorySidebar — kept, not deleted:
<CategorySidebar isDropdown />
*/}
<ul className="max-h-[min(70vh,420px)] overflow-y-auto py-1">
{categoryMenu.map((item, idx) => (
<li key={item.id}>
<Link
href={item.href}
onClick={() => setCategoriesOpen(false)}
className={`flex items-center px-5 py-[13px] text-[13px] font-medium text-[#333] hover:text-primary hover:bg-[#f7f9fc] ${
idx < categoryMenu.length - 1 ? 'border-b border-[#ececec]' : ''
}`}
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
)}
</div>
<div className="flex-1 min-w-0 ml-8 lg:ml-10">
<div className="flex items-center justify-between min-w-0 w-full">
<div className="flex items-center gap-6 xl:gap-8 min-w-0 flex-wrap">
{withoutUtilityNavLinks(navLinks).map((link, idx) => {
const pathOnly = link.url.split('?')[0] || '/';
const linkActive =
pathname === link.url ||
pathname === pathOnly ||
(pathOnly !== '/' && pathname.startsWith(pathOnly));
const megaKind = resolveMegaKind(link);
if (megaKind) {
const open = megaOpen === megaKind;
return (
<div
key={`${link.label}-${idx}`}
className="relative py-[14.5px] shrink-0"
onMouseEnter={() => openMega(megaKind)}
onFocus={() => openMega(megaKind)}
>
<Link
href={link.url}
className={`font-bold text-[14px] hover:text-primary transition-colors flex items-center gap-1 ${
linkActive || open
? 'text-primary'
: 'text-gray-700'
}`}
aria-expanded={open}
aria-haspopup="true"
>
{link.label}
<ChevronDown
size={14}
className={`transition-colors ${
open ? 'text-primary' : 'text-gray-400'
}`}
/>
</Link>
{megaKind === 'faq' && open ? (
<FaqMenu onNavigate={() => setMegaOpen(null)} />
) : null}
</div>
);
}
return (
<Link
key={`${link.label}-${idx}`}
href={link.url}
className={`font-bold text-[14px] hover:text-primary transition-colors py-[14.5px] shrink-0 ${
linkActive ? 'text-primary' : 'text-gray-700'
}`}
>
{link.label}
</Link>
);
})}
</div>
</div>
</div>
</div>
<div
className={`absolute left-0 right-0 top-full z-[120] w-full max-w-full box-border transition-all duration-200 ${
megaOpen && megaOpen !== 'faq'
? 'opacity-100 visible pointer-events-auto'
: 'opacity-0 invisible pointer-events-none'
}`}
onMouseEnter={() => megaOpen && megaOpen !== 'faq' && openMega(megaOpen)}
>
{megaOpen === 'deals' && <TopDealsMegaMenu cms={megaMenus.deals} />}
{megaOpen === 'shop' && <ShopMegaMenu cms={megaMenus.shop} />}
{megaOpen === 'products' && (
<ProductsMegaMenu cms={megaMenus.products} />
)}
</div>
</div>
</div>
</nav>
);
};