AbiirSkincare/Hydrogen/web/app/root.tsx

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