224 lines
6.7 KiB
TypeScript
224 lines
6.7 KiB
TypeScript
import {useOptimisticCart} from '@shopify/hydrogen';
|
|
import {Await, Link} from 'react-router';
|
|
import {Suspense, useId} from 'react';
|
|
import type {
|
|
CartApiQueryFragment,
|
|
FooterQuery,
|
|
HeaderQuery,
|
|
} from 'storefrontapi.generated';
|
|
import type {CustomerFragment} from 'customer-accountapi.generated';
|
|
import type {QueryResponseInitial} from '@sanity/react-loader';
|
|
import type {AbiirHeaderCms} from '~/components/Header';
|
|
import type {AbiirFooterCms} from '~/lib/footer';
|
|
import {Aside} from '~/components/Aside';
|
|
import {Footer} from '~/components/Footer';
|
|
import {Header} from '~/components/Header';
|
|
import {WishlistServerSync} from '~/components/WishlistServerSync';
|
|
import {CartMain} from '~/components/CartMain';
|
|
import {
|
|
SEARCH_ENDPOINT,
|
|
SearchFormPredictive,
|
|
} from '~/components/SearchFormPredictive';
|
|
import {SearchResultsPredictive} from '~/components/SearchResultsPredictive';
|
|
|
|
interface PageLayoutProps {
|
|
cart: CartApiQueryFragment | null;
|
|
footer: FooterQuery | null;
|
|
footerCms: AbiirFooterCms;
|
|
header: HeaderQuery;
|
|
headerCms: AbiirHeaderCms;
|
|
headerSanityInitial: QueryResponseInitial<AbiirHeaderCms | null>;
|
|
isLoggedIn: boolean;
|
|
accountCustomer: CustomerFragment | null;
|
|
publicStoreDomain: string;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
export function PageLayout({
|
|
cart,
|
|
children = null,
|
|
footer,
|
|
footerCms,
|
|
header,
|
|
headerCms,
|
|
headerSanityInitial,
|
|
isLoggedIn,
|
|
accountCustomer,
|
|
publicStoreDomain,
|
|
}: PageLayoutProps) {
|
|
return (
|
|
<Aside.Provider>
|
|
<CartAside cart={cart} />
|
|
<SearchAside />
|
|
<WishlistServerSync isLoggedIn={isLoggedIn} />
|
|
{header && (
|
|
<Header
|
|
header={header}
|
|
headerCms={headerCms}
|
|
headerSanityInitial={headerSanityInitial}
|
|
cart={cart}
|
|
isLoggedIn={isLoggedIn}
|
|
accountCustomer={accountCustomer}
|
|
publicStoreDomain={publicStoreDomain}
|
|
/>
|
|
)}
|
|
<main>{children}</main>
|
|
<Footer
|
|
footer={footer}
|
|
footerCms={footerCms}
|
|
header={header}
|
|
headerCms={headerCms}
|
|
publicStoreDomain={publicStoreDomain}
|
|
/>
|
|
</Aside.Provider>
|
|
);
|
|
}
|
|
|
|
function CartAsideHeading({cart}: {cart: PageLayoutProps['cart']}) {
|
|
const optimistic = useOptimisticCart(cart);
|
|
const n = optimistic?.totalQuantity ?? 0;
|
|
return (
|
|
<span className="cart-aside-heading">
|
|
<svg
|
|
className="cart-aside-heading__bag"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden
|
|
>
|
|
<path
|
|
d="M6 8V7a6 6 0 1 1 12 0v1h1.5A1.5 1.5 0 0 1 21 9.5v9A2.5 2.5 0 0 1 18.5 21h-13A2.5 2.5 0 0 1 3 18.5v-9A1.5 1.5 0 0 1 4.5 8H6Zm2 0h8V7a4 4 0 1 0-8 0v1Z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
<span className="cart-aside-heading__title">Your Bag ({n})</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function CartAside({cart}: {cart: PageLayoutProps['cart']}) {
|
|
return (
|
|
<Aside type="cart" heading={<CartAsideHeading cart={cart} />}>
|
|
<CartMain cart={cart} layout="aside" />
|
|
</Aside>
|
|
);
|
|
}
|
|
|
|
function SearchAsideHeading() {
|
|
return (
|
|
<span className="cart-aside-heading">
|
|
<svg
|
|
className="cart-aside-heading__bag"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
stroke="currentColor"
|
|
strokeWidth="1.75"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden
|
|
>
|
|
<circle cx="10.5" cy="10.5" r="6.5" />
|
|
<path d="M16 16l3.5 3.5" />
|
|
</svg>
|
|
<span className="cart-aside-heading__title">Search</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function SearchAside() {
|
|
const queriesDatalistId = useId();
|
|
return (
|
|
<Aside type="search" heading={<SearchAsideHeading />}>
|
|
<div className="search-aside">
|
|
<SearchFormPredictive className="search-aside-form predictive-search-form">
|
|
{({fetchResults, goToSearch, inputRef}) => (
|
|
<div className="search-aside__fields">
|
|
<input
|
|
name="q"
|
|
onChange={fetchResults}
|
|
onFocus={fetchResults}
|
|
placeholder="Search products, pages…"
|
|
ref={inputRef}
|
|
type="search"
|
|
list={queriesDatalistId}
|
|
className="search-aside__input"
|
|
autoComplete="off"
|
|
aria-label="Search"
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="search-aside__submit"
|
|
onClick={goToSearch}
|
|
>
|
|
Search
|
|
</button>
|
|
</div>
|
|
)}
|
|
</SearchFormPredictive>
|
|
|
|
<div className="search-aside__scroll predictive-search">
|
|
<SearchResultsPredictive>
|
|
{({items, total, term, state, closeSearch}) => {
|
|
const {articles, collections, pages, products, queries} = items;
|
|
|
|
if (state === 'loading' && term.current) {
|
|
return <p className="search-aside__status">Searching…</p>;
|
|
}
|
|
|
|
if (!total) {
|
|
return <SearchResultsPredictive.Empty term={term} />;
|
|
}
|
|
|
|
return (
|
|
<div className="search-aside__results">
|
|
<SearchResultsPredictive.Queries
|
|
queries={queries}
|
|
queriesDatalistId={queriesDatalistId}
|
|
/>
|
|
<SearchResultsPredictive.Products
|
|
products={products}
|
|
closeSearch={closeSearch}
|
|
term={term}
|
|
/>
|
|
<SearchResultsPredictive.Collections
|
|
collections={collections}
|
|
closeSearch={closeSearch}
|
|
term={term}
|
|
/>
|
|
<SearchResultsPredictive.Pages
|
|
pages={pages}
|
|
closeSearch={closeSearch}
|
|
term={term}
|
|
/>
|
|
<SearchResultsPredictive.Articles
|
|
articles={articles}
|
|
closeSearch={closeSearch}
|
|
term={term}
|
|
/>
|
|
{term.current && total ?
|
|
<Link
|
|
className="search-aside__view-all"
|
|
onClick={closeSearch}
|
|
to={`${SEARCH_ENDPOINT}?q=${encodeURIComponent(term.current)}`}
|
|
>
|
|
<span>View all results for </span>
|
|
<q>{term.current}</q>
|
|
<span aria-hidden> →</span>
|
|
</Link>
|
|
: null}
|
|
</div>
|
|
);
|
|
}}
|
|
</SearchResultsPredictive>
|
|
</div>
|
|
</div>
|
|
</Aside>
|
|
);
|
|
}
|
|
|