mirror of
https://github.com/vercel/commerce.git
synced 2025-06-01 05:56:58 +00:00
mas improvements
This commit is contained in:
parent
5071de39ae
commit
bacbe38ffa
@ -6,22 +6,13 @@ import { revalidateTag } from 'next/cache';
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
let cartId: string | undefined;
|
||||||
|
|
||||||
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
|
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
|
||||||
let cartId = cookies().get('cartId')?.value;
|
cartId = cartId || cookies().get('cartId')?.value;
|
||||||
let cart;
|
|
||||||
|
|
||||||
if (cartId) {
|
if (!cartId || !selectedVariantId) {
|
||||||
cart = await getCart(cartId);
|
return 'Error adding item to cart';
|
||||||
}
|
|
||||||
|
|
||||||
if (!cartId || !cart) {
|
|
||||||
cart = await createCart();
|
|
||||||
cartId = cart.id;
|
|
||||||
cookies().set('cartId', cartId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedVariantId) {
|
|
||||||
return 'Missing product variant ID';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -32,16 +23,28 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeItem(prevState: any, lineId: string) {
|
export async function removeItem(prevState: any, merchandiseId: string) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
cartId = cartId || cookies().get('cartId')?.value;
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
return 'Missing cart ID';
|
return 'Missing cart ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeFromCart(cartId, [lineId]);
|
const cart = await getCart(cartId);
|
||||||
revalidateTag(TAGS.cart);
|
|
||||||
|
if (!cart) {
|
||||||
|
return 'Error fetching cart';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineItem = cart.lines.find((line) => line.merchandise.id === merchandiseId);
|
||||||
|
|
||||||
|
if (lineItem) {
|
||||||
|
await removeFromCart(cartId, [lineItem.id]);
|
||||||
|
revalidateTag(TAGS.cart);
|
||||||
|
} else {
|
||||||
|
return 'Item not found in cart';
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error removing item from cart';
|
return 'Error removing item from cart';
|
||||||
}
|
}
|
||||||
@ -50,47 +53,69 @@ export async function removeItem(prevState: any, lineId: string) {
|
|||||||
export async function updateItemQuantity(
|
export async function updateItemQuantity(
|
||||||
prevState: any,
|
prevState: any,
|
||||||
payload: {
|
payload: {
|
||||||
lineId: string;
|
merchandiseId: string;
|
||||||
variantId: string;
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
cartId = cartId || cookies().get('cartId')?.value;
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
return 'Missing cart ID';
|
return 'Missing cart ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lineId, variantId, quantity } = payload;
|
const { merchandiseId, quantity } = payload;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (quantity === 0) {
|
const cart = await getCart(cartId);
|
||||||
await removeFromCart(cartId, [lineId]);
|
|
||||||
revalidateTag(TAGS.cart);
|
if (!cart) {
|
||||||
return;
|
return 'Error fetching cart';
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateCart(cartId, [
|
const lineItem = cart.lines.find((line) => line.merchandise.id === merchandiseId);
|
||||||
{
|
|
||||||
id: lineId,
|
if (lineItem) {
|
||||||
merchandiseId: variantId,
|
if (quantity === 0) {
|
||||||
quantity
|
await removeFromCart(cartId, [lineItem.id]);
|
||||||
|
} else {
|
||||||
|
await updateCart(cartId, [
|
||||||
|
{
|
||||||
|
id: lineItem.id,
|
||||||
|
merchandiseId,
|
||||||
|
quantity
|
||||||
|
}
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
]);
|
} else if (quantity > 0) {
|
||||||
|
// If the item doesn't exist in the cart and quantity > 0, add it
|
||||||
|
await addToCart(cartId, [{ merchandiseId, quantity }]);
|
||||||
|
}
|
||||||
|
|
||||||
revalidateTag(TAGS.cart);
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
return 'Error updating item quantity';
|
return 'Error updating item quantity';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function redirectToCheckout() {
|
export async function redirectToCheckout() {
|
||||||
let cartId = cookies().get('cartId')?.value;
|
cartId = cartId || cookies().get('cartId')?.value;
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
return 'Missing cart ID';
|
||||||
|
}
|
||||||
|
|
||||||
let cart = await getCart(cartId);
|
let cart = await getCart(cartId);
|
||||||
|
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
return;
|
return 'Error fetching cart';
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(cart.checkoutUrl);
|
redirect(cart.checkoutUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createCartAndSetCookie() {
|
||||||
|
let cart = await createCart();
|
||||||
|
cartId = cart.id;
|
||||||
|
cookies().set('cartId', cartId);
|
||||||
|
}
|
||||||
|
@ -6,12 +6,12 @@ import React, { createContext, use, useContext, useMemo, useOptimistic } from 'r
|
|||||||
type UpdateType = 'plus' | 'minus' | 'delete';
|
type UpdateType = 'plus' | 'minus' | 'delete';
|
||||||
|
|
||||||
type CartAction =
|
type CartAction =
|
||||||
| { type: 'UPDATE_ITEM'; payload: { itemId: string; updateType: UpdateType } }
|
| { type: 'UPDATE_ITEM'; payload: { merchandiseId: string; updateType: UpdateType } }
|
||||||
| { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product } };
|
| { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product } };
|
||||||
|
|
||||||
type CartContextType = {
|
type CartContextType = {
|
||||||
cart: Cart | undefined;
|
cart: Cart | null;
|
||||||
updateCartItem: (itemId: string, updateType: UpdateType) => void;
|
updateCartItem: (merchandiseId: string, updateType: UpdateType) => void;
|
||||||
addCartItem: (variant: ProductVariant, product: Product) => void;
|
addCartItem: (variant: ProductVariant, product: Product) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,40 +89,59 @@ function updateCartTotals(lines: CartItem[]): Pick<Cart, 'totalQuantity' | 'cost
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cartReducer(state: Cart | undefined, action: CartAction): Cart | undefined {
|
function createEmptyCart(): Cart {
|
||||||
if (!state) return state;
|
return {
|
||||||
|
id: `optimistic_${Date.now()}`,
|
||||||
|
checkoutUrl: '',
|
||||||
|
totalQuantity: 0,
|
||||||
|
lines: [],
|
||||||
|
cost: {
|
||||||
|
subtotalAmount: { amount: '0', currencyCode: 'USD' },
|
||||||
|
totalAmount: { amount: '0', currencyCode: 'USD' },
|
||||||
|
totalTaxAmount: { amount: '0', currencyCode: 'USD' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cartReducer(state: Cart | null, action: CartAction): Cart {
|
||||||
|
const currentCart = state || createEmptyCart();
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'UPDATE_ITEM': {
|
case 'UPDATE_ITEM': {
|
||||||
const { itemId, updateType } = action.payload;
|
const { merchandiseId, updateType } = action.payload;
|
||||||
const updatedLines = state.lines
|
const updatedLines = currentCart.lines
|
||||||
.map((item) => (item.id === itemId ? updateCartItem(item, updateType) : item))
|
.map((item) =>
|
||||||
|
item.merchandise.id === merchandiseId ? updateCartItem(item, updateType) : item
|
||||||
|
)
|
||||||
.filter(Boolean) as CartItem[];
|
.filter(Boolean) as CartItem[];
|
||||||
|
|
||||||
if (updatedLines.length === 0) {
|
if (updatedLines.length === 0) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...currentCart,
|
||||||
lines: [],
|
lines: [],
|
||||||
totalQuantity: 0,
|
totalQuantity: 0,
|
||||||
cost: { ...state.cost, totalAmount: { ...state.cost.totalAmount, amount: '0' } }
|
cost: {
|
||||||
|
...currentCart.cost,
|
||||||
|
totalAmount: { ...currentCart.cost.totalAmount, amount: '0' }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...state, ...updateCartTotals(updatedLines), lines: updatedLines };
|
return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines };
|
||||||
}
|
}
|
||||||
case 'ADD_ITEM': {
|
case 'ADD_ITEM': {
|
||||||
const { variant, product } = action.payload;
|
const { variant, product } = action.payload;
|
||||||
const existingItem = state.lines.find((item) => item.merchandise.id === variant.id);
|
const existingItem = currentCart.lines.find((item) => item.merchandise.id === variant.id);
|
||||||
const updatedItem = createOrUpdateCartItem(existingItem, variant, product);
|
const updatedItem = createOrUpdateCartItem(existingItem, variant, product);
|
||||||
|
|
||||||
const updatedLines = existingItem
|
const updatedLines = existingItem
|
||||||
? state.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item))
|
? currentCart.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item))
|
||||||
: [...state.lines, updatedItem];
|
: [...currentCart.lines, updatedItem];
|
||||||
|
|
||||||
return { ...state, ...updateCartTotals(updatedLines), lines: updatedLines };
|
return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines };
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return currentCart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,13 +150,13 @@ export function CartProvider({
|
|||||||
cartPromise
|
cartPromise
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
cartPromise: Promise<Cart | undefined>;
|
cartPromise: Promise<Cart | null>;
|
||||||
}) {
|
}) {
|
||||||
const initialCart = use(cartPromise);
|
const initialCart = use(cartPromise);
|
||||||
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
|
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
|
||||||
|
|
||||||
const updateCartItem = (itemId: string, updateType: UpdateType) => {
|
const updateCartItem = (merchandiseId: string, updateType: UpdateType) => {
|
||||||
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { itemId, updateType } });
|
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const addCartItem = (variant: ProductVariant, product: Product) => {
|
const addCartItem = (variant: ProductVariant, product: Product) => {
|
||||||
|
@ -13,13 +13,13 @@ export function DeleteItemButton({
|
|||||||
optimisticUpdate: any;
|
optimisticUpdate: any;
|
||||||
}) {
|
}) {
|
||||||
const [message, formAction] = useFormState(removeItem, null);
|
const [message, formAction] = useFormState(removeItem, null);
|
||||||
const itemId = item.id;
|
const merchandiseId = item.merchandise.id;
|
||||||
const actionWithVariant = formAction.bind(null, itemId);
|
const actionWithVariant = formAction.bind(null, merchandiseId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
action={async () => {
|
action={async () => {
|
||||||
optimisticUpdate(itemId, 'delete');
|
optimisticUpdate(merchandiseId, 'delete');
|
||||||
await actionWithVariant();
|
await actionWithVariant();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -38,8 +38,7 @@ export function EditItemQuantityButton({
|
|||||||
}) {
|
}) {
|
||||||
const [message, formAction] = useFormState(updateItemQuantity, null);
|
const [message, formAction] = useFormState(updateItemQuantity, null);
|
||||||
const payload = {
|
const payload = {
|
||||||
lineId: item.id,
|
merchandiseId: item.merchandise.id,
|
||||||
variantId: item.merchandise.id,
|
|
||||||
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
|
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
|
||||||
};
|
};
|
||||||
const actionWithVariant = formAction.bind(null, payload);
|
const actionWithVariant = formAction.bind(null, payload);
|
||||||
@ -47,7 +46,7 @@ export function EditItemQuantityButton({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
action={async () => {
|
action={async () => {
|
||||||
optimisticUpdate(payload.lineId, type);
|
optimisticUpdate(payload.merchandiseId, type);
|
||||||
await actionWithVariant();
|
await actionWithVariant();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -10,7 +10,7 @@ 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 { useFormStatus } from 'react-dom';
|
import { useFormStatus } from 'react-dom';
|
||||||
import { redirectToCheckout } from './actions';
|
import { createCartAndSetCookie, redirectToCheckout } from './actions';
|
||||||
import { useCart } from './cart-context';
|
import { useCart } from './cart-context';
|
||||||
import CloseCart from './close-cart';
|
import CloseCart from './close-cart';
|
||||||
import { DeleteItemButton } from './delete-item-button';
|
import { DeleteItemButton } from './delete-item-button';
|
||||||
@ -29,7 +29,17 @@ export default function CartModal() {
|
|||||||
const closeCart = () => setIsOpen(false);
|
const closeCart = () => setIsOpen(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cart?.totalQuantity !== quantityRef.current) {
|
if (!cart) {
|
||||||
|
createCartAndSetCookie();
|
||||||
|
}
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
cart?.totalQuantity &&
|
||||||
|
cart?.totalQuantity !== quantityRef.current &&
|
||||||
|
cart?.totalQuantity > 0
|
||||||
|
) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export function WelcomeToast() {
|
|||||||
id: 'welcome-toast',
|
id: 'welcome-toast',
|
||||||
duration: Infinity,
|
duration: Infinity,
|
||||||
onDismiss: () => {
|
onDismiss: () => {
|
||||||
document.cookie += 'welcome-toast=2;max-age=31536000';
|
document.cookie = 'welcome-toast=2; max-age=31536000; path=/';
|
||||||
},
|
},
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
|
@ -262,8 +262,7 @@ export async function getCart(cartId: string | undefined): Promise<Cart | undefi
|
|||||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
const res = await shopifyFetch<ShopifyCartOperation>({
|
||||||
query: getCartQuery,
|
query: getCartQuery,
|
||||||
variables: { cartId },
|
variables: { cartId },
|
||||||
tags: [TAGS.cart],
|
tags: [TAGS.cart]
|
||||||
cache: 'no-store'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Old carts becomes `null` when you checkout.
|
// Old carts becomes `null` when you checkout.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user