npm package: @shopkit/cart
Description: Cart state management, hooks, and services for e-commerce storefronts
Latest version: 0.1.4

# @shopkit/cart

Cart state management for e-commerce storefronts.

## Installation

```bash
npm install @shopkit/cart
# or
bun add @shopkit/cart
```

## Quick Start

### 1. Configure the cart at app startup

```typescript
// app/providers.tsx or similar
import {
  configureCart,
  DefaultCartService,
  DefaultCartStorage,
} from '@shopkit/cart';

// Configure once at app initialization
configureCart({
  merchantName: 'my-store',
  cartService: new DefaultCartService('/api/cart'),
  cartStorage: new DefaultCartStorage('my-store'),
  defaultCurrencyCode: 'USD',
  onCartEvent: (event) => {
    console.log('Cart event:', event);
  },
});
```

### 2. Initialize the cart in your layout

```tsx
// app/layout.tsx
import { CartInitializer } from '@shopkit/cart';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <CartInitializer merchantName="my-store" />
        {children}
      </body>
    </html>
  );
}
```

### 3. Use the cart store in components

```tsx
import { useCartStore, mapProductToCartItem, useCartCount, useCartToggle } from '@shopkit/cart';

function ProductCard({ product }) {
  const { addItem, loadingItems } = useCartStore();
  const cartCount = useCartCount();
  const toggleCart = useCartToggle();

  const variantId = product.variant?.id || product.variantId;
  const isLoading = loadingItems[variantId] || false;

  const handleAddToCart = () => {
    const cartItem = mapProductToCartItem(product);
    addItem(cartItem);
  };

  return (
    <div>
      <h3>{product.title}</h3>
      <button onClick={handleAddToCart} disabled={isLoading}>
        {isLoading ? 'Adding...' : 'Add to Cart'}
      </button>
      <button onClick={toggleCart}>
        Cart ({cartCount})
      </button>
    </div>
  );
}
```

> **Note:** Developers are responsible for mapping product data to the correct `CartItem` format. Use the `mapProductToCartItem` utility for common product structures.

## API Reference

### Configuration

#### `configureCart(config: CartConfig)`

Configure the cart module. Must be called before using any cart functionality.

```typescript
interface CartConfig {
  merchantName: string;
  cartService: ICartService;
  cartStorage: ICartStorage;
  defaultCurrencyCode?: string;
  onCartEvent?: (event: CartEvent) => void;
}
```

### Store

#### `useCartStore()`

The main hook for all cart operations. Provides access to cart state and actions.

**State:**

| Property | Type | Description |
|----------|------|-------------|
| `items` | `CartItem[]` | Items currently in the cart |
| `isOpen` | `boolean` | Whether the cart drawer/modal is open |
| `isLoading` | `boolean` | Whether any cart operation is in progress |
| `loadingItems` | `Record<string, boolean>` | Per-item loading states keyed by variantId |
| `error` | `string \| null` | Current error message, if any |
| `cartToken` | `string \| null` | Cart token for API authentication |
| `checkoutUrl` | `string \| undefined` | URL to proceed to checkout |
| `totalAmount` | `{ amount: number; currencyCode: string } \| undefined` | Server-calculated total |
| `isInitialized` | `boolean` | Whether the cart has been initialized |

**Actions:**

| Method | Signature | Description |
|--------|-----------|-------------|
| `initializeCart` | `(merchantName?: string) => Promise<void>` | Initialize the cart |
| `addItem` | `(item: CartItem) => Promise<void>` | Add a single item to cart |
| `addItems` | `(items: CartItemRequest[]) => Promise<void>` | Add multiple items to cart |
| `removeItem` | `(lineId: string, variantId?: string) => Promise<void>` | Remove an item from cart |
| `incrementItem` | `(lineId: string, variantId?: string) => Promise<void>` | Increment item quantity by 1 |
| `decrementItem` | `(lineId: string, variantId?: string) => Promise<void>` | Decrement item quantity by 1 |
| `setItemQuantity` | `(lineId: string, quantity: number, variantId?: string) => Promise<void>` | Set item quantity directly |
| `clearCart` | `() => Promise<void>` | Clear cart after purchase: removes stale token from localStorage, resets in-memory state, and immediately creates a fresh cart |
| `openCart` | `() => Promise<void>` | Open the cart drawer/modal |
| `closeCart` | `() => void` | Close the cart drawer/modal |
| `toggleCart` | `() => Promise<void>` | Toggle cart open/closed |

**Example - Cart Drawer:**

```tsx
function CartDrawer() {
  const { items, isOpen, closeCart, removeItem, incrementItem, decrementItem, loadingItems } = useCartStore();

  if (!isOpen) return null;

  return (
    <div className="cart-drawer">
      <button onClick={closeCart}>Close</button>
      {items.map((item) => {
        const isItemLoading = loadingItems[item.variantId || item.id] || false;
        return (
          <div key={item.id}>
            <span>{item.title}</span>
            <button onClick={() => decrementItem(item.id, item.variantId)} disabled={isItemLoading}>-</button>
            <span>{item.quantity}</span>
            <button onClick={() => incrementItem(item.id, item.variantId)} disabled={isItemLoading}>+</button>
            <button onClick={() => removeItem(item.id, item.variantId)} disabled={isItemLoading}>Remove</button>
          </div>
        );
      })}
    </div>
  );
}
```

### Per-Item Loading States

The `loadingItems` property tracks loading state for each item individually, allowing you to show loading indicators only for the specific item being modified.

```typescript
const { loadingItems, addItem, incrementItem } = useCartStore();

// Check if a specific product is loading
const isProductLoading = (variantId: string) => loadingItems[variantId] || false;

// Use in component
function ProductButton({ product }) {
  const variantId = product.variant?.id || product.variantId;
  const isLoading = isProductLoading(variantId);

  return (
    <button disabled={isLoading}>
      {isLoading ? <Spinner /> : 'Add to Cart'}
    </button>
  );
}
```

### Selector Hooks

Convenience hooks for specific state slices:

| Hook | Return Type | Description |
|------|-------------|-------------|
| `useCartCount()` | `number` | Number of unique items in cart |
| `useCartTotalQuantity()` | `number` | Total quantity of all items |
| `useCartItems()` | `CartItem[]` | Array of cart items |
| `useCartToggle()` | `() => void` | Function to toggle cart open/closed |
| `useCartStatus()` | `{ isLoading, error }` | Loading and error state |
| `useCartTotal()` | `number` | Client-calculated total price |
| `useCartTotalAmount()` | `{ amount, currencyCode } \| undefined` | Server-calculated total |
| `useIsInCart(productId, variantId?)` | `boolean` | Check if product is in cart |
| `useCartItem(productId, variantId?)` | `CartItem \| undefined` | Get specific cart item |
| `useCheckoutUrl()` | `string \| undefined` | Get checkout URL |

### Utils

#### `mapProductToCartItem(product, variantId?, quantity?, currencyCode?)`

Map a product object to a `CartItem`. Use this to convert your product data structure to the format required by the cart.

```typescript
import { mapProductToCartItem } from '@shopkit/cart';

const cartItem = mapProductToCartItem(product);
addItem(cartItem);
```

#### `formatPrice(amount, currencyCode?, locale?)`

Format a price amount for display.

```typescript
formatPrice(29.99, 'USD', 'en-US'); // "$29.99"
```

#### `calculateCartTotal(items)`

Calculate the total price of cart items (client-side).

#### `calculateSavings(items)`

Calculate total savings from compare-at prices.

### Services

#### `DefaultCartService`

Default HTTP-based cart service implementation.

```typescript
const service = new DefaultCartService('/api/cart');
```

#### `DefaultCartStorage`

Default localStorage-based cart storage.

```typescript
const storage = new DefaultCartStorage('my-store');
```

### Components

#### `<CartInitializer merchantName="..." />`

Initialize the cart on mount. Must be rendered after `configureCart()` is called.

## Custom Services

Implement `ICartService` for custom cart backends:

```typescript
import { ICartService, CartResponse, CartItemRequest } from '@shopkit/cart';

class MyCartService implements ICartService {
  async createCart(merchantName: string): Promise<CartResponse> {
    // Your implementation
  }

  async getCart(cartId: string): Promise<CartResponse> {
    // Your implementation
  }

  async addToCart(cartId: string, items: CartItemRequest[]): Promise<void> {
    // Your implementation
  }

  async removeFromCart(cartId: string, itemIds: string[]): Promise<void> {
    // Your implementation
  }

  async updateCart(cartId: string, items: CartItemRequest[]): Promise<void> {
    // Your implementation
  }
}
```

## License

MIT