import {useEffect, useRef, useState} from 'react'; import {Form, Link, useLocation} from 'react-router'; import { type CartViewPayload, Image, useAnalytics, useOptimisticCart, } from '@shopify/hydrogen'; import { ChevronDown, Heart, LogOut, Menu, Package, Search, ShoppingBag, User, X, } from 'lucide-react'; import type {HeaderQuery, CartApiQueryFragment} from 'storefrontapi.generated'; import type {CustomerFragment} from 'customer-accountapi.generated'; import {useAside} from '~/components/Aside'; import {getCustomerDisplayName, getCustomerInitials} from '~/lib/customerDisplay'; import {useAbiirWishlistCount} from '~/hooks/useAbiirWishlistCount'; import abiirLogoSrc from '~/assets/abiir-logo.png'; import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery'; import {useQuery} from '@sanity/react-loader'; import type {QueryResponseInitial} from '@sanity/react-loader'; /** GROQ: header fields on the Shopify template `settings` singleton. */ export const HEADER_SANITY_QUERY = `*[_type == "settings"][0].header{ announcementText, announcementBackground, announcementTextColor, navAccent, utilityAccent, "navigationItems": navigationItems[]{ label, "href": href }, logo{ alt, asset->{ _id, url, metadata{dimensions{width,height}} } }, "accountMenuItems": accountMenuItems[]{ label, "href": href } }` as const; export type AbiirHeaderNavItem = { label: string; href: string; }; export type AbiirHeaderCms = { announcementText: string; announcementBackground: string; announcementTextColor: string; navAccent: string; utilityAccent: string; navigationItems: AbiirHeaderNavItem[]; logo: { id?: string; url: string; alt: string; width?: number; height?: number; } | null; accountMenuItems: AbiirHeaderNavItem[]; }; /** Legacy nav used `/products` as a catalog index; this app lists all products at `/collections/all`. */ function normalizeCatalogListNavHref(href: string): string { try { const u = new URL(href, 'https://abiir.internal'); if (u.pathname === '/products' || u.pathname === '/products/') { return `/collections/all${u.search}${u.hash}`; } } catch { /* keep href */ } return href; } type SanityHeaderPayload = { announcementText?: string | null; announcementBackground?: string | null; announcementTextColor?: string | null; navAccent?: string | null; utilityAccent?: string | null; navigationItems?: AbiirHeaderNavItem[] | null; logo?: { alt?: string | null; asset?: { _id?: string; url?: string | null; metadata?: {dimensions?: {width?: number | null; height?: number | null}} | null; } | null; } | null; accountMenuItems?: AbiirHeaderNavItem[] | null; } | null; /** Visual / offline fallback — matches the legacy React Navbar defaults. */ export const MOCK_HEADER_CMS: AbiirHeaderCms = { announcementText: 'FREE SHIPPING ON ORDERS OVER $75 · USE CODE: ABiiR15 FOR 15% OFF', announcementBackground: '#5D0B24', announcementTextColor: '#FAF8F3', navAccent: '#b39763', utilityAccent: '#5d0b24', navigationItems: [ {label: 'Collection', href: '/collections/all'}, {label: 'Rituals', href: '/collections/ritual'}, {label: 'Our Story', href: '/about'}, // {label: 'Journal', href: '/journal'}, ], logo: null, accountMenuItems: [ {label: 'My Profile', href: '/account/profile'}, {label: 'My Orders', href: '/account/orders'}, {label: 'Wishlist', href: '/wishlist'}, ], }; const DEFAULT_ACCOUNT_MENU: AbiirHeaderNavItem[] = [ {label: 'My Profile', href: '/account/profile'}, {label: 'My Orders', href: '/account/orders'}, {label: 'Wishlist', href: '/wishlist'}, ]; export function normalizeHeaderSanityPayload( raw: unknown, ): AbiirHeaderCms | null { if (raw == null || typeof raw !== 'object') return null; const h = raw as SanityHeaderPayload; if (!h?.announcementText || !h.navigationItems?.length) return null; const logoUrl = h.logo?.asset?.url; const logo = logoUrl != null ? { id: h.logo?.asset?._id, url: logoUrl, alt: h.logo?.alt ?? 'ABiiR Skincare – Rooted in Ayurveda', width: h.logo?.asset?.metadata?.dimensions?.width ?? undefined, height: h.logo?.asset?.metadata?.dimensions?.height ?? undefined, } : null; return { announcementText: h.announcementText, announcementBackground: h.announcementBackground ?? '#5D0B24', announcementTextColor: h.announcementTextColor ?? '#FAF8F3', navAccent: h.navAccent ?? '#b39763', utilityAccent: h.utilityAccent ?? '#5d0b24', navigationItems: h.navigationItems.map((i) => ({ label: i.label, href: normalizeCatalogListNavHref(i.href ?? ''), })), logo, accountMenuItems: h.accountMenuItems?.filter((i) => i.label && i.href) ?? DEFAULT_ACCOUNT_MENU, }; } type ShopifyBrandImage = NonNullable< NonNullable['logo'] >['image']; /** Shopify brand logo image from `HeaderQuery` (pass `header.shop.brand?.logo?.image`). */ export type AbiirShopifyBrandLogoImage = ShopifyBrandImage; export type AbiirLogoImageData = NonNullable< ReturnType >; function toHydrogenImage( input: { id?: string | null; url: string; altText?: string | null; width?: number | null; height?: number | null; } | null | undefined, ): { id?: string | undefined; url: string; altText?: string | undefined; width?: number | undefined; height?: number | undefined; } | null { if (!input?.url) return null; return { id: input.id ?? undefined, url: input.url, altText: input.altText ?? '', width: input.width ?? undefined, height: input.height ?? undefined, }; } /** Same artwork as React `Navbar` (`figma:asset/11567d6fddae52ee62a5633b5f842f2e7b3e3b29.png` → `app/assets/abiir-logo.png`). */ const REACT_APP_BRAND_LOGO = toHydrogenImage({ id: 'abiir-logo-react', url: abiirLogoSrc, altText: 'ABiiR Skincare – Rooted in Ayurveda', }); /** Sanity header logo → Shopify brand logo → bundled `abiir-logo.png` (header + footer). */ export function pickAbiirLogoData( cms: AbiirHeaderCms, shopifyImage: ShopifyBrandImage | null | undefined, ): AbiirLogoImageData { if (cms.logo?.url) { const fromCms = toHydrogenImage({ id: cms.logo.id, url: cms.logo.url, altText: cms.logo.alt, width: cms.logo.width, height: cms.logo.height, }); if (fromCms) return fromCms; } const fromShopify = toHydrogenImage(shopifyImage); if (fromShopify) return fromShopify; return REACT_APP_BRAND_LOGO; } function isExternalHref(href: string) { return /^https?:\/\//i.test(href); } function NavLinkOrA({ href, className, style, onClick, children, }: { href: string; className?: string; style?: React.CSSProperties; onClick?: () => void; children: React.ReactNode; }) { if (isExternalHref(href)) { return ( {children} ); } return ( {children} ); } export interface HeaderProps { header: HeaderQuery; headerCms: AbiirHeaderCms; /** Pass through from root `loadQuery` for `useQuery` hydration (optional). */ headerSanityInitial?: QueryResponseInitial; cart: CartApiQueryFragment | null; isLoggedIn: boolean; accountCustomer: CustomerFragment | null; publicStoreDomain: string; } /** * Optional: use with a mounted Sanity query store / preview (e.g. `hydrogen-sanity` `Query`). * The default `Header` uses CMS data from the root loader only. */ export function useAbiirHeaderCms( initial: QueryResponseInitial, ) { return useQuery(HEADER_SANITY_QUERY, undefined, { initial, }); } export function Header({ header, headerCms: headerCmsFromLoader, headerSanityInitial: _headerSanityInitial, isLoggedIn, cart, accountCustomer, publicStoreDomain, }: HeaderProps) { void _headerSanityInitial; const cms = headerCmsFromLoader ?? MOCK_HEADER_CMS; const {shop} = header; const primaryDomainUrl = shop.primaryDomain?.url ?? ''; const shopifyLogo = shop.brand?.logo?.image; const logoData = pickAbiirLogoData(cms, shopifyLogo); return ( <>

{cms.announcementText}

); } function AbiirNavBar({ cms, cart, isLoggedIn, accountCustomer, logoData, shopName, primaryDomainUrl, publicStoreDomain, }: { cms: AbiirHeaderCms; cart: CartApiQueryFragment | null; isLoggedIn: boolean; accountCustomer: CustomerFragment | null; logoData: AbiirLogoImageData; shopName: string; primaryDomainUrl: string; publicStoreDomain: string; }) { const wishlistCount = useAbiirWishlistCount(); const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false); const location = useLocation(); const userMenuRef = useRef(null); const navAccent = cms.navAccent; const utilityBurgundy = cms.utilityAccent; const accountLinks = cms.accountMenuItems?.length > 0 ? cms.accountMenuItems : DEFAULT_ACCOUNT_MENU; useEffect(() => { const handleScroll = () => setScrolled(window.scrollY > 40); window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); useEffect(() => { setMenuOpen(false); setUserMenuOpen(false); }, [location.pathname, location.search]); useEffect(() => { const handler = (e: MouseEvent) => { if ( userMenuRef.current && !userMenuRef.current.contains(e.target as Node) ) { setUserMenuOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const navLinks = cms.navigationItems; return ( ); } function MobileSearchButton({onOpen}: {onOpen: () => void}) { const {open} = useAside(); return ( ); } function normalizeStoreHref( href: string, primaryDomainUrl: string, publicStoreDomain: string, ) { if (isExternalHref(href)) return href; if ( href.includes('myshopify.com') || (publicStoreDomain && href.includes(publicStoreDomain)) || (primaryDomainUrl && href.includes(primaryDomainUrl)) ) { try { return new URL(href).pathname; } catch { return href; } } return href; } function AccountDropdown({ customer, userMenuOpen, setUserMenuOpen, utilityBurgundy, accountLinks, }: { customer: CustomerFragment | null; userMenuOpen: boolean; setUserMenuOpen: (v: boolean | ((b: boolean) => boolean)) => void; utilityBurgundy: string; accountLinks: AbiirHeaderNavItem[]; }) { const initials = getInitials(customer); const displayName = getDisplayName(customer); const email = customer?.emailAddress?.emailAddress ?? ''; return ( <> {userMenuOpen && (

{displayName}

{email ? (

{email}

) : (

)}

{accountLinks.map((item) => ( setUserMenuOpen(false)} className="flex items-center gap-[10px] px-5 py-[13px] font-['Raleway',sans-serif] text-[0.8125rem] font-normal text-[#1A1A1A] no-underline border-b border-[rgba(26,26,26,0.05)] transition-colors duration-150 hover:bg-[#F5F1E8]" > {item.label === 'My Profile' ? ( ) : item.label.toLowerCase().includes('order') ? ( ) : ( )} {item.label} ))}
)} ); } function getInitials(customer: CustomerFragment | null) { return getCustomerInitials(customer); } function getDisplayName(customer: CustomerFragment | null) { if (!customer) return 'Customer'; return getCustomerDisplayName(customer) || 'Customer'; } function SearchToggle({className}: {className?: string}) { const {open} = useAside(); return ( ); } function CartToggle({ cart, utilityBurgundy, }: { cart: CartApiQueryFragment | null; utilityBurgundy: string; }) { const cartState = useOptimisticCart(cart); return ( ); } function CartBadge({ count, utilityBurgundy, }: { count: number; utilityBurgundy: string; }) { const {open} = useAside(); const {publish, shop, cart, prevCart} = useAnalytics(); return ( ); } export async function loadAbiirHeaderCms(args: { sanity: import('hydrogen-sanity').SanityContext; }): Promise<{ headerCms: AbiirHeaderCms; headerSanityInitial: QueryResponseInitial; }> { const initial = await args.sanity .loadQuery(HEADER_SANITY_QUERY) .catch( (): QueryResponseInitial => ({ data: null, sourceMap: undefined, }), ); const normalized = normalizeHeaderSanityPayload(initial.data); return { headerCms: normalized ?? MOCK_HEADER_CMS, headerSanityInitial: { data: normalized, sourceMap: undefined, }, }; } export async function loadAccountCustomer(context: { customerAccount: import('@shopify/hydrogen').CustomerAccount; }) { const {customerAccount} = context; const loggedIn = await customerAccount.isLoggedIn(); if (!loggedIn) return null; await customerAccount.handleAuthStatus(); const {data, errors} = await customerAccount.query(CUSTOMER_DETAILS_QUERY, { variables: { language: customerAccount.i18n.language, }, }); if (errors?.length || !data?.customer) return null; return data.customer; }