diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1e2803cf..4031c0576 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel running workflows - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} - name: Checkout repo diff --git a/app/search/(collection)/[...collection]/page.tsx b/app/search/(collection)/[...collection]/page.tsx index 8439e4552..e45b593a1 100644 --- a/app/search/(collection)/[...collection]/page.tsx +++ b/app/search/(collection)/[...collection]/page.tsx @@ -20,7 +20,7 @@ export async function generateMetadata({ }): Promise { // see https://github.com/facebook/react/issues/25994 const collectionName = decodeURIComponent(transformHandle(params?.collection ?? '')); - if (collectionName === 'react_devtools_backend_compact.js.map') { + if (collectionName.includes('.js.map')) { return {}; } @@ -47,7 +47,7 @@ export default async function CategoryPage({ // see https://github.com/facebook/react/issues/25994 const collectionName = decodeURIComponent(transformHandle(params?.collection ?? '')); - if (collectionName === 'react_devtools_backend_compact.js.map') { + if (collectionName.includes('.js.map')) { return null; } diff --git a/components/cart/actions.ts b/components/cart/actions.ts index 5165f69ad..ea0e9969a 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -1,11 +1,13 @@ 'use server'; +import { TAGS } from 'lib/constants'; import { ApiClientError } from '@shopware/api-client'; import { getApiClient } from 'lib/shopware/api'; import { ExtendedCart, ExtendedLineItem, messageKeys } from 'lib/shopware/api-extended'; +import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; -export const fetchCart = async function (cartId?: string): Promise { +async function fetchCart(cartId?: string): Promise { try { const apiClient = getApiClient(cartId); const cart = await apiClient.invoke('readCart get /checkout/cart?name', {}); @@ -19,26 +21,17 @@ export const fetchCart = async function (cartId?: string): Promise', error); } } -}; +} -export const addItem = async (variantId: string | undefined): Promise => { - let cartId = cookies().get('sw-context-token')?.value; - let cart; - - if (cartId) { - cart = await fetchCart(cartId); +export async function addItem(prevState: any, selectedVariantId: string | undefined) { + const cart = await getCart(); + if (!cart) { + return 'Could not get cart'; } + const cartId = updateCartCookie(cart); - if (!cartId || !cart) { - cart = await fetchCart(); - if (cart && cart.token) { - cartId = cart.token; - cookies().set('sw-context-token', cartId); - } - } - - if (!variantId) { - return { message: 'Missing product variant ID' } as Error; + if (!selectedVariantId) { + return 'Missing product variant ID'; } try { @@ -46,7 +39,7 @@ export const addItem = async (variantId: string | undefined): Promise item.id === variantId) as + const itemInCart = cart?.lineItems?.filter((item) => item.id === selectedVariantId) as | ExtendedLineItem | undefined; if (itemInCart && itemInCart.quantity) { @@ -56,9 +49,9 @@ export const addItem = async (variantId: string | undefined): Promise', error); } } -}; +} + +export async function getCart() { + const cartId = cookies().get('sw-context-token')?.value; + + if (cartId) { + return await fetchCart(cartId); + } + + return await fetchCart(); +} + +function updateCartCookie(cart: ExtendedCart): string | undefined { + const cartId = cookies().get('sw-context-token')?.value; + // cartId is set, but not valid anymore, update the cookie + if (cartId && cart && cart.token && cart.token !== cartId) { + cookies().set('sw-context-token', cart.token); + return cart.token; + } + // cartId is not set (undefined), case for new cart, set the cookie + if (!cartId && cart && cart.token) { + cookies().set('sw-context-token', cart.token); + return cart.token; + } + // cartId is set and the same like cart.token, return it + return cartId; +} function alertErrorMessages(response: ExtendedCart): string { let errorMessages: string = ''; @@ -92,11 +112,46 @@ function alertErrorMessages(response: ExtendedCart): string { return errorMessages; } -export const removeItem = async (lineId: string): Promise => { +export async function updateItemQuantity( + prevState: any, + payload: { + lineId: string; + variantId: string; + quantity: number; + } +) { const cartId = cookies().get('sw-context-token')?.value; if (!cartId) { - return { message: 'Missing cart ID' } as Error; + return 'Missing cart ID'; + } + + const { lineId, variantId, quantity } = payload; + + try { + if (quantity === 0) { + await removeItem(null, lineId); + revalidateTag(TAGS.cart); + return; + } + + await updateLineItem(lineId, variantId, quantity); + revalidateTag(TAGS.cart); + } catch (error) { + if (error instanceof ApiClientError) { + console.error(error); + console.error('Details:', error.details); + } else { + return 'Error updating item quantity'; + } + } +} + +export async function removeItem(prevState: any, lineId: string) { + const cartId = cookies().get('sw-context-token')?.value; + + if (!cartId) { + return 'Missing cart ID'; } try { @@ -104,6 +159,7 @@ export const removeItem = async (lineId: string): Promise => await apiClient.invoke('deleteLineItem delete /checkout/cart/line-item?id[]={ids}', { ids: [lineId] }); + revalidateTag(TAGS.cart); } catch (error) { if (error instanceof ApiClientError) { console.error(error); @@ -112,17 +168,9 @@ export const removeItem = async (lineId: string): Promise => console.error('==>', error); } } -}; +} -export const updateItemQuantity = async ({ - lineId, - variantId, - quantity -}: { - lineId: string; - variantId: string; - quantity: number; -}): Promise => { +async function updateLineItem(lineId: string, variantId: string, quantity: number) { const cartId = cookies().get('sw-context-token')?.value; if (!cartId) { @@ -148,4 +196,4 @@ export const updateItemQuantity = async ({ console.error('==>', error); } } -}; +} diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 6849c3bf3..01fca2878 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -5,21 +5,79 @@ import clsx from 'clsx'; import { addItem } from 'components/cart/actions'; import LoadingDots from 'components/loading-dots'; import { ProductVariant, Product } from 'lib/shopware/types'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useTransition } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; + +function SubmitButton({ + availableForSale, + selectedVariantId +}: { + availableForSale: boolean; + selectedVariantId: string | undefined; +}) { + const { pending } = useFormStatus(); + const buttonClasses = + 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white'; + const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60'; + + if (!availableForSale) { + return ( + + ); + } + + if (!selectedVariantId) { + return ( + + ); + } + + return ( + + ); +} export function AddToCart({ product, variants, availableForSale }: { + product: Product; variants: ProductVariant[]; availableForSale: boolean; - product: Product; }) { - const router = useRouter(); + const [message, formAction] = useFormState(addItem, null); const searchParams = useSearchParams(); - const [isPending, startTransition] = useTransition(); const defaultVariantId = variants.length === 1 ? variants[0]?.id : product.id; const variant = variants.find((variant: ProductVariant) => variant.selectedOptions.every( @@ -27,46 +85,16 @@ export function AddToCart({ ) ); const selectedVariantId = variant?.id || defaultVariantId; - const title = !availableForSale - ? 'Out of stock' - : !selectedVariantId - ? 'Please select options' - : undefined; + const actionWithVariant = formAction.bind(null, selectedVariantId); return ( - + ); } diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 01e664804..df6a2d371 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -1,40 +1,35 @@ -import { XMarkIcon } from '@heroicons/react/24/outline'; -import LoadingDots from 'components/loading-dots'; -import { useRouter } from 'next/navigation'; +'use client'; +import { XMarkIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; import { removeItem } from 'components/cart/actions'; +import LoadingDots from 'components/loading-dots'; import type { CartItem } from 'lib/shopware/types'; -import { useTransition } from 'react'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; -export default function DeleteItemButton({ item }: { item: CartItem }) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); +function SubmitButton() { + const { pending } = useFormStatus(); return ( ); } + +export function DeleteItemButton({ item }: { item: CartItem }) { + const [message, formAction] = useFormState(removeItem, null); + const itemId = item.id; + const actionWithVariant = formAction.bind(null, itemId); + + return ( +
+ +

+ {message} +

+ + ); +} diff --git a/components/cart/edit-item-quantity-button.tsx b/components/cart/edit-item-quantity-button.tsx index 3eb2b8f89..0bb39a63f 100644 --- a/components/cart/edit-item-quantity-button.tsx +++ b/components/cart/edit-item-quantity-button.tsx @@ -1,54 +1,34 @@ -import { useRouter } from 'next/navigation'; -import { useTransition } from 'react'; - import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; -import { removeItem, updateItemQuantity } from 'components/cart/actions'; +import { updateItemQuantity } from 'components/cart/actions'; import LoadingDots from 'components/loading-dots'; import type { CartItem } from 'lib/shopware/types'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; -export default function EditItemQuantityButton({ - item, - type -}: { - item: CartItem; - type: 'plus' | 'minus'; -}) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); +function SubmitButton({ type }: { type: 'plus' | 'minus' }) { + const { pending } = useFormStatus(); return ( ); } + +export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) { + const [message, formAction] = useFormState(updateItemQuantity, null); + const payload = { + lineId: item.id, + variantId: item.merchandise.id, + quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1 + }; + const actionWithVariant = formAction.bind(null, payload); + + return ( +
+ +

+ {message} +

+ + ); +} diff --git a/components/cart/index.tsx b/components/cart/index.tsx index 0c732ca99..3b55b54d6 100644 --- a/components/cart/index.tsx +++ b/components/cart/index.tsx @@ -1,17 +1,11 @@ -import { fetchCart } from 'components/cart/actions'; -import { cookies } from 'next/headers'; +import { getCart } from 'components/cart/actions'; import CartModal from './modal'; import { transformCart } from 'lib/shopware/transform'; export default async function Cart() { - let resCart; - const cartId = cookies().get('sw-context-token')?.value; - - if (cartId) { - resCart = await fetchCart(cartId); - } - let cart; + const resCart = await getCart(); + if (resCart) { cart = transformCart(resCart); } diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index 577041bdc..da46f7691 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -11,8 +11,8 @@ import Image from 'next/image'; import Link from 'next/link'; import { Fragment, useEffect, useRef, useState } from 'react'; import CloseCart from './close-cart'; -import DeleteItemButton from './delete-item-button'; -import EditItemQuantityButton from './edit-item-quantity-button'; +import { DeleteItemButton } from './delete-item-button'; +import { EditItemQuantityButton } from './edit-item-quantity-button'; import OpenCart from './open-cart'; type MerchandiseSearchParams = { diff --git a/components/collection/pagination.tsx b/components/collection/pagination.tsx index 84a6c1963..0ff3f8530 100644 --- a/components/collection/pagination.tsx +++ b/components/collection/pagination.tsx @@ -50,7 +50,7 @@ export default function Pagination({
  • handlePageClick(currentPage - 1)} - className="m-2 rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2" + className="m-2 cursor-pointer rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2" > handlePageClick(i)} - className={`m-2 rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2 [&.active]:bg-gray-100${ + className={`m-2 rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2 [&.active]:bg-gray-100 cursor-pointer${ i === currentPage ? ' active ' : '' }`} > @@ -85,7 +85,7 @@ export default function Pagination({
  • handlePageClick(currentPage + 1)} - className="m-2 rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2" + className="m-2 cursor-pointer rounded-lg border border-gray-300 bg-white text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:m-0 sm:mx-2" >