Merge pull request #16 from zenzen-sol/sol/2023-11-12-merge-test

feat: Add partial prerendering
This commit is contained in:
Sol Irvine 2023-11-12 17:20:30 +09:00 committed by GitHub
commit afe9eb3a7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 602 additions and 1081 deletions

View File

@ -5,11 +5,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Cancel running workflows
uses: styfle/cancel-workflow-action@0.11.0
uses: styfle/cancel-workflow-action@0.12.0
with:
access_token: ${{ github.token }}
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set node version
uses: actions/setup-node@v3
with:
@ -27,6 +27,6 @@ jobs:
key: node-modules-cache-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: pnpm install
run: pnpm install --no-frozen-lockfile
- name: Run tests
run: pnpm test

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

211
README.md
View File

@ -23,13 +23,26 @@ A Next.js 13 and App Router-ready ecommerce template featuring:
Vercel will only be actively maintaining a Shopify version [as outlined in our vision and strategy for Next.js Commerce](https://github.com/vercel/commerce/pull/966).
Vercel is more than happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with their own implementation while leaving the rest of the template mostly unchanged.
Vercel is happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with their own implementation while leaving the rest of the template mostly unchanged.
- Shopify (this repository)
- [BigCommerce](https://github.com/bigcommerce/nextjs-commerce) ([Demo](https://next-commerce-v2.vercel.app/))
- [Medusa](https://github.com/medusajs/vercel-commerce) ([Demo](https://medusa-nextjs-commerce.vercel.app/))
- [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/))
- [Shopware](https://github.com/shopwareLabs/vercel-commerce) ([Demo](https://shopware-vercel-commerce-react.vercel.app/))
- [Swell](https://github.com/swellstores/verswell-commerce) ([Demo](https://verswell-commerce.vercel.app/))
- [Umbraco](https://github.com/umbraco/Umbraco.VercelCommerce.Demo) ([Demo](https://vercel-commerce-demo.umbraco.com/))
- [Wix](https://github.com/wix/nextjs-commerce) ([Demo](https://wix-nextjs-commerce.vercel.app/))
> Note: Providers, if you are looking to use similar products for your demo, you can [download these assets](https://drive.google.com/file/d/1q_bKerjrwZgHwCw0ovfUMW6He9VtepO_/view?usp=sharing).
## Integrations
Integrations enable upgraded or additional functionality for Next.js Commerce
- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/))
- Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration.
- Search runs entirely in the browser for smaller catalogs or on a CDN for larger.
## Running locally
@ -55,199 +68,9 @@ Your app should now be running on [localhost:3000](http://localhost:3000/).
1. Select the `Vercel Solutions` scope.
1. Connect to the existing `commerce-shopify` project.
1. Run `vc env pull` to get environment variables.
1. Run `pmpm dev` to ensure everything is working correctly.
1. Run `pnpm dev` to ensure everything is working correctly.
</details>
## How to configure your Shopify store for Next.js Commerce
## Vercel, Next.js Commerce, and Shopify Integration Guide
Next.js Commerce requires a [paid Shopify plan](https://www.shopify.com/pricing).
> Note: Next.js Commerce will not work with a Shopify Starter plan as it does not allow installation of custom themes, which is required to run as a headless storefront.
### Add Shopify domain to an environment variable
Create a `SHOPIFY_STORE_DOMAIN` environment variable and use your Shopify domain as the the value (ie. `[your-shopify-store-subdomain].myshopify.com`).
> Note: Do not include the `https://`.
### Accessing the Shopify Storefront API
Next.js Commerce utilizes [Shopify's Storefront API](https://shopify.dev/docs/api/storefront) to create unique customer experiences. The API offers a full range of commerce options making it possible for customers to control products, collections, menus, pages, cart, checkout, and more.
In order to use the Shopify's Storefront API, you need to install the [Headless app](https://apps.shopify.com/headless) in your Shopify store.
Once installed, you'll need to create a `SHOPIFY_STOREFRONT_ACCESS_TOKEN` environment variable and use the public access token as the value.
> Note: Shopify does offer a Node.js Storefront API SDK. We use the Storefront API via GraphQL directly instead of the Node.js SDK so we have more control over fetching and caching.
<details>
<summary>Expand to view detailed walkthrough</summary>
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/settings/apps`.
1. Click the green `Shopify App Store` button. ![Shopify App Store](https://user-images.githubusercontent.com/446260/233220545-cb4c1461-ebc5-424e-a421-bf0d32044027.jpg)
1. Search for `Headless` and click on the `Headless` app. ![Headless](https://user-images.githubusercontent.com/446260/233220547-6d93b5ef-16c7-45db-99e7-13ae7e18eb39.jpg)
1. Click the black `Add app` button. ![Add app](https://user-images.githubusercontent.com/446260/233220550-a34c8bda-75a8-437a-9673-125f3794ff35.jpg)
1. Click the green `Add sales channel` button. ![Add sales channel](https://user-images.githubusercontent.com/446260/233220553-42d94a74-421d-4f8a-99ab-a95936b707a3.jpg)
1. Click the green `Create storefront` button. ![Create storefront](https://user-images.githubusercontent.com/446260/233220556-1eee15c4-a45d-446e-9f73-2e7c9f56b29c.jpg)
1. Copy and paste the public access token and assign it to a `SHOPIFY_STOREFRONT_ACCESS_TOKEN` environment variable. ![Pubic access token](https://user-images.githubusercontent.com/446260/233220558-5db04ff9-b894-40fe-bfba-0e92f26b8e1f.jpg)
1. If you ever need to reference the public access token again, you can navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/headless_storefronts`.
</details>
### Install a headless theme
When using a headless Shopify setup, you normally don't want customers to access any of the theme pages except for checkout. However, you can't totally disable the theme and a lot of links will still point to the theme (e.g. links in emails, order details, plugins, checkout, etc.).
To enable a seamless flow between your headless site and Shopify, you can install the [Shopify Headless Theme](https://github.com/instantcommerce/shopify-headless-theme).
Follow the installation instructions and configure the theme with your headless site's values.
<details>
<summary>Expand to view detailed walkthrough</summary>
1. Download [Shopify Headless Theme](https://github.com/instantcommerce/shopify-headless-theme). ![Download Shoify Headless Theme](https://user-images.githubusercontent.com/446260/233220560-9f3f5ab0-ffb4-4305-b4ee-2c9d33eea90f.jpg)
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/themes`.
1. Click `Add theme`, then `Upload zip file`. ![Upload zip file](https://user-images.githubusercontent.com/446260/233220561-7a53809e-0d95-45eb-b52f-3a52e3663a9c.jpg)
1. Select the downloaded zip file from above, and click the green `Upload file` button. ![Select and upload file](https://user-images.githubusercontent.com/446260/233220563-135fb9f7-2921-4189-8f17-3b1cc15c0ea6.jpg)
1. Click `Customize`. ![Customize theme](https://user-images.githubusercontent.com/446260/233220565-24b9c954-c18a-46f1-9db5-3d2a00040e48.jpg)
1. Click `Theme settings` (ie. the paintbrush icon), expand the `STOREFRONT` section, enter your headless store domain, click the gray `Publish` button. ![Set headless domain in theme settings](https://user-images.githubusercontent.com/446260/233220566-acaee14d-03f8-400d-a2a2-28e85eb5ecdc.jpg)
1. Confirm the theme change by clicking the green `Save and publish` button. ![Confirm save and publish](https://user-images.githubusercontent.com/446260/233220567-504d5bde-cfb9-426d-a264-f9a12d02af13.jpg)
1. The headless theme should now be your current active theme. ![Headless theme is current and active](https://user-images.githubusercontent.com/446260/233220569-63cab2b4-241b-4bf1-9b5b-451daaeceb91.jpg)
</details>
### Branding & Design
Since you're creating a headless Shopify store, you'll be in full control of your brand and design. However, there are still a few aspects we're leaving within Shopify's control.
- Checkout
- Emails
- Order status
- Order history
- Favicon (for any Shopify controlled pages)
You can use Shopify's admin to customize these pages to match your brand and design.
<details>
<summary>Expand to view detailed walkthrough</summary>
#### Checkout, order status, and order history
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/settings/checkout`.
1. Click the green `Customize` button. ![Customize](https://user-images.githubusercontent.com/446260/233220530-9beda4b4-5008-440a-b923-9d196b722539.jpg)
1. Click `Branding` (ie. the paintbrush icon) and customize your brand. Please note, there are three steps / pages to the checkout flow. Use the dropdown to change pages and adjust branding as needed on each page. Click `Save` when you are done. ![Branding](https://user-images.githubusercontent.com/446260/233220534-e884d9fd-1a39-4f4d-9d09-163dde47c2e8.jpg)
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/settings/branding`.
1. Customize settings to match your brand. ![Branding](https://user-images.githubusercontent.com/446260/233220536-452b8802-9a1e-40f0-9a12-52b3dace84a5.jpg)
#### Emails
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/settings/email_settings`.
1. Customize settings to match your brand. ![Branding](https://user-images.githubusercontent.com/446260/233220538-13c83a9e-55f8-41e6-9b34-a39ee0848a8a.jpg)
#### Favicon
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/themes`.
1. Click the green `Customize` button. ![Customize theme](https://user-images.githubusercontent.com/446260/233220539-4869a6cd-f59f-4de6-8091-95ed81d2302d.jpg)
1. Click `Theme settings` (ie. the paintbrush icon), expand the `FAVICON` section, upload favicon, then click the `Save` button. ![Favicon](https://user-images.githubusercontent.com/446260/233220542-ac81b674-d86e-4172-ab38-c79d1ad1ff36.jpg)
</details>
### Configure webhooks for on-demand incremental static regeneration (ISR)
Utilizing [Shopify's webhooks](https://shopify.dev/docs/apps/webhooks), and listening for select [Shopify webhook event topics](https://shopify.dev/docs/api/admin-rest/2022-04/resources/webhook#event-topics), we can use [Next'js on-demand revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#using-on-demand-revalidation) to keep data fetches indefinitely cached until certain events in the Shopify store occur.
Next.js is pre-configured to listen for the following Shopify webhook events and automatically revalidate fetches.
- `collections/create`
- `collections/delete`
- `collections/update`
- `products/create`
- `products/delete`
- `products/update` (this also includes when variants are added, updated, and removed as well as when products are purchased so inventory and out of stocks can be updated)
<details>
<summary>Expand to view detailed walkthrough</summary>
#### Setup secret for secure revalidation
1. Create your own secret or [generate a random UUID](https://www.uuidgenerator.net/guid).
1. Create a [Vercel Environment Variable](https://vercel.com/docs/concepts/projects/environment-variables) named `SHOPIFY_REVALIDATION_SECRET` and use the value from above.
#### Configure Shopify webhooks
1. Navigate to `https://[your-shopify-store-subdomain].myshopify.com/admin/settings/notifications`.
1. Add webhooks for all six event topics listed above. You can add more sets for other preview urls, environments, or local development. Append `?secret=[SECRET]` to each url, where `[SECRET]` is the secret you created above. ![Shopify store webhooks](https://github.com/vercel/commerce/assets/446260/3d713fd7-b642-46e2-b2ce-f2b695ff6d2b) ![Shopify store add webhook](https://github.com/vercel/commerce/assets/446260/f0240a22-be07-42bc-bf6c-b97873868677)
#### Testing webhooks during local development
The easiest way to test webhooks while developing locally is to use [ngrok](https://ngrok.com).
1. [Install and configure ngrok](https://ngrok.com/download) (you will need to create an account).
1. Run your app locally, `npm run dev`.
1. In a separate terminal session, run `ngrok http 3000`.
1. Use the url generated by ngrok and add or update your webhook urls in Shopify. ![ngrok](https://github.com/vercel/commerce/assets/446260/5dc09c5d-0e48-479c-ab64-de8dc9a2c4b1) ![Shopify store edit webhook](https://github.com/vercel/commerce/assets/446260/13fd397d-4666-4e8d-b25f-4adc674345c0)
1. You can now make changes to your store and your local app should receive updates. You can also use the `Send test notification` button to trigger a generic webhook test. ![Shopify store webhook send test notification](https://github.com/vercel/commerce/assets/446260/e872e233-1663-446d-961f-8c9455358530)
</details>
### Using Shopify as a CMS
Next.js Commerce is fully powered by Shopify in a truly headless and data driven way.
#### Products
`https://[your-shopify-store-subdomain].myshopify.com/admin/products`
Only `Active` products are shown. `Draft` products will not be shown until they are marked as `Active`.
`Active` products can still be hidden and not seen by navigating the site, by adding a `nextjs-frontend-hidden` tag on the product. This tag will also tell search engines to not index or crawl the product. The product is still directly accessible via url. This feature is great for "secret" products you only want to people you share the url with.
Product options and option combinations are driven from Shopify options and variants. When selecting options on the product detail page, other option and variant combinations will be visually validated and verified for availability, like Amazon does.
Products that are active and "out of stock" are still shown on the site, but the ability to add the product to the cart is disabled.
#### Collections
`https://[your-shopify-store-subdomain].myshopify.com/admin/collections`
Create whatever collections you want and configure them however you want. All available collections will show on the search page as filters on the left, with one exception...
Any collection names that start with the word "hidden" will not show up on the headless front end. The Next.js Commerce theme comes pre-configured to look for two hidden collections. Collections were chosen for this over tags so that order of products could be controlled (collections allow for manual ordering).
Create the following collections:
- `Hidden: Homepage Featured Items` -- Products in this collection are displayed in the three featured blocks on the homepage.
- `Hidden: Homepage Carousel` -- Products in this collection are displayed in the auto-scrolling carousel section on the homepage.
![Shopify collections](https://user-images.githubusercontent.com/446260/233220543-81896a2b-7085-4abc-a4f1-ce321e08b953.jpg)
![Shopify collection detail](https://user-images.githubusercontent.com/446260/233220544-ecd4c069-49fc-4a0b-8378-aa5e1b4b5257.jpg)
#### Pages
`https://[your-shopify-store-subdomain].myshopify.com/admin/pages`
Next.js Commerce contains a dynamic `[page]` route. It will use the value to look for a corresponding page in Shopify. If a page is found, it will display its rich content using Tailwind's prose. If a page is not found, a 404 page is displayed.
![Shopify pages](https://user-images.githubusercontent.com/446260/233221142-4dc3fa56-5256-4d84-b0a3-331ffb7d79b2.jpg)
![Shopify page detail](https://user-images.githubusercontent.com/446260/233247700-cbeaf917-fb67-49e9-b9b9-5ee8cb188639.jpg)
#### Navigation menus
`https://[your-shopify-store-subdomain].myshopify.com/admin/menus`
Next.js Commerce's header and footer navigation is pre-configured to be controlled by Shopify navigation menus. This means you have full control over what links go here. They can be to collections, pages, external links, and more.
Create the following navigation menus:
- `Next.js Frontend Header Menu` -- Menu items to be shown in the headless frontend header.
- `Next.js Frontend Footer Menu` -- Menu items to be shown in the headless frontend footer.
![Shopify navigation menus](https://user-images.githubusercontent.com/446260/233220571-33f9d5a8-1206-4ab4-ad79-83b4ca954331.jpg)
![Shopify navigation menu detail](https://user-images.githubusercontent.com/446260/233220573-5f03a51f-4100-461f-a696-f085856e391b.jpg)
#### SEO
Shopify's products, collections, pages, etc. allow you to create custom SEO titles and descriptions. Next.js Commerce is pre-configured to display these custom values, but also comes with sensible default fallbacks if they are not provided.
![Shopify SEO](https://user-images.githubusercontent.com/446260/233247701-0ff2a560-7949-4e6c-b3a8-8168ed6341f8.jpg)
You can use this comprehensive [integration guide](http://vercel.com/docs/integrations/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel.

View File

@ -1,5 +1,3 @@
import { Suspense } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense>{children}</Suspense>;
return <>{children}</>;
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import AboutNaraiDetail from './about-narai-detail';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -39,13 +36,13 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
return (
<div>
<Navbar cart={cart} locale={params?.locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="pt-24 md:pt-32">
<AboutNaraiDetail awards={awardsPage.body} />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -1,5 +1,3 @@
import { Suspense } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense>{children}</Suspense>;
return <>{children}</>;
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import SagyobarDetail from './sagyobar-detail';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -37,13 +34,13 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
return (
<div>
<Navbar cart={cart} locale={params?.locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="pt-12">
<SagyobarDetail />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -1,5 +1,3 @@
import { Suspense } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense>{children}</Suspense>;
return <>{children}</>;
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import CompanyDetail from './company-detail';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -37,13 +34,13 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
return (
<div>
<Navbar cart={cart} locale={params?.locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="pt-12">
<CompanyDetail />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -1,5 +1,3 @@
import { Suspense } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense>{children}</Suspense>;
return <>{children}</>;
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import ConceptDetail from './concept-detail';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -37,13 +34,13 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
return (
<div>
<Navbar cart={cart} locale={params?.locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="pt-12">
<ConceptDetail />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import Disclosures from './disclosures';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -41,13 +38,13 @@ export default async function DisclosuresPage({
return (
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="py-24 md:py-48">
<Disclosures />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,7 +4,6 @@ import { ReactNode, Suspense } from 'react';
import { SupportedLocale } from 'components/layout/navbar/language-control';
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import Analytics from './analytics';
import './globals.css';
@ -81,12 +80,7 @@ export default async function RootLayout({
children: ReactNode;
params: { locale?: SupportedLocale };
}) {
let messages;
try {
messages = (await import(`../../messages/${params?.locale}.json`)).default;
} catch (error) {
notFound();
}
const messages = (await import(`../../messages/${params?.locale}.json`)).default;
return (
<html
@ -95,10 +89,10 @@ export default async function RootLayout({
>
<body className="bg-dark text-white selection:bg-green-800 selection:text-green-400">
<NextIntlClientProvider locale={params?.locale} messages={messages}>
<Suspense>
<Suspense fallback={null}>
<Analytics />
<main>{children}</main>
</Suspense>
<main>{children}</main>
</NextIntlClientProvider>
</body>
</html>

View File

@ -1,7 +1,6 @@
import OpengraphImage from 'components/opengraph-image';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export default async function Image() {
return await OpengraphImage();

View File

@ -26,9 +26,6 @@ import { cookies } from 'next/headers';
import Image from 'next/image';
import { Suspense } from 'react';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -63,7 +60,9 @@ export default async function HomePage({
<HomepageProducts lang={locale} />
</div>
<div className="py-24 md:py-48">
<Suspense fallback={null}>
<NewsletterSignup />
</Suspense>
</div>
<div className="relative mx-auto max-w-screen-xl">
<Image
@ -74,7 +73,9 @@ export default async function HomePage({
/>
</div>
<div className="py-24">
<Suspense fallback={null}>
<Shoplist />
</Suspense>
</div>
<div className="relative pb-48">
@ -152,9 +153,7 @@ export default async function HomePage({
/>
</div>
<Suspense>
<Footer cart={cart} promotedItem={promotedItem} />
</Suspense>
</div>
);
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import PrivacyPolicy from './privacy-policy';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -41,13 +38,13 @@ export default async function PrivacyPage({
return (
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="py-24 md:py-48">
<PrivacyPolicy />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -4,10 +4,7 @@ import { SupportedLocale } from 'components/layout/navbar/language-control';
import Navbar from 'components/layout/navbar';
import { getCart } from 'lib/shopify';
import { cookies } from 'next/headers';
import { ReactNode, Suspense } from 'react';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
import { ReactNode } from 'react';
const { SITE_NAME } = process.env;
@ -37,9 +34,7 @@ export default async function ProductLayout({
<div>
<Navbar cart={cart} locale={locale} compact />
{children}
<Suspense>
<Footer cart={cart} />
</Suspense>
</div>
);
}

View File

@ -1,6 +1,5 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import { ChevronDoubleRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
@ -17,9 +16,7 @@ import { getProduct, getProductRecommendations } from 'lib/shopify';
import { Image as MediaImage, Product } from 'lib/shopify/types';
import Image from 'next/image';
import Link from 'next/link';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
import { Suspense } from 'react';
export async function generateMetadata({
params
@ -136,13 +133,17 @@ export default async function ProductPage({
</div>
<div className="max-w-sm">
<Suspense>
<VariantSelector options={product.options} variants={product.variants} />
</Suspense>
<Suspense>
<AddManyToCart
quantity={1}
variants={product.variants}
availableForSale={product.availableForSale}
/>
</Suspense>
</div>
<div className="border-b border-white/20 pb-6"></div>
@ -165,6 +166,7 @@ export default async function ProductPage({
)}
<div className="grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-0">
<Suspense fallback={null}>
{!!otherImages &&
otherImages?.length > 0 &&
otherImages.map((image, index) => {
@ -189,6 +191,7 @@ export default async function ProductPage({
</div>
);
})}
</Suspense>
</div>
{!!product?.lower?.value && (
@ -197,9 +200,7 @@ export default async function ProductPage({
</div>
)}
<Suspense>
<RelatedProducts id={product.id} />
</Suspense>
</div>
</div>
</>

View File

@ -8,9 +8,6 @@ import { Product } from 'lib/shopify/types';
import { cookies } from 'next/headers';
import { Suspense } from 'react';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -41,13 +38,13 @@ export default async function ProductPage({
return (
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="py-24 md:py-48">
<ProductGrid lang={locale} />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -1,7 +1,3 @@
import { Suspense } from 'react';
export const revalidate = 300; // 5 minutes in seconds
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense>{children}</Suspense>;
return <>{children}</>;
}

View File

@ -1,9 +1,6 @@
import OpengraphImage from 'components/opengraph-image';
import { getPage } from 'lib/shopify';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export default async function Image({ params }: { params: { page: string } }) {
const page = await getPage({ handle: params.page });
const title = page.seo?.title || page.title;

View File

@ -3,22 +3,23 @@ import type { Metadata } from 'next';
import Footer from 'components/layout/footer';
import Navbar from 'components/layout/navbar';
import { SupportedLocale } from 'components/layout/navbar/language-control';
import Prose from 'components/prose';
import { getCart, getPage, getProduct } from 'lib/shopify';
import { Product } from 'lib/shopify/types';
import { cookies } from 'next/headers';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import ShopListDetail from './shop-list-detail';
import ShopsNav from './shops-nav';
export const revalidate = 300; // 5 minutes in seconds
export async function generateMetadata({
params
}: {
params: { locale?: SupportedLocale };
}): Promise<Metadata> {
const page = await getPage({ handle: 'shop-list', language: params?.locale?.toUpperCase() });
const page = await getPage({
handle: 'shop-list',
language: params?.locale?.toUpperCase() || 'JA'
});
if (!page) return notFound();
@ -41,13 +42,9 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
cart = await getCart(cartId);
}
const page = await getPage({ handle: 'shop-list', language: params?.locale?.toUpperCase() });
if (!page) return notFound();
const promotedItem: Product | undefined = await getProduct({
handle: 'gift-bag-and-postcard-set',
language: params?.locale?.toUpperCase()
language: params?.locale?.toUpperCase() || 'JA'
});
return (
@ -57,13 +54,12 @@ export default async function Page({ params }: { params: { locale?: SupportedLoc
<div className="pb-12">
<ShopsNav />
</div>
{/* <h2 className="font-multilingual mb-8 text-3xl font-medium">{page.title}</h2> */}
<Prose html={page.body as string} />
<Suspense fallback={null}>
<ShopListDetail language={params?.locale?.toUpperCase()} />
</Suspense>
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
</div>
);
}

View File

@ -0,0 +1,13 @@
'use server';
import Prose from 'components/prose';
import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation';
export default async function ShopListDetail({ language }: { language?: string }) {
const page = await getPage({ handle: 'shop-list', language });
if (!page) return notFound();
return <Prose html={page.body as string} />;
}

View File

@ -5,10 +5,7 @@ import Navbar from 'components/layout/navbar';
import { getCart, getProduct } from 'lib/shopify';
import { Product } from 'lib/shopify/types';
import { cookies } from 'next/headers';
import { ReactNode, Suspense } from 'react';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
import { ReactNode } from 'react';
const { SITE_NAME } = process.env;
@ -43,9 +40,7 @@ export default async function BlogLayout({
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
{children}
<Suspense>
<Footer cart={cart} />
</Suspense>
</div>
);
}

View File

@ -8,9 +8,6 @@ import { getBlogArticle } from 'lib/shopify';
import { BlogArticle } from 'lib/shopify/types';
import Image from 'next/image';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export async function generateMetadata({
params
}: {

View File

@ -9,9 +9,6 @@ import { Product } from 'lib/shopify/types';
import { cookies } from 'next/headers';
import { Suspense } from 'react';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -42,13 +39,13 @@ export default async function StoriesPage({
return (
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="py-24 md:py-48">
<StoriesDetail handle={BLOG_HANDLE} locale={locale} />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -8,9 +8,6 @@ import { cookies } from 'next/headers';
import { Suspense } from 'react';
import TermsOfUse from './terms-of-use';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
const { SITE_NAME } = process.env;
export const metadata = {
@ -41,13 +38,13 @@ export default async function TermsPage({
return (
<div>
<Navbar cart={cart} locale={locale} compact promotedItem={promotedItem} />
<Suspense fallback={null}>
<div className="py-24 md:py-48">
<TermsOfUse />
</div>
<Suspense>
<Footer cart={cart} />
</Suspense>
<Footer cart={cart} />
</div>
);
}

View File

@ -1,9 +1,11 @@
'use server';
import { TAGS } from 'lib/constants';
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';
export const addItem = async (variantId: string | undefined): Promise<String | undefined> => {
export async function addItem(selectedVariantId: string | undefined) {
let cartId = cookies().get('cartId')?.value;
let cart;
@ -17,16 +19,17 @@ export const addItem = async (variantId: string | undefined): Promise<String | u
cookies().set('cartId', cartId);
}
if (!variantId) {
if (!selectedVariantId) {
return 'Missing product variant ID';
}
try {
await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]);
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error adding item to cart';
}
};
}
export const addItems = async ({
variantId,
@ -59,34 +62,44 @@ export const addItems = async ({
}
};
export const removeItem = async (lineId: string): Promise<String | undefined> => {
export async function removeItem(prevState: any, lineId: string) {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
try {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error removing item from cart';
}
};
}
export const updateItemQuantity = async ({
lineId,
variantId,
quantity
}: {
export async function updateItemQuantity(
prevState: any,
payload: {
lineId: string;
variantId: string;
quantity: number;
}): Promise<String | undefined> => {
}
) {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
const { lineId, variantId, quantity } = payload;
try {
if (quantity === 0) {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
return;
}
await updateCart(cartId, [
{
id: lineId,
@ -94,7 +107,8 @@ export const updateItemQuantity = async ({
quantity
}
]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error updating item quantity';
}
};
}

View File

@ -1,71 +0,0 @@
'use client';
import clsx from 'clsx';
import { addItem } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import { ProductVariant } from 'lib/shopify/types';
import { useTranslations } from 'next-intl';
import { useRouter, useSearchParams } from 'next/navigation';
import { useTransition } from 'react';
export function AddToCart({
variants,
availableForSale
}: {
variants: ProductVariant[];
availableForSale: boolean;
}) {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations('Index');
const [isPending, startTransition] = useTransition();
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
const selectedVariantId = variant?.id || defaultVariantId;
const title = !availableForSale
? 'Out of stock'
: !selectedVariantId
? 'Please select options'
: undefined;
return (
<button
aria-label="Add item to cart"
disabled={isPending || !availableForSale || !selectedVariantId}
title={title}
onClick={() => {
// Safeguard in case someone messes with `disabled` in devtools.
if (!availableForSale || !selectedVariantId) return;
startTransition(async () => {
const error = await addItem(selectedVariantId);
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
}
router.refresh();
});
}}
className={clsx(
'relative flex w-full items-center justify-center bg-white p-4 font-serif text-xl tracking-wide text-black hover:opacity-90',
{
'cursor-not-allowed opacity-60 hover:opacity-60': !availableForSale || !selectedVariantId,
'cursor-not-allowed': isPending
}
)}
>
{!isPending ? (
<span>{availableForSale ? t('cart.add') : t('cart.out-of-stock')}</span>
) : (
<LoadingDots className="my-3 bg-black" />
)}
</button>
);
}

View File

@ -1,40 +1,31 @@
import { XMarkIcon } from '@heroicons/react/24/outline';
import LoadingDots from 'components/loading-dots';
import { useRouter } from 'next/navigation';
'use client';
import { XMarkIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { removeItem } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import type { CartItem } from 'lib/shopify/types';
import { useTransition } from 'react';
import { useFormState, useFormStatus } from 'react-dom';
export default function DeleteItemButton({ item }: { item: CartItem }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
aria-label="Remove cart item"
onClick={() => {
startTransition(async () => {
const error = await removeItem(item.id);
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
}
router.refresh();
});
type="submit"
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
disabled={isPending}
aria-label="Remove cart item"
aria-disabled={pending}
className={clsx(
'ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-dark transition-all duration-200',
{
'cursor-not-allowed px-0': isPending
'cursor-not-allowed px-0': pending
}
)}
>
{isPending ? (
{pending ? (
<LoadingDots className="bg-white" />
) : (
<XMarkIcon className="mx-[1px] h-4 w-4 text-white transition-opacity duration-150 hover:opacity-60 dark:text-black" />
@ -42,3 +33,18 @@ export default function DeleteItemButton({ item }: { item: CartItem }) {
</button>
);
}
export function DeleteItemButton({ item }: { item: CartItem }) {
const [message, formAction] = useFormState(removeItem, null);
const itemId = item.id;
const actionWithVariant = formAction.bind(null, itemId);
return (
<form action={actionWithVariant}>
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}

View File

@ -1,54 +1,32 @@
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
'use client';
import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { removeItem, updateItemQuantity } from 'components/cart/actions';
import { updateItemQuantity } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import type { CartItem } from 'lib/shopify/types';
import { useFormState, useFormStatus } from 'react-dom';
export default function EditItemQuantityButton({
item,
type
}: {
item: CartItem;
type: 'plus' | 'minus';
}) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
const { pending } = useFormStatus();
return (
<button
aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'}
onClick={() => {
startTransition(async () => {
const error =
type === 'minus' && item.quantity - 1 === 0
? await removeItem(item.id)
: await updateItemQuantity({
lineId: item.id,
variantId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
});
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
}
router.refresh();
});
type="submit"
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
disabled={isPending}
aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'}
aria-disabled={pending}
className={clsx(
'ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80',
{
'cursor-not-allowed': isPending,
'cursor-not-allowed': pending,
'ml-auto': type === 'minus'
}
)}
>
{isPending ? (
{pending ? (
<LoadingDots className="bg-black dark:bg-white" />
) : type === 'plus' ? (
<PlusIcon className="h-4 w-4 dark:text-neutral-500" />
@ -58,3 +36,22 @@ export default function EditItemQuantityButton({
</button>
);
}
export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) {
const [message, formAction] = useFormState(updateItemQuantity, null);
const payload = {
lineId: item.id,
variantId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
};
const actionWithVariant = formAction.bind(null, payload);
return (
<form action={actionWithVariant}>
<SubmitButton type={type} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}

View File

@ -13,8 +13,8 @@ import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useEffect, useRef, useState } from 'react';
import CloseCart from './close-cart';
import DeleteItemButton from './delete-item-button';
import EditItemQuantityButton from './edit-item-quantity-button';
import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart';
import { PromotedCartItem } from './promoted-cart-item';

View File

@ -8,9 +8,6 @@ import Link from 'next/link';
import Label from '../label';
import { GridTileImage } from './tile';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
function HomepageProductsItem({ item, priority }: { item: Product; priority?: boolean }) {
const size = item?.variants?.[0]?.selectedOptions?.find((option) => option.name === 'Size');
const image = item?.variants?.[0]?.image;

View File

@ -5,6 +5,7 @@ import InstagramIcon from 'components/icons/instagram';
import KanjiLogo from 'components/icons/kanji';
import { Cart, Product } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
import FooterMenu from './footer-menu';
import NewsletterFooter from './newsletter-footer';
@ -95,7 +96,9 @@ export default async function Footer({
<div className="flex flex-col space-y-2 pt-24">
<div className="flex flex-row justify-between space-x-4">
<Suspense>
<CartModal cart={cart} promotedItem={promotedItem} />
</Suspense>
<div className="flex flex-row items-center space-x-6">
<Link
href="https://www.instagram.com/narai.sake/"

View File

@ -7,7 +7,7 @@ import Logo from 'components/icons/logo';
import MenuIcon from 'components/icons/menu';
import { useLocale, useTranslations } from 'next-intl';
import Link from 'next/link';
import { Fragment, useRef, useState } from 'react';
import { Fragment, Suspense, useRef, useState } from 'react';
import { LanguageControl, SupportedLocale } from '../navbar/language-control';
export function MenuModal({ scrolled }: { scrolled: boolean }) {
@ -72,6 +72,7 @@ export function MenuModal({ scrolled }: { scrolled: boolean }) {
</button>
</div>
<Suspense fallback={null}>
<div className="grid h-[calc(100vh-124px)] grid-cols-1 place-content-center">
<div className="flex flex-row justify-end">
<nav className="flex flex-col space-y-4 px-4 text-right">
@ -149,6 +150,7 @@ export function MenuModal({ scrolled }: { scrolled: boolean }) {
</nav>
</div>
</div>
</Suspense>
</div>
</Dialog.Panel>
</div>

View File

@ -7,6 +7,7 @@ import CartModal from 'components/cart/modal';
import LogoNamemark from 'components/icons/namemark';
import { Cart, Product } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
import { useInView } from 'react-intersection-observer';
import { MenuModal } from '../menu/modal';
import { LanguageControl, SupportedLocale } from './language-control';
@ -62,8 +63,12 @@ export default function Navbar({
</div>
<nav className="flex flex-row items-center space-x-4 px-6">
<div className="flex flex-col-reverse items-center justify-center space-y-2 px-2 md:flex-row md:space-x-6">
<Suspense>
<CartModal cart={cart} promotedItem={promotedItem} />
</Suspense>
<Suspense>
<MenuModal scrolled={!inView} />
</Suspense>
</div>
</nav>
</div>
@ -84,13 +89,19 @@ export default function Navbar({
</Link>
</div>
<nav className="flex flex-row items-center space-x-4 px-2 md:pt-6">
<Suspense fallback={null}>
<div className="hidden md:block">
<LanguageControl lang={locale} />
</div>
<div className="flex flex-col-reverse items-center justify-center space-y-2 rounded md:flex-row md:space-x-6 md:space-y-0">
<Suspense>
<CartModal cart={cart} promotedItem={promotedItem} />
</Suspense>
<Suspense>
<MenuModal scrolled={!inView} />
</Suspense>
</div>
</Suspense>
</nav>
</div>
</div>

View File

@ -3,7 +3,7 @@
import { Dialog, Transition } from '@headlessui/react';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import { Fragment, useEffect, useState } from 'react';
import { Fragment, Suspense, useEffect, useState } from 'react';
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
import { Menu } from 'lib/shopify/types';
@ -72,7 +72,9 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
</button>
<div className="mb-4 w-full">
<Suspense fallback={null}>
<Search />
</Suspense>
</div>
{menu.length ? (
<ul className="flex w-full flex-col">

View File

@ -1,19 +1,12 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { createUrl } from 'lib/utils';
import { useRouter, useSearchParams } from 'next/navigation';
export default function Search() {
const router = useRouter();
const searchParams = useSearchParams();
const [searchValue, setSearchValue] = useState('');
useEffect(() => {
setSearchValue(searchParams?.get('q') || '');
}, [searchParams, setSearchValue]);
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
@ -34,12 +27,12 @@ export default function Search() {
return (
<form onSubmit={onSubmit} className="relative w-full max-w-[550px] lg:w-80 xl:w-full">
<input
key={searchParams?.get('q')}
type="text"
name="search"
placeholder="Search for products..."
autoComplete="off"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
defaultValue={searchParams?.get('q') || ''}
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
/>
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">

View File

@ -1,4 +1,5 @@
import { SortFilterItem } from 'lib/constants';
import { Suspense } from 'react';
import FilterItemDropdown from './dropdown';
import { FilterItem } from './item';
@ -19,12 +20,20 @@ export default function FilterList({ list, title }: { list: ListItem[]; title?:
return (
<>
<nav>
{title ? <h3 className="hidden text-xs text-neutral-500 md:block">{title}</h3> : null}
{title ? (
<h3 className="hidden text-xs text-neutral-500 dark:text-neutral-400 md:block">
{title}
</h3>
) : null}
<ul className="hidden md:block">
<Suspense fallback={null}>
<FilterItemList list={list} />
</Suspense>
</ul>
<ul className="md:hidden">
<Suspense fallback={null}>
<FilterItemDropdown list={list} />
</Suspense>
</ul>
</nav>
</>

View File

@ -1,4 +1,4 @@
import { ImageResponse } from 'next/server';
import { ImageResponse } from 'next/og';
import Logo from './icons/logo';
export type Props = {

View File

@ -3,8 +3,7 @@
import clsx from 'clsx';
import { ProductOption, ProductVariant } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
type Combination = {
id: string;
@ -19,6 +18,7 @@ export function VariantSelector({
options: ProductOption[];
variants: ProductVariant[];
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const hasNoOptionsOrJustOneOption =
@ -76,17 +76,14 @@ export function VariantSelector({
// The option is active if it's in the url params.
const isActive = searchParams.get(optionNameLowerCase) === value;
// You can't disable a link, so we need to render something that isn't clickable.
const DynamicTag = isAvailableForSale ? Link : 'p';
const dynamicProps = {
...(isAvailableForSale && { scroll: false })
};
return (
<DynamicTag
<button
key={value}
aria-disabled={!isAvailableForSale}
href={optionUrl}
disabled={!isAvailableForSale}
onClick={() => {
router.replace(optionUrl, { scroll: false });
}}
title={`${option.name} ${value}${!isAvailableForSale ? ' (Out of Stock)' : ''}`}
className={clsx(
'flex min-w-[48px] items-center justify-center rounded-full border bg-white px-2 py-1 text-sm text-black',
@ -98,10 +95,9 @@ export function VariantSelector({
!isAvailableForSale
}
)}
{...dynamicProps}
>
{value}
</DynamicTag>
</button>
);
})}
</dd>

View File

@ -1,15 +0,0 @@
import Footer from 'components/layout/footer';
import { Suspense } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Suspense>
<div className="w-full">
<div className="mx-8 max-w-2xl py-20 sm:mx-auto">
<Suspense>{children}</Suspense>
</div>
</div>
<Footer />
</Suspense>
);
}

View File

@ -1,12 +0,0 @@
import OpengraphImage from 'components/opengraph-image';
import { getPage } from 'lib/shopify';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export default async function Image({ params }: { params: { page: string } }) {
const page = await getPage({ handle: params.page });
const title = page.seo?.title || page.title;
return await OpengraphImage({ title });
}

View File

@ -1,53 +0,0 @@
import type { Metadata } from 'next';
import { SupportedLocale } from 'components/layout/navbar/language-control';
import Prose from 'components/prose';
import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
export async function generateMetadata({
params
}: {
params: { page: string; locale?: SupportedLocale };
}): Promise<Metadata> {
const page = await getPage({ handle: params.page, language: params?.locale?.toUpperCase() });
if (!page) return notFound();
return {
title: page.seo?.title || page.title,
description: page.seo?.description || page.bodySummary,
openGraph: {
publishedTime: page.createdAt,
modifiedTime: page.updatedAt,
type: 'article'
}
};
}
export default async function Page({
params
}: {
params: { page: string; locale?: SupportedLocale };
}) {
const page = await getPage({ handle: params.page, language: params?.locale?.toUpperCase() });
if (!page) return notFound();
return (
<div className="text-white">
<h1 className="mb-8 text-5xl font-bold">{page.title}</h1>
<Prose className="mb-8" html={page.body as string} />
<p className="text-sm italic">
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(page.updatedAt))}.`}
</p>
</div>
);
}

View File

@ -1,12 +0,0 @@
import OpengraphImage from 'components/opengraph-image';
import { getCollection } from 'lib/shopify';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export default async function Image({ params }: { params: { collection: string } }) {
const collection = await getCollection({ handle: params.collection });
const title = collection?.seo?.title || collection?.title;
return await OpengraphImage({ title });
}

View File

@ -1,50 +0,0 @@
import { getCollection, getCollectionProducts } from 'lib/shopify';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export async function generateMetadata({
params
}: {
params: { collection: string };
}): Promise<Metadata> {
const collection = await getCollection({ handle: params.collection });
if (!collection) return notFound();
return {
title: collection.seo?.title || collection.title,
description:
collection.seo?.description || collection.description || `${collection.title} products`
};
}
export default async function CategoryPage({
params,
searchParams
}: {
params: { collection: string };
searchParams?: { [key: string]: string | string[] | undefined };
}) {
const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
return (
<section>
{products.length === 0 ? (
<p className="py-3 text-lg">{`No products found in this collection`}</p>
) : (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
)}
</section>
);
}

View File

@ -1,22 +0,0 @@
import Footer from 'components/layout/footer';
import Collections from 'components/layout/search/collections';
import FilterList from 'components/layout/search/filter';
import { sorting } from 'lib/constants';
import { Suspense } from 'react';
export default function SearchLayout({ children }: { children: React.ReactNode }) {
return (
<Suspense>
<div className="mx-auto flex max-w-screen-xl flex-col gap-8 px-4 pb-4 text-black dark:text-white md:flex-row">
<div className="order-first w-full flex-none md:max-w-[125px]">
<Collections />
</div>
<div className="order-last min-h-screen w-full md:order-none">{children}</div>
<div className="order-none flex-none md:order-last md:w-[125px]">
<FilterList list={sorting} title="Sort by" />
</div>
</div>
<Footer />
</Suspense>
);
}

View File

@ -1,15 +0,0 @@
import Grid from 'components/grid';
export default function Loading() {
return (
<Grid className="grid-cols-2 lg:grid-cols-3">
{Array(12)
.fill(0)
.map((_, index) => {
return (
<Grid.Item key={index} className="animate-pulse bg-neutral-100 dark:bg-neutral-900" />
);
})}
</Grid>
);
}

View File

@ -1,42 +0,0 @@
import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants';
import { getProducts } from 'lib/shopify';
export const runtime = 'edge';
export const revalidate = 300; // 5 minutes in seconds
export const metadata = {
title: 'Search',
description: 'Search for products in the store.'
};
export default async function SearchPage({
searchParams
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) {
const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getProducts({ sortKey, reverse, query: searchValue });
const resultsText = products.length > 1 ? 'results' : 'result';
return (
<>
{searchValue ? (
<p className="mb-4">
{products.length === 0
? 'There are no products that match '
: `Showing ${products.length} ${resultsText} for `}
<span className="font-bold">&quot;{searchValue}&quot;</span>
</p>
) : null}
{products.length > 0 ? (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
) : null}
</>
);
}

View File

@ -1,49 +0,0 @@
import { getCollections, getPages, getProducts } from 'lib/shopify';
import { MetadataRoute } from 'next';
type Route = {
url: string;
lastModified: string;
};
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const routesMap = [''].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString()
}));
const collectionsPromise = getCollections().then((collections) =>
collections.map((collection) => ({
url: `${baseUrl}${collection.path}`,
lastModified: collection.updatedAt
}))
);
const productsPromise = getProducts({}).then((products) =>
products.map((product) => ({
url: `${baseUrl}/product/${product.handle}`,
lastModified: product.updatedAt
}))
);
const pagesPromise = getPages({}).then((pages) =>
pages.map((page) => ({
url: `${baseUrl}/${page.handle}`,
lastModified: page.updatedAt
}))
);
let fetchedRoutes: Route[] = [];
try {
fetchedRoutes = (await Promise.all([collectionsPromise, productsPromise, pagesPromise])).flat();
} catch (error) {
throw JSON.stringify(error, null, 2);
}
return [...routesMap, ...fetchedRoutes];
}

View File

@ -22,7 +22,8 @@ export const sorting: SortFilterItem[] = [
export const TAGS = {
collections: 'collections',
products: 'products'
products: 'products',
cart: 'cart'
};
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';

View File

@ -1,5 +1,6 @@
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
import { ensureStartsWith } from 'lib/utils';
import { revalidateTag } from 'next/cache';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
@ -55,7 +56,9 @@ import {
ShopifyUpdateCartOperation
} from './types';
const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`;
const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
: '';
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
@ -103,6 +106,7 @@ export async function shopifyFetch<T>({
} catch (e) {
if (isShopifyError(e)) {
throw {
cause: e.cause?.toString() || 'unknown',
status: e.status || 500,
message: e.message,
query
@ -273,6 +277,7 @@ export async function getCart(cartId: string): Promise<Cart | undefined> {
const res = await shopifyFetch<ShopifyCartOperation>({
query: getCartQuery,
variables: { cartId },
tags: [TAGS.cart],
cache: 'no-store'
});
@ -395,6 +400,7 @@ export async function getPage({
}): Promise<Page> {
const res = await shopifyFetch<ShopifyPageOperation>({
query: getPageQuery,
// cache: 'no-store',
variables: { handle, language, country }
});
@ -410,6 +416,7 @@ export async function getPages({
}): Promise<Page[]> {
const res = await shopifyFetch<ShopifyPagesOperation>({
query: getPagesQuery,
// cache: 'no-store',
variables: { language, country }
});

View File

@ -1,6 +1,7 @@
export interface ShopifyErrorLike {
status: number;
message: Error;
cause?: Error;
}
export const isObject = (object: unknown): object is Record<string, unknown> => {

View File

@ -6,3 +6,34 @@ export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyUR
return `${pathname}${queryString}`;
};
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
export const validateEnvironmentVariables = () => {
const requiredEnvironmentVariables = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_STOREFRONT_ACCESS_TOKEN'];
const missingEnvironmentVariables = [] as string[];
requiredEnvironmentVariables.forEach((envVar) => {
if (!process.env[envVar]) {
missingEnvironmentVariables.push(envVar);
}
});
if (missingEnvironmentVariables.length) {
throw new Error(
`The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/shopify#configure-environment-variables\n\n${missingEnvironmentVariables.join(
'\n'
)}\n`
);
}
if (
process.env.SHOPIFY_STORE_DOMAIN?.includes('[') ||
process.env.SHOPIFY_STORE_DOMAIN?.includes(']')
) {
throw new Error(
'Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.'
);
}
};

View File

@ -5,7 +5,7 @@ module.exports = {
ignoreDuringBuilds: true
},
experimental: {
serverActions: true
ppr: true
},
images: {
formats: ['image/avif', 'image/webp'],
@ -16,5 +16,14 @@ module.exports = {
pathname: '/s/files/**'
}
]
},
async redirects() {
return [
{
source: '/password',
destination: '/',
permanent: true
}
];
}
};

View File

@ -23,9 +23,8 @@
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@headlessui/react": "^1.7.15",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@thgh/next-gtm": "^0.1.4",
"@types/react-gtm-module": "^2.0.1",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
@ -34,11 +33,12 @@
"eslint-plugin-unused-imports": "^3.0.0",
"js-cookie": "^3.0.5",
"negotiator": "^0.6.3",
"next": "latest",
"next": "canary",
"next-gtm": "latest",
"next-intl": "latest",
"prettier-plugin-organize-imports": "^3.2.3",
"react": "latest",
"react-dom": "latest",
"react": "canary",
"react-dom": "canary",
"react-ga": "^3.3.1",
"react-gtm-module": "^2.0.11",
"react-intersection-observer": "^9.5.2",
@ -50,18 +50,18 @@
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@types/negotiator": "^0.6.1",
"@types/node": "20.4.4",
"@types/react": "18.2.16",
"@types/react-dom": "18.2.7",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint": "latest",
"eslint-config-next": "latest",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-unicorn": "^48.0.0",
"eslint-plugin-unicorn": "latest",
"lint-staged": "^13.2.3",
"postcss": "^8.4.27",
"prettier": "3.0.1",
"prettier-plugin-tailwindcss": "^0.4.1",
"prettier-plugin-tailwindcss": "latest",
"tailwindcss": "^3.3.3",
"typescript": "5.1.6"
}

View File

@ -1,3 +1,4 @@
/** @type {import('prettier').Config} */
module.exports = {
singleQuote: true,
arrowParens: 'always',

340
yarn.lock
View File

@ -28,6 +28,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.22.20":
version: 7.22.20
resolution: "@babel/helper-validator-identifier@npm:7.22.20"
checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-validator-identifier@npm:7.22.5"
@ -82,9 +89,9 @@ __metadata:
languageName: node
linkType: hard
"@eslint/eslintrc@npm:^2.1.1":
version: 2.1.1
resolution: "@eslint/eslintrc@npm:2.1.1"
"@eslint/eslintrc@npm:^2.1.3":
version: 2.1.3
resolution: "@eslint/eslintrc@npm:2.1.3"
dependencies:
ajv: ^6.12.4
debug: ^4.3.2
@ -95,14 +102,14 @@ __metadata:
js-yaml: ^4.1.0
minimatch: ^3.1.2
strip-json-comments: ^3.1.1
checksum: bf909ea183d27238c257a82d4ffdec38ca94b906b4b8dfae02ecbe7ecc9e5a8182ef5e469c808bb8cb4fea4750f43ac4ca7c4b4a167b6cd7e3aaacd386b2bd25
checksum: 5c6c3878192fe0ddffa9aff08b4e2f3bcc8f1c10d6449b7295a5f58b662019896deabfc19890455ffd7e60a5bd28d25d0eaefb2f78b2d230aae3879af92b89e5
languageName: node
linkType: hard
"@eslint/js@npm:^8.46.0":
version: 8.46.0
resolution: "@eslint/js@npm:8.46.0"
checksum: 7aed479832302882faf5bec37e9d068f270f84c19b3fb529646a7c1b031e73a312f730569c78806492bc09cfce3d7651dfab4ce09a56cbb06bc6469449e56377
"@eslint/js@npm:8.53.0":
version: 8.53.0
resolution: "@eslint/js@npm:8.53.0"
checksum: e0d5cfb0000aaee237c8e6d6d6e366faa60b1ef7f928ce17778373aa44d3b886368f6d5e1f97f913f0f16801aad016db8b8df78418c9d18825c15590328028af
languageName: node
linkType: hard
@ -183,15 +190,15 @@ __metadata:
languageName: node
linkType: hard
"@headlessui/react@npm:^1.7.15":
version: 1.7.16
resolution: "@headlessui/react@npm:1.7.16"
"@headlessui/react@npm:^1.7.17":
version: 1.7.17
resolution: "@headlessui/react@npm:1.7.17"
dependencies:
client-only: ^0.0.1
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
checksum: 85844c96c79796236fa7dec2f1c2f0332d7554d9b785d7b37d9762bb4133088a0cf1334653d6f91c1fe4619960eb569f14ba5f6a962c3305e03f7b362acbabbe
checksum: 0cdb67747e7f606f78214dac0b48573247779e70534b4471515c094b74addda173dc6a9847d33aea9c6e6bc151016c034125328953077e32aa7947ebabed91f7
languageName: node
linkType: hard
@ -204,14 +211,14 @@ __metadata:
languageName: node
linkType: hard
"@humanwhocodes/config-array@npm:^0.11.10":
version: 0.11.10
resolution: "@humanwhocodes/config-array@npm:0.11.10"
"@humanwhocodes/config-array@npm:^0.11.13":
version: 0.11.13
resolution: "@humanwhocodes/config-array@npm:0.11.13"
dependencies:
"@humanwhocodes/object-schema": ^1.2.1
"@humanwhocodes/object-schema": ^2.0.1
debug: ^4.1.1
minimatch: ^3.0.5
checksum: 1b1302e2403d0e35bc43e66d67a2b36b0ad1119efc704b5faff68c41f791a052355b010fb2d27ef022670f550de24cd6d08d5ecf0821c16326b7dcd0ee5d5d8a
checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805
languageName: node
linkType: hard
@ -222,10 +229,10 @@ __metadata:
languageName: node
linkType: hard
"@humanwhocodes/object-schema@npm:^1.2.1":
version: 1.2.1
resolution: "@humanwhocodes/object-schema@npm:1.2.1"
checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1
"@humanwhocodes/object-schema@npm:^2.0.1":
version: 2.0.1
resolution: "@humanwhocodes/object-schema@npm:2.0.1"
checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45
languageName: node
linkType: hard
@ -292,10 +299,10 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:13.4.19":
version: 13.4.19
resolution: "@next/env@npm:13.4.19"
checksum: ace4f82890954ade0164fbe2b7ff988268d2b99b2e80caa6707c51fa4cbfaaa31e48fbbcecd4fd142af3503c544e1b4c91e8185d4af253c8fb46550e9e70ad7e
"@next/env@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/env@npm:14.0.3-canary.3"
checksum: b54b00343ae5d3af64b1dde1f3aacc2915893ef2b95b8f9102df5f8822d17c05c0eb240354c875a33be24a83b62d6ee9cc3e30cdcda634c951c3e8d326246b0e
languageName: node
linkType: hard
@ -308,65 +315,65 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-darwin-arm64@npm:13.4.19"
"@next/swc-darwin-arm64@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-darwin-arm64@npm:14.0.3-canary.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-darwin-x64@npm:13.4.19"
"@next/swc-darwin-x64@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-darwin-x64@npm:14.0.3-canary.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.19"
"@next/swc-linux-arm64-gnu@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-linux-arm64-gnu@npm:14.0.3-canary.3"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-linux-arm64-musl@npm:13.4.19"
"@next/swc-linux-arm64-musl@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-linux-arm64-musl@npm:14.0.3-canary.3"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-linux-x64-gnu@npm:13.4.19"
"@next/swc-linux-x64-gnu@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-linux-x64-gnu@npm:14.0.3-canary.3"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-linux-x64-musl@npm:13.4.19"
"@next/swc-linux-x64-musl@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-linux-x64-musl@npm:14.0.3-canary.3"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.19"
"@next/swc-win32-arm64-msvc@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-win32-arm64-msvc@npm:14.0.3-canary.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.19"
"@next/swc-win32-ia32-msvc@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-win32-ia32-msvc@npm:14.0.3-canary.3"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.4.19":
version: 13.4.19
resolution: "@next/swc-win32-x64-msvc@npm:13.4.19"
"@next/swc-win32-x64-msvc@npm:14.0.3-canary.3":
version: 14.0.3-canary.3
resolution: "@next/swc-win32-x64-msvc@npm:14.0.3-canary.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -435,12 +442,12 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.1":
version: 0.5.1
resolution: "@swc/helpers@npm:0.5.1"
"@swc/helpers@npm:0.5.2":
version: 0.5.2
resolution: "@swc/helpers@npm:0.5.2"
dependencies:
tslib: ^2.4.0
checksum: 71e0e27234590435e4c62b97ef5e796f88e786841a38c7116a5e27a3eafa7b9ead7cdec5249b32165902076de78446945311c973e59bddf77c1e24f33a8f272a
checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258
languageName: node
linkType: hard
@ -467,17 +474,6 @@ __metadata:
languageName: node
linkType: hard
"@thgh/next-gtm@npm:^0.1.4":
version: 0.1.4
resolution: "@thgh/next-gtm@npm:0.1.4"
peerDependencies:
next: ^13
react: ^18
react-dom: ^18
checksum: a0647b1b6a0b8565998d5c2dd247992d009ae8bfe73d2e6684c9b282ab8826de6bba7854179958cf440e3af00e56b9a621600b2fe60db0c2bb065486bab77c81
languageName: node
linkType: hard
"@tootallnate/once@npm:2":
version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0"
@ -506,10 +502,12 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:20.4.4":
version: 20.4.4
resolution: "@types/node@npm:20.4.4"
checksum: 43f3c4a8acc38ae753e15a0e79bae0447d255b3742fa87f8e065d7b9d20ecb0e03d6c5b46c00d5d26f4552160381a00255f49205595a8ee48c2423e00263c930
"@types/node@npm:latest":
version: 20.9.0
resolution: "@types/node@npm:20.9.0"
dependencies:
undici-types: ~5.26.4
checksum: bfd927da6bff8a32051fa44bb47ca32a56d2c8bc8ba0170770f181cc1fa3c0b05863c9b930f0ba8604a48d5eb0d319166601709ca53bf2deae0025d8b6c6b8a3
languageName: node
linkType: hard
@ -527,12 +525,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:18.2.7":
version: 18.2.7
resolution: "@types/react-dom@npm:18.2.7"
"@types/react-dom@npm:latest":
version: 18.2.15
resolution: "@types/react-dom@npm:18.2.15"
dependencies:
"@types/react": "*"
checksum: e02ea908289a7ad26053308248d2b87f6aeafd73d0e2de2a3d435947bcea0422599016ffd1c3e38ff36c42f5e1c87c7417f05b0a157e48649e4a02f21727d54f
checksum: 8e9631600c21ff561328e38a951d1991b3b3b20f538af4c0efbd1327c883a5573a63f50e1b945c34fa51b114b30e1ca5e62317bd54f21e063d6697b4be843a03
languageName: node
linkType: hard
@ -554,14 +552,14 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:18.2.16":
version: 18.2.16
resolution: "@types/react@npm:18.2.16"
"@types/react@npm:latest":
version: 18.2.37
resolution: "@types/react@npm:18.2.37"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: 3d4fdc12509e0098e0dbb4bacdea53e8ccc6632e9df63d9f2711c77aa81ce3b2bb9c76d087f284034b25fd7245680167f4832bf6e4df960c5af2634b52adfd0c
checksum: 2d2599f1a09e4f678509161fea8baeaf76d21deee460f4f3ccc1ca431ebe85f896d7d0b906127de17e97ed57240cec61955eb97d0b5d9cbf4e97fd6620b1acdb
languageName: node
linkType: hard
@ -635,6 +633,13 @@ __metadata:
languageName: node
linkType: hard
"@ungap/structured-clone@npm:^1.2.0":
version: 1.2.0
resolution: "@ungap/structured-clone@npm:1.2.0"
checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524
languageName: node
linkType: hard
"abbrev@npm:^1.0.0":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
@ -1352,38 +1357,38 @@ __metadata:
version: 0.0.0-use.local
resolution: "commerce@workspace:."
dependencies:
"@headlessui/react": ^1.7.15
"@headlessui/react": ^1.7.17
"@heroicons/react": ^2.0.18
"@tailwindcss/container-queries": ^0.1.1
"@tailwindcss/typography": ^0.5.9
"@thgh/next-gtm": ^0.1.4
"@types/js-cookie": ^3.0.3
"@types/negotiator": ^0.6.1
"@types/node": 20.4.4
"@types/react": 18.2.16
"@types/react-dom": 18.2.7
"@types/node": latest
"@types/react": latest
"@types/react-dom": latest
"@types/react-gtm-module": ^2.0.1
autoprefixer: ^10.4.14
clsx: ^2.0.0
date-fns: ^2.30.0
eslint: ^8.45.0
eslint: latest
eslint-config-next: latest
eslint-config-prettier: ^8.8.0
eslint-plugin-prettier: ^5.0.0
eslint-plugin-tailwindcss: ^3.13.0
eslint-plugin-unicorn: ^48.0.0
eslint-plugin-unicorn: latest
eslint-plugin-unused-imports: ^3.0.0
js-cookie: ^3.0.5
lint-staged: ^13.2.3
negotiator: ^0.6.3
next: latest
next: canary
next-gtm: latest
next-intl: latest
postcss: ^8.4.27
prettier: 3.0.1
prettier-plugin-organize-imports: ^3.2.3
prettier-plugin-tailwindcss: ^0.4.1
react: latest
react-dom: latest
prettier-plugin-tailwindcss: latest
react: canary
react-dom: canary
react-ga: ^3.3.1
react-gtm-module: ^2.0.11
react-intersection-observer: ^9.5.2
@ -1983,11 +1988,11 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-unicorn@npm:^48.0.0":
version: 48.0.1
resolution: "eslint-plugin-unicorn@npm:48.0.1"
"eslint-plugin-unicorn@npm:latest":
version: 49.0.0
resolution: "eslint-plugin-unicorn@npm:49.0.0"
dependencies:
"@babel/helper-validator-identifier": ^7.22.5
"@babel/helper-validator-identifier": ^7.22.20
"@eslint-community/eslint-utils": ^4.4.0
ci-info: ^3.8.0
clean-regexp: ^1.0.0
@ -1995,7 +2000,6 @@ __metadata:
indent-string: ^4.0.0
is-builtin-module: ^3.2.1
jsesc: ^3.0.2
lodash: ^4.17.21
pluralize: ^8.0.0
read-pkg-up: ^7.0.1
regexp-tree: ^0.1.27
@ -2003,8 +2007,8 @@ __metadata:
semver: ^7.5.4
strip-indent: ^3.0.0
peerDependencies:
eslint: ">=8.44.0"
checksum: e63112cbaa3a1347cbb427160d7b3c6a1f8cc8ef512075a0ab285c64761772356f4eb5f82c9fb1a8cde63d8794f8aa819eda02fa0a7c44bc9955c5113f87be78
eslint: ">=8.52.0"
checksum: 7580dc96b2edd26d10c1671e575d8ed26df87405f8a535f69f718187aecd64a89402c5dca40ee4a873ed8822496445451b50d9b9a52555b74f053af00fa98c0b
languageName: node
linkType: hard
@ -2040,24 +2044,32 @@ __metadata:
languageName: node
linkType: hard
"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.2":
"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1":
version: 3.4.2
resolution: "eslint-visitor-keys@npm:3.4.2"
checksum: 9e0e7e4aaea705c097ae37c97410e5f167d4d2193be2edcb1f0760762ede3df01545e4820ae314f42dcec687745f2c6dcaf6d83575c4a2a241eb0c8517d724f2
languageName: node
linkType: hard
"eslint@npm:^8.45.0":
version: 8.46.0
resolution: "eslint@npm:8.46.0"
"eslint-visitor-keys@npm:^3.4.3":
version: 3.4.3
resolution: "eslint-visitor-keys@npm:3.4.3"
checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60
languageName: node
linkType: hard
"eslint@npm:latest":
version: 8.53.0
resolution: "eslint@npm:8.53.0"
dependencies:
"@eslint-community/eslint-utils": ^4.2.0
"@eslint-community/regexpp": ^4.6.1
"@eslint/eslintrc": ^2.1.1
"@eslint/js": ^8.46.0
"@humanwhocodes/config-array": ^0.11.10
"@eslint/eslintrc": ^2.1.3
"@eslint/js": 8.53.0
"@humanwhocodes/config-array": ^0.11.13
"@humanwhocodes/module-importer": ^1.0.1
"@nodelib/fs.walk": ^1.2.8
"@ungap/structured-clone": ^1.2.0
ajv: ^6.12.4
chalk: ^4.0.0
cross-spawn: ^7.0.2
@ -2065,7 +2077,7 @@ __metadata:
doctrine: ^3.0.0
escape-string-regexp: ^4.0.0
eslint-scope: ^7.2.2
eslint-visitor-keys: ^3.4.2
eslint-visitor-keys: ^3.4.3
espree: ^9.6.1
esquery: ^1.4.2
esutils: ^2.0.2
@ -2090,7 +2102,7 @@ __metadata:
text-table: ^0.2.0
bin:
eslint: bin/eslint.js
checksum: 7a7d36b1a3bbc12e08fbb5bc36fd482a7a5a1797e62e762499dd45601b9e45aaa53a129f31ce0b4444551a9639b8b681ad535f379893dd1e3ae37b31dccd82aa
checksum: 2da808655c7aa4b33f8970ba30d96b453c3071cc4d6cd60d367163430677e32ff186b65270816b662d29139283138bff81f28dddeb2e73265495245a316ed02c
languageName: node
linkType: hard
@ -3353,13 +3365,6 @@ __metadata:
languageName: node
linkType: hard
"lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
languageName: node
linkType: hard
"log-update@npm:^4.0.0":
version: 4.0.0
resolution: "log-update@npm:4.0.0"
@ -3638,7 +3643,7 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^3.3.4, nanoid@npm:^3.3.6":
"nanoid@npm:^3.3.6":
version: 3.3.6
resolution: "nanoid@npm:3.3.6"
bin:
@ -3668,6 +3673,16 @@ __metadata:
languageName: node
linkType: hard
"next-gtm@npm:latest":
version: 1.0.0
resolution: "next-gtm@npm:1.0.0"
peerDependencies:
react: ^16.6.1
react-dom: ^16.6.3
checksum: 7d2737c83ca1376330ade14d9c3bde3ac4edc13e8b670da6577522b7b32e0642e7941ac3a4bb0b2ab6c3efb6bf2d71f95a978b78fc149d6fbe6814d395331468
languageName: node
linkType: hard
"next-intl@npm:latest":
version: 2.19.1
resolution: "next-intl@npm:2.19.1"
@ -3682,27 +3697,26 @@ __metadata:
languageName: node
linkType: hard
"next@npm:latest":
version: 13.4.19
resolution: "next@npm:13.4.19"
"next@npm:canary":
version: 14.0.3-canary.3
resolution: "next@npm:14.0.3-canary.3"
dependencies:
"@next/env": 13.4.19
"@next/swc-darwin-arm64": 13.4.19
"@next/swc-darwin-x64": 13.4.19
"@next/swc-linux-arm64-gnu": 13.4.19
"@next/swc-linux-arm64-musl": 13.4.19
"@next/swc-linux-x64-gnu": 13.4.19
"@next/swc-linux-x64-musl": 13.4.19
"@next/swc-win32-arm64-msvc": 13.4.19
"@next/swc-win32-ia32-msvc": 13.4.19
"@next/swc-win32-x64-msvc": 13.4.19
"@swc/helpers": 0.5.1
"@next/env": 14.0.3-canary.3
"@next/swc-darwin-arm64": 14.0.3-canary.3
"@next/swc-darwin-x64": 14.0.3-canary.3
"@next/swc-linux-arm64-gnu": 14.0.3-canary.3
"@next/swc-linux-arm64-musl": 14.0.3-canary.3
"@next/swc-linux-x64-gnu": 14.0.3-canary.3
"@next/swc-linux-x64-musl": 14.0.3-canary.3
"@next/swc-win32-arm64-msvc": 14.0.3-canary.3
"@next/swc-win32-ia32-msvc": 14.0.3-canary.3
"@next/swc-win32-x64-msvc": 14.0.3-canary.3
"@swc/helpers": 0.5.2
busboy: 1.6.0
caniuse-lite: ^1.0.30001406
postcss: 8.4.14
postcss: 8.4.31
styled-jsx: 5.1.1
watchpack: 2.4.0
zod: 3.21.4
peerDependencies:
"@opentelemetry/api": ^1.1.0
react: ^18.2.0
@ -3734,7 +3748,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: f4873dab8888ed4dae14d36d7cf8dc54cd042695cf7ee41d05e8757f463d11952a594eb066143cc2f7253ea1d41c6efe681cdc3ab8c2fa6eb0815fa5a94de3dc
checksum: 3d17cec5bde2003192fc727212459f65da8f330e3d645a8e4421e84df339634d16222672d69c6d192ec416e71cccdd6b141066ff53842b118cdec3c8875ccf4a
languageName: node
linkType: hard
@ -4248,14 +4262,14 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:8.4.14":
version: 8.4.14
resolution: "postcss@npm:8.4.14"
"postcss@npm:8.4.31":
version: 8.4.31
resolution: "postcss@npm:8.4.31"
dependencies:
nanoid: ^3.3.4
nanoid: ^3.3.6
picocolors: ^1.0.0
source-map-js: ^1.0.2
checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816
checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea
languageName: node
linkType: hard
@ -4325,26 +4339,24 @@ __metadata:
languageName: node
linkType: hard
"prettier-plugin-tailwindcss@npm:^0.4.1":
version: 0.4.1
resolution: "prettier-plugin-tailwindcss@npm:0.4.1"
"prettier-plugin-tailwindcss@npm:latest":
version: 0.5.7
resolution: "prettier-plugin-tailwindcss@npm:0.5.7"
peerDependencies:
"@ianvs/prettier-plugin-sort-imports": "*"
"@prettier/plugin-pug": "*"
"@shopify/prettier-plugin-liquid": "*"
"@shufo/prettier-plugin-blade": "*"
"@trivago/prettier-plugin-sort-imports": "*"
prettier: ^2.2 || ^3.0
prettier: ^3.0
prettier-plugin-astro: "*"
prettier-plugin-css-order: "*"
prettier-plugin-import-sort: "*"
prettier-plugin-jsdoc: "*"
prettier-plugin-marko: "*"
prettier-plugin-organize-attributes: "*"
prettier-plugin-organize-imports: "*"
prettier-plugin-style-order: "*"
prettier-plugin-svelte: "*"
prettier-plugin-twig-melody: "*"
peerDependenciesMeta:
"@ianvs/prettier-plugin-sort-imports":
optional: true
@ -4376,7 +4388,7 @@ __metadata:
optional: true
prettier-plugin-twig-melody:
optional: true
checksum: 9bdf3b7c270cc544f6bc38bbb869d3a97afc93de5005eee7272a36f374488731349c9362d2bbe4c83bd2a14b8a7c96cd25b9b682d1bea6dccc5bf08b2d02096c
checksum: 616c6eabd319453294fb1d2b241d6ff68b876092a902da67c90c39d4a2120ad605749ead09e08f3be11b4abdb60e63ce5dad1088f277b431b1131ffb365ce679
languageName: node
linkType: hard
@ -4455,15 +4467,15 @@ __metadata:
languageName: node
linkType: hard
"react-dom@npm:latest":
version: 18.2.0
resolution: "react-dom@npm:18.2.0"
"react-dom@npm:canary":
version: 18.3.0-canary-6b3834a45-20231110
resolution: "react-dom@npm:18.3.0-canary-6b3834a45-20231110"
dependencies:
loose-envify: ^1.1.0
scheduler: ^0.23.0
scheduler: 0.24.0-canary-6b3834a45-20231110
peerDependencies:
react: ^18.2.0
checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc
react: 18.3.0-canary-6b3834a45-20231110
checksum: b4d4a79fee9d99d634ce3d0255a3df0450d6923a4302a27b66d08001f5e4f5389367978b4ecd8aa071d70b4613911eae0bb4d643831ecf5b56877634f42b6ca7
languageName: node
linkType: hard
@ -4522,12 +4534,12 @@ __metadata:
languageName: node
linkType: hard
"react@npm:latest":
version: 18.2.0
resolution: "react@npm:18.2.0"
"react@npm:canary":
version: 18.3.0-canary-6b3834a45-20231110
resolution: "react@npm:18.3.0-canary-6b3834a45-20231110"
dependencies:
loose-envify: ^1.1.0
checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b
checksum: 1c89235bd8aaf3681edaf28413d2788bd7fc6ea0bdfc2e2a28e9ed6f855cc79a1ed65be61084baeda82c8652cba8670eb75c12e4059b701365f1a4ed930b8ce3
languageName: node
linkType: hard
@ -4800,12 +4812,12 @@ __metadata:
languageName: node
linkType: hard
"scheduler@npm:^0.23.0":
version: 0.23.0
resolution: "scheduler@npm:0.23.0"
"scheduler@npm:0.24.0-canary-6b3834a45-20231110":
version: 0.24.0-canary-6b3834a45-20231110
resolution: "scheduler@npm:0.24.0-canary-6b3834a45-20231110"
dependencies:
loose-envify: ^1.1.0
checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a
checksum: 0caa252a3b7f85fe5ee3d58cdee6ad5ec60fba3e227fe95b2df7b7f451c20d75bef06c5517992f4dc3e28aae26037e90f6fc3738665d6df28774cd011d9a5231
languageName: node
linkType: hard
@ -5598,6 +5610,13 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~5.26.4":
version: 5.26.5
resolution: "undici-types@npm:5.26.5"
checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487
languageName: node
linkType: hard
"unique-filename@npm:^3.0.0":
version: 3.0.0
resolution: "unique-filename@npm:3.0.0"
@ -5791,10 +5810,3 @@ __metadata:
checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
languageName: node
linkType: hard
"zod@npm:3.21.4":
version: 3.21.4
resolution: "zod@npm:3.21.4"
checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f
languageName: node
linkType: hard