Update to 15 RC 2

This commit is contained in:
Lee Robinson 2024-10-15 22:07:55 -05:00
parent 694c5c17ba
commit 64ca2ac790
18 changed files with 1167 additions and 589 deletions

View File

@ -33,6 +33,7 @@ Vercel is happy to partner and work with any commerce provider to help them get
Integrations enable upgraded or additional functionality for Next.js Commerce Integrations enable upgraded or additional functionality for Next.js Commerce
- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/)) - [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. - 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. - Search runs entirely in the browser for smaller catalogs or on a CDN for larger.

View File

@ -4,11 +4,10 @@ import Prose from 'components/prose';
import { getPage } from 'lib/shopify'; import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
export async function generateMetadata({ export async function generateMetadata(props: {
params params: Promise<{ page: string }>;
}: {
params: { page: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const params = await props.params;
const page = await getPage(params.page); const page = await getPage(params.page);
if (!page) return notFound(); if (!page) return notFound();
@ -24,7 +23,8 @@ export async function generateMetadata({
}; };
} }
export default async function Page({ params }: { params: { page: string } }) { export default async function Page(props: { params: Promise<{ page: string }> }) {
const params = await props.params;
const page = await getPage(params.page); const page = await getPage(params.page);
if (!page) return notFound(); if (!page) return notFound();

View File

@ -37,7 +37,7 @@ export const metadata = {
}; };
export default async function RootLayout({ children }: { children: ReactNode }) { export default async function RootLayout({ children }: { children: ReactNode }) {
const cartId = cookies().get('cartId')?.value; const cartId = (await cookies()).get('cartId')?.value;
// Don't await the fetch, pass the Promise to the context provider // Don't await the fetch, pass the Promise to the context provider
const cart = getCart(cartId); const cart = getCart(cartId);

View File

@ -12,11 +12,10 @@ import { Image } from 'lib/shopify/types';
import Link from 'next/link'; import Link from 'next/link';
import { Suspense } from 'react'; import { Suspense } from 'react';
export async function generateMetadata({ export async function generateMetadata(props: {
params params: Promise<{ handle: string }>;
}: {
params: { handle: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const params = await props.params;
const product = await getProduct(params.handle); const product = await getProduct(params.handle);
if (!product) return notFound(); if (!product) return notFound();
@ -50,7 +49,8 @@ export async function generateMetadata({
}; };
} }
export default async function ProductPage({ params }: { params: { handle: string } }) { export default async function ProductPage(props: { params: Promise<{ handle: string }> }) {
const params = await props.params;
const product = await getProduct(params.handle); const product = await getProduct(params.handle);
if (!product) return notFound(); if (!product) return notFound();

View File

@ -6,11 +6,10 @@ import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items'; import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants'; import { defaultSort, sorting } from 'lib/constants';
export async function generateMetadata({ export async function generateMetadata(props: {
params params: Promise<{ collection: string }>;
}: {
params: { collection: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const params = await props.params;
const collection = await getCollection(params.collection); const collection = await getCollection(params.collection);
if (!collection) return notFound(); if (!collection) return notFound();
@ -22,13 +21,12 @@ export async function generateMetadata({
}; };
} }
export default async function CategoryPage({ export default async function CategoryPage(props: {
params, params: Promise<{ collection: string }>;
searchParams searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}: {
params: { collection: string };
searchParams?: { [key: string]: string | string[] | undefined };
}) { }) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sort } = searchParams as { [key: string]: string }; const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse }); const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });

View File

@ -8,11 +8,10 @@ export const metadata = {
description: 'Search for products in the store.' description: 'Search for products in the store.'
}; };
export default async function SearchPage({ export default async function SearchPage(props: {
searchParams searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) { }) {
const searchParams = await props.searchParams;
const { sort, q: searchValue } = searchParams as { [key: string]: string }; const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;

View File

@ -7,7 +7,7 @@ import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export async function addItem(prevState: any, selectedVariantId: string | undefined) { export async function addItem(prevState: any, selectedVariantId: string | undefined) {
let cartId = cookies().get('cartId')?.value; let cartId = (await cookies()).get('cartId')?.value;
if (!cartId || !selectedVariantId) { if (!cartId || !selectedVariantId) {
return 'Error adding item to cart'; return 'Error adding item to cart';
@ -22,7 +22,7 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi
} }
export async function removeItem(prevState: any, merchandiseId: string) { export async function removeItem(prevState: any, merchandiseId: string) {
let cartId = cookies().get('cartId')?.value; let cartId = (await cookies()).get('cartId')?.value;
if (!cartId) { if (!cartId) {
return 'Missing cart ID'; return 'Missing cart ID';
@ -55,7 +55,7 @@ export async function updateItemQuantity(
quantity: number; quantity: number;
} }
) { ) {
let cartId = cookies().get('cartId')?.value; let cartId = (await cookies()).get('cartId')?.value;
if (!cartId) { if (!cartId) {
return 'Missing cart ID'; return 'Missing cart ID';
@ -97,7 +97,7 @@ export async function updateItemQuantity(
} }
export async function redirectToCheckout() { export async function redirectToCheckout() {
let cartId = cookies().get('cartId')?.value; let cartId = (await cookies()).get('cartId')?.value;
if (!cartId) { if (!cartId) {
return 'Missing cart ID'; return 'Missing cart ID';
@ -114,5 +114,5 @@ export async function redirectToCheckout() {
export async function createCartAndSetCookie() { export async function createCartAndSetCookie() {
let cart = await createCart(); let cart = await createCart();
cookies().set('cartId', cart.id!); (await cookies()).set('cartId', cart.id!);
} }

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { addItem } from 'components/cart/actions'; import { addItem } from 'components/cart/actions';
import { useProduct } from 'components/product/product-context'; import { useProduct } from 'components/product/product-context';
import { Product, ProductVariant } from 'lib/shopify/types'; import { Product, ProductVariant } from 'lib/shopify/types';
import { useFormState } from 'react-dom'; import { useActionState } from 'react';
import { useCart } from './cart-context'; import { useCart } from './cart-context';
function SubmitButton({ function SubmitButton({
@ -62,7 +62,7 @@ export function AddToCart({ product }: { product: Product }) {
const { variants, availableForSale } = product; const { variants, availableForSale } = product;
const { addCartItem } = useCart(); const { addCartItem } = useCart();
const { state } = useProduct(); const { state } = useProduct();
const [message, formAction] = useFormState(addItem, null); const [message, formAction] = useActionState(addItem, null);
const variant = variants.find((variant: ProductVariant) => const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every((option) => option.value === state[option.name.toLowerCase()]) variant.selectedOptions.every((option) => option.value === state[option.name.toLowerCase()])

View File

@ -3,7 +3,7 @@
import { XMarkIcon } from '@heroicons/react/24/outline'; import { XMarkIcon } from '@heroicons/react/24/outline';
import { removeItem } from 'components/cart/actions'; import { removeItem } from 'components/cart/actions';
import type { CartItem } from 'lib/shopify/types'; import type { CartItem } from 'lib/shopify/types';
import { useFormState } from 'react-dom'; import { useActionState } from 'react';
export function DeleteItemButton({ export function DeleteItemButton({
item, item,
@ -12,7 +12,7 @@ export function DeleteItemButton({
item: CartItem; item: CartItem;
optimisticUpdate: any; optimisticUpdate: any;
}) { }) {
const [message, formAction] = useFormState(removeItem, null); const [message, formAction] = useActionState(removeItem, null);
const merchandiseId = item.merchandise.id; const merchandiseId = item.merchandise.id;
const actionWithVariant = formAction.bind(null, merchandiseId); const actionWithVariant = formAction.bind(null, merchandiseId);

View File

@ -4,7 +4,7 @@ import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx'; import clsx from 'clsx';
import { updateItemQuantity } from 'components/cart/actions'; import { updateItemQuantity } from 'components/cart/actions';
import type { CartItem } from 'lib/shopify/types'; import type { CartItem } from 'lib/shopify/types';
import { useFormState } from 'react-dom'; import { useActionState } from 'react';
function SubmitButton({ type }: { type: 'plus' | 'minus' }) { function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
return ( return (
@ -36,7 +36,7 @@ export function EditItemQuantityButton({
type: 'plus' | 'minus'; type: 'plus' | 'minus';
optimisticUpdate: any; optimisticUpdate: any;
}) { }) {
const [message, formAction] = useFormState(updateItemQuantity, null); const [message, formAction] = useActionState(updateItemQuantity, null);
const payload = { const payload = {
merchandiseId: item.merchandise.id, merchandiseId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1 quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1

View File

@ -428,7 +428,7 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
// otherwise it will continue to retry the request. // otherwise it will continue to retry the request.
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update']; const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
const productWebhooks = ['products/create', 'products/delete', 'products/update']; const productWebhooks = ['products/create', 'products/delete', 'products/update'];
const topic = headers().get('x-shopify-topic') || 'unknown'; const topic = (await headers()).get('x-shopify-topic') || 'unknown';
const secret = req.nextUrl.searchParams.get('secret'); const secret = req.nextUrl.searchParams.get('secret');
const isCollectionUpdate = collectionWebhooks.includes(topic); const isCollectionUpdate = collectionWebhooks.includes(topic);
const isProductUpdate = productWebhooks.includes(topic); const isProductUpdate = productWebhooks.includes(topic);

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2023 Vercel, Inc. Copyright (c) 2024 Vercel, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,4 @@
/** @type {import('next').NextConfig} */ export default {
module.exports = {
images: { images: {
formats: ['image/avif', 'image/webp'], formats: ['image/avif', 'image/webp'],
remotePatterns: [ remotePatterns: [

View File

@ -17,22 +17,28 @@
"@heroicons/react": "^2.1.5", "@heroicons/react": "^2.1.5",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"geist": "^1.3.1", "geist": "^1.3.1",
"next": "15.0.0-canary.113", "next": "15.0.0-rc.1",
"react": "19.0.0-rc-3208e73e-20240730", "react": "19.0.0-rc-cd22717c-20241013",
"react-dom": "19.0.0-rc-3208e73e-20240730", "react-dom": "19.0.0-rc-cd22717c-20241013",
"sonner": "^1.5.0" "sonner": "^1.5.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/node": "20.14.12", "@types/node": "20.14.12",
"@types/react": "18.3.3", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "18.3.0", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.6", "tailwindcss": "^3.4.6",
"typescript": "5.5.4" "typescript": "5.5.4"
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
} }
} }

1632
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@ -1,7 +1,7 @@
const plugin = require('tailwindcss/plugin'); import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
/** @type {import('tailwindcss').Config} */ const config: Config = {
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {
@ -10,17 +10,17 @@ module.exports = {
}, },
keyframes: { keyframes: {
fadeIn: { fadeIn: {
from: { opacity: 0 }, from: { opacity: '0' },
to: { opacity: 1 } to: { opacity: '1' }
}, },
marquee: { marquee: {
'0%': { transform: 'translateX(0%)' }, '0%': { transform: 'translateX(0%)' },
'100%': { transform: 'translateX(-100%)' } '100%': { transform: 'translateX(-100%)' }
}, },
blink: { blink: {
'0%': { opacity: 0.2 }, '0%': { opacity: '0.2' },
'20%': { opacity: 1 }, '20%': { opacity: '1' },
'100% ': { opacity: 0.2 } '100% ': { opacity: '0.2' }
} }
}, },
animation: { animation: {
@ -52,3 +52,5 @@ module.exports = {
}) })
] ]
}; };
export default config;