mas improvements

This commit is contained in:
Lee Robinson 2024-07-28 14:27:16 -05:00
parent 5071de39ae
commit bacbe38ffa
7 changed files with 117 additions and 65 deletions

View File

@ -6,22 +6,13 @@ import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
let cartId: string | undefined;
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
let cartId = cookies().get('cartId')?.value;
let cart;
cartId = cartId || cookies().get('cartId')?.value;
if (cartId) {
cart = await getCart(cartId);
}
if (!cartId || !cart) {
cart = await createCart();
cartId = cart.id;
cookies().set('cartId', cartId);
}
if (!selectedVariantId) {
return 'Missing product variant ID';
if (!cartId || !selectedVariantId) {
return 'Error adding item to cart';
}
try {
@ -32,16 +23,28 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi
}
}
export async function removeItem(prevState: any, lineId: string) {
const cartId = cookies().get('cartId')?.value;
export async function removeItem(prevState: any, merchandiseId: string) {
cartId = cartId || cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
try {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
const cart = await getCart(cartId);
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) {
return 'Error removing item from cart';
}
@ -50,47 +53,69 @@ export async function removeItem(prevState: any, lineId: string) {
export async function updateItemQuantity(
prevState: any,
payload: {
lineId: string;
variantId: string;
merchandiseId: string;
quantity: number;
}
) {
const cartId = cookies().get('cartId')?.value;
cartId = cartId || cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
const { lineId, variantId, quantity } = payload;
const { merchandiseId, quantity } = payload;
try {
if (quantity === 0) {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
return;
const cart = await getCart(cartId);
if (!cart) {
return 'Error fetching cart';
}
await updateCart(cartId, [
{
id: lineId,
merchandiseId: variantId,
quantity
const lineItem = cart.lines.find((line) => line.merchandise.id === merchandiseId);
if (lineItem) {
if (quantity === 0) {
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);
} catch (e) {
console.log(e);
console.error(e);
return 'Error updating item quantity';
}
}
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);
if (!cart) {
return;
return 'Error fetching cart';
}
redirect(cart.checkoutUrl);
}
export async function createCartAndSetCookie() {
let cart = await createCart();
cartId = cart.id;
cookies().set('cartId', cartId);
}

View File

@ -6,12 +6,12 @@ import React, { createContext, use, useContext, useMemo, useOptimistic } from 'r
type UpdateType = 'plus' | 'minus' | 'delete';
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 CartContextType = {
cart: Cart | undefined;
updateCartItem: (itemId: string, updateType: UpdateType) => void;
cart: Cart | null;
updateCartItem: (merchandiseId: string, updateType: UpdateType) => 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 {
if (!state) return state;
function createEmptyCart(): Cart {
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) {
case 'UPDATE_ITEM': {
const { itemId, updateType } = action.payload;
const updatedLines = state.lines
.map((item) => (item.id === itemId ? updateCartItem(item, updateType) : item))
const { merchandiseId, updateType } = action.payload;
const updatedLines = currentCart.lines
.map((item) =>
item.merchandise.id === merchandiseId ? updateCartItem(item, updateType) : item
)
.filter(Boolean) as CartItem[];
if (updatedLines.length === 0) {
return {
...state,
...currentCart,
lines: [],
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': {
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 updatedLines = existingItem
? state.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item))
: [...state.lines, updatedItem];
? currentCart.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item))
: [...currentCart.lines, updatedItem];
return { ...state, ...updateCartTotals(updatedLines), lines: updatedLines };
return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines };
}
default:
return state;
return currentCart;
}
}
@ -131,13 +150,13 @@ export function CartProvider({
cartPromise
}: {
children: React.ReactNode;
cartPromise: Promise<Cart | undefined>;
cartPromise: Promise<Cart | null>;
}) {
const initialCart = use(cartPromise);
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
const updateCartItem = (itemId: string, updateType: UpdateType) => {
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { itemId, updateType } });
const updateCartItem = (merchandiseId: string, updateType: UpdateType) => {
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
};
const addCartItem = (variant: ProductVariant, product: Product) => {

View File

@ -13,13 +13,13 @@ export function DeleteItemButton({
optimisticUpdate: any;
}) {
const [message, formAction] = useFormState(removeItem, null);
const itemId = item.id;
const actionWithVariant = formAction.bind(null, itemId);
const merchandiseId = item.merchandise.id;
const actionWithVariant = formAction.bind(null, merchandiseId);
return (
<form
action={async () => {
optimisticUpdate(itemId, 'delete');
optimisticUpdate(merchandiseId, 'delete');
await actionWithVariant();
}}
>

View File

@ -38,8 +38,7 @@ export function EditItemQuantityButton({
}) {
const [message, formAction] = useFormState(updateItemQuantity, null);
const payload = {
lineId: item.id,
variantId: item.merchandise.id,
merchandiseId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
};
const actionWithVariant = formAction.bind(null, payload);
@ -47,7 +46,7 @@ export function EditItemQuantityButton({
return (
<form
action={async () => {
optimisticUpdate(payload.lineId, type);
optimisticUpdate(payload.merchandiseId, type);
await actionWithVariant();
}}
>

View File

@ -10,7 +10,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useFormStatus } from 'react-dom';
import { redirectToCheckout } from './actions';
import { createCartAndSetCookie, redirectToCheckout } from './actions';
import { useCart } from './cart-context';
import CloseCart from './close-cart';
import { DeleteItemButton } from './delete-item-button';
@ -29,7 +29,17 @@ export default function CartModal() {
const closeCart = () => setIsOpen(false);
useEffect(() => {
if (cart?.totalQuantity !== quantityRef.current) {
if (!cart) {
createCartAndSetCookie();
}
}, [cart]);
useEffect(() => {
if (
cart?.totalQuantity &&
cart?.totalQuantity !== quantityRef.current &&
cart?.totalQuantity > 0
) {
if (!isOpen) {
setIsOpen(true);
}

View File

@ -13,7 +13,7 @@ export function WelcomeToast() {
id: 'welcome-toast',
duration: Infinity,
onDismiss: () => {
document.cookie += 'welcome-toast=2;max-age=31536000';
document.cookie = 'welcome-toast=2; max-age=31536000; path=/';
},
description: (
<>

View File

@ -262,8 +262,7 @@ export async function getCart(cartId: string | undefined): Promise<Cart | undefi
const res = await shopifyFetch<ShopifyCartOperation>({
query: getCartQuery,
variables: { cartId },
tags: [TAGS.cart],
cache: 'no-store'
tags: [TAGS.cart]
});
// Old carts becomes `null` when you checkout.