| import {Suspense} from 'react'; |
| import {Await, NavLink} from '@remix-run/react'; |
| import {type CartViewPayload, useAnalytics} from '@shopify/hydrogen'; |
| import type {HeaderQuery, CartApiQueryFragment} from 'storefrontapi.generated'; |
| import {useAside} from '~/components/Aside'; |
|
|
| interface HeaderProps { |
| header: HeaderQuery; |
| cart: Promise<CartApiQueryFragment | null>; |
| isLoggedIn: Promise<boolean>; |
| publicStoreDomain: string; |
| } |
|
|
| type Viewport = 'desktop' | 'mobile'; |
|
|
| export function Header({ |
| header, |
| isLoggedIn, |
| cart, |
| publicStoreDomain, |
| }: HeaderProps) { |
| const {shop, menu} = header; |
| return ( |
| <header className="header"> |
| <NavLink prefetch="intent" to="/" style={activeLinkStyle} end> |
| <strong>{shop.name}</strong> |
| </NavLink> |
| <HeaderMenu |
| menu={menu} |
| viewport="desktop" |
| primaryDomainUrl={header.shop.primaryDomain.url} |
| publicStoreDomain={publicStoreDomain} |
| /> |
| <HeaderCtas isLoggedIn={isLoggedIn} cart={cart} /> |
| </header> |
| ); |
| } |
|
|
| export function HeaderMenu({ |
| menu, |
| primaryDomainUrl, |
| viewport, |
| publicStoreDomain, |
| }: { |
| menu: HeaderProps['header']['menu']; |
| primaryDomainUrl: HeaderProps['header']['shop']['primaryDomain']['url']; |
| viewport: Viewport; |
| publicStoreDomain: HeaderProps['publicStoreDomain']; |
| }) { |
| const className = `header-menu-${viewport}`; |
| const {close} = useAside(); |
|
|
| return ( |
| <nav className={className} role="navigation"> |
| {viewport === 'mobile' && ( |
| <NavLink |
| end |
| onClick={close} |
| prefetch="intent" |
| style={activeLinkStyle} |
| to="/" |
| > |
| Home |
| </NavLink> |
| )} |
| {(menu || FALLBACK_HEADER_MENU).items.map((item) => { |
| if (!item.url) return null; |
| |
| // if the url is internal, we strip the domain |
| const url = |
| item.url.includes('myshopify.com') || |
| item.url.includes(publicStoreDomain) || |
| item.url.includes(primaryDomainUrl) |
| ? new URL(item.url).pathname |
| : item.url; |
| return ( |
| <NavLink |
| className="header-menu-item" |
| end |
| key={item.id} |
| onClick={close} |
| prefetch="intent" |
| style={activeLinkStyle} |
| to={url} |
| > |
| {item.title} |
| </NavLink> |
| ); |
| })} |
| </nav> |
| ); |
| } |
|
|
| function HeaderCtas({ |
| isLoggedIn, |
| cart, |
| }: Pick<HeaderProps, 'isLoggedIn' | 'cart'>) { |
| return ( |
| <nav className="header-ctas" role="navigation"> |
| <HeaderMenuMobileToggle /> |
| <NavLink prefetch="intent" to="/account" style={activeLinkStyle}> |
| <Suspense fallback="Sign in"> |
| <Await resolve={isLoggedIn} errorElement="Sign in"> |
| {(isLoggedIn) => (isLoggedIn ? 'Account' : 'Sign in')} |
| </Await> |
| </Suspense> |
| </NavLink> |
| <SearchToggle /> |
| <CartToggle cart={cart} /> |
| </nav> |
| ); |
| } |
|
|
| function HeaderMenuMobileToggle() { |
| const {open} = useAside(); |
| return ( |
| <button |
| className="header-menu-mobile-toggle reset" |
| onClick={() => open('mobile')} |
| > |
| <h3>☰</h3> |
| </button> |
| ); |
| } |
|
|
| function SearchToggle() { |
| const {open} = useAside(); |
| return ( |
| <button className="reset" onClick={() => open('search')}> |
| Search |
| </button> |
| ); |
| } |
|
|
| function CartBadge({count}: {count: number | null}) { |
| const {open} = useAside(); |
| const {publish, shop, cart, prevCart} = useAnalytics(); |
|
|
| return ( |
| <a |
| href="/cart" |
| onClick={(e) => { |
| e.preventDefault(); |
| open('cart'); |
| publish('cart_viewed', { |
| cart, |
| prevCart, |
| shop, |
| url: window.location.href || '', |
| } as CartViewPayload); |
| }} |
| > |
| Cart {count === null ? <span> </span> : count} |
| </a> |
| ); |
| } |
|
|
| function CartToggle({cart}: Pick<HeaderProps, 'cart'>) { |
| return ( |
| <Suspense fallback={<CartBadge count={null} />}> |
| <Await resolve={cart}> |
| {(cart) => { |
| if (!cart) return <CartBadge count={0} />; |
| return <CartBadge count={cart.totalQuantity || 0} />; |
| }} |
| </Await> |
| </Suspense> |
| ); |
| } |
|
|
| const FALLBACK_HEADER_MENU = { |
| id: 'gid://shopify/Menu/199655587896', |
| items: [ |
| { |
| id: 'gid://shopify/MenuItem/461609500728', |
| resourceId: null, |
| tags: [], |
| title: 'Collections', |
| type: 'HTTP', |
| url: '/collections', |
| items: [], |
| }, |
| { |
| id: 'gid://shopify/MenuItem/461609533496', |
| resourceId: null, |
| tags: [], |
| title: 'Blog', |
| type: 'HTTP', |
| url: '/blogs/journal', |
| items: [], |
| }, |
| { |
| id: 'gid://shopify/MenuItem/461609566264', |
| resourceId: null, |
| tags: [], |
| title: 'Policies', |
| type: 'HTTP', |
| url: '/policies', |
| items: [], |
| }, |
| { |
| id: 'gid://shopify/MenuItem/461609599032', |
| resourceId: 'gid://shopify/Page/92591030328', |
| tags: [], |
| title: 'About', |
| type: 'PAGE', |
| url: '/pages/about', |
| items: [], |
| }, |
| ], |
| }; |
|
|
| function activeLinkStyle({ |
| isActive, |
| isPending, |
| }: { |
| isActive: boolean; |
| isPending: boolean; |
| }) { |
| return { |
| fontWeight: isActive ? 'bold' : undefined, |
| color: isPending ? 'grey' : 'black', |
| }; |
| } |
|
|