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 { 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);
}

View File

@ -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) => {

View File

@ -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();
}} }}
> >

View File

@ -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();
}} }}
> >

View File

@ -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);
} }

View File

@ -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: (
<> <>

View File

@ -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.