[
  {
    "path": ".env.example",
    "content": "COMPANY_NAME=\"Vercel Inc.\"\nSITE_NAME=\"Next.js Commerce\"\nSHOPIFY_REVALIDATION_SECRET=\"\"\nSHOPIFY_STOREFRONT_ACCESS_TOKEN=\"\"\nSHOPIFY_STORE_DOMAIN=\"[your-shopify-store-subdomain].myshopify.com\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n.playwright\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*\n!.env.example\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env*.local\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Next.js: debug server-side\",\n      \"type\": \"node-terminal\",\n      \"request\": \"launch\",\n      \"command\": \"pnpm dev\"\n    },\n    {\n      \"name\": \"Next.js: debug client-side\",\n      \"type\": \"chrome\",\n      \"request\": \"launch\",\n      \"url\": \"http://localhost:3000\"\n    },\n    {\n      \"name\": \"Next.js: debug full stack\",\n      \"type\": \"node-terminal\",\n      \"request\": \"launch\",\n      \"command\": \"pnpm dev\",\n      \"serverReadyAction\": {\n        \"pattern\": \"started server on .+, url: (https?://.+)\",\n        \"uriFormat\": \"%s\",\n        \"action\": \"debugWithChrome\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll\": \"explicit\",\n    \"source.organizeImports\": \"explicit\",\n    \"source.sortMembers\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "README.md",
    "content": "[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&env=COMPANY_NAME,SHOPIFY_REVALIDATION_SECRET,SHOPIFY_STORE_DOMAIN,SHOPIFY_STOREFRONT_ACCESS_TOKEN,SITE_NAME)\n\n# Next.js Commerce\n\nA high-performance, server-rendered Next.js App Router ecommerce application.\n\nThis template uses React Server Components, Server Actions, `Suspense`, `useOptimistic`, and more.\n\n<h3 id=\"v1-note\"></h3>\n\n> Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1).\n\n## Providers\n\nVercel 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).\n\nVercel 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.\n\n- Shopify (this repository)\n- [BigCommerce](https://github.com/bigcommerce/nextjs-commerce) ([Demo](https://next-commerce-v2.vercel.app/))\n- [Ecwid by Lightspeed](https://github.com/Ecwid/ecwid-nextjs-commerce/) ([Demo](https://ecwid-nextjs-commerce.vercel.app/))\n- [Geins](https://github.com/geins-io/vercel-nextjs-commerce) ([Demo](https://geins-nextjs-commerce-starter.vercel.app/))\n- [Medusa](https://github.com/medusajs/vercel-commerce) ([Demo](https://medusa-nextjs-commerce.vercel.app/))\n- [Prodigy Commerce](https://github.com/prodigycommerce/nextjs-commerce) ([Demo](https://prodigy-nextjs-commerce.vercel.app/))\n- [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/))\n- [Shopware](https://github.com/shopwareLabs/vercel-commerce) ([Demo](https://shopware-vercel-commerce-react.vercel.app/))\n- [Swell](https://github.com/swellstores/verswell-commerce) ([Demo](https://verswell-commerce.vercel.app/))\n- [Umbraco](https://github.com/umbraco/Umbraco.VercelCommerce.Demo) ([Demo](https://vercel-commerce-demo.umbraco.com/))\n- [Wix](https://github.com/wix/headless-templates/tree/main/nextjs/commerce) ([Demo](https://wix-nextjs-commerce.vercel.app/))\n- [Fourthwall](https://github.com/FourthwallHQ/vercel-commerce) ([Demo](https://vercel-storefront.fourthwall.app/))\n\n> 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).\n\n## Integrations\n\nIntegrations enable upgraded or additional functionality for Next.js Commerce\n\n- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/))\n\n  - Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration.\n  - Search runs entirely in the browser for smaller catalogs or on a CDN for larger.\n\n- [React Bricks](https://github.com/ReactBricks/nextjs-commerce-rb) ([Demo](https://nextjs-commerce.reactbricks.com/))\n  - Edit pages, product details, and footer content visually using [React Bricks](https://www.reactbricks.com) visual headless CMS.\n\n## Running locally\n\nYou will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.\n\n> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Shopify store.\n\n1. Install Vercel CLI: `npm i -g vercel`\n2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`\n3. Download your environment variables: `vercel env pull`\n\n```bash\npnpm install\npnpm dev\n```\n\nYour app should now be running on [localhost:3000](http://localhost:3000/).\n\n<details>\n  <summary>Expand if you work at Vercel and want to run locally and / or contribute</summary>\n\n1. Run `vc link`.\n1. Select the `Vercel Solutions` scope.\n1. Connect to the existing `commerce-shopify` project.\n1. Run `vc env pull` to get environment variables.\n1. Run `pnpm dev` to ensure everything is working correctly.\n</details>\n\n## Vercel, Next.js Commerce, and Shopify Integration Guide\n\nYou can use this comprehensive [integration guide](https://vercel.com/docs/integrations/ecommerce/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.\n"
  },
  {
    "path": "app/[page]/layout.tsx",
    "content": "import Footer from \"components/layout/footer\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <>\n      <div className=\"w-full\">\n        <div className=\"mx-8 max-w-2xl py-20 sm:mx-auto\">{children}</div>\n      </div>\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[page]/opengraph-image.tsx",
    "content": "import OpengraphImage from \"components/opengraph-image\";\nimport { getPage } from \"lib/shopify\";\n\nexport default async function Image({ params }: { params: { page: string } }) {\n  const page = await getPage(params.page);\n  const title = page.seo?.title || page.title;\n\n  return await OpengraphImage({ title });\n}\n"
  },
  {
    "path": "app/[page]/page.tsx",
    "content": "import type { Metadata } from \"next\";\n\nimport Prose from \"components/prose\";\nimport { getPage } from \"lib/shopify\";\nimport { notFound } from \"next/navigation\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ page: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  const page = await getPage(params.page);\n\n  if (!page) return notFound();\n\n  return {\n    title: page.seo?.title || page.title,\n    description: page.seo?.description || page.bodySummary,\n    openGraph: {\n      publishedTime: page.createdAt,\n      modifiedTime: page.updatedAt,\n      type: \"article\",\n    },\n  };\n}\n\nexport default async function Page(props: {\n  params: Promise<{ page: string }>;\n}) {\n  const params = await props.params;\n  const page = await getPage(params.page);\n\n  if (!page) return notFound();\n\n  return (\n    <>\n      <h1 className=\"mb-8 text-5xl font-bold\">{page.title}</h1>\n      <Prose className=\"mb-8\" html={page.body} />\n      <p className=\"text-sm italic\">\n        {`This document was last updated on ${new Intl.DateTimeFormat(\n          undefined,\n          {\n            year: \"numeric\",\n            month: \"long\",\n            day: \"numeric\",\n          },\n        ).format(new Date(page.updatedAt))}.`}\n      </p>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/api/revalidate/route.ts",
    "content": "import { revalidate } from \"lib/shopify\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n  return revalidate(req);\n}\n"
  },
  {
    "path": "app/error.tsx",
    "content": "\"use client\";\n\nexport default function Error({ reset }: { reset: () => void }) {\n  return (\n    <div className=\"mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black\">\n      <h2 className=\"text-xl font-bold\">Oh no!</h2>\n      <p className=\"my-2\">\n        There was an issue with our storefront. This could be a temporary issue,\n        please try your action again.\n      </p>\n      <button\n        className=\"mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90\"\n        onClick={() => reset()}\n      >\n        Try Again\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/globals.css",
    "content": "@import \"tailwindcss\";\n\n@plugin \"@tailwindcss/container-queries\";\n@plugin \"@tailwindcss/typography\";\n\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentColor);\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n}\n\n@supports (font: -apple-system-body) and (-webkit-appearance: none) {\n  img[loading=\"lazy\"] {\n    clip-path: inset(0.6px);\n  }\n}\n\na,\ninput,\nbutton {\n  @apply focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900;\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import { CartProvider } from \"components/cart/cart-context\";\nimport { Navbar } from \"components/layout/navbar\";\nimport { WelcomeToast } from \"components/welcome-toast\";\nimport { GeistSans } from \"geist/font/sans\";\nimport { getCart } from \"lib/shopify\";\nimport { ReactNode } from \"react\";\nimport { Toaster } from \"sonner\";\nimport \"./globals.css\";\nimport { baseUrl } from \"lib/utils\";\n\nconst { SITE_NAME } = process.env;\n\nexport const metadata = {\n  metadataBase: new URL(baseUrl),\n  title: {\n    default: SITE_NAME!,\n    template: `%s | ${SITE_NAME}`,\n  },\n  robots: {\n    follow: true,\n    index: true,\n  },\n};\n\nexport default async function RootLayout({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  // Don't await the fetch, pass the Promise to the context provider\n  const cart = getCart();\n\n  return (\n    <html lang=\"en\" className={GeistSans.variable}>\n      <body className=\"bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white\">\n        <CartProvider cartPromise={cart}>\n          <Navbar />\n          <main>\n            {children}\n            <Toaster closeButton />\n            <WelcomeToast />\n          </main>\n        </CartProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/opengraph-image.tsx",
    "content": "import OpengraphImage from \"components/opengraph-image\";\n\nexport default async function Image() {\n  return await OpengraphImage();\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import { Carousel } from \"components/carousel\";\nimport { ThreeItemGrid } from \"components/grid/three-items\";\nimport Footer from \"components/layout/footer\";\n\nexport const metadata = {\n  description:\n    \"High-performance ecommerce store built with Next.js, Vercel, and Shopify.\",\n  openGraph: {\n    type: \"website\",\n  },\n};\n\nexport default function HomePage() {\n  return (\n    <>\n      <ThreeItemGrid />\n      <Carousel />\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/product/[handle]/page.tsx",
    "content": "import { GridTileImage } from \"components/grid/tile\";\nimport Footer from \"components/layout/footer\";\nimport { Gallery } from \"components/product/gallery\";\nimport { ProductDescription } from \"components/product/product-description\";\nimport { HIDDEN_PRODUCT_TAG } from \"lib/constants\";\nimport { getProduct, getProductRecommendations } from \"lib/shopify\";\nimport type { Image } from \"lib/shopify/types\";\nimport type { Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { notFound } from \"next/navigation\";\nimport { Suspense } from \"react\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ handle: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  const product = await getProduct(params.handle);\n\n  if (!product) return notFound();\n\n  const { url, width, height, altText: alt } = product.featuredImage || {};\n  const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);\n\n  return {\n    title: product.seo.title || product.title,\n    description: product.seo.description || product.description,\n    robots: {\n      index: indexable,\n      follow: indexable,\n      googleBot: {\n        index: indexable,\n        follow: indexable,\n      },\n    },\n    openGraph: url\n      ? {\n          images: [\n            {\n              url,\n              width,\n              height,\n              alt,\n            },\n          ],\n        }\n      : null,\n  };\n}\n\nexport default async function ProductPage(props: {\n  params: Promise<{ handle: string }>;\n}) {\n  const params = await props.params;\n  const product = await getProduct(params.handle);\n\n  if (!product) return notFound();\n\n  const productJsonLd = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Product\",\n    name: product.title,\n    description: product.description,\n    image: product.featuredImage.url,\n    offers: {\n      \"@type\": \"AggregateOffer\",\n      availability: product.availableForSale\n        ? \"https://schema.org/InStock\"\n        : \"https://schema.org/OutOfStock\",\n      priceCurrency: product.priceRange.minVariantPrice.currencyCode,\n      highPrice: product.priceRange.maxVariantPrice.amount,\n      lowPrice: product.priceRange.minVariantPrice.amount,\n    },\n  };\n\n  return (\n    <>\n      <script\n        type=\"application/ld+json\"\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(productJsonLd),\n        }}\n      />\n      <div className=\"mx-auto max-w-(--breakpoint-2xl) px-4\">\n        <div className=\"flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black\">\n          <div className=\"h-full w-full basis-full lg:basis-4/6\">\n            <Suspense\n              fallback={\n                <div className=\"relative aspect-square h-full max-h-[550px] w-full overflow-hidden\" />\n              }\n            >\n              <Gallery\n                images={product.images.slice(0, 5).map((image: Image) => ({\n                  src: image.url,\n                  altText: image.altText,\n                }))}\n              />\n            </Suspense>\n          </div>\n\n          <div className=\"basis-full lg:basis-2/6\">\n            <Suspense fallback={null}>\n              <ProductDescription product={product} />\n            </Suspense>\n          </div>\n        </div>\n        <RelatedProducts id={product.id} />\n      </div>\n      <Footer />\n    </>\n  );\n}\n\nasync function RelatedProducts({ id }: { id: string }) {\n  const relatedProducts = await getProductRecommendations(id);\n\n  if (!relatedProducts.length) return null;\n\n  return (\n    <div className=\"py-8\">\n      <h2 className=\"mb-4 text-2xl font-bold\">Related Products</h2>\n      <ul className=\"flex w-full gap-4 overflow-x-auto pt-1\">\n        {relatedProducts.map((product) => (\n          <li\n            key={product.handle}\n            className=\"aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5\"\n          >\n            <Link\n              className=\"relative h-full w-full\"\n              href={`/product/${product.handle}`}\n              prefetch={true}\n            >\n              <GridTileImage\n                alt={product.title}\n                label={{\n                  title: product.title,\n                  amount: product.priceRange.maxVariantPrice.amount,\n                  currencyCode: product.priceRange.maxVariantPrice.currencyCode,\n                }}\n                src={product.featuredImage?.url}\n                fill\n                sizes=\"(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw\"\n              />\n            </Link>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/robots.ts",
    "content": "import { baseUrl } from \"lib/utils\";\n\nexport default function robots() {\n  return {\n    rules: [\n      {\n        userAgent: \"*\",\n      },\n    ],\n    sitemap: `${baseUrl}/sitemap.xml`,\n    host: baseUrl,\n  };\n}\n"
  },
  {
    "path": "app/search/[collection]/opengraph-image.tsx",
    "content": "import OpengraphImage from \"components/opengraph-image\";\nimport { getCollection } from \"lib/shopify\";\n\nexport default async function Image({\n  params,\n}: {\n  params: { collection: string };\n}) {\n  const collection = await getCollection(params.collection);\n  const title = collection?.seo?.title || collection?.title;\n\n  return await OpengraphImage({ title });\n}\n"
  },
  {
    "path": "app/search/[collection]/page.tsx",
    "content": "import { getCollection, getCollectionProducts } from \"lib/shopify\";\nimport { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\n\nimport Grid from \"components/grid\";\nimport ProductGridItems from \"components/layout/product-grid-items\";\nimport { defaultSort, sorting } from \"lib/constants\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ collection: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  const collection = await getCollection(params.collection);\n\n  if (!collection) return notFound();\n\n  return {\n    title: collection.seo?.title || collection.title,\n    description:\n      collection.seo?.description ||\n      collection.description ||\n      `${collection.title} products`,\n  };\n}\n\nexport default async function CategoryPage(props: {\n  params: Promise<{ collection: string }>;\n  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;\n}) {\n  const searchParams = await props.searchParams;\n  const params = await props.params;\n  const { sort } = searchParams as { [key: string]: string };\n  const { sortKey, reverse } =\n    sorting.find((item) => item.slug === sort) || defaultSort;\n  const products = await getCollectionProducts({\n    collection: params.collection,\n    sortKey,\n    reverse,\n  });\n\n  return (\n    <section>\n      {products.length === 0 ? (\n        <p className=\"py-3 text-lg\">{`No products found in this collection`}</p>\n      ) : (\n        <Grid className=\"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n          <ProductGridItems products={products} />\n        </Grid>\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/search/children-wrapper.tsx",
    "content": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\nimport { Fragment } from \"react\";\n\n// Ensure children are re-rendered when the search query changes\nexport default function ChildrenWrapper({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const searchParams = useSearchParams();\n  return <Fragment key={searchParams.get(\"q\")}>{children}</Fragment>;\n}\n"
  },
  {
    "path": "app/search/layout.tsx",
    "content": "import Footer from \"components/layout/footer\";\nimport Collections from \"components/layout/search/collections\";\nimport FilterList from \"components/layout/search/filter\";\nimport { sorting } from \"lib/constants\";\nimport ChildrenWrapper from \"./children-wrapper\";\nimport { Suspense } from \"react\";\n\nexport default function SearchLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <>\n      <div className=\"mx-auto flex max-w-(--breakpoint-2xl) flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white\">\n        <div className=\"order-first w-full flex-none md:max-w-[125px]\">\n          <Collections />\n        </div>\n        <div className=\"order-last min-h-screen w-full md:order-none\">\n          <Suspense fallback={null}>\n            <ChildrenWrapper>{children}</ChildrenWrapper>\n          </Suspense>\n        </div>\n        <div className=\"order-none flex-none md:order-last md:w-[125px]\">\n          <FilterList list={sorting} title=\"Sort by\" />\n        </div>\n      </div>\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/search/loading.tsx",
    "content": "import Grid from \"components/grid\";\n\nexport default function Loading() {\n  return (\n    <>\n      <div className=\"mb-4 h-6\" />\n      <Grid className=\"grid-cols-2 lg:grid-cols-3\">\n        {Array(12)\n          .fill(0)\n          .map((_, index) => {\n            return (\n              <Grid.Item\n                key={index}\n                className=\"animate-pulse bg-neutral-100 dark:bg-neutral-800\"\n              />\n            );\n          })}\n      </Grid>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/search/page.tsx",
    "content": "import Grid from \"components/grid\";\nimport ProductGridItems from \"components/layout/product-grid-items\";\nimport { defaultSort, sorting } from \"lib/constants\";\nimport { getProducts } from \"lib/shopify\";\n\nexport const metadata = {\n  title: \"Search\",\n  description: \"Search for products in the store.\",\n};\n\nexport default async function SearchPage(props: {\n  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;\n}) {\n  const searchParams = await props.searchParams;\n  const { sort, q: searchValue } = searchParams as { [key: string]: string };\n  const { sortKey, reverse } =\n    sorting.find((item) => item.slug === sort) || defaultSort;\n\n  const products = await getProducts({ sortKey, reverse, query: searchValue });\n  const resultsText = products.length > 1 ? \"results\" : \"result\";\n\n  return (\n    <>\n      {searchValue ? (\n        <p className=\"mb-4\">\n          {products.length === 0\n            ? \"There are no products that match \"\n            : `Showing ${products.length} ${resultsText} for `}\n          <span className=\"font-bold\">&quot;{searchValue}&quot;</span>\n        </p>\n      ) : null}\n      {products.length > 0 ? (\n        <Grid className=\"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n          <ProductGridItems products={products} />\n        </Grid>\n      ) : null}\n    </>\n  );\n}\n"
  },
  {
    "path": "app/sitemap.ts",
    "content": "import { getCollections, getPages, getProducts } from \"lib/shopify\";\nimport { baseUrl, validateEnvironmentVariables } from \"lib/utils\";\nimport { MetadataRoute } from \"next\";\n\ntype Route = {\n  url: string;\n  lastModified: string;\n};\n\nexport const dynamic = \"force-dynamic\";\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n  validateEnvironmentVariables();\n\n  const routesMap = [\"\"].map((route) => ({\n    url: `${baseUrl}${route}`,\n    lastModified: new Date().toISOString(),\n  }));\n\n  const collectionsPromise = getCollections().then((collections) =>\n    collections.map((collection) => ({\n      url: `${baseUrl}${collection.path}`,\n      lastModified: collection.updatedAt,\n    })),\n  );\n\n  const productsPromise = getProducts({}).then((products) =>\n    products.map((product) => ({\n      url: `${baseUrl}/product/${product.handle}`,\n      lastModified: product.updatedAt,\n    })),\n  );\n\n  const pagesPromise = getPages().then((pages) =>\n    pages.map((page) => ({\n      url: `${baseUrl}/${page.handle}`,\n      lastModified: page.updatedAt,\n    })),\n  );\n\n  let fetchedRoutes: Route[] = [];\n\n  try {\n    fetchedRoutes = (\n      await Promise.all([collectionsPromise, productsPromise, pagesPromise])\n    ).flat();\n  } catch (error) {\n    throw JSON.stringify(error, null, 2);\n  }\n\n  return [...routesMap, ...fetchedRoutes];\n}\n"
  },
  {
    "path": "components/carousel.tsx",
    "content": "import { getCollectionProducts } from \"lib/shopify\";\nimport Link from \"next/link\";\nimport { GridTileImage } from \"./grid/tile\";\n\nexport async function Carousel() {\n  // Collections that start with `hidden-*` are hidden from the search page.\n  const products = await getCollectionProducts({\n    collection: \"hidden-homepage-carousel\",\n  });\n\n  if (!products?.length) return null;\n\n  // Purposefully duplicating products to make the carousel loop and not run out of products on wide screens.\n  const carouselProducts = [...products, ...products, ...products];\n\n  return (\n    <div className=\"w-full overflow-x-auto pb-6 pt-1\">\n      <ul className=\"flex animate-carousel gap-4\">\n        {carouselProducts.map((product, i) => (\n          <li\n            key={`${product.handle}${i}`}\n            className=\"relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3\"\n          >\n            <Link\n              href={`/product/${product.handle}`}\n              className=\"relative h-full w-full\"\n            >\n              <GridTileImage\n                alt={product.title}\n                label={{\n                  title: product.title,\n                  amount: product.priceRange.maxVariantPrice.amount,\n                  currencyCode: product.priceRange.maxVariantPrice.currencyCode,\n                }}\n                src={product.featuredImage?.url}\n                fill\n                sizes=\"(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw\"\n              />\n            </Link>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/cart/actions.ts",
    "content": "\"use server\";\n\nimport { TAGS } from \"lib/constants\";\nimport {\n  addToCart,\n  createCart,\n  getCart,\n  removeFromCart,\n  updateCart,\n} from \"lib/shopify\";\nimport { updateTag } from \"next/cache\";\nimport { cookies } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\n\nexport async function addItem(\n  prevState: any,\n  selectedVariantId: string | undefined\n) {\n  if (!selectedVariantId) {\n    return \"Error adding item to cart\";\n  }\n\n  try {\n    await addToCart([{ merchandiseId: selectedVariantId, quantity: 1 }]);\n    updateTag(TAGS.cart);\n  } catch (e) {\n    return \"Error adding item to cart\";\n  }\n}\n\nexport async function removeItem(prevState: any, merchandiseId: string) {\n  try {\n    const cart = await getCart();\n\n    if (!cart) {\n      return \"Error fetching cart\";\n    }\n\n    const lineItem = cart.lines.find(\n      (line) => line.merchandise.id === merchandiseId\n    );\n\n    if (lineItem && lineItem.id) {\n      await removeFromCart([lineItem.id]);\n      updateTag(TAGS.cart);\n    } else {\n      return \"Item not found in cart\";\n    }\n  } catch (e) {\n    return \"Error removing item from cart\";\n  }\n}\n\nexport async function updateItemQuantity(\n  prevState: any,\n  payload: {\n    merchandiseId: string;\n    quantity: number;\n  }\n) {\n  const { merchandiseId, quantity } = payload;\n\n  try {\n    const cart = await getCart();\n\n    if (!cart) {\n      return \"Error fetching cart\";\n    }\n\n    const lineItem = cart.lines.find(\n      (line) => line.merchandise.id === merchandiseId\n    );\n\n    if (lineItem && lineItem.id) {\n      if (quantity === 0) {\n        await removeFromCart([lineItem.id]);\n      } else {\n        await updateCart([\n          {\n            id: lineItem.id,\n            merchandiseId,\n            quantity,\n          },\n        ]);\n      }\n    } else if (quantity > 0) {\n      // If the item doesn't exist in the cart and quantity > 0, add it\n      await addToCart([{ merchandiseId, quantity }]);\n    }\n\n    updateTag(TAGS.cart);\n  } catch (e) {\n    console.error(e);\n    return \"Error updating item quantity\";\n  }\n}\n\nexport async function redirectToCheckout() {\n  let cart = await getCart();\n  redirect(cart!.checkoutUrl);\n}\n\nexport async function createCartAndSetCookie() {\n  let cart = await createCart();\n  (await cookies()).set(\"cartId\", cart.id!);\n}\n"
  },
  {
    "path": "components/cart/add-to-cart.tsx",
    "content": "\"use client\";\n\nimport { PlusIcon } from \"@heroicons/react/24/outline\";\nimport clsx from \"clsx\";\nimport { addItem } from \"components/cart/actions\";\nimport { Product, ProductVariant } from \"lib/shopify/types\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useActionState } from \"react\";\nimport { useCart } from \"./cart-context\";\n\nfunction SubmitButton({\n  availableForSale,\n  selectedVariantId,\n}: {\n  availableForSale: boolean;\n  selectedVariantId: string | undefined;\n}) {\n  const buttonClasses =\n    \"relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white\";\n  const disabledClasses = \"cursor-not-allowed opacity-60 hover:opacity-60\";\n\n  if (!availableForSale) {\n    return (\n      <button disabled className={clsx(buttonClasses, disabledClasses)}>\n        Out Of Stock\n      </button>\n    );\n  }\n\n  if (!selectedVariantId) {\n    return (\n      <button\n        aria-label=\"Please select an option\"\n        disabled\n        className={clsx(buttonClasses, disabledClasses)}\n      >\n        <div className=\"absolute left-0 ml-4\">\n          <PlusIcon className=\"h-5\" />\n        </div>\n        Add To Cart\n      </button>\n    );\n  }\n\n  return (\n    <button\n      aria-label=\"Add to cart\"\n      className={clsx(buttonClasses, {\n        \"hover:opacity-90\": true,\n      })}\n    >\n      <div className=\"absolute left-0 ml-4\">\n        <PlusIcon className=\"h-5\" />\n      </div>\n      Add To Cart\n    </button>\n  );\n}\n\nexport function AddToCart({ product }: { product: Product }) {\n  const { variants, availableForSale } = product;\n  const { addCartItem } = useCart();\n  const searchParams = useSearchParams();\n  const [message, formAction] = useActionState(addItem, null);\n\n  const variant = variants.find((variant: ProductVariant) =>\n    variant.selectedOptions.every(\n      (option) => option.value === searchParams.get(option.name.toLowerCase()),\n    ),\n  );\n  const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;\n  const selectedVariantId = variant?.id || defaultVariantId;\n  const addItemAction = formAction.bind(null, selectedVariantId);\n  const finalVariant = variants.find(\n    (variant) => variant.id === selectedVariantId,\n  )!;\n\n  return (\n    <form\n      action={async () => {\n        addCartItem(finalVariant, product);\n        addItemAction();\n      }}\n    >\n      <SubmitButton\n        availableForSale={availableForSale}\n        selectedVariantId={selectedVariantId}\n      />\n      <p aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {message}\n      </p>\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/cart/cart-context.tsx",
    "content": "\"use client\";\n\nimport type {\n  Cart,\n  CartItem,\n  Product,\n  ProductVariant,\n} from \"lib/shopify/types\";\nimport React, {\n  createContext,\n  use,\n  useContext,\n  useMemo,\n  useOptimistic,\n} from \"react\";\n\ntype UpdateType = \"plus\" | \"minus\" | \"delete\";\n\ntype CartAction =\n  | {\n      type: \"UPDATE_ITEM\";\n      payload: { merchandiseId: string; updateType: UpdateType };\n    }\n  | {\n      type: \"ADD_ITEM\";\n      payload: { variant: ProductVariant; product: Product };\n    };\n\ntype CartContextType = {\n  cartPromise: Promise<Cart | undefined>;\n};\n\nconst CartContext = createContext<CartContextType | undefined>(undefined);\n\nfunction calculateItemCost(quantity: number, price: string): string {\n  return (Number(price) * quantity).toString();\n}\n\nfunction updateCartItem(\n  item: CartItem,\n  updateType: UpdateType,\n): CartItem | null {\n  if (updateType === \"delete\") return null;\n\n  const newQuantity =\n    updateType === \"plus\" ? item.quantity + 1 : item.quantity - 1;\n  if (newQuantity === 0) return null;\n\n  const singleItemAmount = Number(item.cost.totalAmount.amount) / item.quantity;\n  const newTotalAmount = calculateItemCost(\n    newQuantity,\n    singleItemAmount.toString(),\n  );\n\n  return {\n    ...item,\n    quantity: newQuantity,\n    cost: {\n      ...item.cost,\n      totalAmount: {\n        ...item.cost.totalAmount,\n        amount: newTotalAmount,\n      },\n    },\n  };\n}\n\nfunction createOrUpdateCartItem(\n  existingItem: CartItem | undefined,\n  variant: ProductVariant,\n  product: Product,\n): CartItem {\n  const quantity = existingItem ? existingItem.quantity + 1 : 1;\n  const totalAmount = calculateItemCost(quantity, variant.price.amount);\n\n  return {\n    id: existingItem?.id,\n    quantity,\n    cost: {\n      totalAmount: {\n        amount: totalAmount,\n        currencyCode: variant.price.currencyCode,\n      },\n    },\n    merchandise: {\n      id: variant.id,\n      title: variant.title,\n      selectedOptions: variant.selectedOptions,\n      product: {\n        id: product.id,\n        handle: product.handle,\n        title: product.title,\n        featuredImage: product.featuredImage,\n      },\n    },\n  };\n}\n\nfunction updateCartTotals(\n  lines: CartItem[],\n): Pick<Cart, \"totalQuantity\" | \"cost\"> {\n  const totalQuantity = lines.reduce((sum, item) => sum + item.quantity, 0);\n  const totalAmount = lines.reduce(\n    (sum, item) => sum + Number(item.cost.totalAmount.amount),\n    0,\n  );\n  const currencyCode = lines[0]?.cost.totalAmount.currencyCode ?? \"USD\";\n\n  return {\n    totalQuantity,\n    cost: {\n      subtotalAmount: { amount: totalAmount.toString(), currencyCode },\n      totalAmount: { amount: totalAmount.toString(), currencyCode },\n      totalTaxAmount: { amount: \"0\", currencyCode },\n    },\n  };\n}\n\nfunction createEmptyCart(): Cart {\n  return {\n    id: undefined,\n    checkoutUrl: \"\",\n    totalQuantity: 0,\n    lines: [],\n    cost: {\n      subtotalAmount: { amount: \"0\", currencyCode: \"USD\" },\n      totalAmount: { amount: \"0\", currencyCode: \"USD\" },\n      totalTaxAmount: { amount: \"0\", currencyCode: \"USD\" },\n    },\n  };\n}\n\nfunction cartReducer(state: Cart | undefined, action: CartAction): Cart {\n  const currentCart = state || createEmptyCart();\n\n  switch (action.type) {\n    case \"UPDATE_ITEM\": {\n      const { merchandiseId, updateType } = action.payload;\n      const updatedLines = currentCart.lines\n        .map((item) =>\n          item.merchandise.id === merchandiseId\n            ? updateCartItem(item, updateType)\n            : item,\n        )\n        .filter(Boolean) as CartItem[];\n\n      if (updatedLines.length === 0) {\n        return {\n          ...currentCart,\n          lines: [],\n          totalQuantity: 0,\n          cost: {\n            ...currentCart.cost,\n            totalAmount: { ...currentCart.cost.totalAmount, amount: \"0\" },\n          },\n        };\n      }\n\n      return {\n        ...currentCart,\n        ...updateCartTotals(updatedLines),\n        lines: updatedLines,\n      };\n    }\n    case \"ADD_ITEM\": {\n      const { variant, product } = action.payload;\n      const existingItem = currentCart.lines.find(\n        (item) => item.merchandise.id === variant.id,\n      );\n      const updatedItem = createOrUpdateCartItem(\n        existingItem,\n        variant,\n        product,\n      );\n\n      const updatedLines = existingItem\n        ? currentCart.lines.map((item) =>\n            item.merchandise.id === variant.id ? updatedItem : item,\n          )\n        : [...currentCart.lines, updatedItem];\n\n      return {\n        ...currentCart,\n        ...updateCartTotals(updatedLines),\n        lines: updatedLines,\n      };\n    }\n    default:\n      return currentCart;\n  }\n}\n\nexport function CartProvider({\n  children,\n  cartPromise,\n}: {\n  children: React.ReactNode;\n  cartPromise: Promise<Cart | undefined>;\n}) {\n  return (\n    <CartContext.Provider value={{ cartPromise }}>\n      {children}\n    </CartContext.Provider>\n  );\n}\n\nexport function useCart() {\n  const context = useContext(CartContext);\n  if (context === undefined) {\n    throw new Error(\"useCart must be used within a CartProvider\");\n  }\n\n  const initialCart = use(context.cartPromise);\n  const [optimisticCart, updateOptimisticCart] = useOptimistic(\n    initialCart,\n    cartReducer,\n  );\n\n  const updateCartItem = (merchandiseId: string, updateType: UpdateType) => {\n    updateOptimisticCart({\n      type: \"UPDATE_ITEM\",\n      payload: { merchandiseId, updateType },\n    });\n  };\n\n  const addCartItem = (variant: ProductVariant, product: Product) => {\n    updateOptimisticCart({ type: \"ADD_ITEM\", payload: { variant, product } });\n  };\n\n  return useMemo(\n    () => ({\n      cart: optimisticCart,\n      updateCartItem,\n      addCartItem,\n    }),\n    [optimisticCart],\n  );\n}\n"
  },
  {
    "path": "components/cart/delete-item-button.tsx",
    "content": "\"use client\";\n\nimport { XMarkIcon } from \"@heroicons/react/24/outline\";\nimport { removeItem } from \"components/cart/actions\";\nimport type { CartItem } from \"lib/shopify/types\";\nimport { useActionState } from \"react\";\n\nexport function DeleteItemButton({\n  item,\n  optimisticUpdate,\n}: {\n  item: CartItem;\n  optimisticUpdate: any;\n}) {\n  const [message, formAction] = useActionState(removeItem, null);\n  const merchandiseId = item.merchandise.id;\n  const removeItemAction = formAction.bind(null, merchandiseId);\n\n  return (\n    <form\n      action={async () => {\n        optimisticUpdate(merchandiseId, \"delete\");\n        removeItemAction();\n      }}\n    >\n      <button\n        type=\"submit\"\n        aria-label=\"Remove cart item\"\n        className=\"flex h-[24px] w-[24px] items-center justify-center rounded-full bg-neutral-500\"\n      >\n        <XMarkIcon className=\"mx-[1px] h-4 w-4 text-white dark:text-black\" />\n      </button>\n      <p aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {message}\n      </p>\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/cart/edit-item-quantity-button.tsx",
    "content": "\"use client\";\n\nimport { MinusIcon, PlusIcon } from \"@heroicons/react/24/outline\";\nimport clsx from \"clsx\";\nimport { updateItemQuantity } from \"components/cart/actions\";\nimport type { CartItem } from \"lib/shopify/types\";\nimport { useActionState } from \"react\";\n\nfunction SubmitButton({ type }: { type: \"plus\" | \"minus\" }) {\n  return (\n    <button\n      type=\"submit\"\n      aria-label={\n        type === \"plus\" ? \"Increase item quantity\" : \"Reduce item quantity\"\n      }\n      className={clsx(\n        \"ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full p-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80\",\n        {\n          \"ml-auto\": type === \"minus\",\n        },\n      )}\n    >\n      {type === \"plus\" ? (\n        <PlusIcon className=\"h-4 w-4 dark:text-neutral-500\" />\n      ) : (\n        <MinusIcon className=\"h-4 w-4 dark:text-neutral-500\" />\n      )}\n    </button>\n  );\n}\n\nexport function EditItemQuantityButton({\n  item,\n  type,\n  optimisticUpdate,\n}: {\n  item: CartItem;\n  type: \"plus\" | \"minus\";\n  optimisticUpdate: any;\n}) {\n  const [message, formAction] = useActionState(updateItemQuantity, null);\n  const payload = {\n    merchandiseId: item.merchandise.id,\n    quantity: type === \"plus\" ? item.quantity + 1 : item.quantity - 1,\n  };\n  const updateItemQuantityAction = formAction.bind(null, payload);\n\n  return (\n    <form\n      action={async () => {\n        optimisticUpdate(payload.merchandiseId, type);\n        updateItemQuantityAction();\n      }}\n    >\n      <SubmitButton type={type} />\n      <p aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {message}\n      </p>\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/cart/modal.tsx",
    "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport { Dialog, Transition } from \"@headlessui/react\";\nimport { ShoppingCartIcon, XMarkIcon } from \"@heroicons/react/24/outline\";\nimport LoadingDots from \"components/loading-dots\";\nimport Price from \"components/price\";\nimport { DEFAULT_OPTION } from \"lib/constants\";\nimport { createUrl } from \"lib/utils\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { Fragment, useEffect, useRef, useState } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport { createCartAndSetCookie, redirectToCheckout } from \"./actions\";\nimport { useCart } from \"./cart-context\";\nimport { DeleteItemButton } from \"./delete-item-button\";\nimport { EditItemQuantityButton } from \"./edit-item-quantity-button\";\nimport OpenCart from \"./open-cart\";\n\ntype MerchandiseSearchParams = {\n  [key: string]: string;\n};\n\nexport default function CartModal() {\n  const { cart, updateCartItem } = useCart();\n  const [isOpen, setIsOpen] = useState(false);\n  const quantityRef = useRef(cart?.totalQuantity);\n  const openCart = () => setIsOpen(true);\n  const closeCart = () => setIsOpen(false);\n\n  useEffect(() => {\n    if (!cart) {\n      createCartAndSetCookie();\n    }\n  }, [cart]);\n\n  useEffect(() => {\n    if (\n      cart?.totalQuantity &&\n      cart?.totalQuantity !== quantityRef.current &&\n      cart?.totalQuantity > 0\n    ) {\n      if (!isOpen) {\n        setIsOpen(true);\n      }\n      quantityRef.current = cart?.totalQuantity;\n    }\n  }, [isOpen, cart?.totalQuantity, quantityRef]);\n\n  return (\n    <>\n      <button aria-label=\"Open cart\" onClick={openCart}>\n        <OpenCart quantity={cart?.totalQuantity} />\n      </button>\n      <Transition show={isOpen}>\n        <Dialog onClose={closeCart} className=\"relative z-50\">\n          <Transition.Child\n            as={Fragment}\n            enter=\"transition-all ease-in-out duration-300\"\n            enterFrom=\"opacity-0 backdrop-blur-none\"\n            enterTo=\"opacity-100 backdrop-blur-[.5px]\"\n            leave=\"transition-all ease-in-out duration-200\"\n            leaveFrom=\"opacity-100 backdrop-blur-[.5px]\"\n            leaveTo=\"opacity-0 backdrop-blur-none\"\n          >\n            <div className=\"fixed inset-0 bg-black/30\" aria-hidden=\"true\" />\n          </Transition.Child>\n          <Transition.Child\n            as={Fragment}\n            enter=\"transition-all ease-in-out duration-300\"\n            enterFrom=\"translate-x-full\"\n            enterTo=\"translate-x-0\"\n            leave=\"transition-all ease-in-out duration-200\"\n            leaveFrom=\"translate-x-0\"\n            leaveTo=\"translate-x-full\"\n          >\n            <Dialog.Panel className=\"fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white\">\n              <div className=\"flex items-center justify-between\">\n                <p className=\"text-lg font-semibold\">My Cart</p>\n                <button aria-label=\"Close cart\" onClick={closeCart}>\n                  <CloseCart />\n                </button>\n              </div>\n\n              {!cart || cart.lines.length === 0 ? (\n                <div className=\"mt-20 flex w-full flex-col items-center justify-center overflow-hidden\">\n                  <ShoppingCartIcon className=\"h-16\" />\n                  <p className=\"mt-6 text-center text-2xl font-bold\">\n                    Your cart is empty.\n                  </p>\n                </div>\n              ) : (\n                <div className=\"flex h-full flex-col justify-between overflow-hidden p-1\">\n                  <ul className=\"grow overflow-auto py-4\">\n                    {cart.lines\n                      .sort((a, b) =>\n                        a.merchandise.product.title.localeCompare(\n                          b.merchandise.product.title,\n                        ),\n                      )\n                      .map((item, i) => {\n                        const merchandiseSearchParams =\n                          {} as MerchandiseSearchParams;\n\n                        item.merchandise.selectedOptions.forEach(\n                          ({ name, value }) => {\n                            if (value !== DEFAULT_OPTION) {\n                              merchandiseSearchParams[name.toLowerCase()] =\n                                value;\n                            }\n                          },\n                        );\n\n                        const merchandiseUrl = createUrl(\n                          `/product/${item.merchandise.product.handle}`,\n                          new URLSearchParams(merchandiseSearchParams),\n                        );\n\n                        return (\n                          <li\n                            key={i}\n                            className=\"flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700\"\n                          >\n                            <div className=\"relative flex w-full flex-row justify-between px-1 py-4\">\n                              <div className=\"absolute z-40 -ml-1 -mt-2\">\n                                <DeleteItemButton\n                                  item={item}\n                                  optimisticUpdate={updateCartItem}\n                                />\n                              </div>\n                              <div className=\"flex flex-row\">\n                                <div className=\"relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800\">\n                                  <Image\n                                    className=\"h-full w-full object-cover\"\n                                    width={64}\n                                    height={64}\n                                    alt={\n                                      item.merchandise.product.featuredImage\n                                        .altText ||\n                                      item.merchandise.product.title\n                                    }\n                                    src={\n                                      item.merchandise.product.featuredImage.url\n                                    }\n                                  />\n                                </div>\n                                <Link\n                                  href={merchandiseUrl}\n                                  onClick={closeCart}\n                                  className=\"z-30 ml-2 flex flex-row space-x-4\"\n                                >\n                                  <div className=\"flex flex-1 flex-col text-base\">\n                                    <span className=\"leading-tight\">\n                                      {item.merchandise.product.title}\n                                    </span>\n                                    {item.merchandise.title !==\n                                    DEFAULT_OPTION ? (\n                                      <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n                                        {item.merchandise.title}\n                                      </p>\n                                    ) : null}\n                                  </div>\n                                </Link>\n                              </div>\n                              <div className=\"flex h-16 flex-col justify-between\">\n                                <Price\n                                  className=\"flex justify-end space-y-2 text-right text-sm\"\n                                  amount={item.cost.totalAmount.amount}\n                                  currencyCode={\n                                    item.cost.totalAmount.currencyCode\n                                  }\n                                />\n                                <div className=\"ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700\">\n                                  <EditItemQuantityButton\n                                    item={item}\n                                    type=\"minus\"\n                                    optimisticUpdate={updateCartItem}\n                                  />\n                                  <p className=\"w-6 text-center\">\n                                    <span className=\"w-full text-sm\">\n                                      {item.quantity}\n                                    </span>\n                                  </p>\n                                  <EditItemQuantityButton\n                                    item={item}\n                                    type=\"plus\"\n                                    optimisticUpdate={updateCartItem}\n                                  />\n                                </div>\n                              </div>\n                            </div>\n                          </li>\n                        );\n                      })}\n                  </ul>\n                  <div className=\"py-4 text-sm text-neutral-500 dark:text-neutral-400\">\n                    <div className=\"mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700\">\n                      <p>Taxes</p>\n                      <Price\n                        className=\"text-right text-base text-black dark:text-white\"\n                        amount={cart.cost.totalTaxAmount.amount}\n                        currencyCode={cart.cost.totalTaxAmount.currencyCode}\n                      />\n                    </div>\n                    <div className=\"mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700\">\n                      <p>Shipping</p>\n                      <p className=\"text-right\">Calculated at checkout</p>\n                    </div>\n                    <div className=\"mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700\">\n                      <p>Total</p>\n                      <Price\n                        className=\"text-right text-base text-black dark:text-white\"\n                        amount={cart.cost.totalAmount.amount}\n                        currencyCode={cart.cost.totalAmount.currencyCode}\n                      />\n                    </div>\n                  </div>\n                  <form action={redirectToCheckout}>\n                    <CheckoutButton />\n                  </form>\n                </div>\n              )}\n            </Dialog.Panel>\n          </Transition.Child>\n        </Dialog>\n      </Transition>\n    </>\n  );\n}\n\nfunction CloseCart({ className }: { className?: string }) {\n  return (\n    <div className=\"relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white\">\n      <XMarkIcon\n        className={clsx(\n          \"h-6 transition-all ease-in-out hover:scale-110\",\n          className,\n        )}\n      />\n    </div>\n  );\n}\n\nfunction CheckoutButton() {\n  const { pending } = useFormStatus();\n\n  return (\n    <button\n      className=\"block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100\"\n      type=\"submit\"\n      disabled={pending}\n    >\n      {pending ? <LoadingDots className=\"bg-white\" /> : \"Proceed to Checkout\"}\n    </button>\n  );\n}\n"
  },
  {
    "path": "components/cart/open-cart.tsx",
    "content": "import { ShoppingCartIcon } from \"@heroicons/react/24/outline\";\nimport clsx from \"clsx\";\n\nexport default function OpenCart({\n  className,\n  quantity,\n}: {\n  className?: string;\n  quantity?: number;\n}) {\n  return (\n    <div className=\"relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white\">\n      <ShoppingCartIcon\n        className={clsx(\n          \"h-4 transition-all ease-in-out hover:scale-110\",\n          className,\n        )}\n      />\n\n      {quantity ? (\n        <div className=\"absolute right-0 top-0 -mr-2 -mt-2 h-4 w-4 rounded-sm bg-blue-600 text-[11px] font-medium text-white\">\n          {quantity}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/grid/index.tsx",
    "content": "import clsx from \"clsx\";\n\nfunction Grid(props: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      {...props}\n      className={clsx(\"grid grid-flow-row gap-4\", props.className)}\n    >\n      {props.children}\n    </ul>\n  );\n}\n\nfunction GridItem(props: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      {...props}\n      className={clsx(\"aspect-square transition-opacity\", props.className)}\n    >\n      {props.children}\n    </li>\n  );\n}\n\nGrid.Item = GridItem;\n\nexport default Grid;\n"
  },
  {
    "path": "components/grid/three-items.tsx",
    "content": "import { GridTileImage } from \"components/grid/tile\";\nimport { getCollectionProducts } from \"lib/shopify\";\nimport type { Product } from \"lib/shopify/types\";\nimport Link from \"next/link\";\n\nfunction ThreeItemGridItem({\n  item,\n  size,\n  priority,\n}: {\n  item: Product;\n  size: \"full\" | \"half\";\n  priority?: boolean;\n}) {\n  return (\n    <div\n      className={\n        size === \"full\"\n          ? \"md:col-span-4 md:row-span-2\"\n          : \"md:col-span-2 md:row-span-1\"\n      }\n    >\n      <Link\n        className=\"relative block aspect-square h-full w-full\"\n        href={`/product/${item.handle}`}\n        prefetch={true}\n      >\n        <GridTileImage\n          src={item.featuredImage.url}\n          fill\n          sizes={\n            size === \"full\"\n              ? \"(min-width: 768px) 66vw, 100vw\"\n              : \"(min-width: 768px) 33vw, 100vw\"\n          }\n          priority={priority}\n          alt={item.title}\n          label={{\n            position: size === \"full\" ? \"center\" : \"bottom\",\n            title: item.title as string,\n            amount: item.priceRange.maxVariantPrice.amount,\n            currencyCode: item.priceRange.maxVariantPrice.currencyCode,\n          }}\n        />\n      </Link>\n    </div>\n  );\n}\n\nexport async function ThreeItemGrid() {\n  // Collections that start with `hidden-*` are hidden from the search page.\n  const homepageItems = await getCollectionProducts({\n    collection: \"hidden-homepage-featured-items\",\n  });\n\n  if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;\n\n  const [firstProduct, secondProduct, thirdProduct] = homepageItems;\n\n  return (\n    <section className=\"mx-auto grid max-w-(--breakpoint-2xl) gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]\">\n      <ThreeItemGridItem size=\"full\" item={firstProduct} priority={true} />\n      <ThreeItemGridItem size=\"half\" item={secondProduct} priority={true} />\n      <ThreeItemGridItem size=\"half\" item={thirdProduct} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "components/grid/tile.tsx",
    "content": "import clsx from \"clsx\";\nimport Image from \"next/image\";\nimport Label from \"../label\";\n\nexport function GridTileImage({\n  isInteractive = true,\n  active,\n  label,\n  ...props\n}: {\n  isInteractive?: boolean;\n  active?: boolean;\n  label?: {\n    title: string;\n    amount: string;\n    currencyCode: string;\n    position?: \"bottom\" | \"center\";\n  };\n} & React.ComponentProps<typeof Image>) {\n  return (\n    <div\n      className={clsx(\n        \"group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-blue-600 dark:bg-black\",\n        {\n          relative: label,\n          \"border-2 border-blue-600\": active,\n          \"border-neutral-200 dark:border-neutral-800\": !active,\n        },\n      )}\n    >\n      {props.src ? (\n        <Image\n          className={clsx(\"relative h-full w-full object-contain\", {\n            \"transition duration-300 ease-in-out group-hover:scale-105\":\n              isInteractive,\n          })}\n          {...props}\n        />\n      ) : null}\n      {label ? (\n        <Label\n          title={label.title}\n          amount={label.amount}\n          currencyCode={label.currencyCode}\n          position={label.position}\n        />\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/icons/logo.tsx",
    "content": "import clsx from \"clsx\";\n\nexport default function LogoIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      aria-label={`${process.env.SITE_NAME} logo`}\n      viewBox=\"0 0 32 28\"\n      {...props}\n      className={clsx(\"h-4 w-4 fill-black dark:fill-white\", props.className)}\n    >\n      <path d=\"M21.5758 9.75769L16 0L0 28H11.6255L21.5758 9.75769Z\" />\n      <path d=\"M26.2381 17.9167L20.7382 28H32L26.2381 17.9167Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "components/label.tsx",
    "content": "import clsx from \"clsx\";\nimport Price from \"./price\";\n\nconst Label = ({\n  title,\n  amount,\n  currencyCode,\n  position = \"bottom\",\n}: {\n  title: string;\n  amount: string;\n  currencyCode: string;\n  position?: \"bottom\" | \"center\";\n}) => {\n  return (\n    <div\n      className={clsx(\n        \"absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label\",\n        {\n          \"lg:px-20 lg:pb-[35%]\": position === \"center\",\n        },\n      )}\n    >\n      <div className=\"flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white\">\n        <h3 className=\"mr-4 line-clamp-2 grow pl-2 leading-none tracking-tight\">\n          {title}\n        </h3>\n        <Price\n          className=\"flex-none rounded-full bg-blue-600 p-2 text-white\"\n          amount={amount}\n          currencyCode={currencyCode}\n          currencyCodeClassName=\"hidden @[275px]/label:inline\"\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default Label;\n"
  },
  {
    "path": "components/layout/footer-menu.tsx",
    "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport { Menu } from \"lib/shopify/types\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { useEffect, useState } from \"react\";\n\nexport function FooterMenuItem({ item }: { item: Menu }) {\n  const pathname = usePathname();\n  const [active, setActive] = useState(pathname === item.path);\n\n  useEffect(() => {\n    setActive(pathname === item.path);\n  }, [pathname, item.path]);\n\n  return (\n    <li>\n      <Link\n        href={item.path}\n        className={clsx(\n          \"block p-2 text-lg underline-offset-4 hover:text-black hover:underline md:inline-block md:text-sm dark:hover:text-neutral-300\",\n          {\n            \"text-black dark:text-neutral-300\": active,\n          },\n        )}\n      >\n        {item.title}\n      </Link>\n    </li>\n  );\n}\n\nexport default function FooterMenu({ menu }: { menu: Menu[] }) {\n  if (!menu.length) return null;\n\n  return (\n    <nav>\n      <ul>\n        {menu.map((item: Menu) => {\n          return <FooterMenuItem key={item.title} item={item} />;\n        })}\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "components/layout/footer.tsx",
    "content": "import Link from \"next/link\";\n\nimport FooterMenu from \"components/layout/footer-menu\";\nimport LogoSquare from \"components/logo-square\";\nimport { getMenu } from \"lib/shopify\";\nimport { Suspense } from \"react\";\n\nconst { COMPANY_NAME, SITE_NAME } = process.env;\n\nexport default async function Footer() {\n  const currentYear = new Date().getFullYear();\n  const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : \"\");\n  const skeleton =\n    \"w-full h-6 animate-pulse rounded-sm bg-neutral-200 dark:bg-neutral-700\";\n  const menu = await getMenu(\"next-js-frontend-footer-menu\");\n  const copyrightName = COMPANY_NAME || SITE_NAME || \"\";\n\n  return (\n    <footer className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 border-t border-neutral-200 px-6 py-12 text-sm md:flex-row md:gap-12 md:px-4 min-[1320px]:px-0 dark:border-neutral-700\">\n        <div>\n          <Link\n            className=\"flex items-center gap-2 text-black md:pt-1 dark:text-white\"\n            href=\"/\"\n          >\n            <LogoSquare size=\"sm\" />\n            <span className=\"uppercase\">{SITE_NAME}</span>\n          </Link>\n        </div>\n        <Suspense\n          fallback={\n            <div className=\"flex h-[188px] w-[200px] flex-col gap-2\">\n              <div className={skeleton} />\n              <div className={skeleton} />\n              <div className={skeleton} />\n              <div className={skeleton} />\n              <div className={skeleton} />\n              <div className={skeleton} />\n            </div>\n          }\n        >\n          <FooterMenu menu={menu} />\n        </Suspense>\n        <div className=\"md:ml-auto\">\n          <a\n            className=\"flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white\"\n            aria-label=\"Deploy on Vercel\"\n            href=\"https://vercel.com/templates/next.js/nextjs-commerce\"\n          >\n            <span className=\"px-3\">▲</span>\n            <hr className=\"h-full border-r border-neutral-200 dark:border-neutral-700\" />\n            <span className=\"px-3\">Deploy</span>\n          </a>\n        </div>\n      </div>\n      <div className=\"border-t border-neutral-200 py-6 text-sm dark:border-neutral-700\">\n        <div className=\"mx-auto flex w-full max-w-7xl flex-col items-center gap-1 px-4 md:flex-row md:gap-0 md:px-4 min-[1320px]:px-0\">\n          <p>\n            &copy; {copyrightDate} {copyrightName}\n            {copyrightName.length && !copyrightName.endsWith(\".\")\n              ? \".\"\n              : \"\"}{\" \"}\n            All rights reserved.\n          </p>\n          <hr className=\"mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block\" />\n          <p>\n            <a href=\"https://github.com/vercel/commerce\">View the source</a>\n          </p>\n          <p className=\"md:ml-auto\">\n            <a href=\"https://vercel.com\" className=\"text-black dark:text-white\">\n              Created by ▲ Vercel\n            </a>\n          </p>\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "components/layout/navbar/index.tsx",
    "content": "import CartModal from \"components/cart/modal\";\nimport LogoSquare from \"components/logo-square\";\nimport { getMenu } from \"lib/shopify\";\nimport { Menu } from \"lib/shopify/types\";\nimport Link from \"next/link\";\nimport { Suspense } from \"react\";\nimport MobileMenu from \"./mobile-menu\";\nimport Search, { SearchSkeleton } from \"./search\";\n\nconst { SITE_NAME } = process.env;\n\nexport async function Navbar() {\n  const menu = await getMenu(\"next-js-frontend-header-menu\");\n\n  return (\n    <nav className=\"relative flex items-center justify-between p-4 lg:px-6\">\n      <div className=\"block flex-none md:hidden\">\n        <Suspense fallback={null}>\n          <MobileMenu menu={menu} />\n        </Suspense>\n      </div>\n      <div className=\"flex w-full items-center\">\n        <div className=\"flex w-full md:w-1/3\">\n          <Link\n            href=\"/\"\n            prefetch={true}\n            className=\"mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6\"\n          >\n            <LogoSquare />\n            <div className=\"ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block\">\n              {SITE_NAME}\n            </div>\n          </Link>\n          {menu.length ? (\n            <ul className=\"hidden gap-6 text-sm md:flex md:items-center\">\n              {menu.map((item: Menu) => (\n                <li key={item.title}>\n                  <Link\n                    href={item.path}\n                    prefetch={true}\n                    className=\"text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300\"\n                  >\n                    {item.title}\n                  </Link>\n                </li>\n              ))}\n            </ul>\n          ) : null}\n        </div>\n        <div className=\"hidden justify-center md:flex md:w-1/3\">\n          <Suspense fallback={<SearchSkeleton />}>\n            <Search />\n          </Suspense>\n        </div>\n        <div className=\"flex justify-end md:w-1/3\">\n          <CartModal />\n        </div>\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "components/layout/navbar/mobile-menu.tsx",
    "content": "\"use client\";\n\nimport { Dialog, Transition } from \"@headlessui/react\";\nimport Link from \"next/link\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport { Fragment, Suspense, useEffect, useState } from \"react\";\n\nimport { Bars3Icon, XMarkIcon } from \"@heroicons/react/24/outline\";\nimport { Menu } from \"lib/shopify/types\";\nimport Search, { SearchSkeleton } from \"./search\";\n\nexport default function MobileMenu({ menu }: { menu: Menu[] }) {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const [isOpen, setIsOpen] = useState(false);\n  const openMobileMenu = () => setIsOpen(true);\n  const closeMobileMenu = () => setIsOpen(false);\n\n  useEffect(() => {\n    const handleResize = () => {\n      if (window.innerWidth > 768) {\n        setIsOpen(false);\n      }\n    };\n    window.addEventListener(\"resize\", handleResize);\n    return () => window.removeEventListener(\"resize\", handleResize);\n  }, [isOpen]);\n\n  useEffect(() => {\n    setIsOpen(false);\n  }, [pathname, searchParams]);\n\n  return (\n    <>\n      <button\n        onClick={openMobileMenu}\n        aria-label=\"Open mobile menu\"\n        className=\"flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors md:hidden dark:border-neutral-700 dark:text-white\"\n      >\n        <Bars3Icon className=\"h-4\" />\n      </button>\n      <Transition show={isOpen}>\n        <Dialog onClose={closeMobileMenu} className=\"relative z-50\">\n          <Transition.Child\n            as={Fragment}\n            enter=\"transition-all ease-in-out duration-300\"\n            enterFrom=\"opacity-0 backdrop-blur-none\"\n            enterTo=\"opacity-100 backdrop-blur-[.5px]\"\n            leave=\"transition-all ease-in-out duration-200\"\n            leaveFrom=\"opacity-100 backdrop-blur-[.5px]\"\n            leaveTo=\"opacity-0 backdrop-blur-none\"\n          >\n            <div className=\"fixed inset-0 bg-black/30\" aria-hidden=\"true\" />\n          </Transition.Child>\n          <Transition.Child\n            as={Fragment}\n            enter=\"transition-all ease-in-out duration-300\"\n            enterFrom=\"translate-x-[-100%]\"\n            enterTo=\"translate-x-0\"\n            leave=\"transition-all ease-in-out duration-200\"\n            leaveFrom=\"translate-x-0\"\n            leaveTo=\"translate-x-[-100%]\"\n          >\n            <Dialog.Panel className=\"fixed bottom-0 left-0 right-0 top-0 flex h-full w-full flex-col bg-white pb-6 dark:bg-black\">\n              <div className=\"p-4\">\n                <button\n                  className=\"mb-4 flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white\"\n                  onClick={closeMobileMenu}\n                  aria-label=\"Close mobile menu\"\n                >\n                  <XMarkIcon className=\"h-6\" />\n                </button>\n\n                <div className=\"mb-4 w-full\">\n                  <Suspense fallback={<SearchSkeleton />}>\n                    <Search />\n                  </Suspense>\n                </div>\n                {menu.length ? (\n                  <ul className=\"flex w-full flex-col\">\n                    {menu.map((item: Menu) => (\n                      <li\n                        className=\"py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white\"\n                        key={item.title}\n                      >\n                        <Link\n                          href={item.path}\n                          prefetch={true}\n                          onClick={closeMobileMenu}\n                        >\n                          {item.title}\n                        </Link>\n                      </li>\n                    ))}\n                  </ul>\n                ) : null}\n              </div>\n            </Dialog.Panel>\n          </Transition.Child>\n        </Dialog>\n      </Transition>\n    </>\n  );\n}\n"
  },
  {
    "path": "components/layout/navbar/search.tsx",
    "content": "\"use client\";\n\nimport { MagnifyingGlassIcon } from \"@heroicons/react/24/outline\";\nimport Form from \"next/form\";\nimport { useSearchParams } from \"next/navigation\";\n\nexport default function Search() {\n  const searchParams = useSearchParams();\n\n  return (\n    <Form\n      action=\"/search\"\n      className=\"w-max-[550px] relative w-full lg:w-80 xl:w-full\"\n    >\n      <input\n        key={searchParams?.get(\"q\")}\n        type=\"text\"\n        name=\"q\"\n        placeholder=\"Search for products...\"\n        autoComplete=\"off\"\n        defaultValue={searchParams?.get(\"q\") || \"\"}\n        className=\"text-md w-full rounded-lg border bg-white px-4 py-2 text-black placeholder:text-neutral-500 md:text-sm dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400\"\n      />\n      <div className=\"absolute right-0 top-0 mr-3 flex h-full items-center\">\n        <MagnifyingGlassIcon className=\"h-4\" />\n      </div>\n    </Form>\n  );\n}\n\nexport function SearchSkeleton() {\n  return (\n    <form className=\"w-max-[550px] relative w-full lg:w-80 xl:w-full\">\n      <input\n        placeholder=\"Search for products...\"\n        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\"\n      />\n      <div className=\"absolute right-0 top-0 mr-3 flex h-full items-center\">\n        <MagnifyingGlassIcon className=\"h-4\" />\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/layout/product-grid-items.tsx",
    "content": "import Grid from \"components/grid\";\nimport { GridTileImage } from \"components/grid/tile\";\nimport { Product } from \"lib/shopify/types\";\nimport Link from \"next/link\";\n\nexport default function ProductGridItems({\n  products,\n}: {\n  products: Product[];\n}) {\n  return (\n    <>\n      {products.map((product) => (\n        <Grid.Item key={product.handle} className=\"animate-fadeIn\">\n          <Link\n            className=\"relative inline-block h-full w-full\"\n            href={`/product/${product.handle}`}\n            prefetch={true}\n          >\n            <GridTileImage\n              alt={product.title}\n              label={{\n                title: product.title,\n                amount: product.priceRange.maxVariantPrice.amount,\n                currencyCode: product.priceRange.maxVariantPrice.currencyCode,\n              }}\n              src={product.featuredImage?.url}\n              fill\n              sizes=\"(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw\"\n            />\n          </Link>\n        </Grid.Item>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "components/layout/search/collections.tsx",
    "content": "import clsx from \"clsx\";\nimport { Suspense } from \"react\";\n\nimport { getCollections } from \"lib/shopify\";\nimport FilterList from \"./filter\";\n\nasync function CollectionList() {\n  const collections = await getCollections();\n  return <FilterList list={collections} title=\"Collections\" />;\n}\n\nconst skeleton = \"mb-3 h-4 w-5/6 animate-pulse rounded-sm\";\nconst activeAndTitles = \"bg-neutral-800 dark:bg-neutral-300\";\nconst items = \"bg-neutral-400 dark:bg-neutral-700\";\n\nexport default function Collections() {\n  return (\n    <Suspense\n      fallback={\n        <div className=\"col-span-2 hidden h-[400px] w-full flex-none py-4 lg:block\">\n          <div className={clsx(skeleton, activeAndTitles)} />\n          <div className={clsx(skeleton, activeAndTitles)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n          <div className={clsx(skeleton, items)} />\n        </div>\n      }\n    >\n      <CollectionList />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "components/layout/search/filter/dropdown.tsx",
    "content": "\"use client\";\n\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { ChevronDownIcon } from \"@heroicons/react/24/outline\";\nimport type { ListItem } from \".\";\nimport { FilterItem } from \"./item\";\n\nexport default function FilterItemDropdown({ list }: { list: ListItem[] }) {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const [active, setActive] = useState(\"\");\n  const [openSelect, setOpenSelect] = useState(false);\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (ref.current && !ref.current.contains(event.target as Node)) {\n        setOpenSelect(false);\n      }\n    };\n\n    window.addEventListener(\"click\", handleClickOutside);\n    return () => window.removeEventListener(\"click\", handleClickOutside);\n  }, []);\n\n  useEffect(() => {\n    list.forEach((listItem: ListItem) => {\n      if (\n        (\"path\" in listItem && pathname === listItem.path) ||\n        (\"slug\" in listItem && searchParams.get(\"sort\") === listItem.slug)\n      ) {\n        setActive(listItem.title);\n      }\n    });\n  }, [pathname, list, searchParams]);\n\n  return (\n    <div className=\"relative\" ref={ref}>\n      <div\n        onClick={() => {\n          setOpenSelect(!openSelect);\n        }}\n        className=\"flex w-full items-center justify-between rounded-sm border border-black/30 px-4 py-2 text-sm dark:border-white/30\"\n      >\n        <div>{active}</div>\n        <ChevronDownIcon className=\"h-4\" />\n      </div>\n      {openSelect && (\n        <div\n          onClick={() => {\n            setOpenSelect(false);\n          }}\n          className=\"absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black\"\n        >\n          {list.map((item: ListItem, i) => (\n            <FilterItem key={i} item={item} />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/layout/search/filter/index.tsx",
    "content": "import { SortFilterItem } from \"lib/constants\";\nimport { Suspense } from \"react\";\nimport FilterItemDropdown from \"./dropdown\";\nimport { FilterItem } from \"./item\";\n\nexport type ListItem = SortFilterItem | PathFilterItem;\nexport type PathFilterItem = { title: string; path: string };\n\nfunction FilterItemList({ list }: { list: ListItem[] }) {\n  return (\n    <>\n      {list.map((item: ListItem, i) => (\n        <FilterItem key={i} item={item} />\n      ))}\n    </>\n  );\n}\n\nexport default function FilterList({\n  list,\n  title,\n}: {\n  list: ListItem[];\n  title?: string;\n}) {\n  return (\n    <>\n      <nav>\n        {title ? (\n          <h3 className=\"hidden text-xs text-neutral-500 md:block dark:text-neutral-400\">\n            {title}\n          </h3>\n        ) : null}\n        <ul className=\"hidden md:block\">\n          <Suspense fallback={null}>\n            <FilterItemList list={list} />\n          </Suspense>\n        </ul>\n        <ul className=\"md:hidden\">\n          <Suspense fallback={null}>\n            <FilterItemDropdown list={list} />\n          </Suspense>\n        </ul>\n      </nav>\n    </>\n  );\n}\n"
  },
  {
    "path": "components/layout/search/filter/item.tsx",
    "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport type { SortFilterItem } from \"lib/constants\";\nimport { createUrl } from \"lib/utils\";\nimport Link from \"next/link\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport type { ListItem, PathFilterItem } from \".\";\n\nfunction PathFilterItem({ item }: { item: PathFilterItem }) {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const active = pathname === item.path;\n  const newParams = new URLSearchParams(searchParams.toString());\n  const DynamicTag = active ? \"p\" : Link;\n\n  newParams.delete(\"q\");\n\n  return (\n    <li className=\"mt-2 flex text-black dark:text-white\" key={item.title}>\n      <DynamicTag\n        href={createUrl(item.path, newParams)}\n        className={clsx(\n          \"w-full text-sm underline-offset-4 hover:underline dark:hover:text-neutral-100\",\n          {\n            \"underline underline-offset-4\": active,\n          },\n        )}\n      >\n        {item.title}\n      </DynamicTag>\n    </li>\n  );\n}\n\nfunction SortFilterItem({ item }: { item: SortFilterItem }) {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const active = searchParams.get(\"sort\") === item.slug;\n  const q = searchParams.get(\"q\");\n  const href = createUrl(\n    pathname,\n    new URLSearchParams({\n      ...(q && { q }),\n      ...(item.slug && item.slug.length && { sort: item.slug }),\n    }),\n  );\n  const DynamicTag = active ? \"p\" : Link;\n\n  return (\n    <li\n      className=\"mt-2 flex text-sm text-black dark:text-white\"\n      key={item.title}\n    >\n      <DynamicTag\n        prefetch={!active ? false : undefined}\n        href={href}\n        className={clsx(\"w-full hover:underline hover:underline-offset-4\", {\n          \"underline underline-offset-4\": active,\n        })}\n      >\n        {item.title}\n      </DynamicTag>\n    </li>\n  );\n}\n\nexport function FilterItem({ item }: { item: ListItem }) {\n  return \"path\" in item ? (\n    <PathFilterItem item={item} />\n  ) : (\n    <SortFilterItem item={item} />\n  );\n}\n"
  },
  {
    "path": "components/loading-dots.tsx",
    "content": "import clsx from \"clsx\";\n\nconst dots = \"mx-[1px] inline-block h-1 w-1 animate-blink rounded-md\";\n\nconst LoadingDots = ({ className }: { className: string }) => {\n  return (\n    <span className=\"mx-2 inline-flex items-center\">\n      <span className={clsx(dots, className)} />\n      <span className={clsx(dots, \"animation-delay-[200ms]\", className)} />\n      <span className={clsx(dots, \"animation-delay-[400ms]\", className)} />\n    </span>\n  );\n};\n\nexport default LoadingDots;\n"
  },
  {
    "path": "components/logo-square.tsx",
    "content": "import clsx from \"clsx\";\nimport LogoIcon from \"./icons/logo\";\n\nexport default function LogoSquare({ size }: { size?: \"sm\" | undefined }) {\n  return (\n    <div\n      className={clsx(\n        \"flex flex-none items-center justify-center border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-black\",\n        {\n          \"h-[40px] w-[40px] rounded-xl\": !size,\n          \"h-[30px] w-[30px] rounded-lg\": size === \"sm\",\n        },\n      )}\n    >\n      <LogoIcon\n        className={clsx({\n          \"h-[16px] w-[16px]\": !size,\n          \"h-[10px] w-[10px]\": size === \"sm\",\n        })}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/opengraph-image.tsx",
    "content": "import { ImageResponse } from \"next/og\";\nimport LogoIcon from \"./icons/logo\";\nimport { join } from \"path\";\nimport { readFile } from \"fs/promises\";\n\nexport type Props = {\n  title?: string;\n};\n\nexport default async function OpengraphImage(\n  props?: Props,\n): Promise<ImageResponse> {\n  const { title } = {\n    ...{\n      title: process.env.SITE_NAME,\n    },\n    ...props,\n  };\n\n  const file = await readFile(join(process.cwd(), \"./fonts/Inter-Bold.ttf\"));\n  const font = Uint8Array.from(file).buffer;\n\n  return new ImageResponse(\n    (\n      <div tw=\"flex h-full w-full flex-col items-center justify-center bg-black\">\n        <div tw=\"flex flex-none items-center justify-center border border-neutral-700 h-[160px] w-[160px] rounded-3xl\">\n          <LogoIcon width=\"64\" height=\"58\" fill=\"white\" />\n        </div>\n        <p tw=\"mt-12 text-6xl font-bold text-white\">{title}</p>\n      </div>\n    ),\n    {\n      width: 1200,\n      height: 630,\n      fonts: [\n        {\n          name: \"Inter\",\n          data: font,\n          style: \"normal\",\n          weight: 700,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "components/price.tsx",
    "content": "import clsx from \"clsx\";\n\nconst Price = ({\n  amount,\n  className,\n  currencyCode = \"USD\",\n  currencyCodeClassName,\n}: {\n  amount: string;\n  className?: string;\n  currencyCode: string;\n  currencyCodeClassName?: string;\n} & React.ComponentProps<\"p\">) => (\n  <p suppressHydrationWarning={true} className={className}>\n    {`${new Intl.NumberFormat(undefined, {\n      style: \"currency\",\n      currency: currencyCode,\n      currencyDisplay: \"narrowSymbol\",\n    }).format(parseFloat(amount))}`}\n    <span\n      className={clsx(\"ml-1 inline\", currencyCodeClassName)}\n    >{`${currencyCode}`}</span>\n  </p>\n);\n\nexport default Price;\n"
  },
  {
    "path": "components/product/gallery.tsx",
    "content": "\"use client\";\n\nimport { ArrowLeftIcon, ArrowRightIcon } from \"@heroicons/react/24/outline\";\nimport { GridTileImage } from \"components/grid/tile\";\nimport Image from \"next/image\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nexport function Gallery({\n  images,\n}: {\n  images: { src: string; altText: string }[];\n}) {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const imageIndex = searchParams.has(\"image\")\n    ? parseInt(searchParams.get(\"image\")!)\n    : 0;\n\n  const updateImage = (index: string) => {\n    const params = new URLSearchParams(searchParams.toString());\n    params.set(\"image\", index);\n    router.replace(`?${params.toString()}`, { scroll: false });\n  };\n\n  const nextImageIndex = imageIndex + 1 < images.length ? imageIndex + 1 : 0;\n  const previousImageIndex =\n    imageIndex === 0 ? images.length - 1 : imageIndex - 1;\n\n  const buttonClassName =\n    \"h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white flex items-center justify-center\";\n\n  return (\n    <form>\n      <div className=\"relative aspect-square h-full max-h-[550px] w-full overflow-hidden\">\n        {images[imageIndex] && (\n          <Image\n            className=\"h-full w-full object-contain\"\n            fill\n            sizes=\"(min-width: 1024px) 66vw, 100vw\"\n            alt={images[imageIndex]?.altText as string}\n            src={images[imageIndex]?.src as string}\n            priority={true}\n          />\n        )}\n\n        {images.length > 1 ? (\n          <div className=\"absolute bottom-[15%] flex w-full justify-center\">\n            <div className=\"mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur-sm dark:border-black dark:bg-neutral-900/80\">\n              <button\n                formAction={() => updateImage(previousImageIndex.toString())}\n                aria-label=\"Previous product image\"\n                className={buttonClassName}\n              >\n                <ArrowLeftIcon className=\"h-5\" />\n              </button>\n              <div className=\"mx-1 h-6 w-px bg-neutral-500\"></div>\n              <button\n                formAction={() => updateImage(nextImageIndex.toString())}\n                aria-label=\"Next product image\"\n                className={buttonClassName}\n              >\n                <ArrowRightIcon className=\"h-5\" />\n              </button>\n            </div>\n          </div>\n        ) : null}\n      </div>\n\n      {images.length > 1 ? (\n        <ul className=\"my-12 flex items-center flex-wrap justify-center gap-2 overflow-auto py-1 lg:mb-0\">\n          {images.map((image, index) => {\n            const isActive = index === imageIndex;\n\n            return (\n              <li key={image.src} className=\"h-20 w-20\">\n                <button\n                  formAction={() => updateImage(index.toString())}\n                  aria-label=\"Select product image\"\n                  className=\"h-full w-full\"\n                >\n                  <GridTileImage\n                    alt={image.altText}\n                    src={image.src}\n                    width={80}\n                    height={80}\n                    active={isActive}\n                  />\n                </button>\n              </li>\n            );\n          })}\n        </ul>\n      ) : null}\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/product/product-description.tsx",
    "content": "import { AddToCart } from \"components/cart/add-to-cart\";\nimport Price from \"components/price\";\nimport Prose from \"components/prose\";\nimport { Product } from \"lib/shopify/types\";\nimport { VariantSelector } from \"./variant-selector\";\n\nexport function ProductDescription({ product }: { product: Product }) {\n  return (\n    <>\n      <div className=\"mb-6 flex flex-col border-b pb-6 dark:border-neutral-700\">\n        <h1 className=\"mb-2 text-5xl font-medium\">{product.title}</h1>\n        <div className=\"mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white\">\n          <Price\n            amount={product.priceRange.maxVariantPrice.amount}\n            currencyCode={product.priceRange.maxVariantPrice.currencyCode}\n          />\n        </div>\n      </div>\n      <VariantSelector options={product.options} variants={product.variants} />\n      {product.descriptionHtml ? (\n        <Prose\n          className=\"mb-6 text-sm leading-tight dark:text-white/[60%]\"\n          html={product.descriptionHtml}\n        />\n      ) : null}\n      <AddToCart product={product} />\n    </>\n  );\n}\n"
  },
  {
    "path": "components/product/variant-selector.tsx",
    "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport { ProductOption, ProductVariant } from \"lib/shopify/types\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\ntype Combination = {\n  id: string;\n  availableForSale: boolean;\n  [key: string]: string | boolean;\n};\n\nexport function VariantSelector({\n  options,\n  variants,\n}: {\n  options: ProductOption[];\n  variants: ProductVariant[];\n}) {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const hasNoOptionsOrJustOneOption =\n    !options.length ||\n    (options.length === 1 && options[0]?.values.length === 1);\n\n  if (hasNoOptionsOrJustOneOption) {\n    return null;\n  }\n\n  const combinations: Combination[] = variants.map((variant) => ({\n    id: variant.id,\n    availableForSale: variant.availableForSale,\n    ...variant.selectedOptions.reduce(\n      (accumulator, option) => ({\n        ...accumulator,\n        [option.name.toLowerCase()]: option.value,\n      }),\n      {},\n    ),\n  }));\n\n  const updateOption = (name: string, value: string) => {\n    const params = new URLSearchParams(searchParams.toString());\n    params.set(name, value);\n    router.replace(`?${params.toString()}`, { scroll: false });\n  };\n\n  return options.map((option) => (\n    <form key={option.id}>\n      <dl className=\"mb-8\">\n        <dt className=\"mb-4 text-sm uppercase tracking-wide\">{option.name}</dt>\n        <dd className=\"flex flex-wrap gap-3\">\n          {option.values.map((value) => {\n            const optionNameLowerCase = option.name.toLowerCase();\n\n            // Base option params on current searchParams so we can preserve any other param state.\n            const optionParams: Record<string, string> = {};\n            searchParams.forEach((v, k) => (optionParams[k] = v));\n            optionParams[optionNameLowerCase] = value;\n\n            // Filter out invalid options and check if the option combination is available for sale.\n            const filtered = Object.entries(optionParams).filter(\n              ([key, value]) =>\n                options.find(\n                  (option) =>\n                    option.name.toLowerCase() === key &&\n                    option.values.includes(value),\n                ),\n            );\n            const isAvailableForSale = combinations.find((combination) =>\n              filtered.every(\n                ([key, value]) =>\n                  combination[key] === value && combination.availableForSale,\n              ),\n            );\n\n            // The option is active if it's in the selected options.\n            const isActive = searchParams.get(optionNameLowerCase) === value;\n\n            return (\n              <button\n                formAction={() => updateOption(optionNameLowerCase, value)}\n                key={value}\n                aria-disabled={!isAvailableForSale}\n                disabled={!isAvailableForSale}\n                title={`${option.name} ${value}${!isAvailableForSale ? \" (Out of Stock)\" : \"\"}`}\n                className={clsx(\n                  \"flex min-w-[48px] items-center justify-center rounded-full border bg-neutral-100 px-2 py-1 text-sm dark:border-neutral-800 dark:bg-neutral-900\",\n                  {\n                    \"cursor-default ring-2 ring-blue-600\": isActive,\n                    \"ring-1 ring-transparent transition duration-300 ease-in-out hover:ring-blue-600\":\n                      !isActive && isAvailableForSale,\n                    \"relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 dark:before:bg-neutral-700\":\n                      !isAvailableForSale,\n                  },\n                )}\n              >\n                {value}\n              </button>\n            );\n          })}\n        </dd>\n      </dl>\n    </form>\n  ));\n}\n"
  },
  {
    "path": "components/prose.tsx",
    "content": "import clsx from \"clsx\";\n\nconst Prose = ({ html, className }: { html: string; className?: string }) => {\n  return (\n    <div\n      className={clsx(\n        \"prose mx-auto max-w-6xl text-base leading-7 text-black prose-headings:mt-8 prose-headings:font-semibold prose-headings:tracking-wide prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg prose-a:text-black prose-a:underline prose-a:hover:text-neutral-300 prose-strong:text-black prose-ol:mt-8 prose-ol:list-decimal prose-ol:pl-6 prose-ul:mt-8 prose-ul:list-disc prose-ul:pl-6 dark:text-white dark:prose-headings:text-white dark:prose-a:text-white dark:prose-strong:text-white\",\n        className,\n      )}\n      dangerouslySetInnerHTML={{ __html: html }}\n    />\n  );\n};\n\nexport default Prose;\n"
  },
  {
    "path": "components/welcome-toast.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { toast } from \"sonner\";\n\nexport function WelcomeToast() {\n  useEffect(() => {\n    // ignore if screen height is too small\n    if (window.innerHeight < 650) return;\n    if (!document.cookie.includes(\"welcome-toast=2\")) {\n      toast(\"🛍️ Welcome to Next.js Commerce!\", {\n        id: \"welcome-toast\",\n        duration: Infinity,\n        onDismiss: () => {\n          document.cookie = \"welcome-toast=2; max-age=31536000; path=/\";\n        },\n        description: (\n          <>\n            This is a high-performance, SSR storefront powered by Shopify,\n            Next.js, and Vercel.{\" \"}\n            <a\n              href=\"https://vercel.com/templates/next.js/nextjs-commerce\"\n              className=\"text-blue-600 hover:underline\"\n              target=\"_blank\"\n            >\n              Deploy your own\n            </a>\n            .\n          </>\n        ),\n      });\n    }\n  }, []);\n\n  return null;\n}\n"
  },
  {
    "path": "lib/constants.ts",
    "content": "export type SortFilterItem = {\n  title: string;\n  slug: string | null;\n  sortKey: \"RELEVANCE\" | \"BEST_SELLING\" | \"CREATED_AT\" | \"PRICE\";\n  reverse: boolean;\n};\n\nexport const defaultSort: SortFilterItem = {\n  title: \"Relevance\",\n  slug: null,\n  sortKey: \"RELEVANCE\",\n  reverse: false,\n};\n\nexport const sorting: SortFilterItem[] = [\n  defaultSort,\n  {\n    title: \"Trending\",\n    slug: \"trending-desc\",\n    sortKey: \"BEST_SELLING\",\n    reverse: false,\n  }, // asc\n  {\n    title: \"Latest arrivals\",\n    slug: \"latest-desc\",\n    sortKey: \"CREATED_AT\",\n    reverse: true,\n  },\n  {\n    title: \"Price: Low to high\",\n    slug: \"price-asc\",\n    sortKey: \"PRICE\",\n    reverse: false,\n  }, // asc\n  {\n    title: \"Price: High to low\",\n    slug: \"price-desc\",\n    sortKey: \"PRICE\",\n    reverse: true,\n  },\n];\n\nexport const TAGS = {\n  collections: \"collections\",\n  products: \"products\",\n  cart: \"cart\",\n};\n\nexport const HIDDEN_PRODUCT_TAG = \"nextjs-frontend-hidden\";\nexport const DEFAULT_OPTION = \"Default Title\";\nexport const SHOPIFY_GRAPHQL_API_ENDPOINT = \"/api/2023-01/graphql.json\";\n"
  },
  {
    "path": "lib/shopify/fragments/cart.ts",
    "content": "import productFragment from \"./product\";\n\nconst cartFragment = /* GraphQL */ `\n  fragment cart on Cart {\n    id\n    checkoutUrl\n    cost {\n      subtotalAmount {\n        amount\n        currencyCode\n      }\n      totalAmount {\n        amount\n        currencyCode\n      }\n      totalTaxAmount {\n        amount\n        currencyCode\n      }\n    }\n    lines(first: 100) {\n      edges {\n        node {\n          id\n          quantity\n          cost {\n            totalAmount {\n              amount\n              currencyCode\n            }\n          }\n          merchandise {\n            ... on ProductVariant {\n              id\n              title\n              selectedOptions {\n                name\n                value\n              }\n              product {\n                ...product\n              }\n            }\n          }\n        }\n      }\n    }\n    totalQuantity\n  }\n  ${productFragment}\n`;\n\nexport default cartFragment;\n"
  },
  {
    "path": "lib/shopify/fragments/image.ts",
    "content": "const imageFragment = /* GraphQL */ `\n  fragment image on Image {\n    url\n    altText\n    width\n    height\n  }\n`;\n\nexport default imageFragment;\n"
  },
  {
    "path": "lib/shopify/fragments/product.ts",
    "content": "import imageFragment from \"./image\";\nimport seoFragment from \"./seo\";\n\nconst productFragment = /* GraphQL */ `\n  fragment product on Product {\n    id\n    handle\n    availableForSale\n    title\n    description\n    descriptionHtml\n    options {\n      id\n      name\n      values\n    }\n    priceRange {\n      maxVariantPrice {\n        amount\n        currencyCode\n      }\n      minVariantPrice {\n        amount\n        currencyCode\n      }\n    }\n    variants(first: 250) {\n      edges {\n        node {\n          id\n          title\n          availableForSale\n          selectedOptions {\n            name\n            value\n          }\n          price {\n            amount\n            currencyCode\n          }\n        }\n      }\n    }\n    featuredImage {\n      ...image\n    }\n    images(first: 20) {\n      edges {\n        node {\n          ...image\n        }\n      }\n    }\n    seo {\n      ...seo\n    }\n    tags\n    updatedAt\n  }\n  ${imageFragment}\n  ${seoFragment}\n`;\n\nexport default productFragment;\n"
  },
  {
    "path": "lib/shopify/fragments/seo.ts",
    "content": "const seoFragment = /* GraphQL */ `\n  fragment seo on SEO {\n    description\n    title\n  }\n`;\n\nexport default seoFragment;\n"
  },
  {
    "path": "lib/shopify/index.ts",
    "content": "import {\n  HIDDEN_PRODUCT_TAG,\n  SHOPIFY_GRAPHQL_API_ENDPOINT,\n  TAGS,\n} from \"lib/constants\";\nimport { isShopifyError } from \"lib/type-guards\";\nimport { ensureStartsWith } from \"lib/utils\";\nimport {\n  unstable_cacheLife as cacheLife,\n  unstable_cacheTag as cacheTag,\n  revalidateTag,\n} from \"next/cache\";\nimport { cookies, headers } from \"next/headers\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport {\n  addToCartMutation,\n  createCartMutation,\n  editCartItemsMutation,\n  removeFromCartMutation,\n} from \"./mutations/cart\";\nimport { getCartQuery } from \"./queries/cart\";\nimport {\n  getCollectionProductsQuery,\n  getCollectionQuery,\n  getCollectionsQuery,\n} from \"./queries/collection\";\nimport { getMenuQuery } from \"./queries/menu\";\nimport { getPageQuery, getPagesQuery } from \"./queries/page\";\nimport {\n  getProductQuery,\n  getProductRecommendationsQuery,\n  getProductsQuery,\n} from \"./queries/product\";\nimport {\n  Cart,\n  Collection,\n  Connection,\n  Image,\n  Menu,\n  Page,\n  Product,\n  ShopifyAddToCartOperation,\n  ShopifyCart,\n  ShopifyCartOperation,\n  ShopifyCollection,\n  ShopifyCollectionOperation,\n  ShopifyCollectionProductsOperation,\n  ShopifyCollectionsOperation,\n  ShopifyCreateCartOperation,\n  ShopifyMenuOperation,\n  ShopifyPageOperation,\n  ShopifyPagesOperation,\n  ShopifyProduct,\n  ShopifyProductOperation,\n  ShopifyProductRecommendationsOperation,\n  ShopifyProductsOperation,\n  ShopifyRemoveFromCartOperation,\n  ShopifyUpdateCartOperation,\n} from \"./types\";\n\nconst domain = process.env.SHOPIFY_STORE_DOMAIN\n  ? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, \"https://\")\n  : \"\";\nconst endpoint = domain ? `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}` : \"\";\nconst key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;\n\ntype ExtractVariables<T> = T extends { variables: object }\n  ? T[\"variables\"]\n  : never;\n\nexport async function shopifyFetch<T>({\n  headers,\n  query,\n  variables,\n}: {\n  headers?: HeadersInit;\n  query: string;\n  variables?: ExtractVariables<T>;\n}): Promise<{ status: number; body: T } | never> {\n  try {\n    if (!endpoint) {\n      throw new Error(\"SHOPIFY_STORE_DOMAIN environment variable is not set\");\n    }\n\n    const result = await fetch(endpoint, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Shopify-Storefront-Access-Token\": key,\n        ...headers,\n      },\n      body: JSON.stringify({\n        ...(query && { query }),\n        ...(variables && { variables }),\n      }),\n    });\n\n    const body = await result.json();\n\n    if (body.errors) {\n      throw body.errors[0];\n    }\n\n    return {\n      status: result.status,\n      body,\n    };\n  } catch (e) {\n    if (isShopifyError(e)) {\n      throw {\n        cause: e.cause?.toString() || \"unknown\",\n        status: e.status || 500,\n        message: e.message,\n        query,\n      };\n    }\n\n    throw {\n      error: e,\n      query,\n    };\n  }\n}\n\nconst removeEdgesAndNodes = <T>(array: Connection<T>): T[] => {\n  return array.edges.map((edge) => edge?.node);\n};\n\nconst reshapeCart = (cart: ShopifyCart): Cart => {\n  if (!cart.cost?.totalTaxAmount) {\n    cart.cost.totalTaxAmount = {\n      amount: \"0.0\",\n      currencyCode: cart.cost.totalAmount.currencyCode,\n    };\n  }\n\n  return {\n    ...cart,\n    lines: removeEdgesAndNodes(cart.lines),\n  };\n};\n\nconst reshapeCollection = (\n  collection: ShopifyCollection\n): Collection | undefined => {\n  if (!collection) {\n    return undefined;\n  }\n\n  return {\n    ...collection,\n    path: `/search/${collection.handle}`,\n  };\n};\n\nconst reshapeCollections = (collections: ShopifyCollection[]) => {\n  const reshapedCollections = [];\n\n  for (const collection of collections) {\n    if (collection) {\n      const reshapedCollection = reshapeCollection(collection);\n\n      if (reshapedCollection) {\n        reshapedCollections.push(reshapedCollection);\n      }\n    }\n  }\n\n  return reshapedCollections;\n};\n\nconst reshapeImages = (images: Connection<Image>, productTitle: string) => {\n  const flattened = removeEdgesAndNodes(images);\n\n  return flattened.map((image) => {\n    const filename = image.url.match(/.*\\/(.*)\\..*/)?.[1];\n    return {\n      ...image,\n      altText: image.altText || `${productTitle} - ${filename}`,\n    };\n  });\n};\n\nconst reshapeProduct = (\n  product: ShopifyProduct,\n  filterHiddenProducts: boolean = true\n) => {\n  if (\n    !product ||\n    (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))\n  ) {\n    return undefined;\n  }\n\n  const { images, variants, ...rest } = product;\n\n  return {\n    ...rest,\n    images: reshapeImages(images, product.title),\n    variants: removeEdgesAndNodes(variants),\n  };\n};\n\nconst reshapeProducts = (products: ShopifyProduct[]) => {\n  const reshapedProducts = [];\n\n  for (const product of products) {\n    if (product) {\n      const reshapedProduct = reshapeProduct(product);\n\n      if (reshapedProduct) {\n        reshapedProducts.push(reshapedProduct);\n      }\n    }\n  }\n\n  return reshapedProducts;\n};\n\nexport async function createCart(): Promise<Cart> {\n  const res = await shopifyFetch<ShopifyCreateCartOperation>({\n    query: createCartMutation,\n  });\n\n  return reshapeCart(res.body.data.cartCreate.cart);\n}\n\nexport async function addToCart(\n  lines: { merchandiseId: string; quantity: number }[]\n): Promise<Cart> {\n  const cartId = (await cookies()).get(\"cartId\")?.value!;\n  const res = await shopifyFetch<ShopifyAddToCartOperation>({\n    query: addToCartMutation,\n    variables: {\n      cartId,\n      lines,\n    },\n  });\n  return reshapeCart(res.body.data.cartLinesAdd.cart);\n}\n\nexport async function removeFromCart(lineIds: string[]): Promise<Cart> {\n  const cartId = (await cookies()).get(\"cartId\")?.value!;\n  const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({\n    query: removeFromCartMutation,\n    variables: {\n      cartId,\n      lineIds,\n    },\n  });\n\n  return reshapeCart(res.body.data.cartLinesRemove.cart);\n}\n\nexport async function updateCart(\n  lines: { id: string; merchandiseId: string; quantity: number }[]\n): Promise<Cart> {\n  const cartId = (await cookies()).get(\"cartId\")?.value!;\n  const res = await shopifyFetch<ShopifyUpdateCartOperation>({\n    query: editCartItemsMutation,\n    variables: {\n      cartId,\n      lines,\n    },\n  });\n\n  return reshapeCart(res.body.data.cartLinesUpdate.cart);\n}\n\nexport async function getCart(): Promise<Cart | undefined> {\n  \"use cache: private\";\n  cacheTag(TAGS.cart);\n  cacheLife(\"seconds\");\n\n  const cartId = (await cookies()).get(\"cartId\")?.value;\n\n  if (!cartId) {\n    return undefined;\n  }\n\n  const res = await shopifyFetch<ShopifyCartOperation>({\n    query: getCartQuery,\n    variables: { cartId },\n  });\n\n  // Old carts becomes `null` when you checkout.\n  if (!res.body.data.cart) {\n    return undefined;\n  }\n\n  return reshapeCart(res.body.data.cart);\n}\n\nexport async function getCollection(\n  handle: string\n): Promise<Collection | undefined> {\n  \"use cache\";\n  cacheTag(TAGS.collections);\n  cacheLife(\"days\");\n\n  const res = await shopifyFetch<ShopifyCollectionOperation>({\n    query: getCollectionQuery,\n    variables: {\n      handle,\n    },\n  });\n\n  return reshapeCollection(res.body.data.collection);\n}\n\nexport async function getCollectionProducts({\n  collection,\n  reverse,\n  sortKey,\n}: {\n  collection: string;\n  reverse?: boolean;\n  sortKey?: string;\n}): Promise<Product[]> {\n  \"use cache\";\n  cacheTag(TAGS.collections, TAGS.products);\n  cacheLife(\"days\");\n\n  if (!endpoint) {\n    console.log(\n      `Skipping getCollectionProducts for '${collection}' - Shopify not configured`\n    );\n    return [];\n  }\n\n  const res = await shopifyFetch<ShopifyCollectionProductsOperation>({\n    query: getCollectionProductsQuery,\n    variables: {\n      handle: collection,\n      reverse,\n      sortKey: sortKey === \"CREATED_AT\" ? \"CREATED\" : sortKey,\n    },\n  });\n\n  if (!res.body.data.collection) {\n    console.log(`No collection found for \\`${collection}\\``);\n    return [];\n  }\n\n  return reshapeProducts(\n    removeEdgesAndNodes(res.body.data.collection.products)\n  );\n}\n\nexport async function getCollections(): Promise<Collection[]> {\n  \"use cache\";\n  cacheTag(TAGS.collections);\n  cacheLife(\"days\");\n\n  if (!endpoint) {\n    console.log(\"Skipping getCollections - Shopify not configured\");\n    return [\n      {\n        handle: \"\",\n        title: \"All\",\n        description: \"All products\",\n        seo: {\n          title: \"All\",\n          description: \"All products\",\n        },\n        path: \"/search\",\n        updatedAt: new Date().toISOString(),\n      },\n    ];\n  }\n\n  const res = await shopifyFetch<ShopifyCollectionsOperation>({\n    query: getCollectionsQuery,\n  });\n  const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);\n  const collections = [\n    {\n      handle: \"\",\n      title: \"All\",\n      description: \"All products\",\n      seo: {\n        title: \"All\",\n        description: \"All products\",\n      },\n      path: \"/search\",\n      updatedAt: new Date().toISOString(),\n    },\n    // Filter out the `hidden` collections.\n    // Collections that start with `hidden-*` need to be hidden on the search page.\n    ...reshapeCollections(shopifyCollections).filter(\n      (collection) => !collection.handle.startsWith(\"hidden\")\n    ),\n  ];\n\n  return collections;\n}\n\nexport async function getMenu(handle: string): Promise<Menu[]> {\n  \"use cache\";\n  cacheTag(TAGS.collections);\n  cacheLife(\"days\");\n\n  if (!endpoint) {\n    console.log(`Skipping getMenu for '${handle}' - Shopify not configured`);\n    return [];\n  }\n\n  const res = await shopifyFetch<ShopifyMenuOperation>({\n    query: getMenuQuery,\n    variables: {\n      handle,\n    },\n  });\n\n  return (\n    res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({\n      title: item.title,\n      path: item.url\n        .replace(domain, \"\")\n        .replace(\"/collections\", \"/search\")\n        .replace(\"/pages\", \"\"),\n    })) || []\n  );\n}\n\nexport async function getPage(handle: string): Promise<Page> {\n  const res = await shopifyFetch<ShopifyPageOperation>({\n    query: getPageQuery,\n    variables: { handle },\n  });\n\n  return res.body.data.pageByHandle;\n}\n\nexport async function getPages(): Promise<Page[]> {\n  const res = await shopifyFetch<ShopifyPagesOperation>({\n    query: getPagesQuery,\n  });\n\n  return removeEdgesAndNodes(res.body.data.pages);\n}\n\nexport async function getProduct(handle: string): Promise<Product | undefined> {\n  \"use cache\";\n  cacheTag(TAGS.products);\n  cacheLife(\"days\");\n\n  if (!endpoint) {\n    console.log(`Skipping getProduct for '${handle}' - Shopify not configured`);\n    return undefined;\n  }\n\n  const res = await shopifyFetch<ShopifyProductOperation>({\n    query: getProductQuery,\n    variables: {\n      handle,\n    },\n  });\n\n  return reshapeProduct(res.body.data.product, false);\n}\n\nexport async function getProductRecommendations(\n  productId: string\n): Promise<Product[]> {\n  \"use cache\";\n  cacheTag(TAGS.products);\n  cacheLife(\"days\");\n\n  const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({\n    query: getProductRecommendationsQuery,\n    variables: {\n      productId,\n    },\n  });\n\n  return reshapeProducts(res.body.data.productRecommendations);\n}\n\nexport async function getProducts({\n  query,\n  reverse,\n  sortKey,\n}: {\n  query?: string;\n  reverse?: boolean;\n  sortKey?: string;\n}): Promise<Product[]> {\n  \"use cache\";\n  cacheTag(TAGS.products);\n  cacheLife(\"days\");\n\n  const res = await shopifyFetch<ShopifyProductsOperation>({\n    query: getProductsQuery,\n    variables: {\n      query,\n      reverse,\n      sortKey,\n    },\n  });\n\n  return reshapeProducts(removeEdgesAndNodes(res.body.data.products));\n}\n\n// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.\nexport async function revalidate(req: NextRequest): Promise<NextResponse> {\n  // We always need to respond with a 200 status code to Shopify,\n  // otherwise it will continue to retry the request.\n  const collectionWebhooks = [\n    \"collections/create\",\n    \"collections/delete\",\n    \"collections/update\",\n  ];\n  const productWebhooks = [\n    \"products/create\",\n    \"products/delete\",\n    \"products/update\",\n  ];\n  const topic = (await headers()).get(\"x-shopify-topic\") || \"unknown\";\n  const secret = req.nextUrl.searchParams.get(\"secret\");\n  const isCollectionUpdate = collectionWebhooks.includes(topic);\n  const isProductUpdate = productWebhooks.includes(topic);\n\n  if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {\n    console.error(\"Invalid revalidation secret.\");\n    return NextResponse.json({ status: 401 });\n  }\n\n  if (!isCollectionUpdate && !isProductUpdate) {\n    // We don't need to revalidate anything for any other topics.\n    return NextResponse.json({ status: 200 });\n  }\n\n  if (isCollectionUpdate) {\n    revalidateTag(TAGS.collections, \"seconds\");\n  }\n\n  if (isProductUpdate) {\n    revalidateTag(TAGS.products, \"seconds\");\n  }\n\n  return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });\n}\n"
  },
  {
    "path": "lib/shopify/mutations/cart.ts",
    "content": "import cartFragment from \"../fragments/cart\";\n\nexport const addToCartMutation = /* GraphQL */ `\n  mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) {\n    cartLinesAdd(cartId: $cartId, lines: $lines) {\n      cart {\n        ...cart\n      }\n    }\n  }\n  ${cartFragment}\n`;\n\nexport const createCartMutation = /* GraphQL */ `\n  mutation createCart($lineItems: [CartLineInput!]) {\n    cartCreate(input: { lines: $lineItems }) {\n      cart {\n        ...cart\n      }\n    }\n  }\n  ${cartFragment}\n`;\n\nexport const editCartItemsMutation = /* GraphQL */ `\n  mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) {\n    cartLinesUpdate(cartId: $cartId, lines: $lines) {\n      cart {\n        ...cart\n      }\n    }\n  }\n  ${cartFragment}\n`;\n\nexport const removeFromCartMutation = /* GraphQL */ `\n  mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) {\n    cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {\n      cart {\n        ...cart\n      }\n    }\n  }\n  ${cartFragment}\n`;\n"
  },
  {
    "path": "lib/shopify/queries/cart.ts",
    "content": "import cartFragment from \"../fragments/cart\";\n\nexport const getCartQuery = /* GraphQL */ `\n  query getCart($cartId: ID!) {\n    cart(id: $cartId) {\n      ...cart\n    }\n  }\n  ${cartFragment}\n`;\n"
  },
  {
    "path": "lib/shopify/queries/collection.ts",
    "content": "import productFragment from \"../fragments/product\";\nimport seoFragment from \"../fragments/seo\";\n\nconst collectionFragment = /* GraphQL */ `\n  fragment collection on Collection {\n    handle\n    title\n    description\n    seo {\n      ...seo\n    }\n    updatedAt\n  }\n  ${seoFragment}\n`;\n\nexport const getCollectionQuery = /* GraphQL */ `\n  query getCollection($handle: String!) {\n    collection(handle: $handle) {\n      ...collection\n    }\n  }\n  ${collectionFragment}\n`;\n\nexport const getCollectionsQuery = /* GraphQL */ `\n  query getCollections {\n    collections(first: 100, sortKey: TITLE) {\n      edges {\n        node {\n          ...collection\n        }\n      }\n    }\n  }\n  ${collectionFragment}\n`;\n\nexport const getCollectionProductsQuery = /* GraphQL */ `\n  query getCollectionProducts(\n    $handle: String!\n    $sortKey: ProductCollectionSortKeys\n    $reverse: Boolean\n  ) {\n    collection(handle: $handle) {\n      products(sortKey: $sortKey, reverse: $reverse, first: 100) {\n        edges {\n          node {\n            ...product\n          }\n        }\n      }\n    }\n  }\n  ${productFragment}\n`;\n"
  },
  {
    "path": "lib/shopify/queries/menu.ts",
    "content": "export const getMenuQuery = /* GraphQL */ `\n  query getMenu($handle: String!) {\n    menu(handle: $handle) {\n      items {\n        title\n        url\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "lib/shopify/queries/page.ts",
    "content": "import seoFragment from \"../fragments/seo\";\n\nconst pageFragment = /* GraphQL */ `\n  fragment page on Page {\n    ... on Page {\n      id\n      title\n      handle\n      body\n      bodySummary\n      seo {\n        ...seo\n      }\n      createdAt\n      updatedAt\n    }\n  }\n  ${seoFragment}\n`;\n\nexport const getPageQuery = /* GraphQL */ `\n  query getPage($handle: String!) {\n    pageByHandle(handle: $handle) {\n      ...page\n    }\n  }\n  ${pageFragment}\n`;\n\nexport const getPagesQuery = /* GraphQL */ `\n  query getPages {\n    pages(first: 100) {\n      edges {\n        node {\n          ...page\n        }\n      }\n    }\n  }\n  ${pageFragment}\n`;\n"
  },
  {
    "path": "lib/shopify/queries/product.ts",
    "content": "import productFragment from \"../fragments/product\";\n\nexport const getProductQuery = /* GraphQL */ `\n  query getProduct($handle: String!) {\n    product(handle: $handle) {\n      ...product\n    }\n  }\n  ${productFragment}\n`;\n\nexport const getProductsQuery = /* GraphQL */ `\n  query getProducts(\n    $sortKey: ProductSortKeys\n    $reverse: Boolean\n    $query: String\n  ) {\n    products(sortKey: $sortKey, reverse: $reverse, query: $query, first: 100) {\n      edges {\n        node {\n          ...product\n        }\n      }\n    }\n  }\n  ${productFragment}\n`;\n\nexport const getProductRecommendationsQuery = /* GraphQL */ `\n  query getProductRecommendations($productId: ID!) {\n    productRecommendations(productId: $productId) {\n      ...product\n    }\n  }\n  ${productFragment}\n`;\n"
  },
  {
    "path": "lib/shopify/types.ts",
    "content": "export type Maybe<T> = T | null;\n\nexport type Connection<T> = {\n  edges: Array<Edge<T>>;\n};\n\nexport type Edge<T> = {\n  node: T;\n};\n\nexport type Cart = Omit<ShopifyCart, \"lines\"> & {\n  lines: CartItem[];\n};\n\nexport type CartProduct = {\n  id: string;\n  handle: string;\n  title: string;\n  featuredImage: Image;\n};\n\nexport type CartItem = {\n  id: string | undefined;\n  quantity: number;\n  cost: {\n    totalAmount: Money;\n  };\n  merchandise: {\n    id: string;\n    title: string;\n    selectedOptions: {\n      name: string;\n      value: string;\n    }[];\n    product: CartProduct;\n  };\n};\n\nexport type Collection = ShopifyCollection & {\n  path: string;\n};\n\nexport type Image = {\n  url: string;\n  altText: string;\n  width: number;\n  height: number;\n};\n\nexport type Menu = {\n  title: string;\n  path: string;\n};\n\nexport type Money = {\n  amount: string;\n  currencyCode: string;\n};\n\nexport type Page = {\n  id: string;\n  title: string;\n  handle: string;\n  body: string;\n  bodySummary: string;\n  seo?: SEO;\n  createdAt: string;\n  updatedAt: string;\n};\n\nexport type Product = Omit<ShopifyProduct, \"variants\" | \"images\"> & {\n  variants: ProductVariant[];\n  images: Image[];\n};\n\nexport type ProductOption = {\n  id: string;\n  name: string;\n  values: string[];\n};\n\nexport type ProductVariant = {\n  id: string;\n  title: string;\n  availableForSale: boolean;\n  selectedOptions: {\n    name: string;\n    value: string;\n  }[];\n  price: Money;\n};\n\nexport type SEO = {\n  title: string;\n  description: string;\n};\n\nexport type ShopifyCart = {\n  id: string | undefined;\n  checkoutUrl: string;\n  cost: {\n    subtotalAmount: Money;\n    totalAmount: Money;\n    totalTaxAmount: Money;\n  };\n  lines: Connection<CartItem>;\n  totalQuantity: number;\n};\n\nexport type ShopifyCollection = {\n  handle: string;\n  title: string;\n  description: string;\n  seo: SEO;\n  updatedAt: string;\n};\n\nexport type ShopifyProduct = {\n  id: string;\n  handle: string;\n  availableForSale: boolean;\n  title: string;\n  description: string;\n  descriptionHtml: string;\n  options: ProductOption[];\n  priceRange: {\n    maxVariantPrice: Money;\n    minVariantPrice: Money;\n  };\n  variants: Connection<ProductVariant>;\n  featuredImage: Image;\n  images: Connection<Image>;\n  seo: SEO;\n  tags: string[];\n  updatedAt: string;\n};\n\nexport type ShopifyCartOperation = {\n  data: {\n    cart: ShopifyCart;\n  };\n  variables: {\n    cartId: string;\n  };\n};\n\nexport type ShopifyCreateCartOperation = {\n  data: { cartCreate: { cart: ShopifyCart } };\n};\n\nexport type ShopifyAddToCartOperation = {\n  data: {\n    cartLinesAdd: {\n      cart: ShopifyCart;\n    };\n  };\n  variables: {\n    cartId: string;\n    lines: {\n      merchandiseId: string;\n      quantity: number;\n    }[];\n  };\n};\n\nexport type ShopifyRemoveFromCartOperation = {\n  data: {\n    cartLinesRemove: {\n      cart: ShopifyCart;\n    };\n  };\n  variables: {\n    cartId: string;\n    lineIds: string[];\n  };\n};\n\nexport type ShopifyUpdateCartOperation = {\n  data: {\n    cartLinesUpdate: {\n      cart: ShopifyCart;\n    };\n  };\n  variables: {\n    cartId: string;\n    lines: {\n      id: string;\n      merchandiseId: string;\n      quantity: number;\n    }[];\n  };\n};\n\nexport type ShopifyCollectionOperation = {\n  data: {\n    collection: ShopifyCollection;\n  };\n  variables: {\n    handle: string;\n  };\n};\n\nexport type ShopifyCollectionProductsOperation = {\n  data: {\n    collection: {\n      products: Connection<ShopifyProduct>;\n    };\n  };\n  variables: {\n    handle: string;\n    reverse?: boolean;\n    sortKey?: string;\n  };\n};\n\nexport type ShopifyCollectionsOperation = {\n  data: {\n    collections: Connection<ShopifyCollection>;\n  };\n};\n\nexport type ShopifyMenuOperation = {\n  data: {\n    menu?: {\n      items: {\n        title: string;\n        url: string;\n      }[];\n    };\n  };\n  variables: {\n    handle: string;\n  };\n};\n\nexport type ShopifyPageOperation = {\n  data: { pageByHandle: Page };\n  variables: { handle: string };\n};\n\nexport type ShopifyPagesOperation = {\n  data: {\n    pages: Connection<Page>;\n  };\n};\n\nexport type ShopifyProductOperation = {\n  data: { product: ShopifyProduct };\n  variables: {\n    handle: string;\n  };\n};\n\nexport type ShopifyProductRecommendationsOperation = {\n  data: {\n    productRecommendations: ShopifyProduct[];\n  };\n  variables: {\n    productId: string;\n  };\n};\n\nexport type ShopifyProductsOperation = {\n  data: {\n    products: Connection<ShopifyProduct>;\n  };\n  variables: {\n    query?: string;\n    reverse?: boolean;\n    sortKey?: string;\n  };\n};\n"
  },
  {
    "path": "lib/type-guards.ts",
    "content": "export interface ShopifyErrorLike {\n  status: number;\n  message: Error;\n  cause?: Error;\n}\n\nexport const isObject = (\n  object: unknown,\n): object is Record<string, unknown> => {\n  return (\n    typeof object === \"object\" && object !== null && !Array.isArray(object)\n  );\n};\n\nexport const isShopifyError = (error: unknown): error is ShopifyErrorLike => {\n  if (!isObject(error)) return false;\n\n  if (error instanceof Error) return true;\n\n  return findError(error);\n};\n\nfunction findError<T extends object>(error: T): boolean {\n  if (Object.prototype.toString.call(error) === \"[object Error]\") {\n    return true;\n  }\n\n  const prototype = Object.getPrototypeOf(error) as T | null;\n\n  return prototype === null ? false : findError(prototype);\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { ReadonlyURLSearchParams } from \"next/navigation\";\n\nexport const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL\n  ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`\n  : \"http://localhost:3000\";\n\nexport const createUrl = (\n  pathname: string,\n  params: URLSearchParams | ReadonlyURLSearchParams,\n) => {\n  const paramsString = params.toString();\n  const queryString = `${paramsString.length ? \"?\" : \"\"}${paramsString}`;\n\n  return `${pathname}${queryString}`;\n};\n\nexport const ensureStartsWith = (stringToCheck: string, startsWith: string) =>\n  stringToCheck.startsWith(startsWith)\n    ? stringToCheck\n    : `${startsWith}${stringToCheck}`;\n\nexport const validateEnvironmentVariables = () => {\n  const requiredEnvironmentVariables = [\n    \"SHOPIFY_STORE_DOMAIN\",\n    \"SHOPIFY_STOREFRONT_ACCESS_TOKEN\",\n  ];\n  const missingEnvironmentVariables = [] as string[];\n\n  requiredEnvironmentVariables.forEach((envVar) => {\n    if (!process.env[envVar]) {\n      missingEnvironmentVariables.push(envVar);\n    }\n  });\n\n  if (missingEnvironmentVariables.length) {\n    throw new Error(\n      `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\",\n      )}\\n`,\n    );\n  }\n\n  if (\n    process.env.SHOPIFY_STORE_DOMAIN?.includes(\"[\") ||\n    process.env.SHOPIFY_STORE_DOMAIN?.includes(\"]\")\n  ) {\n    throw new Error(\n      \"Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.\",\n    );\n  }\n};\n"
  },
  {
    "path": "license.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2025 Vercel, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "next.config.ts",
    "content": "export default {\n  experimental: {\n    ppr: true,\n    inlineCss: true,\n    useCache: true,\n  },\n  images: {\n    formats: [\"image/avif\", \"image/webp\"],\n    remotePatterns: [\n      {\n        protocol: \"https\",\n        hostname: \"cdn.shopify.com\",\n        pathname: \"/s/files/**\",\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"prettier\": \"prettier --write --ignore-unknown .\",\n    \"prettier:check\": \"prettier --check --ignore-unknown .\",\n    \"test\": \"pnpm prettier:check\"\n  },\n  \"dependencies\": {\n    \"@headlessui/react\": \"^2.2.0\",\n    \"@heroicons/react\": \"^2.2.0\",\n    \"clsx\": \"^2.1.1\",\n    \"geist\": \"^1.3.1\",\n    \"next\": \"15.6.0-canary.60\",\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\",\n    \"sonner\": \"^2.0.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/container-queries\": \"^0.1.1\",\n    \"@tailwindcss/postcss\": \"^4.0.14\",\n    \"@tailwindcss/typography\": \"^0.5.16\",\n    \"@types/node\": \"22.13.10\",\n    \"@types/react\": \"19.0.12\",\n    \"@types/react-dom\": \"19.0.4\",\n    \"postcss\": \"^8.5.3\",\n    \"prettier\": \"3.5.3\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.11\",\n    \"tailwindcss\": \"^4.0.14\",\n    \"typescript\": \"5.8.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nexport default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"downlevelIteration\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"noUncheckedIndexedAccess\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]