chore: transfer repo
This commit is contained in:
217
components/cart/cart-context.tsx
Normal file
217
components/cart/cart-context.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
'use client';
|
||||
|
||||
import type { Cart, CartItem, Product, ProductVariant } from 'lib/shopify/types';
|
||||
import React, { createContext, use, useContext, useEffect, useMemo, useOptimistic, useTransition } from 'react';
|
||||
|
||||
type UpdateType = 'plus' | 'minus' | 'delete';
|
||||
|
||||
type CartAction =
|
||||
| { type: 'UPDATE_ITEM'; payload: { merchandiseId: string; updateType: UpdateType; itemId?: string } }
|
||||
| { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product, quantity: number } };
|
||||
|
||||
type CartContextType = {
|
||||
cart: Cart | undefined;
|
||||
updateCartItem: (merchandiseId: string, updateType: UpdateType, itemId?: string) => void;
|
||||
addCartItem: (variant: ProductVariant, product: Product, quantity?: number) => 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,
|
||||
quantity: number = 1
|
||||
): CartItem {
|
||||
const newQuantity = existingItem ? existingItem.quantity + quantity : quantity;
|
||||
const totalAmount = calculateItemCost(newQuantity, variant.price.amount);
|
||||
|
||||
return {
|
||||
id: existingItem?.id,
|
||||
quantity: newQuantity,
|
||||
attributes: existingItem?.attributes || [],
|
||||
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, itemId } = action.payload;
|
||||
const updatedLines = currentCart.lines
|
||||
.map((item) => {
|
||||
// If itemId is provided, only update the specific item
|
||||
if (itemId && item.id !== itemId) {
|
||||
return item;
|
||||
}
|
||||
// Otherwise, update by merchandise ID
|
||||
if (!itemId && item.merchandise.id !== merchandiseId) {
|
||||
return item;
|
||||
}
|
||||
// Update the matching item
|
||||
return updateCartItem(item, updateType);
|
||||
})
|
||||
.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, quantity = 1 } = action.payload;
|
||||
const existingItem = currentCart.lines.find((item) => item.merchandise.id === variant.id);
|
||||
const updatedItem = createOrUpdateCartItem(existingItem, variant, product, quantity);
|
||||
|
||||
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 [isPending, startTransition] = useTransition();
|
||||
|
||||
// More detailed debugging for cart loading
|
||||
useEffect(() => {
|
||||
if (initialCart?.lines?.length) {
|
||||
// Detailed inspection of the cart items and their attributes
|
||||
const itemsWithAttributes = initialCart.lines.filter(item =>
|
||||
item.attributes && item.attributes.length > 0
|
||||
);
|
||||
|
||||
// Log each item with its attributes for debugging
|
||||
initialCart.lines.forEach((item, index) => {
|
||||
const attrs = item.attributes || [];
|
||||
const boxType = attrs.find(a => a.key === 'box_type')?.value;
|
||||
const boxGroupId = attrs.find(a => a.key === 'box_group_id')?.value;
|
||||
});
|
||||
}
|
||||
}, [initialCart]);
|
||||
|
||||
const updateCartItem = (merchandiseId: string, updateType: UpdateType, itemId?: string) => {
|
||||
startTransition(() => {
|
||||
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType, itemId } });
|
||||
});
|
||||
};
|
||||
|
||||
const addCartItem = (variant: ProductVariant, product: Product, quantity: number = 1) => {
|
||||
startTransition(() => {
|
||||
updateOptimisticCart({ type: 'ADD_ITEM', payload: { variant, product, quantity } });
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user