All files / lib/cart cart-reducer.ts

100% Statements 34/34
95% Branches 19/20
100% Functions 13/13
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106    4x           9x 7x     2x               3x 3x 3x             27x   9x 9x   9x 8x             1x               2x     5x     2x   2x 1x     1x             3x     2x   2x 1x     1x             2x     2x   2x           22x             22x    
import type { CartAction, CartLineItem, CartState } from "@/lib/cart/cart-types";
 
export const initialCartState: CartState = {
  isOpen: false,
  items: [],
};
 
function normalizeQuantity(quantity: number | undefined): number {
  if (!quantity || !Number.isFinite(quantity)) {
    return 1;
  }
 
  return Math.max(1, Math.floor(quantity));
}
 
function replaceLineItem(
  items: readonly CartLineItem[],
  slug: string,
  quantity: number,
): CartLineItem[] {
  return items
    .map((item) => (item.product.slug === slug ? { ...item, quantity } : item))
    .filter((item) => item.quantity > 0);
}
 
/**
 * Pure cart state reducer used by the cart context and unit tests.
 */
export function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case "add": {
      const quantity = normalizeQuantity(action.quantity);
      const existingItem = state.items.find((item) => item.product.slug === action.product.slug);
 
      if (!existingItem) {
        return {
          ...state,
          isOpen: true,
          items: [...state.items, { product: action.product, quantity }],
        };
      }
 
      return {
        ...state,
        isOpen: true,
        items: replaceLineItem(state.items, action.product.slug, existingItem.quantity + quantity),
      };
    }
 
    case "clear":
      return { ...state, items: [] };
 
    case "close":
      return { ...state, isOpen: false };
 
    case "decrement": {
      const existingItem = state.items.find((item) => item.product.slug === action.slug);
 
      if (!existingItem) {
        return state;
      }
 
      return {
        ...state,
        items: replaceLineItem(state.items, action.slug, existingItem.quantity - 1),
      };
    }
 
    case "hydrate":
      return { ...state, items: action.items };
 
    case "increment": {
      const existingItem = state.items.find((item) => item.product.slug === action.slug);
 
      if (!existingItem) {
        return state;
      }
 
      return {
        ...state,
        items: replaceLineItem(state.items, action.slug, existingItem.quantity + 1),
      };
    }
 
    case "open":
      return { ...state, isOpen: true };
 
    case "remove":
      return {
        ...state,
        items: state.items.filter((item) => item.product.slug !== action.slug),
      };
  }
}
 
export function getCartItemCount(items: readonly CartLineItem[]): number {
  return items.reduce((total, item) => total + item.quantity, 0);
}
 
/**
 * Calculates the shopper-facing subtotal in integer cents.
 */
export function getCartSubtotal(items: readonly CartLineItem[]): number {
  return items.reduce((total, item) => total + item.product.priceCents * item.quantity, 0);
}