mirror of
https://github.com/vercel/commerce.git
synced 2025-05-19 16:07:01 +00:00
More!
This commit is contained in:
parent
0024f4d3ee
commit
f369746e76
@ -1,7 +1,13 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { TAGS } from 'lib/constants';
|
import { TAGS } from 'lib/constants';
|
||||||
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
|
import {
|
||||||
|
addToCart,
|
||||||
|
createCart,
|
||||||
|
getCart,
|
||||||
|
removeFromCart,
|
||||||
|
updateCart,
|
||||||
|
} from 'lib/shopify';
|
||||||
import { revalidateTag } from 'next/cache';
|
import { revalidateTag } from 'next/cache';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
@ -24,49 +30,62 @@ export async function addItem(prevState: any, selectedVariantId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
|
await addToCart(cartId, [
|
||||||
revalidateTag(TAGS.cart)
|
{ merchandiseId: selectedVariantId, quantity: 1 }
|
||||||
|
]);
|
||||||
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error adding item to cart';
|
return 'Error adding item to cart';
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const removeItem = async (lineId: string): Promise<String | undefined> => {
|
export async function removeItem(prevState: any, lineId: string) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
return 'Missing cart ID';
|
return 'Missing cart ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeFromCart(cartId, [lineId]);
|
await removeFromCart(cartId, [lineId]);
|
||||||
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error removing item from cart';
|
return 'Error removing item from cart';
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const updateItemQuantity = async ({
|
export async function updateItemQuantity(
|
||||||
lineId,
|
prevState: any,
|
||||||
variantId,
|
payload: {
|
||||||
quantity
|
lineId: string;
|
||||||
}: {
|
variantId: string;
|
||||||
lineId: string;
|
quantity: number;
|
||||||
variantId: string;
|
}
|
||||||
quantity: number;
|
) {
|
||||||
}): Promise<String | undefined> => {
|
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
return 'Missing cart ID';
|
return 'Missing cart ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { lineId, variantId, quantity } = payload;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (quantity === 0) {
|
||||||
|
await removeFromCart(cartId, [lineId]);
|
||||||
|
revalidateTag(TAGS.cart);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await updateCart(cartId, [
|
await updateCart(cartId, [
|
||||||
{
|
{
|
||||||
id: lineId,
|
id: lineId,
|
||||||
merchandiseId: variantId,
|
merchandiseId: variantId,
|
||||||
quantity
|
quantity,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error updating item quantity';
|
return 'Error updating item quantity';
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -6,8 +6,8 @@ import { addItem } from 'components/cart/actions';
|
|||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import { ProductVariant } from 'lib/shopify/types';
|
import { ProductVariant } from 'lib/shopify/types';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
// @ts-ignore
|
|
||||||
import {
|
import {
|
||||||
|
// @ts-ignore
|
||||||
experimental_useFormState as useFormState,
|
experimental_useFormState as useFormState,
|
||||||
experimental_useFormStatus as useFormStatus
|
experimental_useFormStatus as useFormStatus
|
||||||
} from 'react-dom';
|
} from 'react-dom';
|
||||||
@ -58,10 +58,10 @@ function SubmitButton({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 ml-4">
|
<div className="absolute left-0 ml-4">
|
||||||
{!pending ? (
|
{pending ? (
|
||||||
<PlusIcon className="h-5" />
|
|
||||||
) : (
|
|
||||||
<LoadingDots className="mb-3 bg-white" />
|
<LoadingDots className="mb-3 bg-white" />
|
||||||
|
) : (
|
||||||
|
<PlusIcon className="h-5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
Add To Cart
|
Add To Cart
|
||||||
|
@ -1,44 +1,49 @@
|
|||||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
'use client';
|
||||||
import LoadingDots from 'components/loading-dots';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { removeItem } from 'components/cart/actions';
|
import { removeItem } from 'components/cart/actions';
|
||||||
|
import LoadingDots from 'components/loading-dots';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem } from 'lib/shopify/types';
|
||||||
import { useTransition } from 'react';
|
import {
|
||||||
|
// @ts-ignore
|
||||||
export default function DeleteItemButton({ item }: { item: CartItem }) {
|
experimental_useFormState as useFormState,
|
||||||
const router = useRouter();
|
experimental_useFormStatus as useFormStatus
|
||||||
const [isPending, startTransition] = useTransition();
|
} from 'react-dom';
|
||||||
|
|
||||||
|
function SubmitButton() {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="submit"
|
||||||
aria-label="Remove cart item"
|
aria-label="Remove cart item"
|
||||||
onClick={() => {
|
aria-disabled={pending}
|
||||||
startTransition(async () => {
|
className={clsx('ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200', {
|
||||||
const error = await removeItem(item.id);
|
'cursor-not-allowed px-0': pending
|
||||||
|
})}
|
||||||
if (error) {
|
|
||||||
// Trigger the error boundary in the root error.js
|
|
||||||
throw new Error(error.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={isPending}
|
|
||||||
className={clsx(
|
|
||||||
'ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200',
|
|
||||||
{
|
|
||||||
'cursor-not-allowed px-0': isPending
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{pending ? (
|
||||||
<LoadingDots className="bg-white" />
|
<LoadingDots className="bg-white" />
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<XMarkIcon className="hover:text-accent-3 mx-[1px] h-4 w-4 text-white dark:text-black" />
|
<XMarkIcon className="hover:text-accent-3 mx-[1px] h-4 w-4 text-white dark:text-black" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DeleteItemButton({ item }: { item: CartItem }) {
|
||||||
|
const [message, formAction] = useFormState(removeItem, null);
|
||||||
|
const itemId = item.id;
|
||||||
|
const actionWithVariant = formAction.bind(null, itemId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={actionWithVariant}>
|
||||||
|
<SubmitButton />
|
||||||
|
<p aria-live="polite" className="sr-only" role="status">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,60 +1,66 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
'use client';
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
|
import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { removeItem, updateItemQuantity } from 'components/cart/actions';
|
import { updateItemQuantity } from 'components/cart/actions';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem } from 'lib/shopify/types';
|
||||||
|
import {
|
||||||
|
// @ts-ignore
|
||||||
|
experimental_useFormState as useFormState,
|
||||||
|
experimental_useFormStatus as useFormStatus,
|
||||||
|
} from 'react-dom';
|
||||||
|
|
||||||
export default function EditItemQuantityButton({
|
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
|
||||||
item,
|
const { pending } = useFormStatus();
|
||||||
type
|
|
||||||
}: {
|
|
||||||
item: CartItem;
|
|
||||||
type: 'plus' | 'minus';
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'}
|
type='submit'
|
||||||
onClick={() => {
|
aria-label={
|
||||||
startTransition(async () => {
|
type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'
|
||||||
const error =
|
}
|
||||||
type === 'minus' && item.quantity - 1 === 0
|
aria-disabled={pending}
|
||||||
? await removeItem(item.id)
|
|
||||||
: await updateItemQuantity({
|
|
||||||
lineId: item.id,
|
|
||||||
variantId: item.merchandise.id,
|
|
||||||
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
// Trigger the error boundary in the root error.js
|
|
||||||
throw new Error(error.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={isPending}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80',
|
'ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80',
|
||||||
{
|
{
|
||||||
'cursor-not-allowed': isPending,
|
'cursor-not-allowed': pending,
|
||||||
'ml-auto': type === 'minus'
|
'ml-auto': type === 'minus',
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{pending ? (
|
||||||
<LoadingDots className="bg-black dark:bg-white" />
|
<LoadingDots className='bg-black dark:bg-white' />
|
||||||
) : type === 'plus' ? (
|
) : type === 'plus' ? (
|
||||||
<PlusIcon className="h-4 w-4 dark:text-neutral-500" />
|
<PlusIcon className='h-4 w-4 dark:text-neutral-500' />
|
||||||
) : (
|
) : (
|
||||||
<MinusIcon className="h-4 w-4 dark:text-neutral-500" />
|
<MinusIcon className='h-4 w-4 dark:text-neutral-500' />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<form action={actionWithVariant}>
|
||||||
|
<SubmitButton type={type} />
|
||||||
|
<p aria-live='polite' className='sr-only' role='status'>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -10,8 +10,8 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
import CloseCart from './close-cart';
|
import CloseCart from './close-cart';
|
||||||
import DeleteItemButton from './delete-item-button';
|
import { DeleteItemButton } from './delete-item-button';
|
||||||
import EditItemQuantityButton from './edit-item-quantity-button';
|
import { EditItemQuantityButton } from './edit-item-quantity-button';
|
||||||
import OpenCart from './open-cart';
|
import OpenCart from './open-cart';
|
||||||
|
|
||||||
type MerchandiseSearchParams = {
|
type MerchandiseSearchParams = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user