From feaa87a9c843af42f45545e95bfc1ccca938f746 Mon Sep 17 00:00:00 2001 From: leonmargaritis <141131511+leonmargaritis@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:39:54 +0100 Subject: [PATCH] feat: init commercetools setup (#1) * feat: init commercetools setup --------- Co-authored-by: Anja-Janina Stiefermann --- .env.example | 7 + .eslintrc.js | 23 - .github/dependabot.yml | 6 +- .github/workflows/test.yml | 4 +- .prettierignore | 2 +- .prettierrc | 8 + .vscode/launch.json | 4 +- README.md | 56 +- app/[page]/layout.tsx | 4 +- app/[page]/opengraph-image.tsx | 6 +- app/[page]/page.tsx | 18 +- app/api/revalidate/route.ts | 6 +- app/error.tsx | 2 +- app/globals.css | 2 +- app/layout.tsx | 18 +- app/opengraph-image.tsx | 4 +- app/page.tsx | 14 +- app/product/[handle]/page.tsx | 34 +- app/robots.ts | 4 +- app/search/[collection]/opengraph-image.tsx | 6 +- app/search/[collection]/page.tsx | 14 +- app/search/layout.tsx | 10 +- app/search/loading.tsx | 2 +- app/search/page.tsx | 18 +- app/sitemap.ts | 10 +- components/carousel.tsx | 8 +- components/cart/actions.ts | 30 +- components/cart/add-to-cart.tsx | 22 +- components/cart/close-cart.tsx | 6 +- components/cart/delete-item-button.tsx | 18 +- components/cart/edit-item-quantity-button.tsx | 30 +- components/cart/index.tsx | 8 +- components/cart/modal.tsx | 28 +- components/cart/open-cart.tsx | 6 +- components/grid/index.tsx | 10 +- components/grid/three-items.tsx | 18 +- components/grid/tile.tsx | 18 +- components/icons/logo.tsx | 6 +- components/label.tsx | 12 +- components/layout/footer-menu.tsx | 16 +- components/layout/footer.tsx | 20 +- components/layout/navbar/index.tsx | 20 +- components/layout/navbar/mobile-menu.tsx | 20 +- components/layout/navbar/search.tsx | 18 +- components/layout/product-grid-items.tsx | 8 +- components/layout/search/collections.tsx | 14 +- components/layout/search/filter/dropdown.tsx | 22 +- components/layout/search/filter/index.tsx | 6 +- components/layout/search/filter/item.tsx | 34 +- components/loading-dots.tsx | 8 +- components/logo-square.tsx | 16 +- components/opengraph-image.tsx | 10 +- components/price.tsx | 12 +- components/product/gallery.tsx | 24 +- components/product/product-description.tsx | 10 +- components/product/variant-selector.tsx | 20 +- components/prose.tsx | 6 +- eslintrc.js | 23 + lib/client-builder.ts | 40 + lib/commercetools/index.ts | 0 lib/commercetools/types.ts | 107 + lib/constants.ts | 26 +- lib/shopify/fragments/cart.ts | 2 +- lib/shopify/fragments/product.ts | 4 +- lib/shopify/index.ts | 84 +- lib/shopify/mutations/cart.ts | 2 +- lib/shopify/queries/cart.ts | 2 +- lib/shopify/queries/collection.ts | 4 +- lib/shopify/queries/page.ts | 2 +- lib/shopify/queries/product.ts | 2 +- lib/shopify/types.ts | 4 +- lib/type-guards.ts | 4 +- lib/utils.ts | 14 +- next.config.js | 12 +- package.json | 12 +- pnpm-lock.yaml | 3355 ----------------- prettier.config.js | 9 - tailwind.config.js | 32 +- yarn.lock | 3219 ++++++++++++++++ 79 files changed, 3861 insertions(+), 3884 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 .prettierrc create mode 100644 eslintrc.js create mode 100644 lib/client-builder.ts create mode 100644 lib/commercetools/index.ts create mode 100644 lib/commercetools/types.ts delete mode 100644 pnpm-lock.yaml delete mode 100644 prettier.config.js create mode 100644 yarn.lock diff --git a/.env.example b/.env.example index 9ff0463db..cc431cb7f 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,10 @@ SITE_NAME="Next.js Commerce" SHOPIFY_REVALIDATION_SECRET="" SHOPIFY_STOREFRONT_ACCESS_TOKEN="" SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com" + +CTP_PROJECT_KEY="" +CTP_CLIENT_SECRET="" +CTP_CLIENT_ID="" +CTP_AUTH_URL="" +CTP_API_URL="" +CTP_SCOPES="" \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b3e65ae8c..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - extends: ['next', 'prettier'], - plugins: ['unicorn'], - rules: { - 'no-unused-vars': [ - 'error', - { - args: 'after-used', - caughtErrors: 'none', - ignoreRestSiblings: true, - vars: 'all' - } - ], - 'prefer-const': 'error', - 'react-hooks/exhaustive-deps': 'error', - 'unicorn/filename-case': [ - 'error', - { - case: 'kebabCase' - } - ] - } -}; diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b18fd2935..5ace4600a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - - package-ecosystem: 'github-actions' - directory: '/' + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: 'weekly' + interval: "weekly" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2fee2f8d..463dde9c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - name: Set node version uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Set pnpm version uses: pnpm/action-setup@v2 with: @@ -26,7 +26,7 @@ jobs: id: node-modules-cache uses: actions/cache@v3 with: - path: '**/node_modules' + path: "**/node_modules" key: node-modules-cache-${{ hashFiles('**/pnpm-lock.yaml') }} - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' diff --git a/.prettierignore b/.prettierignore index 71df57cbc..ba8fc98bf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ .vercel .next -pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..2352ff2ca --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": false, + "arrowParens": "always", + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 2, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 448434dd0..7259cda97 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Next.js: debug server-side", "type": "node-terminal", "request": "launch", - "command": "pnpm dev" + "command": "yarn dev" }, { "name": "Next.js: debug client-side", @@ -17,7 +17,7 @@ "name": "Next.js: debug full stack", "type": "node-terminal", "request": "launch", - "command": "pnpm dev", + "command": "yarn dev", "serverReadyAction": { "pattern": "started server on .+, url: (https?://.+)", "uriFormat": "%s", diff --git a/README.md b/README.md index 981685d2b..76eb930e2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -[![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,TWITTER_CREATOR,TWITTER_SITE) +# Next.js Commerce + commercetools -# Next.js Commerce - -A Next.js 14 and App Router-ready ecommerce template featuring: +A Next.js 14 and App Router-ready ecommerce template for commercetools, featuring: - Next.js App Router - Optimized for SEO using Next.js's Metadata @@ -12,38 +10,8 @@ A Next.js 14 and App Router-ready ecommerce template featuring: - New fetching and caching paradigms - Dynamic OG images - Styling with Tailwind CSS -- Checkout and payments with Shopify - Automatic light/dark mode based on system settings -

- -> 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). - -## Providers - -Vercel will only be actively maintaining a Shopify version [as outlined in our vision and strategy for Next.js Commerce](https://github.com/vercel/commerce/pull/966). - -Vercel is happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with their own implementation while leaving the rest of the template mostly unchanged. - -- Shopify (this repository) -- [BigCommerce](https://github.com/bigcommerce/nextjs-commerce) ([Demo](https://next-commerce-v2.vercel.app/)) -- [Medusa](https://github.com/medusajs/vercel-commerce) ([Demo](https://medusa-nextjs-commerce.vercel.app/)) -- [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/)) -- [Shopware](https://github.com/shopwareLabs/vercel-commerce) ([Demo](https://shopware-vercel-commerce-react.vercel.app/)) -- [Swell](https://github.com/swellstores/verswell-commerce) ([Demo](https://verswell-commerce.vercel.app/)) -- [Umbraco](https://github.com/umbraco/Umbraco.VercelCommerce.Demo) ([Demo](https://vercel-commerce-demo.umbraco.com/)) -- [Wix](https://github.com/wix/nextjs-commerce) ([Demo](https://wix-nextjs-commerce.vercel.app/)) - -> Note: Providers, if you are looking to use similar products for your demo, you can [download these assets](https://drive.google.com/file/d/1q_bKerjrwZgHwCw0ovfUMW6He9VtepO_/view?usp=sharing). - -## Integrations - -Integrations enable upgraded or additional functionality for Next.js Commerce - -- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/)) - - Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration. - - Search runs entirely in the browser for smaller catalogs or on a CDN for larger. - ## Running locally You 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. @@ -55,22 +23,10 @@ You will need to use the environment variables [defined in `.env.example`](.env. 3. Download your environment variables: `vercel env pull` ```bash -pnpm install -pnpm dev +yarn install +yarn dev ``` +> For `windows users`: A symlink error may occur during the initial installation with `yarn`. To fix this you have to open an administrator console and run `yarn`. + Your app should now be running on [localhost:3000](http://localhost:3000/). - -
- Expand if you work at Vercel and want to run locally and / or contribute - -1. Run `vc link`. -1. Select the `Vercel Solutions` scope. -1. Connect to the existing `commerce-shopify` project. -1. Run `vc env pull` to get environment variables. -1. Run `pnpm dev` to ensure everything is working correctly. -
- -## Vercel, Next.js Commerce, and Shopify Integration Guide - -You can use this comprehensive [integration guide](http://vercel.com/docs/integrations/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel. diff --git a/app/[page]/layout.tsx b/app/[page]/layout.tsx index 453253dca..12fc6ba62 100644 --- a/app/[page]/layout.tsx +++ b/app/[page]/layout.tsx @@ -1,5 +1,5 @@ -import Footer from 'components/layout/footer'; -import { Suspense } from 'react'; +import Footer from "components/layout/footer"; +import { Suspense } from "react"; export default function Layout({ children }: { children: React.ReactNode }) { return ( diff --git a/app/[page]/opengraph-image.tsx b/app/[page]/opengraph-image.tsx index 2fd59281e..31639796f 100644 --- a/app/[page]/opengraph-image.tsx +++ b/app/[page]/opengraph-image.tsx @@ -1,7 +1,7 @@ -import OpengraphImage from 'components/opengraph-image'; -import { getPage } from 'lib/shopify'; +import OpengraphImage from "components/opengraph-image"; +import { getPage } from "lib/shopify"; -export const runtime = 'edge'; +export const runtime = "edge"; export default async function Image({ params }: { params: { page: string } }) { const page = await getPage(params.page); diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx index 3dfde9f92..69c5af037 100644 --- a/app/[page]/page.tsx +++ b/app/[page]/page.tsx @@ -1,10 +1,10 @@ -import type { Metadata } from 'next'; +import type { Metadata } from "next"; -import Prose from 'components/prose'; -import { getPage } from 'lib/shopify'; -import { notFound } from 'next/navigation'; +import Prose from "components/prose"; +import { getPage } from "lib/shopify"; +import { notFound } from "next/navigation"; -export const runtime = 'edge'; +export const runtime = "edge"; export const revalidate = 43200; // 12 hours in seconds @@ -23,7 +23,7 @@ export async function generateMetadata({ openGraph: { publishedTime: page.createdAt, modifiedTime: page.updatedAt, - type: 'article' + type: "article" } }; } @@ -39,9 +39,9 @@ export default async function Page({ params }: { params: { page: string } }) {

{`This document was last updated on ${new Intl.DateTimeFormat(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric' + year: "numeric", + month: "long", + day: "numeric" }).format(new Date(page.updatedAt))}.`}

diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts index 47af2a4a4..03c3c7ece 100644 --- a/app/api/revalidate/route.ts +++ b/app/api/revalidate/route.ts @@ -1,7 +1,7 @@ -import { revalidate } from 'lib/shopify'; -import { NextRequest, NextResponse } from 'next/server'; +import { revalidate } from "lib/shopify"; +import { NextRequest, NextResponse } from "next/server"; -export const runtime = 'edge'; +export const runtime = "edge"; export async function POST(req: NextRequest): Promise { return revalidate(req); diff --git a/app/error.tsx b/app/error.tsx index e0a7416a3..9d3343fef 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -1,4 +1,4 @@ -'use client'; +"use client"; export default function Error({ reset }: { reset: () => void }) { return ( diff --git a/app/globals.css b/app/globals.css index 0a6d36768..69bc4a590 100644 --- a/app/globals.css +++ b/app/globals.css @@ -9,7 +9,7 @@ } @supports (font: -apple-system-body) and (-webkit-appearance: none) { - img[loading='lazy'] { + img[loading="lazy"] { clip-path: inset(0.6px); } } diff --git a/app/layout.tsx b/app/layout.tsx index 58f5a9708..4c70047ea 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,15 @@ -import Navbar from 'components/layout/navbar'; -import { GeistSans } from 'geist/font'; -import { ensureStartsWith } from 'lib/utils'; -import { ReactNode, Suspense } from 'react'; -import './globals.css'; +import Navbar from "components/layout/navbar"; +import { GeistSans } from "geist/font"; +import { ensureStartsWith } from "lib/utils"; +import { ReactNode, Suspense } from "react"; +import "./globals.css"; const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` - : 'http://localhost:3000'; -const twitterCreator = TWITTER_CREATOR ? ensureStartsWith(TWITTER_CREATOR, '@') : undefined; -const twitterSite = TWITTER_SITE ? ensureStartsWith(TWITTER_SITE, 'https://') : undefined; + : "http://localhost:3000"; +const twitterCreator = TWITTER_CREATOR ? ensureStartsWith(TWITTER_CREATOR, "@") : undefined; +const twitterSite = TWITTER_SITE ? ensureStartsWith(TWITTER_SITE, "https://") : undefined; export const metadata = { metadataBase: new URL(baseUrl), @@ -24,7 +24,7 @@ export const metadata = { ...(twitterCreator && twitterSite && { twitter: { - card: 'summary_large_image', + card: "summary_large_image", creator: twitterCreator, site: twitterSite } diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx index 23762cbdd..6e7878a23 100644 --- a/app/opengraph-image.tsx +++ b/app/opengraph-image.tsx @@ -1,6 +1,6 @@ -import OpengraphImage from 'components/opengraph-image'; +import OpengraphImage from "components/opengraph-image"; -export const runtime = 'edge'; +export const runtime = "edge"; export default async function Image() { return await OpengraphImage(); diff --git a/app/page.tsx b/app/page.tsx index aefd19396..2ad0a4f71 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,14 @@ -import { Carousel } from 'components/carousel'; -import { ThreeItemGrid } from 'components/grid/three-items'; -import Footer from 'components/layout/footer'; -import { Suspense } from 'react'; +import { Carousel } from "components/carousel"; +import { ThreeItemGrid } from "components/grid/three-items"; +import Footer from "components/layout/footer"; +import { Suspense } from "react"; -export const runtime = 'edge'; +export const runtime = "edge"; export const metadata = { - description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopify.', + description: "High-performance ecommerce store built with Next.js, Vercel, and Shopify.", openGraph: { - type: 'website' + type: "website" } }; diff --git a/app/product/[handle]/page.tsx b/app/product/[handle]/page.tsx index cf31f0021..6c3d4ee91 100644 --- a/app/product/[handle]/page.tsx +++ b/app/product/[handle]/page.tsx @@ -1,17 +1,17 @@ -import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { Suspense } from 'react'; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; -import { GridTileImage } from 'components/grid/tile'; -import Footer from 'components/layout/footer'; -import { Gallery } from 'components/product/gallery'; -import { ProductDescription } from 'components/product/product-description'; -import { HIDDEN_PRODUCT_TAG } from 'lib/constants'; -import { getProduct, getProductRecommendations } from 'lib/shopify'; -import { Image } from 'lib/shopify/types'; -import Link from 'next/link'; +import { GridTileImage } from "components/grid/tile"; +import Footer from "components/layout/footer"; +import { Gallery } from "components/product/gallery"; +import { ProductDescription } from "components/product/product-description"; +import { HIDDEN_PRODUCT_TAG } from "lib/constants"; +import { getProduct, getProductRecommendations } from "lib/shopify"; +import { Image } from "lib/shopify/types"; +import Link from "next/link"; -export const runtime = 'edge'; +export const runtime = "edge"; export async function generateMetadata({ params @@ -57,16 +57,16 @@ export default async function ProductPage({ params }: { params: { handle: string if (!product) return notFound(); const productJsonLd = { - '@context': 'https://schema.org', - '@type': 'Product', + "@context": "https://schema.org", + "@type": "Product", name: product.title, description: product.description, image: product.featuredImage.url, offers: { - '@type': 'AggregateOffer', + "@type": "AggregateOffer", availability: product.availableForSale - ? 'https://schema.org/InStock' - : 'https://schema.org/OutOfStock', + ? "https://schema.org/InStock" + : "https://schema.org/OutOfStock", priceCurrency: product.priceRange.minVariantPrice.currencyCode, highPrice: product.priceRange.maxVariantPrice.amount, lowPrice: product.priceRange.minVariantPrice.amount diff --git a/app/robots.ts b/app/robots.ts index c9849a276..0c124008c 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -1,12 +1,12 @@ const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` - : 'http://localhost:3000'; + : "http://localhost:3000"; export default function robots() { return { rules: [ { - userAgent: '*' + userAgent: "*" } ], sitemap: `${baseUrl}/sitemap.xml`, diff --git a/app/search/[collection]/opengraph-image.tsx b/app/search/[collection]/opengraph-image.tsx index 9eb9c47f7..09472dcef 100644 --- a/app/search/[collection]/opengraph-image.tsx +++ b/app/search/[collection]/opengraph-image.tsx @@ -1,7 +1,7 @@ -import OpengraphImage from 'components/opengraph-image'; -import { getCollection } from 'lib/shopify'; +import OpengraphImage from "components/opengraph-image"; +import { getCollection } from "lib/shopify"; -export const runtime = 'edge'; +export const runtime = "edge"; export default async function Image({ params }: { params: { collection: string } }) { const collection = await getCollection(params.collection); diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx index 25416d544..1caf3bc58 100644 --- a/app/search/[collection]/page.tsx +++ b/app/search/[collection]/page.tsx @@ -1,12 +1,12 @@ -import { getCollection, getCollectionProducts } from 'lib/shopify'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; +import { getCollection, getCollectionProducts } from "lib/shopify"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; -import Grid from 'components/grid'; -import ProductGridItems from 'components/layout/product-grid-items'; -import { defaultSort, sorting } from 'lib/constants'; +import Grid from "components/grid"; +import ProductGridItems from "components/layout/product-grid-items"; +import { defaultSort, sorting } from "lib/constants"; -export const runtime = 'edge'; +export const runtime = "edge"; export async function generateMetadata({ params diff --git a/app/search/layout.tsx b/app/search/layout.tsx index 24d1480d3..4c1915735 100644 --- a/app/search/layout.tsx +++ b/app/search/layout.tsx @@ -1,8 +1,8 @@ -import Footer from 'components/layout/footer'; -import Collections from 'components/layout/search/collections'; -import FilterList from 'components/layout/search/filter'; -import { sorting } from 'lib/constants'; -import { Suspense } from 'react'; +import Footer from "components/layout/footer"; +import Collections from "components/layout/search/collections"; +import FilterList from "components/layout/search/filter"; +import { sorting } from "lib/constants"; +import { Suspense } from "react"; export default function SearchLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/app/search/loading.tsx b/app/search/loading.tsx index 855c371bc..193e6b459 100644 --- a/app/search/loading.tsx +++ b/app/search/loading.tsx @@ -1,4 +1,4 @@ -import Grid from 'components/grid'; +import Grid from "components/grid"; export default function Loading() { return ( diff --git a/app/search/page.tsx b/app/search/page.tsx index 2f7a53bd4..1fe8c9628 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -1,13 +1,13 @@ -import Grid from 'components/grid'; -import ProductGridItems from 'components/layout/product-grid-items'; -import { defaultSort, sorting } from 'lib/constants'; -import { getProducts } from 'lib/shopify'; +import Grid from "components/grid"; +import ProductGridItems from "components/layout/product-grid-items"; +import { defaultSort, sorting } from "lib/constants"; +import { getProducts } from "lib/shopify"; -export const runtime = 'edge'; +export const runtime = "edge"; export const metadata = { - title: 'Search', - description: 'Search for products in the store.' + title: "Search", + description: "Search for products in the store." }; export default async function SearchPage({ @@ -19,14 +19,14 @@ export default async function SearchPage({ const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; const products = await getProducts({ sortKey, reverse, query: searchValue }); - const resultsText = products.length > 1 ? 'results' : 'result'; + const resultsText = products.length > 1 ? "results" : "result"; return ( <> {searchValue ? (

{products.length === 0 - ? 'There are no products that match ' + ? "There are no products that match " : `Showing ${products.length} ${resultsText} for `} "{searchValue}"

diff --git a/app/sitemap.ts b/app/sitemap.ts index fe8ed96ac..01ab20b34 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,6 +1,6 @@ -import { getCollections, getPages, getProducts } from 'lib/shopify'; -import { validateEnvironmentVariables } from 'lib/utils'; -import { MetadataRoute } from 'next'; +import { getCollections, getPages, getProducts } from "lib/shopify"; +import { validateEnvironmentVariables } from "lib/utils"; +import { MetadataRoute } from "next"; type Route = { url: string; @@ -9,12 +9,12 @@ type Route = { const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` - : 'http://localhost:3000'; + : "http://localhost:3000"; export default async function sitemap(): Promise { validateEnvironmentVariables(); - const routesMap = [''].map((route) => ({ + const routesMap = [""].map((route) => ({ url: `${baseUrl}${route}`, lastModified: new Date().toISOString() })); diff --git a/components/carousel.tsx b/components/carousel.tsx index 286d4dfea..9dc3d40fe 100644 --- a/components/carousel.tsx +++ b/components/carousel.tsx @@ -1,10 +1,10 @@ -import { getCollectionProducts } from 'lib/shopify'; -import Link from 'next/link'; -import { GridTileImage } from './grid/tile'; +import { getCollectionProducts } from "lib/shopify"; +import Link from "next/link"; +import { GridTileImage } from "./grid/tile"; export async function Carousel() { // Collections that start with `hidden-*` are hidden from the search page. - const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' }); + const products = await getCollectionProducts({ collection: "hidden-homepage-carousel" }); if (!products?.length) return null; diff --git a/components/cart/actions.ts b/components/cart/actions.ts index fa2c34d37..187ede5a4 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -1,12 +1,12 @@ -'use server'; +"use server"; -import { TAGS } from 'lib/constants'; -import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify'; -import { revalidateTag } from 'next/cache'; -import { cookies } from 'next/headers'; +import { TAGS } from "lib/constants"; +import { addToCart, createCart, getCart, removeFromCart, updateCart } from "lib/shopify"; +import { revalidateTag } from "next/cache"; +import { cookies } from "next/headers"; export async function addItem(prevState: any, selectedVariantId: string | undefined) { - let cartId = cookies().get('cartId')?.value; + let cartId = cookies().get("cartId")?.value; let cart; if (cartId) { @@ -16,33 +16,33 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi if (!cartId || !cart) { cart = await createCart(); cartId = cart.id; - cookies().set('cartId', cartId); + cookies().set("cartId", cartId); } if (!selectedVariantId) { - return 'Missing product variant ID'; + return "Missing product variant ID"; } try { await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]); revalidateTag(TAGS.cart); } catch (e) { - return 'Error adding item to cart'; + return "Error adding item to cart"; } } export async function removeItem(prevState: any, lineId: string) { - const cartId = cookies().get('cartId')?.value; + const cartId = cookies().get("cartId")?.value; if (!cartId) { - return 'Missing cart ID'; + return "Missing cart ID"; } try { await removeFromCart(cartId, [lineId]); revalidateTag(TAGS.cart); } catch (e) { - return 'Error removing item from cart'; + return "Error removing item from cart"; } } @@ -54,10 +54,10 @@ export async function updateItemQuantity( quantity: number; } ) { - const cartId = cookies().get('cartId')?.value; + const cartId = cookies().get("cartId")?.value; if (!cartId) { - return 'Missing cart ID'; + return "Missing cart ID"; } const { lineId, variantId, quantity } = payload; @@ -78,6 +78,6 @@ export async function updateItemQuantity( ]); revalidateTag(TAGS.cart); } catch (e) { - return 'Error updating item quantity'; + return "Error updating item quantity"; } } diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 5e7afbff9..77b5196b3 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -1,12 +1,12 @@ -'use client'; +"use client"; -import { PlusIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; -import { addItem } from 'components/cart/actions'; -import LoadingDots from 'components/loading-dots'; -import { ProductVariant } from 'lib/shopify/types'; -import { useSearchParams } from 'next/navigation'; -import { useFormState, useFormStatus } from 'react-dom'; +import { PlusIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import { addItem } from "components/cart/actions"; +import LoadingDots from "components/loading-dots"; +import { ProductVariant } from "lib/shopify/types"; +import { useSearchParams } from "next/navigation"; +import { useFormState, useFormStatus } from "react-dom"; function SubmitButton({ availableForSale, @@ -17,8 +17,8 @@ function SubmitButton({ }) { const { pending } = useFormStatus(); const buttonClasses = - 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white'; - const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60'; + "relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white"; + const disabledClasses = "cursor-not-allowed opacity-60 hover:opacity-60"; if (!availableForSale) { return ( @@ -51,7 +51,7 @@ function SubmitButton({ aria-label="Add to cart" aria-disabled={pending} className={clsx(buttonClasses, { - 'hover:opacity-90': true, + "hover:opacity-90": true, disabledClasses: pending })} > diff --git a/components/cart/close-cart.tsx b/components/cart/close-cart.tsx index 515b94843..6e313f37b 100644 --- a/components/cart/close-cart.tsx +++ b/components/cart/close-cart.tsx @@ -1,10 +1,10 @@ -import { XMarkIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; export default function CloseCart({ className }: { className?: string }) { return (
- +
); } diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 814e1f389..451765256 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; -import { XMarkIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; -import { removeItem } from 'components/cart/actions'; -import LoadingDots from 'components/loading-dots'; -import type { CartItem } from 'lib/shopify/types'; -import { useFormState, useFormStatus } from 'react-dom'; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import { removeItem } from "components/cart/actions"; +import LoadingDots from "components/loading-dots"; +import type { CartItem } from "lib/shopify/types"; +import { useFormState, useFormStatus } from "react-dom"; function SubmitButton() { const { pending } = useFormStatus(); @@ -19,9 +19,9 @@ function SubmitButton() { aria-label="Remove cart item" aria-disabled={pending} className={clsx( - 'ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200', + "ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200", { - 'cursor-not-allowed px-0': pending + "cursor-not-allowed px-0": pending } )} > diff --git a/components/cart/edit-item-quantity-button.tsx b/components/cart/edit-item-quantity-button.tsx index b743ab704..7828bd330 100644 --- a/components/cart/edit-item-quantity-button.tsx +++ b/components/cart/edit-item-quantity-button.tsx @@ -1,13 +1,13 @@ -'use client'; +"use client"; -import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; -import { updateItemQuantity } from 'components/cart/actions'; -import LoadingDots from 'components/loading-dots'; -import type { CartItem } from 'lib/shopify/types'; -import { useFormState, useFormStatus } from 'react-dom'; +import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import { updateItemQuantity } from "components/cart/actions"; +import LoadingDots from "components/loading-dots"; +import type { CartItem } from "lib/shopify/types"; +import { useFormState, useFormStatus } from "react-dom"; -function SubmitButton({ type }: { type: 'plus' | 'minus' }) { +function SubmitButton({ type }: { type: "plus" | "minus" }) { const { pending } = useFormStatus(); return ( @@ -16,19 +16,19 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) { onClick={(e: React.FormEvent) => { if (pending) e.preventDefault(); }} - aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'} + aria-label={type === "plus" ? "Increase item quantity" : "Reduce item quantity"} aria-disabled={pending} className={clsx( - 'ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80', + "ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80", { - 'cursor-not-allowed': pending, - 'ml-auto': type === 'minus' + "cursor-not-allowed": pending, + "ml-auto": type === "minus" } )} > {pending ? ( - ) : type === 'plus' ? ( + ) : type === "plus" ? ( ) : ( @@ -37,12 +37,12 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) { ); } -export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) { +export function EditItemQuantityButton({ item, type }: { item: CartItem; type: "plus" | "minus" }) { const [message, formAction] = useFormState(updateItemQuantity, null); const payload = { lineId: item.id, variantId: item.merchandise.id, - quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1 + quantity: type === "plus" ? item.quantity + 1 : item.quantity - 1 }; const actionWithVariant = formAction.bind(null, payload); diff --git a/components/cart/index.tsx b/components/cart/index.tsx index 3e250ba93..eeb6e983a 100644 --- a/components/cart/index.tsx +++ b/components/cart/index.tsx @@ -1,9 +1,9 @@ -import { getCart } from 'lib/shopify'; -import { cookies } from 'next/headers'; -import CartModal from './modal'; +import { getCart } from "lib/shopify"; +import { cookies } from "next/headers"; +import CartModal from "./modal"; export default async function Cart() { - const cartId = cookies().get('cartId')?.value; + const cartId = cookies().get("cartId")?.value; let cart; if (cartId) { diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index aee2f7a47..56c600eaa 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -1,18 +1,18 @@ -'use client'; +"use client"; -import { Dialog, Transition } from '@headlessui/react'; -import { ShoppingCartIcon } from '@heroicons/react/24/outline'; -import Price from 'components/price'; -import { DEFAULT_OPTION } from 'lib/constants'; -import type { Cart } from 'lib/shopify/types'; -import { createUrl } from 'lib/utils'; -import Image from 'next/image'; -import Link from 'next/link'; -import { Fragment, useEffect, useRef, useState } from 'react'; -import CloseCart from './close-cart'; -import { DeleteItemButton } from './delete-item-button'; -import { EditItemQuantityButton } from './edit-item-quantity-button'; -import OpenCart from './open-cart'; +import { Dialog, Transition } from "@headlessui/react"; +import { ShoppingCartIcon } from "@heroicons/react/24/outline"; +import Price from "components/price"; +import { DEFAULT_OPTION } from "lib/constants"; +import type { Cart } from "lib/shopify/types"; +import { createUrl } from "lib/utils"; +import Image from "next/image"; +import Link from "next/link"; +import { Fragment, useEffect, useRef, useState } from "react"; +import CloseCart from "./close-cart"; +import { DeleteItemButton } from "./delete-item-button"; +import { EditItemQuantityButton } from "./edit-item-quantity-button"; +import OpenCart from "./open-cart"; type MerchandiseSearchParams = { [key: string]: string; diff --git a/components/cart/open-cart.tsx b/components/cart/open-cart.tsx index fa8226ab5..03a1c28c8 100644 --- a/components/cart/open-cart.tsx +++ b/components/cart/open-cart.tsx @@ -1,5 +1,5 @@ -import { ShoppingCartIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; +import { ShoppingCartIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; export default function OpenCart({ className, @@ -11,7 +11,7 @@ export default function OpenCart({ return (
{quantity ? ( diff --git a/components/grid/index.tsx b/components/grid/index.tsx index 92681555a..7c570618f 100644 --- a/components/grid/index.tsx +++ b/components/grid/index.tsx @@ -1,16 +1,16 @@ -import clsx from 'clsx'; +import clsx from "clsx"; -function Grid(props: React.ComponentProps<'ul'>) { +function Grid(props: React.ComponentProps<"ul">) { return ( -
    +
      {props.children}
    ); } -function GridItem(props: React.ComponentProps<'li'>) { +function GridItem(props: React.ComponentProps<"li">) { return ( -
  • +
  • {props.children}
  • ); diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx index 23b3f8991..06d8e7424 100644 --- a/components/grid/three-items.tsx +++ b/components/grid/three-items.tsx @@ -1,7 +1,7 @@ -import { GridTileImage } from 'components/grid/tile'; -import { getCollectionProducts } from 'lib/shopify'; -import type { Product } from 'lib/shopify/types'; -import Link from 'next/link'; +import { GridTileImage } from "components/grid/tile"; +import { getCollectionProducts } from "lib/shopify"; +import type { Product } from "lib/shopify/types"; +import Link from "next/link"; function ThreeItemGridItem({ item, @@ -9,24 +9,24 @@ function ThreeItemGridItem({ priority }: { item: Product; - size: 'full' | 'half'; + size: "full" | "half"; priority?: boolean; }) { return (