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           17x 15x     2x               4x 4x 4x             45x   17x 17x   17x 16x             1x               2x     5x     3x   3x 1x     2x             11x     2x   2x 1x     1x             2x     3x   3x           51x             51x    
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);
}