1 Commits

Author SHA1 Message Date
Michael Novotny
4ef2cce1f9 Test 2023-08-01 21:00:54 -05:00
31 changed files with 409 additions and 235 deletions

View File

@@ -2,6 +2,6 @@ COMPANY_NAME="Vercel Inc."
TWITTER_CREATOR="@vercel"
TWITTER_SITE="https://nextjs.org/commerce"
SITE_NAME="Next.js Commerce"
SHOPIFY_REVALIDATION_SECRET=""
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
SHOPIFY_REVALIDATION_SECRET=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
SHOPIFY_STORE_DOMAIN=

2
.nvmrc
View File

@@ -1 +1 @@
18
16

219
README.md
View File

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

View File

@@ -2,18 +2,9 @@
export default function Error({ reset }: { reset: () => void }) {
return (
<div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12">
<h2 className="text-xl font-bold">Oh no!</h2>
<p className="my-2">
There was an issue with our storefront. This could be a temporary issue, please try your
action again.
</p>
<button
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"
onClick={() => reset()}
>
Try Again
</button>
<div>
<h2>Something went wrong.</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -1,32 +1,25 @@
import Navbar from 'components/layout/navbar';
import { ensureStartsWith } from 'lib/utils';
import { Inter } from 'next/font/google';
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;
export const metadata = {
metadataBase: new URL(baseUrl),
title: {
default: SITE_NAME!,
default: SITE_NAME,
template: `%s | ${SITE_NAME}`
},
robots: {
follow: true,
index: true
},
...(twitterCreator &&
twitterSite && {
...(TWITTER_CREATOR &&
TWITTER_SITE && {
twitter: {
card: 'summary_large_image',
creator: twitterCreator,
site: twitterSite
creator: TWITTER_CREATOR,
site: TWITTER_SITE
}
})
};

View File

@@ -82,8 +82,8 @@ export default async function ProductPage({ params }: { params: { handle: string
}}
/>
<div className="mx-auto max-w-screen-2xl px-4">
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8">
<div className="h-full w-full basis-full lg:basis-4/6">
<div className="rounded-lg border border-neutral-200 bg-white p-8 px-4 dark:border-neutral-800 dark:bg-black md:p-12 lg:grid lg:grid-cols-6">
<div className="lg:col-span-4">
<Gallery
images={product.images.map((image: Image) => ({
src: image.url,
@@ -92,7 +92,7 @@ export default async function ProductPage({ params }: { params: { handle: string
/>
</div>
<div className="basis-full lg:basis-2/6">
<div className="py-6 pr-8 md:pr-12 lg:col-span-2">
<ProductDescription product={product} />
</div>
</div>
@@ -115,13 +115,14 @@ async function RelatedProducts({ id }: { id: string }) {
return (
<div className="py-8">
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
{relatedProducts.map((product) => (
<li
key={product.handle}
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
>
<Link className="relative h-full w-full" href={`/product/${product.handle}`}>
<div className="flex w-full gap-4 overflow-x-auto pt-1">
{relatedProducts.map((product, i) => {
return (
<Link
key={i}
className="w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
href={`/product/${product.handle}`}
>
<GridTileImage
alt={product.title}
label={{
@@ -130,13 +131,13 @@ async function RelatedProducts({ id }: { id: string }) {
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
width={600}
height={600}
/>
</Link>
</li>
))}
</ul>
);
})}
</div>
</div>
);
}

View File

@@ -24,7 +24,7 @@ export default async function SearchPage({
return (
<>
{searchValue ? (
<p className="mb-4">
<p>
{products.length === 0
? 'There are no products that match '
: `Showing ${products.length} ${resultsText} for `}

View File

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

View File

@@ -8,33 +8,29 @@ export async function Carousel() {
if (!products?.length) return null;
// Purposefully duplicating products to make the carousel loop and not run out of products on wide screens.
const carouselProducts = [...products, ...products, ...products];
return (
<div className=" w-full overflow-x-auto pb-6 pt-1">
<ul className="flex animate-carousel gap-4">
{carouselProducts.map((product, i) => (
<li
<div className="flex animate-carousel gap-4">
{[...products, ...products].map((product, i) => (
<Link
key={`${product.handle}${i}`}
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
href={`/product/${product.handle}`}
className="h-[30vh] w-2/3 flex-none md:w-1/3"
>
<Link href={`/product/${product.handle}`} className="relative h-full w-full">
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
/>
</Link>
</li>
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
width={600}
height={600}
/>
</Link>
))}
</ul>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
import { cookies } from 'next/headers';
export const addItem = async (variantId: string | undefined): Promise<String | undefined> => {
export const addItem = async (variantId: string | undefined): Promise<Error | undefined> => {
let cartId = cookies().get('cartId')?.value;
let cart;
@@ -18,26 +18,25 @@ export const addItem = async (variantId: string | undefined): Promise<String | u
}
if (!variantId) {
return 'Missing product variant ID';
return new Error('Missing variantId');
}
try {
await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]);
} catch (e) {
return 'Error adding item to cart';
return new Error('Error adding item', { cause: e });
}
};
export const removeItem = async (lineId: string): Promise<String | undefined> => {
export const removeItem = async (lineId: string): Promise<Error | undefined> => {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
return new Error('Missing cartId');
}
try {
await removeFromCart(cartId, [lineId]);
} catch (e) {
return 'Error removing item from cart';
return new Error('Error removing item', { cause: e });
}
};
@@ -49,11 +48,11 @@ export const updateItemQuantity = async ({
lineId: string;
variantId: string;
quantity: number;
}): Promise<String | undefined> => {
}): Promise<Error | undefined> => {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
return new Error('Missing cartId');
}
try {
await updateCart(cartId, [
@@ -64,6 +63,6 @@ export const updateItemQuantity = async ({
}
]);
} catch (e) {
return 'Error updating item quantity';
return new Error('Error updating item quantity', { cause: e });
}
};

View File

@@ -44,8 +44,8 @@ export function AddToCart({
const error = await addItem(selectedVariantId);
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
alert(error);
return;
}
router.refresh();

View File

@@ -19,8 +19,8 @@ export default function DeleteItemButton({ item }: { item: CartItem }) {
const error = await removeItem(item.id);
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
alert(error);
return;
}
router.refresh();

View File

@@ -32,8 +32,8 @@ export default function EditItemQuantityButton({
});
if (error) {
// Trigger the error boundary in the root error.js
throw new Error(error.toString());
alert(error);
return;
}
router.refresh();

View File

@@ -111,7 +111,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
>
<div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
className="h-full w-full object-cover "
width={64}
height={64}
alt={
@@ -141,7 +141,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
/>
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">
<p className="w-6 text-center ">
<span className="w-full text-sm">{item.quantity}</span>
</p>
<EditItemQuantityButton item={item} type="plus" />

View File

@@ -3,27 +3,17 @@ import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Link from 'next/link';
function ThreeItemGridItem({
item,
size,
priority
}: {
item: Product;
size: 'full' | 'half';
priority?: boolean;
}) {
function ThreeItemGridItem({ item, size }: { item: Product; size: 'full' | 'half' }) {
return (
<div
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
className={size === 'full' ? 'lg:col-span-4 lg:row-span-2' : 'lg:col-span-2 lg:row-span-1'}
>
<Link className="relative block aspect-square h-full w-full" href={`/product/${item.handle}`}>
<Link className="block h-full" href={`/product/${item.handle}`}>
<GridTileImage
src={item.featuredImage.url}
fill
sizes={
size === 'full' ? '(min-width: 768px) 66vw, 100vw' : '(min-width: 768px) 33vw, 100vw'
}
priority={priority}
width={size === 'full' ? 1080 : 540}
height={size === 'full' ? 1080 : 540}
priority={true}
alt={item.title}
label={{
position: size === 'full' ? 'center' : 'bottom',
@@ -48,9 +38,9 @@ export async function ThreeItemGrid() {
const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
<ThreeItemGridItem size="full" item={firstProduct} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} priority={true} />
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 lg:grid-cols-6 lg:grid-rows-2">
<ThreeItemGridItem size="full" item={firstProduct} />
<ThreeItemGridItem size="half" item={secondProduct} />
<ThreeItemGridItem size="half" item={thirdProduct} />
</section>
);

View File

@@ -20,7 +20,7 @@ export function GridTileImage({
return (
<div
className={clsx(
'group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-blue-600 dark:bg-black',
'flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-blue-600 dark:bg-black',
{
relative: label,
'border-2 border-blue-600': active,
@@ -32,7 +32,7 @@ export function GridTileImage({
// eslint-disable-next-line jsx-a11y/alt-text -- `alt` is inherited from `props`, which is being enforced with TypeScript
<Image
className={clsx('relative h-full w-full object-contain', {
'transition duration-300 ease-in-out group-hover:scale-105': isInteractive
'transition duration-300 ease-in-out hover:scale-105': isInteractive
})}
{...props}
/>

View File

@@ -0,0 +1,13 @@
export default function GitHubIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
shapeRendering="geometricPrecision"
className={className}
>
<path d="M12 0C5.37 0 0 5.50583 0 12.3035C0 17.7478 3.435 22.3463 8.205 23.9765C8.805 24.0842 9.03 23.715 9.03 23.3921C9.03 23.0999 9.015 22.131 9.015 21.1005C6 21.6696 5.22 20.347 4.98 19.6549C4.845 19.3012 4.26 18.2092 3.75 17.917C3.33 17.6863 2.73 17.1173 3.735 17.1019C4.68 17.0865 5.355 17.9939 5.58 18.363C6.66 20.2239 8.385 19.701 9.075 19.3781C9.18 18.5783 9.495 18.04 9.84 17.7325C7.17 17.4249 4.38 16.3637 4.38 11.6576C4.38 10.3196 4.845 9.21227 5.61 8.35102C5.49 8.04343 5.07 6.78232 5.73 5.09058C5.73 5.09058 6.735 4.76762 9.03 6.3517C9.99 6.07487 11.01 5.93645 12.03 5.93645C13.05 5.93645 14.07 6.07487 15.03 6.3517C17.325 4.75224 18.33 5.09058 18.33 5.09058C18.99 6.78232 18.57 8.04343 18.45 8.35102C19.215 9.21227 19.68 10.3042 19.68 11.6576C19.68 16.3791 16.875 17.4249 14.205 17.7325C14.64 18.1169 15.015 18.8552 15.015 20.0086C15.015 21.6542 15 22.9768 15 23.3921C15 23.715 15.225 24.0995 15.825 23.9765C18.2072 23.1519 20.2773 21.5822 21.7438 19.4882C23.2103 17.3942 23.9994 14.8814 24 12.3035C24 5.50583 18.63 0 12 0Z" />
</svg>
);
}

View File

@@ -18,8 +18,8 @@ const Label = ({
'lg:px-20 lg:pb-[35%]': position === 'center'
})}
>
<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">
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight">{title}</h3>
<div className="flex items-center rounded-full border bg-white/70 p-1 text-[10px] font-semibold text-black backdrop-blur-md @[275px]/label:text-xs dark:border-neutral-800 dark:bg-black/70 dark:text-white">
<h3 className="mr-4 inline pl-2 leading-none tracking-tight">{title}</h3>
<Price
className="flex-none rounded-full bg-blue-600 p-2 text-white"
amount={amount}

View File

@@ -1,5 +1,6 @@
import Link from 'next/link';
import GitHubIcon from 'components/icons/github';
import FooterMenu from 'components/layout/footer-menu';
import LogoSquare from 'components/logo-square';
import { getMenu } from 'lib/shopify';
@@ -39,13 +40,12 @@ export default async function Footer() {
</Suspense>
<div className="md:ml-auto">
<a
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"
aria-label="Deploy on Vercel"
href="https://vercel.com/templates/next.js/nextjs-commerce"
className="flex items-center gap-2 text-lg hover:text-black dark:hover:text-neutral-300 md:text-sm"
aria-label="Github Repository"
href="https://github.com/vercel/commerce"
>
<span className="px-3"></span>
<hr className="h-full border-r border-neutral-200 dark:border-neutral-700" />
<span className="px-3">Deploy</span>
<GitHubIcon className="h-6" />
<p>Source</p>
</a>
</div>
</div>

View File

@@ -19,7 +19,11 @@ export default async function Navbar() {
</div>
<div className="flex w-full items-center">
<div className="flex w-full md:w-1/3">
<Link href="/" className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6">
<Link
href="/"
aria-label="Go back home"
className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"
>
<LogoSquare />
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">
{SITE_NAME}

View File

@@ -8,7 +8,7 @@ export default function ProductGridItems({ products }: { products: Product[] })
<>
{products.map((product) => (
<Grid.Item key={product.handle} className="animate-fadeIn">
<Link className="relative inline-block h-full w-full" href={`/product/${product.handle}`}>
<Link className="inline-block h-full w-full" href={`/product/${product.handle}`}>
<GridTileImage
alt={product.title}
label={{
@@ -17,8 +17,8 @@ export default function ProductGridItems({ products }: { products: Product[] })
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
width={600}
height={600}
/>
</Link>
</Grid.Item>

View File

@@ -52,7 +52,7 @@ function SortFilterItem({ item }: { item: SortFilterItem }) {
<DynamicTag
prefetch={!active ? false : undefined}
href={href}
className={clsx('w-full hover:underline hover:underline-offset-4', {
className={clsx('w-full', {
'underline underline-offset-4': active
})}
>

View File

@@ -27,13 +27,13 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white flex items-center justify-center';
return (
<>
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
<div className="mr-8 h-full">
<div className="relative mb-12 h-full max-h-[550px] overflow-hidden">
{images[imageIndex] && (
<Image
className="h-full w-full object-contain"
fill
sizes="(min-width: 1024px) 66vw, 100vw"
className="relative h-full w-full object-contain"
height={600}
width={600}
alt={images[imageIndex]?.altText as string}
src={images[imageIndex]?.src as string}
priority={true}
@@ -47,17 +47,11 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
aria-label="Previous product image"
href={previousUrl}
className={buttonClassName}
scroll={false}
>
<ArrowLeftIcon className="h-5" />
</Link>
<div className="mx-1 h-6 w-px bg-neutral-500"></div>
<Link
aria-label="Next product image"
href={nextUrl}
className={buttonClassName}
scroll={false}
>
<Link aria-label="Next product image" href={nextUrl} className={buttonClassName}>
<ArrowRightIcon className="h-5" />
</Link>
</div>
@@ -66,7 +60,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
</div>
{images.length > 1 ? (
<ul className="my-12 flex items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
<div className="flex items-center justify-center gap-2 overflow-auto py-1">
{images.map((image, index) => {
const isActive = index === imageIndex;
const imageSearchParams = new URLSearchParams(searchParams.toString());
@@ -74,26 +68,25 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
imageSearchParams.set('image', index.toString());
return (
<li key={image.src} className="h-20 w-20">
<Link
aria-label="Enlarge product image"
href={createUrl(pathname, imageSearchParams)}
scroll={false}
className="h-full w-full"
>
<GridTileImage
alt={image.altText}
src={image.src}
width={80}
height={80}
active={isActive}
/>
</Link>
</li>
<Link
aria-label="Enlarge product image"
key={image.src}
className="h-auto w-20"
href={createUrl(pathname, imageSearchParams)}
scroll={false}
>
<GridTileImage
alt={image.altText}
src={image.src}
width={600}
height={600}
active={isActive}
/>
</Link>
);
})}
</ul>
</div>
) : null}
</>
</div>
);
}

View File

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

View File

@@ -1,6 +1,5 @@
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
import { ensureStartsWith } from 'lib/utils';
import { revalidateTag } from 'next/cache';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
@@ -50,9 +49,7 @@ import {
ShopifyUpdateCartOperation
} from './types';
const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
: '';
const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`;
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
@@ -100,7 +97,6 @@ export async function shopifyFetch<T>({
} catch (e) {
if (isShopifyError(e)) {
throw {
cause: e.cause?.toString() || 'unknown',
status: e.status || 500,
message: e.message,
query

View File

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

View File

@@ -6,6 +6,3 @@ export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyUR
return `${pathname}${queryString}`;
};
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;

View File

@@ -16,14 +16,5 @@ module.exports = {
pathname: '/s/files/**'
}
]
},
async redirects() {
return [
{
source: '/password',
destination: '/',
permanent: true
}
];
}
};

View File

@@ -2,7 +2,7 @@
"private": true,
"packageManager": "pnpm@8.2.0",
"engines": {
"node": ">=18",
"node": ">=16",
"pnpm": ">=7"
},
"scripts": {
@@ -25,7 +25,7 @@
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"clsx": "^2.0.0",
"next": "13.4.13-canary.15",
"next": "13.4.12",
"react": "18.2.0",
"react-dom": "18.2.0"
},
@@ -43,7 +43,7 @@
"eslint-plugin-unicorn": "^48.0.0",
"lint-staged": "^13.2.3",
"postcss": "^8.4.27",
"prettier": "3.0.1",
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.4.1",
"tailwindcss": "^3.3.3",
"typescript": "5.1.6"

85
pnpm-lock.yaml generated
View File

@@ -11,8 +11,8 @@ dependencies:
specifier: ^2.0.0
version: 2.0.0
next:
specifier: 13.4.13-canary.15
version: 13.4.13-canary.15(react-dom@18.2.0)(react@18.2.0)
specifier: 13.4.12
version: 13.4.12(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -61,11 +61,11 @@ devDependencies:
specifier: ^8.4.27
version: 8.4.27
prettier:
specifier: 3.0.1
version: 3.0.1
specifier: ^3.0.0
version: 3.0.0
prettier-plugin-tailwindcss:
specifier: ^0.4.1
version: 0.4.1(prettier@3.0.1)
version: 0.4.1(prettier@3.0.0)
tailwindcss:
specifier: ^3.3.3
version: 3.3.3
@@ -224,8 +224,8 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@next/env@13.4.13-canary.15:
resolution: {integrity: sha512-AljMmO5a2uB0ZTDcBVhcfkE7WtdQDfnPg2zz/e6jKjVMRFPSvxaoRoSGUwONIhk9CAPbX9px7bZYom2wbhrTkw==}
/@next/env@13.4.12:
resolution: {integrity: sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==}
dev: false
/@next/eslint-plugin-next@13.4.12:
@@ -234,8 +234,8 @@ packages:
glob: 7.1.7
dev: true
/@next/swc-darwin-arm64@13.4.13-canary.15:
resolution: {integrity: sha512-ymE/tPjf5DXIqWxEefkqGX094ZDpKw/0sKb7xmzF0m8Kolac1eqA6ZnCsb1TKXYVQyrGUx/Z0xmxCK4cm2dEdw==}
/@next/swc-darwin-arm64@13.4.12:
resolution: {integrity: sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -243,8 +243,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@13.4.13-canary.15:
resolution: {integrity: sha512-B9fCPRjE1t5r1bmivq5fqHvU8mLNX7hkS2zj9arVrZEC7HdOugbSOpmQb5+yr5ZmNKMItQbPDJIATY+ZAiUtww==}
/@next/swc-darwin-x64@13.4.12:
resolution: {integrity: sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -252,8 +252,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.4.13-canary.15:
resolution: {integrity: sha512-K30IPFxZPtZLs1gqir95oNdCNgmu0awbC7MMLqOu9+wmW+LYjA6M3ltRe2Duy9nZ7JQob1oRl/s7MMbtCuzVAA==}
/@next/swc-linux-arm64-gnu@13.4.12:
resolution: {integrity: sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -261,8 +261,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.4.13-canary.15:
resolution: {integrity: sha512-ClJvWIhvCLXM3iSMet9bqKxyxifN7DGo8+wiV8gwIU+OMWHGNgGtmZ3xXae3R91w8DOLrsREyBN4uGLlgpwRXg==}
/@next/swc-linux-arm64-musl@13.4.12:
resolution: {integrity: sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -270,8 +270,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.4.13-canary.15:
resolution: {integrity: sha512-/B0xaPcdx2HWDC9Bxks3dLIUyu9Falmd7ENRanYizfdihgM+kV2zIQe/5h5zaESKMEltLt2ELPOPCaFU5gOnYA==}
/@next/swc-linux-x64-gnu@13.4.12:
resolution: {integrity: sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -279,8 +279,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@13.4.13-canary.15:
resolution: {integrity: sha512-YZZlKne+5iwsPe9yN8QP5sfyDN7ybpWTuYukfv6sKL68STuAVqqp4QX2g7a3Fw+LMJiDwyCFJaUDgO9KSLEqDw==}
/@next/swc-linux-x64-musl@13.4.12:
resolution: {integrity: sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -288,8 +288,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.4.13-canary.15:
resolution: {integrity: sha512-nOi9w+E+ajqJuQhcB260AMJERJPYS1K5pL+5Rymyt9VWCZEJZiHTRuaf8y/H7sObZcQKwRVa7C/EWyZjj658XA==}
/@next/swc-win32-arm64-msvc@13.4.12:
resolution: {integrity: sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -297,8 +297,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.4.13-canary.15:
resolution: {integrity: sha512-EqV5Bt7TmdFWa00KkoEeb5K4uRFrV1BAiwqylsk+d+2U1N2UIad/dTOyTzobjDcZ9uii1EaCVMiTuMfcsGIahw==}
/@next/swc-win32-ia32-msvc@13.4.12:
resolution: {integrity: sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -306,8 +306,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.4.13-canary.15:
resolution: {integrity: sha512-hZcZ0vS1eevRTr1JUyDfTYXF36DBEowNZWyzNW8ZrlGMZyQloE9yf/jHoLtutxr2jV6GoHWGGqVSw4exOyjjKw==}
/@next/swc-win32-x64-msvc@13.4.12:
resolution: {integrity: sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2241,22 +2241,25 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/next@13.4.13-canary.15(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dSOzenhqdjH6fNbSKYZ4PkqmKLOviFSVUd75Csz+zZPoTWmAKR+9waUAttOyRnUgYd/qutt8KXGH+DiU0nmhVA==}
/next@13.4.12(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==}
engines: {node: '>=16.8.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
fibers: '>= 3.1.0'
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
fibers:
optional: true
sass:
optional: true
dependencies:
'@next/env': 13.4.13-canary.15
'@next/env': 13.4.12
'@swc/helpers': 0.5.1
busboy: 1.6.0
caniuse-lite: 1.0.30001517
@@ -2267,15 +2270,15 @@ packages:
watchpack: 2.4.0
zod: 3.21.4
optionalDependencies:
'@next/swc-darwin-arm64': 13.4.13-canary.15
'@next/swc-darwin-x64': 13.4.13-canary.15
'@next/swc-linux-arm64-gnu': 13.4.13-canary.15
'@next/swc-linux-arm64-musl': 13.4.13-canary.15
'@next/swc-linux-x64-gnu': 13.4.13-canary.15
'@next/swc-linux-x64-musl': 13.4.13-canary.15
'@next/swc-win32-arm64-msvc': 13.4.13-canary.15
'@next/swc-win32-ia32-msvc': 13.4.13-canary.15
'@next/swc-win32-x64-msvc': 13.4.13-canary.15
'@next/swc-darwin-arm64': 13.4.12
'@next/swc-darwin-x64': 13.4.12
'@next/swc-linux-arm64-gnu': 13.4.12
'@next/swc-linux-arm64-musl': 13.4.12
'@next/swc-linux-x64-gnu': 13.4.12
'@next/swc-linux-x64-musl': 13.4.12
'@next/swc-win32-arm64-msvc': 13.4.12
'@next/swc-win32-ia32-msvc': 13.4.12
'@next/swc-win32-x64-msvc': 13.4.12
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -2630,7 +2633,7 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/prettier-plugin-tailwindcss@0.4.1(prettier@3.0.1):
/prettier-plugin-tailwindcss@0.4.1(prettier@3.0.0):
resolution: {integrity: sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag==}
engines: {node: '>=12.17.0'}
peerDependencies:
@@ -2682,11 +2685,11 @@ packages:
prettier-plugin-twig-melody:
optional: true
dependencies:
prettier: 3.0.1
prettier: 3.0.0
dev: true
/prettier@3.0.1:
resolution: {integrity: sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==}
/prettier@3.0.0:
resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
engines: {node: '>=14'}
hasBin: true
dev: true