AbiirSkincare/Hydrogen/web/app/components/Header.tsx

858 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HeaderQuery['shop']['brand']>['logo']
>['image'];
/** Shopify brand logo image from `HeaderQuery` (pass `header.shop.brand?.logo?.image`). */
export type AbiirShopifyBrandLogoImage = ShopifyBrandImage;
export type AbiirLogoImageData = NonNullable<
ReturnType<typeof toHydrogenImage>
>;
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 (
<a
href={href}
className={className}
style={style}
onClick={onClick}
rel="noreferrer"
>
{children}
</a>
);
}
return (
<Link
to={href}
className={className}
style={style}
onClick={onClick}
prefetch="intent"
>
{children}
</Link>
);
}
export interface HeaderProps {
header: HeaderQuery;
headerCms: AbiirHeaderCms;
/** Pass through from root `loadQuery` for `useQuery` hydration (optional). */
headerSanityInitial?: QueryResponseInitial<AbiirHeaderCms | null>;
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<AbiirHeaderCms | null>,
) {
return useQuery<AbiirHeaderCms | null>(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 (
<>
<div
className="w-full text-center py-[10px] px-4 font-['Raleway',sans-serif]"
style={{
background: cms.announcementBackground,
color: cms.announcementTextColor,
}}
>
<p className="m-0 text-[0.8125rem] font-normal tracking-[0.18em]">
{cms.announcementText}
</p>
</div>
<AbiirNavBar
cart={cart}
cms={cms}
isLoggedIn={isLoggedIn}
accountCustomer={accountCustomer}
logoData={logoData}
shopName={shop.name}
primaryDomainUrl={primaryDomainUrl}
publicStoreDomain={publicStoreDomain}
/>
</>
);
}
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<HTMLDivElement>(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 (
<nav
className="sticky top-0 z-50 w-full transition-[background,backdrop-filter] duration-300"
style={{
background: scrolled
? 'rgba(250, 248, 243, 0.97)'
: '#FAF8F3',
backdropFilter: scrolled ? 'blur(12px)' : 'none',
borderBottom: '1px solid rgba(0, 0, 0, 0.2)',
}}
>
<div className="max-w-[1600px] mx-auto px-6 sm:px-10 lg:px-16 xl:px-24 2xl:px-[206px]">
<div className="flex items-center justify-between h-[72px] lg:h-[96px] gap-4">
<div className="hidden lg:flex flex-1 items-center justify-between min-w-0 pr-6">
<Link
to="/"
prefetch="intent"
className="flex items-center select-none shrink-0 no-underline"
>
{logoData ? (
<Image
data={logoData}
alt={logoData.altText || 'ABiiR Skincare'}
width={148}
sizes="148px"
className="block w-[148px] h-auto max-w-[148px]"
/>
) : (
<span className="font-['Cormorant_Garamond',serif] text-[1.25rem] text-[#1A1A1A]">
{shopName}
</span>
)}
</Link>
<div className="flex items-center gap-10 shrink-0">
{navLinks.map((link) => {
const [pathname, query = ''] = link.href.split('?');
const isActive =
location.pathname === pathname &&
(!query || location.search.slice(1) === query);
return (
<NavLinkOrA
key={link.href}
href={normalizeStoreHref(
link.href,
primaryDomainUrl,
publicStoreDomain,
)}
className="flex flex-col gap-[3px] items-stretch transition-opacity duration-200 hover:opacity-60 no-underline uppercase font-medium text-[#1A1A1A] font-['Raleway',sans-serif] text-[13.813px] leading-[20.719px] tracking-[2.21px]"
>
<span>{link.label}</span>
<span
className="h-px w-full transition-transform duration-300 origin-left"
style={{
background: navAccent,
transform: isActive ? 'scaleX(1)' : 'scaleX(0)',
}}
/>
</NavLinkOrA>
);
})}
</div>
</div>
<Link
to="/"
prefetch="intent"
className="flex lg:hidden items-center select-none shrink-0 no-underline"
>
{logoData ? (
<Image
data={logoData}
alt={logoData.altText || 'ABiiR Skincare'}
width={132}
className="block w-[132px] h-auto max-w-[132px]"
sizes="132px"
/>
) : (
<span className="font-['Cormorant_Garamond',serif] text-[1.1rem] text-[#1A1A1A]">
{shopName}
</span>
)}
</Link>
<div
className="flex items-center h-[30px] gap-6 lg:gap-[39px] shrink-0 pl-6 border-l border-[rgba(26,26,24,0.10)]"
>
<SearchToggle className="hover:opacity-50 transition-opacity hidden lg:flex lg:items-center text-[#1A1A1A]" />
<NavLinkOrA
href="/wishlist"
className="relative hover:opacity-50 transition-opacity hidden lg:flex lg:items-center text-[#1A1A1A]"
aria-label="Wishlist"
>
<Heart size={18} strokeWidth={1.4} />
{wishlistCount > 0 && (
<span
className="absolute flex items-center justify-center rounded-full min-w-[15px] h-[15px] left-[9px] top-[-6px] px-1 text-white font-['Raleway',sans-serif] font-semibold text-[8.5px] leading-[12.75px]"
style={{background: utilityBurgundy}}
>
{wishlistCount > 99 ? '99+' : wishlistCount}
</span>
)}
</NavLinkOrA>
<div
className="hidden lg:flex lg:items-center relative"
ref={userMenuRef}
>
{isLoggedIn ? (
<AccountDropdown
customer={accountCustomer}
userMenuOpen={userMenuOpen}
setUserMenuOpen={setUserMenuOpen}
utilityBurgundy={utilityBurgundy}
accountLinks={accountLinks}
/>
) : (
<Link
to="/account/login"
prefetch="intent"
className="hover:opacity-50 transition-opacity flex items-center text-[#1A1A1A] bg-transparent border-none cursor-pointer p-0"
aria-label="Sign in"
>
<User size={18} strokeWidth={1.4} />
</Link>
)}
</div>
<CartToggle cart={cart} utilityBurgundy={utilityBurgundy} />
<button
type="button"
className="lg:hidden hover:opacity-50 transition-opacity text-[#1A1A1A] bg-transparent border-none cursor-pointer p-0"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Menu"
>
{menuOpen ? (
<X size={21} strokeWidth={1.4} />
) : (
<Menu size={21} strokeWidth={1.4} />
)}
</button>
</div>
</div>
</div>
<div
className="lg:hidden overflow-hidden transition-all duration-500"
style={{
maxHeight: menuOpen ? '700px' : '0px',
background: '#FAF8F3',
borderTop: menuOpen ? '1px solid rgba(26, 26, 26, 0.08)' : 'none',
}}
>
<div className="px-8 py-8 flex flex-col gap-6">
{navLinks.map((link) => (
<NavLinkOrA
key={link.href}
href={normalizeStoreHref(
link.href,
primaryDomainUrl,
publicStoreDomain,
)}
onClick={() => setMenuOpen(false)}
className="no-underline font-['Cormorant_Garamond',serif] text-[1.75rem] font-light text-[#1A1A1A] tracking-[0.02em]"
>
{link.label}
</NavLinkOrA>
))}
<div
className="pt-6 flex flex-col gap-4 border-t border-[rgba(26,26,26,0.08)]"
>
<MobileSearchButton onOpen={() => setMenuOpen(false)} />
<NavLinkOrA
href="/wishlist"
onClick={() => setMenuOpen(false)}
className="font-['Raleway',sans-serif] text-[0.8125rem] tracking-[0.14em] text-[#5A5550] uppercase no-underline"
>
Wishlist {wishlistCount > 0 && `(${wishlistCount})`}
</NavLinkOrA>
{isLoggedIn ? (
<>
<NavLinkOrA
href="/account/profile"
onClick={() => setMenuOpen(false)}
className="font-['Raleway',sans-serif] text-[0.8125rem] tracking-[0.14em] text-[#5A5550] uppercase no-underline"
>
My Profile
</NavLinkOrA>
<Form method="post" action="/account/logout">
<button
type="submit"
className="bg-transparent border-none cursor-pointer font-['Raleway',sans-serif] text-[0.8125rem] tracking-[0.14em] text-[#c0392b] uppercase text-left p-0"
onClick={() => setMenuOpen(false)}
>
Sign Out
</button>
</Form>
</>
) : (
<Link
to="/account/login"
prefetch="intent"
onClick={() => setMenuOpen(false)}
className="bg-transparent border-none cursor-pointer font-['Raleway',sans-serif] text-[0.8125rem] tracking-[0.14em] text-[#7A2340] uppercase text-left p-0 no-underline"
>
Sign In / Register
</Link>
)}
</div>
</div>
</div>
</nav>
);
}
function MobileSearchButton({onOpen}: {onOpen: () => void}) {
const {open} = useAside();
return (
<button
type="button"
onClick={() => {
onOpen();
open('search');
}}
className="text-left bg-transparent border-none cursor-pointer p-0 font-['Raleway',sans-serif] text-[0.8125rem] tracking-[0.14em] text-[#5A5550] uppercase"
>
Search
</button>
);
}
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 (
<>
<button
type="button"
onClick={() => setUserMenuOpen((o) => !o)}
className="flex items-center gap-[8.5px] hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer p-0 text-[#1A1A1A] h-[30px]"
aria-label="Account menu"
>
<div
className="w-[30px] h-[30px] rounded-[15px] flex items-center justify-center"
style={{background: utilityBurgundy}}
>
<span className="font-['Cormorant_Garamond',serif] text-[13.813px] font-medium text-[#FAF8F3] tracking-[1.63px] leading-[20.719px] uppercase">
{initials}
</span>
</div>
<ChevronDown
size={13}
strokeWidth={1.8}
className="text-[#1A1A1A] transition-transform duration-200"
style={{transform: userMenuOpen ? 'rotate(180deg)' : 'none'}}
/>
</button>
{userMenuOpen && (
<div className="absolute top-[calc(100%+14px)] right-0 min-w-[220px] z-[200] bg-[#FAF8F3] border border-[rgba(26,26,26,0.10)] shadow-[0_16px_48px_rgba(26,26,26,0.13)]">
<div className="px-5 py-4 border-b border-[rgba(26,26,26,0.07)]">
<p className="font-['Cormorant_Garamond',serif] text-[1.0625rem] font-normal text-[#1A1A1A] leading-[1.2] m-0">
{displayName}
</p>
{email ? (
<p className="font-['Raleway',sans-serif] text-[0.75rem] text-[#8A8480] font-light mt-[3px] m-0">
{email}
</p>
) : (
<p className="font-['Raleway',sans-serif] text-[0.75rem] text-[#8A8480] font-light mt-[3px] m-0 min-h-[14px]" />
)}
</div>
{accountLinks.map((item) => (
<NavLinkOrA
key={item.label}
href={item.href}
onClick={() => 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]"
>
<span className="text-[#7A2340]">
{item.label === 'My Profile' ? (
<User size={14} strokeWidth={1.5} />
) : item.label.toLowerCase().includes('order') ? (
<Package size={14} strokeWidth={1.5} />
) : (
<Heart size={14} strokeWidth={1.5} />
)}
</span>
{item.label}
</NavLinkOrA>
))}
<Form method="post" action="/account/logout" className="m-0">
<button
type="submit"
className="w-full flex items-center gap-[10px] px-5 py-[13px] bg-transparent border-none cursor-pointer font-['Raleway',sans-serif] text-[0.8125rem] font-normal text-[#c0392b] text-left normal-case tracking-normal hover:bg-[#fce4ec] transition-colors duration-150"
>
<LogOut size={14} strokeWidth={1.5} /> Sign Out
</button>
</Form>
</div>
)}
</>
);
}
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 (
<button
type="button"
className={className}
onClick={() => open('search')}
aria-label="Search"
>
<Search size={18} strokeWidth={1.4} />
</button>
);
}
function CartToggle({
cart,
utilityBurgundy,
}: {
cart: CartApiQueryFragment | null;
utilityBurgundy: string;
}) {
const cartState = useOptimisticCart(cart);
return (
<CartBadge
count={cartState?.totalQuantity ?? 0}
utilityBurgundy={utilityBurgundy}
/>
);
}
function CartBadge({
count,
utilityBurgundy,
}: {
count: number;
utilityBurgundy: string;
}) {
const {open} = useAside();
const {publish, shop, cart, prevCart} = useAnalytics();
return (
<button
type="button"
onClick={(e) => {
e.preventDefault();
open('cart');
publish('cart_viewed', {
cart,
prevCart,
shop,
url: typeof window !== 'undefined' ? window.location.href : '',
} as CartViewPayload);
}}
className="relative hover:opacity-50 transition-opacity flex items-center text-[#1A1A1A] bg-transparent border-none cursor-pointer p-0"
aria-label="Cart"
>
<ShoppingBag size={18} strokeWidth={1.4} />
{count > 0 && (
<span
className="absolute flex items-center justify-center rounded-full min-w-[15px] h-[15px] left-[9px] top-[-6px] px-1 text-white font-['Raleway',sans-serif] font-semibold text-[8.5px] leading-[12.75px]"
style={{background: utilityBurgundy}}
>
{count > 99 ? '99+' : count}
</span>
)}
</button>
);
}
export async function loadAbiirHeaderCms(args: {
sanity: import('hydrogen-sanity').SanityContext;
}): Promise<{
headerCms: AbiirHeaderCms;
headerSanityInitial: QueryResponseInitial<AbiirHeaderCms | null>;
}> {
const initial = await args.sanity
.loadQuery<SanityHeaderPayload>(HEADER_SANITY_QUERY)
.catch(
(): QueryResponseInitial<SanityHeaderPayload> => ({
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;
}