283 lines
8.1 KiB
TypeScript
283 lines
8.1 KiB
TypeScript
import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
|
|
import {
|
|
Outlet,
|
|
useRouteError,
|
|
isRouteErrorResponse,
|
|
type ShouldRevalidateFunction,
|
|
Links,
|
|
Meta,
|
|
Scripts,
|
|
ScrollRestoration,
|
|
useRouteLoaderData,
|
|
} from 'react-router';
|
|
import type {Route} from '~/lib/reactRouterRouteStubs';
|
|
import favicon from '~/assets/favicon.png';
|
|
import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
|
|
import resetStyles from '~/styles/reset.css?url';
|
|
import appStyles from '~/styles/app.css?url';
|
|
import tailwindCss from './styles/tailwind.css?url';
|
|
import {MetaPixelPageView} from './components/MetaPixelPageView';
|
|
import {PageLayout} from './components/PageLayout';
|
|
import {META_PIXEL_NOSCRIPT_IMG} from '~/lib/metaPixel';
|
|
import {FOOTER_SECTION_GROQ} from '~/components/Footer';
|
|
import {loadAbiirHeaderCms, loadAccountCustomer} from '~/components/Header';
|
|
import {loadFooterFromSanity} from '~/lib/footer';
|
|
import {shopRootMetaDescriptors} from '~/lib/shopSeo';
|
|
|
|
export type RootLoader = typeof loader;
|
|
|
|
/**
|
|
* This is important to avoid re-fetching root queries on sub-navigations
|
|
*/
|
|
export const shouldRevalidate: ShouldRevalidateFunction = ({
|
|
formMethod,
|
|
currentUrl,
|
|
nextUrl,
|
|
}) => {
|
|
// revalidate when a mutation is performed e.g add to cart, login...
|
|
if (formMethod && formMethod !== 'GET') return true;
|
|
|
|
// revalidate when manually revalidating via useRevalidator
|
|
if (currentUrl.toString() === nextUrl.toString()) return true;
|
|
|
|
// Defaulting to no revalidation for root loader data to improve performance.
|
|
// When using this feature, you risk your UI getting out of sync with your server.
|
|
// Use with caution. If you are uncomfortable with this optimization, update the
|
|
// line below to `return defaultShouldRevalidate` instead.
|
|
// For more details see: https://remix.run/docs/en/main/route/should-revalidate
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* The main and reset stylesheets are added in the Layout component
|
|
* to prevent a bug in development HMR updates.
|
|
*
|
|
* This avoids the "failed to execute 'insertBefore' on 'Node'" error
|
|
* that occurs after editing and navigating to another page.
|
|
*
|
|
* It's a temporary fix until the issue is resolved.
|
|
* https://github.com/remix-run/remix/issues/9242
|
|
*/
|
|
export const meta: Route.MetaFunction = ({data}) => {
|
|
const shop = (data as Awaited<ReturnType<typeof loader>> | undefined)?.header
|
|
?.shop;
|
|
return shopRootMetaDescriptors(shop ?? null);
|
|
};
|
|
|
|
export function links() {
|
|
return [
|
|
{
|
|
rel: 'preconnect',
|
|
href: 'https://cdn.shopify.com',
|
|
},
|
|
{
|
|
rel: 'preconnect',
|
|
href: 'https://shop.app',
|
|
},
|
|
{
|
|
rel: 'preconnect',
|
|
href: 'https://fonts.googleapis.com',
|
|
},
|
|
{
|
|
rel: 'preconnect',
|
|
href: 'https://fonts.gstatic.com',
|
|
crossOrigin: 'anonymous',
|
|
},
|
|
{
|
|
rel: 'stylesheet',
|
|
href: 'https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,400&family=Jost:ital,wght@0,300;0,400;0,500&family=Raleway:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap',
|
|
},
|
|
{rel: 'icon', type: 'image/png', href: favicon},
|
|
];
|
|
}
|
|
|
|
export async function loader(args: Route.LoaderArgs) {
|
|
const deferredData = loadDeferredData(args);
|
|
|
|
// Await the critical data required to render initial state of the page
|
|
const criticalData = await loadCriticalData(args);
|
|
|
|
const {storefront, env} = args.context;
|
|
|
|
// Resolve shell data here (not as embedded Promises) so header/cart render
|
|
// without `<Await>` / `Suspense` and SSR HTML always matches hydration.
|
|
const [cart, isLoggedIn, accountCustomer, footer] = await Promise.all([
|
|
deferredData.cart,
|
|
deferredData.isLoggedIn,
|
|
deferredData.accountCustomer,
|
|
deferredData.footer,
|
|
]);
|
|
|
|
return {
|
|
...criticalData,
|
|
cart,
|
|
isLoggedIn,
|
|
accountCustomer,
|
|
footer,
|
|
publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
|
|
shop: await getShopAnalytics({
|
|
storefront,
|
|
publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
|
|
}),
|
|
consent: {
|
|
checkoutDomain:
|
|
env.PUBLIC_CHECKOUT_DOMAIN ||
|
|
env.PUBLIC_STORE_DOMAIN ||
|
|
(import.meta.env.DEV ? 'mock.shop' : undefined),
|
|
storefrontAccessToken:
|
|
env.PUBLIC_STOREFRONT_API_TOKEN ||
|
|
(import.meta.env.DEV ? 'abcdefghijklmnopqrstuvwxyz123456' : undefined),
|
|
withPrivacyBanner: false,
|
|
// localize the privacy banner
|
|
country: args.context.storefront.i18n.country,
|
|
language: args.context.storefront.i18n.language,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load data necessary for rendering content above the fold. This is the critical data
|
|
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
*/
|
|
async function loadCriticalData({context}: Route.LoaderArgs) {
|
|
const {storefront, sanity} = context;
|
|
|
|
const [header, sanityHeader, footerCmsBundle] = await Promise.all([
|
|
storefront.query(HEADER_QUERY, {
|
|
cache: storefront.CacheLong(),
|
|
variables: {
|
|
headerMenuHandle: 'main-menu', // Adjust to your header menu handle
|
|
},
|
|
}),
|
|
loadAbiirHeaderCms({sanity}),
|
|
loadFooterFromSanity({sanity, groq: FOOTER_SECTION_GROQ}),
|
|
]);
|
|
|
|
return {
|
|
header,
|
|
headerCms: sanityHeader.headerCms,
|
|
headerSanityInitial: sanityHeader.headerSanityInitial,
|
|
footerCms: footerCmsBundle.footerCms,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load data for rendering content below the fold. This data is deferred and will be
|
|
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
*/
|
|
function loadDeferredData({context}: Route.LoaderArgs) {
|
|
const {storefront, customerAccount, cart} = context;
|
|
|
|
const accountCustomer = loadAccountCustomer(context).catch((err: Error) => {
|
|
console.error(err);
|
|
return null;
|
|
});
|
|
|
|
// defer the footer query (below the fold)
|
|
const footer = storefront
|
|
.query(FOOTER_QUERY, {
|
|
cache: storefront.CacheLong(),
|
|
variables: {
|
|
footerMenuHandle: 'footer', // Adjust to your footer menu handle
|
|
},
|
|
})
|
|
.catch((error: Error) => {
|
|
// Log query errors, but don't throw them so the page can still render
|
|
console.error(error);
|
|
return null;
|
|
});
|
|
return {
|
|
cart: cart.get(),
|
|
isLoggedIn: customerAccount.isLoggedIn(),
|
|
accountCustomer,
|
|
footer,
|
|
};
|
|
}
|
|
|
|
export function Layout({children}: {children?: React.ReactNode}) {
|
|
const nonce = useNonce();
|
|
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<link rel="stylesheet" href={tailwindCss}></link>
|
|
<link rel="stylesheet" href={resetStyles}></link>
|
|
<link rel="stylesheet" href={appStyles}></link>
|
|
<Meta />
|
|
<Links />
|
|
</head>
|
|
<body>
|
|
<noscript>
|
|
<img
|
|
alt=""
|
|
height="1"
|
|
src={META_PIXEL_NOSCRIPT_IMG}
|
|
style={{display: 'none'}}
|
|
width="1"
|
|
/>
|
|
</noscript>
|
|
{children}
|
|
<ScrollRestoration nonce={nonce} />
|
|
<Scripts nonce={nonce} />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
const data = useRouteLoaderData<RootLoader>('root');
|
|
|
|
if (!data) {
|
|
return (
|
|
<>
|
|
<MetaPixelPageView />
|
|
<Outlet />
|
|
</>
|
|
);
|
|
}
|
|
|
|
const consentReady = Boolean(data.consent.checkoutDomain);
|
|
|
|
return (
|
|
<Analytics.Provider
|
|
cart={data.cart}
|
|
shop={data.shop}
|
|
consent={data.consent}
|
|
disableThrowOnError={!consentReady}
|
|
>
|
|
<MetaPixelPageView />
|
|
<PageLayout {...data}>
|
|
<Outlet />
|
|
</PageLayout>
|
|
</Analytics.Provider>
|
|
);
|
|
}
|
|
|
|
export function ErrorBoundary() {
|
|
const error = useRouteError();
|
|
let errorMessage = 'Unknown error';
|
|
let errorStatus = 500;
|
|
|
|
if (isRouteErrorResponse(error)) {
|
|
errorMessage = error?.data?.message ?? error.data;
|
|
errorStatus = error.status;
|
|
} else if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
return (
|
|
<div className="route-error">
|
|
<h1>Oops</h1>
|
|
<h2>{errorStatus}</h2>
|
|
{errorMessage && (
|
|
<fieldset>
|
|
<pre>{errorMessage}</pre>
|
|
</fieldset>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|