mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 14:06:59 +00:00
Merge branch 'vercel:main' into main
This commit is contained in:
commit
7bda174e1c
@ -1,37 +1,8 @@
|
||||
import { TAGS } from 'lib/constants';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import { revalidate } from 'lib/shopify';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
// We always need to respond with a 200 status code to Shopify,
|
||||
// otherwise it will continue to retry the request.
|
||||
export async function POST(req: NextRequest): Promise<Response> {
|
||||
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
||||
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
||||
const topic = headers().get('x-shopify-topic') || 'unknown';
|
||||
const secret = req.nextUrl.searchParams.get('secret');
|
||||
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
||||
const isProductUpdate = productWebhooks.includes(topic);
|
||||
|
||||
if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {
|
||||
console.error('Invalid revalidation secret.');
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (!isCollectionUpdate && !isProductUpdate) {
|
||||
// We don't need to revalidate anything for any other topics.
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (isCollectionUpdate) {
|
||||
revalidateTag(TAGS.collections);
|
||||
}
|
||||
|
||||
if (isProductUpdate) {
|
||||
revalidateTag(TAGS.products);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
return revalidate(req);
|
||||
}
|
||||
|
@ -2,9 +2,18 @@
|
||||
|
||||
export default function Error({ reset }: { reset: () => void }) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong.</h2>
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 15 KiB |
@ -4,10 +4,14 @@ 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';
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL(baseUrl),
|
||||
title: {
|
||||
default: SITE_NAME,
|
||||
default: SITE_NAME!,
|
||||
template: `%s | ${SITE_NAME}`
|
||||
},
|
||||
robots: {
|
||||
|
@ -23,17 +23,17 @@ export async function generateMetadata({
|
||||
if (!product) return notFound();
|
||||
|
||||
const { url, width, height, altText: alt } = product.featuredImage || {};
|
||||
const hide = !product.tags.includes(HIDDEN_PRODUCT_TAG);
|
||||
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
|
||||
|
||||
return {
|
||||
title: product.seo.title || product.title,
|
||||
description: product.seo.description || product.description,
|
||||
robots: {
|
||||
index: hide,
|
||||
follow: hide,
|
||||
index: indexable,
|
||||
follow: indexable,
|
||||
googleBot: {
|
||||
index: hide,
|
||||
follow: hide
|
||||
index: indexable,
|
||||
follow: indexable
|
||||
}
|
||||
},
|
||||
openGraph: url
|
||||
@ -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="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">
|
||||
<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">
|
||||
<div className="h-full w-full basis-full lg:basis-4/6">
|
||||
<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="py-6 pr-8 md:pr-12 lg:col-span-2">
|
||||
<div className="basis-full lg:basis-2/6">
|
||||
<ProductDescription product={product} />
|
||||
</div>
|
||||
</div>
|
||||
@ -115,14 +115,13 @@ async function RelatedProducts({ id }: { id: string }) {
|
||||
return (
|
||||
<div className="py-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
|
||||
<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}`}
|
||||
<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}`}>
|
||||
<GridTileImage
|
||||
alt={product.title}
|
||||
label={{
|
||||
@ -131,13 +130,13 @@ async function RelatedProducts({ id }: { id: string }) {
|
||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
||||
}}
|
||||
src={product.featuredImage?.url}
|
||||
width={600}
|
||||
height={600}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export default async function SearchPage({
|
||||
return (
|
||||
<>
|
||||
{searchValue ? (
|
||||
<p>
|
||||
<p className="mb-4">
|
||||
{products.length === 0
|
||||
? 'There are no products that match '
|
||||
: `Showing ${products.length} ${resultsText} for `}
|
||||
|
@ -1,11 +1,16 @@
|
||||
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<Promise<Promise<MetadataRoute.Sitemap>>> {
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const routesMap = [''].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date().toISOString()
|
||||
@ -32,9 +37,13 @@ export default async function sitemap(): Promise<Promise<Promise<MetadataRoute.S
|
||||
}))
|
||||
);
|
||||
|
||||
const fetchedRoutes = (
|
||||
await Promise.all([collectionsPromise, productsPromise, pagesPromise])
|
||||
).flat();
|
||||
let fetchedRoutes: Route[] = [];
|
||||
|
||||
try {
|
||||
fetchedRoutes = (await Promise.all([collectionsPromise, productsPromise, pagesPromise])).flat();
|
||||
} catch (error) {
|
||||
throw JSON.stringify(error, null, 2);
|
||||
}
|
||||
|
||||
return [...routesMap, ...fetchedRoutes];
|
||||
}
|
||||
|
@ -8,15 +8,18 @@ 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">
|
||||
<div className="flex animate-carousel gap-4">
|
||||
{[...products, ...products].map((product, i) => (
|
||||
<Link
|
||||
<ul className="flex animate-carousel gap-4">
|
||||
{carouselProducts.map((product, i) => (
|
||||
<li
|
||||
key={`${product.handle}${i}`}
|
||||
href={`/product/${product.handle}`}
|
||||
className="h-[30vh] w-2/3 flex-none md:w-1/3"
|
||||
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
|
||||
>
|
||||
<Link href={`/product/${product.handle}`} className="relative h-full w-full">
|
||||
<GridTileImage
|
||||
alt={product.title}
|
||||
label={{
|
||||
@ -25,12 +28,13 @@ export async function Carousel() {
|
||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
||||
}}
|
||||
src={product.featuredImage?.url}
|
||||
width={600}
|
||||
height={600}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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<Error | undefined> => {
|
||||
export const addItem = async (variantId: string | undefined): Promise<String | undefined> => {
|
||||
let cartId = cookies().get('cartId')?.value;
|
||||
let cart;
|
||||
|
||||
@ -18,25 +18,26 @@ export const addItem = async (variantId: string | undefined): Promise<Error | un
|
||||
}
|
||||
|
||||
if (!variantId) {
|
||||
return new Error('Missing variantId');
|
||||
return 'Missing product variant ID';
|
||||
}
|
||||
|
||||
try {
|
||||
await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]);
|
||||
} catch (e) {
|
||||
return new Error('Error adding item', { cause: e });
|
||||
return 'Error adding item to cart';
|
||||
}
|
||||
};
|
||||
|
||||
export const removeItem = async (lineId: string): Promise<Error | undefined> => {
|
||||
export const removeItem = async (lineId: string): Promise<String | undefined> => {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
|
||||
if (!cartId) {
|
||||
return new Error('Missing cartId');
|
||||
return 'Missing cart ID';
|
||||
}
|
||||
try {
|
||||
await removeFromCart(cartId, [lineId]);
|
||||
} catch (e) {
|
||||
return new Error('Error removing item', { cause: e });
|
||||
return 'Error removing item from cart';
|
||||
}
|
||||
};
|
||||
|
||||
@ -48,11 +49,11 @@ export const updateItemQuantity = async ({
|
||||
lineId: string;
|
||||
variantId: string;
|
||||
quantity: number;
|
||||
}): Promise<Error | undefined> => {
|
||||
}): Promise<String | undefined> => {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
|
||||
if (!cartId) {
|
||||
return new Error('Missing cartId');
|
||||
return 'Missing cart ID';
|
||||
}
|
||||
try {
|
||||
await updateCart(cartId, [
|
||||
@ -63,6 +64,6 @@ export const updateItemQuantity = async ({
|
||||
}
|
||||
]);
|
||||
} catch (e) {
|
||||
return new Error('Error updating item quantity', { cause: e });
|
||||
return 'Error updating item quantity';
|
||||
}
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { addItem } from 'components/cart/actions';
|
||||
import LoadingDots from 'components/loading-dots';
|
||||
import { ProductVariant } from 'lib/shopify/types';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState, useTransition } from 'react';
|
||||
import { useTransition } from 'react';
|
||||
|
||||
export function AddToCart({
|
||||
variants,
|
||||
@ -15,21 +15,16 @@ export function AddToCart({
|
||||
variants: ProductVariant[];
|
||||
availableForSale: boolean;
|
||||
}) {
|
||||
const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>(undefined);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
|
||||
const variant = variants.find((variant: ProductVariant) =>
|
||||
variant.selectedOptions.every(
|
||||
(option) => option.value === searchParams.get(option.name.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
setSelectedVariantId(variant?.id);
|
||||
}, [searchParams, variants, setSelectedVariantId]);
|
||||
|
||||
const selectedVariantId = variant?.id || defaultVariantId;
|
||||
const title = !availableForSale
|
||||
? 'Out of stock'
|
||||
: !selectedVariantId
|
||||
@ -49,8 +44,8 @@ export function AddToCart({
|
||||
const error = await addItem(selectedVariantId);
|
||||
|
||||
if (error) {
|
||||
alert(error);
|
||||
return;
|
||||
// Trigger the error boundary in the root error.js
|
||||
throw new Error(error.toString());
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
|
@ -19,8 +19,8 @@ export default function DeleteItemButton({ item }: { item: CartItem }) {
|
||||
const error = await removeItem(item.id);
|
||||
|
||||
if (error) {
|
||||
alert(error);
|
||||
return;
|
||||
// Trigger the error boundary in the root error.js
|
||||
throw new Error(error.toString());
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
|
@ -32,8 +32,8 @@ export default function EditItemQuantityButton({
|
||||
});
|
||||
|
||||
if (error) {
|
||||
alert(error);
|
||||
return;
|
||||
// Trigger the error boundary in the root error.js
|
||||
throw new Error(error.toString());
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
|
@ -25,7 +25,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
const closeCart = () => setIsOpen(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Open cart modal when when quantity changes.
|
||||
// Open cart modal when quantity changes.
|
||||
if (cart?.totalQuantity !== quantityRef.current) {
|
||||
// But only if it's not already open (quantity also changes when editing items in cart).
|
||||
if (!isOpen) {
|
||||
@ -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" />
|
||||
|
@ -3,17 +3,27 @@ import { getCollectionProducts } from 'lib/shopify';
|
||||
import type { Product } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
function ThreeItemGridItem({ item, size }: { item: Product; size: 'full' | 'half' }) {
|
||||
function ThreeItemGridItem({
|
||||
item,
|
||||
size,
|
||||
priority
|
||||
}: {
|
||||
item: Product;
|
||||
size: 'full' | 'half';
|
||||
priority?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={size === 'full' ? 'lg:col-span-4 lg:row-span-2' : 'lg:col-span-2 lg:row-span-1'}
|
||||
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
|
||||
>
|
||||
<Link className="block h-full" href={`/product/${item.handle}`}>
|
||||
<Link className="relative block aspect-square h-full w-full" href={`/product/${item.handle}`}>
|
||||
<GridTileImage
|
||||
src={item.featuredImage.url}
|
||||
width={size === 'full' ? 1080 : 540}
|
||||
height={size === 'full' ? 1080 : 540}
|
||||
priority={true}
|
||||
fill
|
||||
sizes={
|
||||
size === 'full' ? '(min-width: 768px) 66vw, 100vw' : '(min-width: 768px) 33vw, 100vw'
|
||||
}
|
||||
priority={priority}
|
||||
alt={item.title}
|
||||
label={{
|
||||
position: size === 'full' ? 'center' : 'bottom',
|
||||
@ -38,9 +48,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 lg:grid-cols-6 lg:grid-rows-2">
|
||||
<ThreeItemGridItem size="full" item={firstProduct} />
|
||||
<ThreeItemGridItem size="half" item={secondProduct} />
|
||||
<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} />
|
||||
<ThreeItemGridItem size="half" item={thirdProduct} />
|
||||
</section>
|
||||
);
|
||||
|
@ -1,13 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -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-[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>
|
||||
<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>
|
||||
<Price
|
||||
className="flex-none rounded-full bg-blue-600 p-2 text-white"
|
||||
amount={amount}
|
||||
|
@ -15,11 +15,11 @@ const FooterMenuItem = ({ item }: { item: Menu }) => {
|
||||
}, [pathname, item.path]);
|
||||
|
||||
return (
|
||||
<li className="mt-2 first:mt-1">
|
||||
<li>
|
||||
<Link
|
||||
href={item.path}
|
||||
className={clsx(
|
||||
'underline-offset-4 hover:text-black hover:underline dark:hover:text-neutral-300',
|
||||
'block p-2 text-lg underline-offset-4 hover:text-black hover:underline dark:hover:text-neutral-300 md:inline-block md:text-sm',
|
||||
{
|
||||
'text-black dark:text-neutral-300': active
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
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';
|
||||
@ -19,7 +18,7 @@ export default async function Footer() {
|
||||
<footer className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<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 dark:border-neutral-700 md:flex-row md:gap-12 md:px-4 xl:px-0">
|
||||
<div>
|
||||
<Link className="flex items-center gap-2 text-black dark:text-white" href="/">
|
||||
<Link className="flex items-center gap-2 text-black dark:text-white md:pt-1" href="/">
|
||||
<LogoSquare size="sm" />
|
||||
<span className="uppercase">{SITE_NAME}</span>
|
||||
</Link>
|
||||
@ -40,12 +39,13 @@ export default async function Footer() {
|
||||
</Suspense>
|
||||
<div className="md:ml-auto">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-black dark:hover:text-neutral-300"
|
||||
aria-label="Github Repository"
|
||||
href="https://github.com/vercel/commerce"
|
||||
className="flex h-8 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"
|
||||
>
|
||||
<GitHubIcon className="h-6" />
|
||||
<p>Source</p>
|
||||
<span className="px-3">▲</span>
|
||||
<hr className="h-full border-r border-neutral-200 dark:border-neutral-700" />
|
||||
<span className="px-3">Deploy</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,12 +30,12 @@ export default async function Navbar() {
|
||||
</div>
|
||||
</Link>
|
||||
{menu.length ? (
|
||||
<ul className="hidden text-sm md:flex md:items-center">
|
||||
<ul className="hidden gap-6 text-sm md:flex md:items-center">
|
||||
{menu.map((item: Menu) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
className="mr-3 text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300 lg:mr-8"
|
||||
className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
|
@ -32,8 +32,12 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={openMobileMenu} aria-label="Open mobile menu" className="md:hidden">
|
||||
<Bars3Icon className="h-6" />
|
||||
<button
|
||||
onClick={openMobileMenu}
|
||||
aria-label="Open mobile menu"
|
||||
className="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 md:hidden"
|
||||
>
|
||||
<Bars3Icon className="h-4" />
|
||||
</button>
|
||||
<Transition show={isOpen}>
|
||||
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
||||
@ -59,7 +63,11 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
>
|
||||
<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">
|
||||
<div className="p-4">
|
||||
<button className="mb-4" onClick={closeMobileMenu} aria-label="Close mobile menu">
|
||||
<button
|
||||
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"
|
||||
onClick={closeMobileMenu}
|
||||
aria-label="Close mobile menu"
|
||||
>
|
||||
<XMarkIcon className="h-6" />
|
||||
</button>
|
||||
|
||||
@ -67,14 +75,13 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
<Search />
|
||||
</div>
|
||||
{menu.length ? (
|
||||
<ul className="flex flex-col">
|
||||
<ul className="flex w-full flex-col">
|
||||
{menu.map((item: Menu) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
className="rounded-lg py-1 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white"
|
||||
onClick={closeMobileMenu}
|
||||
<li
|
||||
className="py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white"
|
||||
key={item.title}
|
||||
>
|
||||
<Link href={item.path} onClick={closeMobileMenu}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -32,7 +32,7 @@ export default function Search() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="relative w-full lg:w-[320px]">
|
||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
|
@ -8,7 +8,7 @@ export default function ProductGridItems({ products }: { products: Product[] })
|
||||
<>
|
||||
{products.map((product) => (
|
||||
<Grid.Item key={product.handle} className="animate-fadeIn">
|
||||
<Link className="inline-block h-full w-full" href={`/product/${product.handle}`}>
|
||||
<Link className="relative 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}
|
||||
width={600}
|
||||
height={600}
|
||||
fill
|
||||
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
/>
|
||||
</Link>
|
||||
</Grid.Item>
|
||||
|
@ -52,7 +52,7 @@ function SortFilterItem({ item }: { item: SortFilterItem }) {
|
||||
<DynamicTag
|
||||
prefetch={!active ? false : undefined}
|
||||
href={href}
|
||||
className={clsx('w-full', {
|
||||
className={clsx('w-full hover:underline hover:underline-offset-4', {
|
||||
'underline underline-offset-4': active
|
||||
})}
|
||||
>
|
||||
|
@ -2,33 +2,40 @@
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
||||
export function Gallery({ images }: { images: { src: string; altText: string }[] }) {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const imageSearchParam = searchParams.get('image');
|
||||
const imageIndex = imageSearchParam ? parseInt(imageSearchParam) : 0;
|
||||
|
||||
function handleNavigate(direction: 'next' | 'previous') {
|
||||
if (direction === 'next') {
|
||||
setCurrentImageIndex(currentImageIndex + 1 < images.length ? currentImageIndex + 1 : 0);
|
||||
} else {
|
||||
setCurrentImageIndex(currentImageIndex === 0 ? images.length - 1 : currentImageIndex - 1);
|
||||
}
|
||||
}
|
||||
const nextSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const nextImageIndex = imageIndex + 1 < images.length ? imageIndex + 1 : 0;
|
||||
nextSearchParams.set('image', nextImageIndex.toString());
|
||||
const nextUrl = createUrl(pathname, nextSearchParams);
|
||||
|
||||
const previousSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const previousImageIndex = imageIndex === 0 ? images.length - 1 : imageIndex - 1;
|
||||
previousSearchParams.set('image', previousImageIndex.toString());
|
||||
const previousUrl = createUrl(pathname, previousSearchParams);
|
||||
|
||||
const buttonClassName =
|
||||
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white';
|
||||
'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="mr-8 h-full">
|
||||
<div className="relative mb-12 h-full max-h-[550px] overflow-hidden">
|
||||
{images[currentImageIndex] && (
|
||||
<>
|
||||
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
|
||||
{images[imageIndex] && (
|
||||
<Image
|
||||
className="relative h-full w-full object-contain"
|
||||
height={600}
|
||||
width={600}
|
||||
alt={images[currentImageIndex]?.altText as string}
|
||||
src={images[currentImageIndex]?.src as string}
|
||||
className="h-full w-full object-contain"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 66vw, 100vw"
|
||||
alt={images[imageIndex]?.altText as string}
|
||||
src={images[imageIndex]?.src as string}
|
||||
priority={true}
|
||||
/>
|
||||
)}
|
||||
@ -36,49 +43,57 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
{images.length > 1 ? (
|
||||
<div className="absolute bottom-[15%] flex w-full justify-center">
|
||||
<div className="mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
|
||||
<button
|
||||
<Link
|
||||
aria-label="Previous product image"
|
||||
onClick={() => handleNavigate('previous')}
|
||||
href={previousUrl}
|
||||
className={buttonClassName}
|
||||
scroll={false}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<div className="mx-1 h-6 w-px bg-neutral-500"></div>
|
||||
<button
|
||||
<Link
|
||||
aria-label="Next product image"
|
||||
onClick={() => handleNavigate('next')}
|
||||
href={nextUrl}
|
||||
className={buttonClassName}
|
||||
scroll={false}
|
||||
>
|
||||
<ArrowRightIcon className="h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{images.length > 1 ? (
|
||||
<div className="flex items-center justify-center gap-2 overflow-auto py-1">
|
||||
<ul className="my-12 flex items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
|
||||
{images.map((image, index) => {
|
||||
const isActive = index === currentImageIndex;
|
||||
const isActive = index === imageIndex;
|
||||
const imageSearchParams = new URLSearchParams(searchParams.toString());
|
||||
|
||||
imageSearchParams.set('image', index.toString());
|
||||
|
||||
return (
|
||||
<button
|
||||
<li key={image.src} className="h-auto w-20">
|
||||
<Link
|
||||
aria-label="Enlarge product image"
|
||||
key={image.src}
|
||||
className="h-auto w-20"
|
||||
onClick={() => setCurrentImageIndex(index)}
|
||||
href={createUrl(pathname, imageSearchParams)}
|
||||
scroll={false}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<GridTileImage
|
||||
alt={image.altText}
|
||||
src={image.src}
|
||||
width={600}
|
||||
height={600}
|
||||
width={80}
|
||||
height={80}
|
||||
active={isActive}
|
||||
/>
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
|
||||
import { isShopifyError } from 'lib/type-guards';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
addToCartMutation,
|
||||
createCartMutation,
|
||||
@ -408,3 +411,35 @@ export async function getProducts({
|
||||
|
||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
|
||||
}
|
||||
|
||||
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
|
||||
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
// We always need to respond with a 200 status code to Shopify,
|
||||
// otherwise it will continue to retry the request.
|
||||
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
||||
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
||||
const topic = headers().get('x-shopify-topic') || 'unknown';
|
||||
const secret = req.nextUrl.searchParams.get('secret');
|
||||
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
||||
const isProductUpdate = productWebhooks.includes(topic);
|
||||
|
||||
if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {
|
||||
console.error('Invalid revalidation secret.');
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (!isCollectionUpdate && !isProductUpdate) {
|
||||
// We don't need to revalidate anything for any other topics.
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (isCollectionUpdate) {
|
||||
revalidateTag(TAGS.collections);
|
||||
}
|
||||
|
||||
if (isProductUpdate) {
|
||||
revalidateTag(TAGS.products);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.2.0",
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"node": ">=18",
|
||||
"pnpm": ">=7"
|
||||
},
|
||||
"scripts": {
|
||||
@ -25,7 +25,7 @@
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"clsx": "^2.0.0",
|
||||
"next": "13.4.12",
|
||||
"next": "13.4.13-canary.15",
|
||||
"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.0",
|
||||
"prettier": "3.0.1",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
|
85
pnpm-lock.yaml
generated
85
pnpm-lock.yaml
generated
@ -11,8 +11,8 @@ dependencies:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
next:
|
||||
specifier: 13.4.12
|
||||
version: 13.4.12(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: 13.4.13-canary.15
|
||||
version: 13.4.13-canary.15(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.0
|
||||
version: 3.0.0
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(prettier@3.0.0)
|
||||
version: 0.4.1(prettier@3.0.1)
|
||||
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.12:
|
||||
resolution: {integrity: sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==}
|
||||
/@next/env@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-AljMmO5a2uB0ZTDcBVhcfkE7WtdQDfnPg2zz/e6jKjVMRFPSvxaoRoSGUwONIhk9CAPbX9px7bZYom2wbhrTkw==}
|
||||
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.12:
|
||||
resolution: {integrity: sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==}
|
||||
/@next/swc-darwin-arm64@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-ymE/tPjf5DXIqWxEefkqGX094ZDpKw/0sKb7xmzF0m8Kolac1eqA6ZnCsb1TKXYVQyrGUx/Z0xmxCK4cm2dEdw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@ -243,8 +243,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-x64@13.4.12:
|
||||
resolution: {integrity: sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==}
|
||||
/@next/swc-darwin-x64@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-B9fCPRjE1t5r1bmivq5fqHvU8mLNX7hkS2zj9arVrZEC7HdOugbSOpmQb5+yr5ZmNKMItQbPDJIATY+ZAiUtww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@ -252,8 +252,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-gnu@13.4.12:
|
||||
resolution: {integrity: sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==}
|
||||
/@next/swc-linux-arm64-gnu@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-K30IPFxZPtZLs1gqir95oNdCNgmu0awbC7MMLqOu9+wmW+LYjA6M3ltRe2Duy9nZ7JQob1oRl/s7MMbtCuzVAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@ -261,8 +261,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-musl@13.4.12:
|
||||
resolution: {integrity: sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==}
|
||||
/@next/swc-linux-arm64-musl@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-ClJvWIhvCLXM3iSMet9bqKxyxifN7DGo8+wiV8gwIU+OMWHGNgGtmZ3xXae3R91w8DOLrsREyBN4uGLlgpwRXg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@ -270,8 +270,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-gnu@13.4.12:
|
||||
resolution: {integrity: sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==}
|
||||
/@next/swc-linux-x64-gnu@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-/B0xaPcdx2HWDC9Bxks3dLIUyu9Falmd7ENRanYizfdihgM+kV2zIQe/5h5zaESKMEltLt2ELPOPCaFU5gOnYA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@ -279,8 +279,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-musl@13.4.12:
|
||||
resolution: {integrity: sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==}
|
||||
/@next/swc-linux-x64-musl@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-YZZlKne+5iwsPe9yN8QP5sfyDN7ybpWTuYukfv6sKL68STuAVqqp4QX2g7a3Fw+LMJiDwyCFJaUDgO9KSLEqDw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@ -288,8 +288,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-arm64-msvc@13.4.12:
|
||||
resolution: {integrity: sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==}
|
||||
/@next/swc-win32-arm64-msvc@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-nOi9w+E+ajqJuQhcB260AMJERJPYS1K5pL+5Rymyt9VWCZEJZiHTRuaf8y/H7sObZcQKwRVa7C/EWyZjj658XA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@ -297,8 +297,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-ia32-msvc@13.4.12:
|
||||
resolution: {integrity: sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==}
|
||||
/@next/swc-win32-ia32-msvc@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-EqV5Bt7TmdFWa00KkoEeb5K4uRFrV1BAiwqylsk+d+2U1N2UIad/dTOyTzobjDcZ9uii1EaCVMiTuMfcsGIahw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@ -306,8 +306,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-x64-msvc@13.4.12:
|
||||
resolution: {integrity: sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==}
|
||||
/@next/swc-win32-x64-msvc@13.4.13-canary.15:
|
||||
resolution: {integrity: sha512-hZcZ0vS1eevRTr1JUyDfTYXF36DBEowNZWyzNW8ZrlGMZyQloE9yf/jHoLtutxr2jV6GoHWGGqVSw4exOyjjKw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@ -2241,25 +2241,22 @@ packages:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: true
|
||||
|
||||
/next@13.4.12(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==}
|
||||
/next@13.4.13-canary.15(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-dSOzenhqdjH6fNbSKYZ4PkqmKLOviFSVUd75Csz+zZPoTWmAKR+9waUAttOyRnUgYd/qutt8KXGH+DiU0nmhVA==}
|
||||
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.12
|
||||
'@next/env': 13.4.13-canary.15
|
||||
'@swc/helpers': 0.5.1
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001517
|
||||
@ -2270,15 +2267,15 @@ packages:
|
||||
watchpack: 2.4.0
|
||||
zod: 3.21.4
|
||||
optionalDependencies:
|
||||
'@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
|
||||
'@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
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
@ -2633,7 +2630,7 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/prettier-plugin-tailwindcss@0.4.1(prettier@3.0.0):
|
||||
/prettier-plugin-tailwindcss@0.4.1(prettier@3.0.1):
|
||||
resolution: {integrity: sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
peerDependencies:
|
||||
@ -2685,11 +2682,11 @@ packages:
|
||||
prettier-plugin-twig-melody:
|
||||
optional: true
|
||||
dependencies:
|
||||
prettier: 3.0.0
|
||||
prettier: 3.0.1
|
||||
dev: true
|
||||
|
||||
/prettier@3.0.0:
|
||||
resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
|
||||
/prettier@3.0.1:
|
||||
resolution: {integrity: sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
Loading…
x
Reference in New Issue
Block a user