mirror of
https://github.com/vercel/commerce.git
synced 2025-05-03 00:07:52 +00:00
commit 408d6eb7583470eb84fd0e85895f97dad864b981 Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 21:28:45 2024 -0500 added content commit af62089872de543c8f741c3092f431a8b790feec Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 20:43:02 2024 -0500 fixed product recommendations commit 5c921be7b1eab4ea3b4acc922d2bde842bb0c5c8 Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 20:33:28 2024 -0500 fixed cart total commit 63e150e822ab0b4f7690221ee5d1eafaaf5f930a Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 20:14:47 2024 -0500 fixed update cart commit 85bd6bee403e19c7b3f66c0d6e938a8432cee62b Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 19:00:42 2024 -0500 remove unnecessary cookie usage from sfcc calls commit 2401bed81143508993fdd403d9d5a419ac8904e5 Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 18:55:39 2024 -0500 fixed issue with broken getCart commit f8cc8c3c3c1c64d7cf4b69a60ed87497ad626e65 Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 18:23:03 2024 -0500 updated lib/sfcc for guest tokens commit bd6129e3ca15125c87c8186e9ff27d835fb2f683 Author: Alex <alex.hawley@vercel.com> Date: Wed Sep 4 15:19:40 2024 -0500 added now required channel_id commit eeb805fd11219d8512c1cadefe047019d63d4b60 Author: Alex <alex.hawley@vercel.com> Date: Tue Sep 3 17:43:27 2024 -0500 split out scapi commit e4f3bb1c827137245367152c1ff0401db76e7082 Author: Alex <alex.hawley@vercel.com> Date: Tue Sep 3 16:55:11 2024 -0500 carried over sfcc work commit 2616869f56f330f44ad3dfff9ad488eaaf1dbe51 Author: Alex <alex.hawley@vercel.com> Date: Thu Aug 22 15:03:30 2024 -0400 initial sfcc work
185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
import { Cart, CartItem, Product, ProductVariant } from 'lib/sfcc/types';
|
|
import React, { createContext, use, useContext, useMemo, useOptimistic } from 'react';
|
|
|
|
type UpdateType = 'plus' | 'minus' | 'delete';
|
|
|
|
type CartAction =
|
|
| { type: 'UPDATE_ITEM'; payload: { merchandiseId: string; updateType: UpdateType } }
|
|
| { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product } };
|
|
|
|
type CartContextType = {
|
|
cart: Cart | undefined;
|
|
updateCartItem: (merchandiseId: string, updateType: UpdateType) => void;
|
|
addCartItem: (variant: ProductVariant, product: Product) => void;
|
|
};
|
|
|
|
const CartContext = createContext<CartContextType | undefined>(undefined);
|
|
|
|
function calculateItemCost(quantity: number, price: string): string {
|
|
return (Number(price) * quantity).toString();
|
|
}
|
|
|
|
function updateCartItem(item: CartItem, updateType: UpdateType): CartItem | null {
|
|
if (updateType === 'delete') return null;
|
|
|
|
const newQuantity = updateType === 'plus' ? item.quantity + 1 : item.quantity - 1;
|
|
if (newQuantity === 0) return null;
|
|
|
|
const singleItemAmount = Number(item.cost.totalAmount.amount) / item.quantity;
|
|
const newTotalAmount = calculateItemCost(newQuantity, singleItemAmount.toString());
|
|
|
|
return {
|
|
...item,
|
|
quantity: newQuantity,
|
|
cost: {
|
|
...item.cost,
|
|
totalAmount: {
|
|
...item.cost.totalAmount,
|
|
amount: newTotalAmount
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function createOrUpdateCartItem(
|
|
existingItem: CartItem | undefined,
|
|
variant: ProductVariant,
|
|
product: Product
|
|
): CartItem {
|
|
const quantity = existingItem ? existingItem.quantity + 1 : 1;
|
|
const totalAmount = calculateItemCost(quantity, variant.price.amount);
|
|
|
|
return {
|
|
id: existingItem?.id,
|
|
quantity,
|
|
cost: {
|
|
totalAmount: {
|
|
amount: totalAmount,
|
|
currencyCode: variant.price.currencyCode
|
|
}
|
|
},
|
|
merchandise: {
|
|
id: variant.id,
|
|
title: variant.title,
|
|
selectedOptions: variant.selectedOptions,
|
|
product: {
|
|
id: product.id,
|
|
handle: product.handle,
|
|
title: product.title,
|
|
featuredImage: product.featuredImage
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function updateCartTotals(lines: CartItem[]): Pick<Cart, 'totalQuantity' | 'cost'> {
|
|
const totalQuantity = lines.reduce((sum, item) => sum + item.quantity, 0);
|
|
const totalAmount = lines.reduce((sum, item) => sum + Number(item.cost.totalAmount.amount), 0);
|
|
const currencyCode = lines[0]?.cost.totalAmount.currencyCode ?? 'USD';
|
|
|
|
return {
|
|
totalQuantity,
|
|
cost: {
|
|
subtotalAmount: { amount: totalAmount.toString(), currencyCode },
|
|
totalAmount: { amount: totalAmount.toString(), currencyCode },
|
|
totalTaxAmount: { amount: '0', currencyCode }
|
|
}
|
|
};
|
|
}
|
|
|
|
function createEmptyCart(): Cart {
|
|
return {
|
|
id: undefined,
|
|
checkoutUrl: '',
|
|
totalQuantity: 0,
|
|
lines: [],
|
|
cost: {
|
|
subtotalAmount: { amount: '0', currencyCode: 'USD' },
|
|
totalAmount: { amount: '0', currencyCode: 'USD' },
|
|
totalTaxAmount: { amount: '0', currencyCode: 'USD' }
|
|
}
|
|
};
|
|
}
|
|
|
|
function cartReducer(state: Cart | undefined, action: CartAction): Cart {
|
|
const currentCart = state || createEmptyCart();
|
|
|
|
switch (action.type) {
|
|
case 'UPDATE_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 {
|
|
...currentCart,
|
|
lines: [],
|
|
totalQuantity: 0,
|
|
cost: {
|
|
...currentCart.cost,
|
|
totalAmount: { ...currentCart.cost.totalAmount, amount: '0' }
|
|
}
|
|
};
|
|
}
|
|
|
|
return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines };
|
|
}
|
|
case 'ADD_ITEM': {
|
|
const { variant, product } = action.payload;
|
|
const existingItem = currentCart.lines.find((item) => item.merchandise.id === variant.id);
|
|
const updatedItem = createOrUpdateCartItem(existingItem, variant, product);
|
|
|
|
const updatedLines = existingItem
|
|
? currentCart.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item))
|
|
: [...currentCart.lines, updatedItem];
|
|
|
|
return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines };
|
|
}
|
|
default:
|
|
return currentCart;
|
|
}
|
|
}
|
|
|
|
export function CartProvider({
|
|
children,
|
|
cartPromise
|
|
}: {
|
|
children: React.ReactNode;
|
|
cartPromise: Promise<Cart | undefined>;
|
|
}) {
|
|
const initialCart = use(cartPromise);
|
|
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
|
|
|
|
const updateCartItem = (merchandiseId: string, updateType: UpdateType) => {
|
|
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
|
|
};
|
|
|
|
const addCartItem = (variant: ProductVariant, product: Product) => {
|
|
updateOptimisticCart({ type: 'ADD_ITEM', payload: { variant, product } });
|
|
};
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
cart: optimisticCart,
|
|
updateCartItem,
|
|
addCartItem
|
|
}),
|
|
[optimisticCart]
|
|
);
|
|
|
|
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
|
}
|
|
|
|
export function useCart() {
|
|
const context = useContext(CartContext);
|
|
if (context === undefined) {
|
|
throw new Error('useCart must be used within a CartProvider');
|
|
}
|
|
return context;
|
|
}
|