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

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>
);
}