npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@weareconceptstudio/cart

v0.1.0

Published

Concept Studio Cart

Readme

@weareconceptstudio/cart

Zustand-based cart state for Concept Studio frontends. Supports simple products, variant products (color/size), guest cookie carts, logged-in server carts, checkout, promotions, gifts, and utensils.

Project-specific behavior (tour bookings, custom API fields, alternate endpoints) is configured outside the package via CartIntegration — the cart package itself stays generic.


Table of contents

  1. Installation
  2. Quick start
  3. Architecture
  4. Initialization
  5. Cart flows
  6. Hooks
  7. Store API
  8. Configuration (CartConfig)
  9. Cart integration (per project)
  10. Backend API contract
  11. Types
  12. Utilities & defaults
  13. Examples
  14. Package structure
  15. Migration from Context API

Installation

npm install @weareconceptstudio/cart zustand

Peer dependency: @weareconceptstudio/core (API client, cookies, translations).


Quick start

With @weareconceptstudio/account (recommended)

AccountProvider initializes the cart automatically:

import { AccountProvider } from '@weareconceptstudio/account';

<AccountProvider
  globalSettings={{
    hasVariants: false,
    supportsGuestCart: true,
    hasGiftSystem: true,
    hasUtensilsSystem: true,
  }}
  cartIntegration={myCartIntegration} // optional — see below
>
  {children}
</AccountProvider>

Manual initialization

import { initCartStore } from '@weareconceptstudio/cart';

initCartStore(
  {
    hasVariants: false,
    supportsGuestCart: true,
    integration: myCartIntegration, // optional
  },
  isLoggedIn,
  selectedLang,
  user
);

Add to cart in a component

import { useSimpleCart } from '@weareconceptstudio/cart';

function ProductCard({ productId }) {
  const { addProduct, getProductQty } = useSimpleCart();

  return (
    <button onClick={() => addProduct(productId, 1)}>
      Add to cart ({getProductQty(productId)})
    </button>
  );
}

Architecture

┌─────────────────────────────────────────────────────────────┐
│  React components                                           │
│  useCart · useSimpleCart · useVariantCart · useCheckout     │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  Zustand store (cartStore)                                  │
│  addItem · removeItem · getCart · checkout · mergeCart …    │
└───────────────────────────┬─────────────────────────────────┘
                            │
              ┌─────────────┴─────────────┐
              │                           │
     isLoggedIn = true            isLoggedIn = false
              │                           │
     Server API (auth)            Cookie + guest-cart API
     toggle-cart-item             (if supportsGuestCart)
     delete-cart-item
              │                           │
              └─────────────┬─────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  CartIntegration (project config)                           │
│  endpoints · buildAddItemRequest · guest.buildCookieItem …  │
└─────────────────────────────────────────────────────────────┘

Design principle: the store owns flow and state; projects inject how requests are built and how guest cookies behave. Never fork the package for one backend.


Initialization

initCartStore(config, isLoggedIn, selectedLang?, user?)

Call once when the app starts (or when auth/language changes). AccountProvider does this for you.

| Argument | Description | |----------|-------------| | config | CartConfig — flags + optional integration | | isLoggedIn | Whether the user has a valid session | | selectedLang | Current locale (default 'en') | | user | User object (payment method, cards, etc.) |

Also sets isCheckoutPage / isCartPage from the URL, hydrates checkoutData from sessionStorage, and handles ?cardSaved=1&cardId= query cleanup.


Cart flows

Logged-in user

| Action | Endpoint (default) | Method | |--------|-------------------|--------| | Fetch cart | cart | GET | | Add / update line | toggle-cart-item | POST | | Remove line | delete-cart-item | POST | | Clear cart | clear-cart | POST | | Checkout | checkout | POST | | Merge guest cookie on login | merge-cart | POST | | Sync variant cookie cart | sync-cart | POST | | Reorder | reorder | POST |

Each request body is built by CartIntegration hooks (defaults merge checkoutData, params, and cartResourceType).

Guest user (supportsGuestCart !== false)

  1. Add / update / remove → update browser cookie (config.cookieName, default cart).
  2. Fetch cartPOST guest-cart with { items, cart, …checkoutData }.
  3. CheckoutPOST checkout with checkout data + guest items (cookie/session).
  4. On loginmerge-cart with cookie items, then cookie is cleared.

Set supportsGuestCart: false for backends without guest endpoints (cart stays empty until login).

cartResourceType

Sent on get/add/remove requests:

  • 'cart' on /cart or /checkout pages
  • 'cart-summary' elsewhere (e.g. header mini-cart)

Override via buildGetCartParams / buildAddItemRequest if your API differs.


Hooks

useCart()

Full cart + checkout API. Use in cart pages, checkout, and custom UIs.

State: items, itemsCount, subtotal, total, formatted_total, currency, shippingCost, discount, cashback, appliedPromotions, utensils, messages, loading, actionLoading, checkoutData, promotion_code, promotion_error, useBalance, user, config, isCheckoutPage, isCartPage, isLoggedIn, isRedirecting, error, checkoutBtnDisabled, hasOutOfStockItems, …

Actions:

| Method | Description | |--------|-------------| | addItem(params) | Add or update a line | | removeItem(params) | Remove a line | | updateItem(params) | Update qty (qty: 0 removes) | | clearCart() | Empty cart | | getCart(params?, showLoading?) | Refresh from server / guest-cart | | mergeCart() | Merge cookie into server cart (after login) | | syncCart() | Sync cookie cart (variants) | | checkout() | Submit order | | updateCheckoutData(partial) | Merge checkout fields (persisted to session) | | fillCheckoutData(keyOrObject, value?) | Legacy flexible checkout update | | fillCart(key, value) | Update checkout field + refetch cart | | setGifts / togglePromotion | Promotion gifts | | setUtensils / toggleUtensil | Utensils selection | | reorder(orderId) | Reorder + redirect to cart | | resetCart() | Reset store + clear cookie/session checkout | | setUser(user) | Update user on store |

Legacy (backward compatible):

| Method | Maps to | |--------|---------| | toggleCartItem({ productId, qty, variantId?, optionId?, itemId? }) | addItem / removeItem / updateItem | | deleteCartItem(params) | removeItem | | editCartItem(params) | remove old variant + add new | | handleCheckout | checkout |

addItem / removeItem params:

// Add
addItem({
  productId: 123,
  qty: 1,
  variantId?: number,
  optionId?: number,
  itemId?: number,      // existing line id when editing
  [key: string]: unknown, // extra fields — use CartIntegration to persist/send
});

// Remove
removeItem({
  productId: 123,
  variantId?: number,
  optionId?: number,
  itemId?: number,
  id?: number,
  cartItemId?: number,
});

useSimpleCart()

For shops without variants.

  • addProduct(productId, qty?)
  • removeProduct(productId)
  • updateProductQty(productId, qty)
  • isInCart(productId)
  • getProductQty(productId)
  • items, itemsCount, loading

useVariantCart()

For shops with variants (color, size, …). Requires hasVariants: true.

  • addVariantProduct(productId, variantId, optionId, qty?)
  • removeVariantProduct(...)
  • updateVariantProductQty(...)
  • isVariantInCart(...)
  • getVariantQty(...)

useCheckout()

Checkout helpers.

  • checkout() — complete order
  • setAddress(addressId)
  • setPaymentMethod(paymentType, cardId?)
  • setNote(note)
  • applyPromoCode(code)
  • isReadyForCheckout()
  • checkoutData, itemsCount, total, subtotal, loading

Store API

useCartStore()

Direct Zustand access. Prefer hooks in components.

import { useCartStore } from '@weareconceptstudio/cart';

const itemsCount = useCartStore((s) => s.itemsCount);
await useCartStore.getState().addItem({ productId: 1, qty: 1 });

Configuration (CartConfig)

interface CartConfig {
  hasVariants?: boolean;           // default false
  supportsGuestCart?: boolean;     // default true
  hasGifts?: boolean;              // default true
  hasUtensils?: boolean;           // default true
  cookieName?: string;             // default 'cart'
  abandonPaymentEndpoint?: string; // e.g. 'checkout/abandon-payment'
  integration?: CartIntegration;
  guestCart?: GuestCartIntegration; // deprecated → use integration.guest
}

| Flag | Effect | |------|--------| | hasVariants | Variant matching in add/remove; syncCart vs mergeCart on login | | supportsGuestCart | Guest cookie + guest-cart API; when false, guest cart ops are no-ops | | hasGifts | Gift selection in checkout; stripped from requests when false | | hasUtensils | Utensils in checkout; stripped when false | | cookieName | Guest cart cookie key | | abandonPaymentEndpoint | Called when user returns from failed redirect payment |


Cart integration (per project)

Define a CartIntegration object in your app (e.g. src/helpers/cartIntegration.ts) and pass it as cartIntegration on AccountProvider or integration in initCartStore.

defineCartIntegration(overrides)

import { defineCartIntegration } from '@weareconceptstudio/cart';

export const myCartIntegration = defineCartIntegration({
  // only override what you need
});

Hooks reference

| Hook | When to override | |------|------------------| | endpoints | Non-standard API paths | | guest.buildCookieItem | Extra fields stored in guest cookie | | guest.matchCookieItems | How guest lines match on upsert/remove | | guest.upsertCookieItems | Full control of cookie array updates | | guest.buildGuestCartRequest | Extra POST guest-cart body fields | | guest.buildRemoveCookieTarget | Shape of remove target for cookie matching | | findCartItem | Match existing server cart line | | buildAddItemRequest | Logged-in add/update POST body | | buildRemoveItemRequest | Logged-in remove POST body | | buildGetCartParams | GET cart query/body | | buildMergeCartRequest | Login merge body | | buildSyncCartRequest | Variant sync body | | buildCheckoutRequest | Checkout POST body (guest items by default) | | normalizeCartResponse | Map API JSON → store shape | | shouldRetryRemoveItem | Backend-specific remove error handling |

CartIntegrationContext

Passed to most builders:

{
  config: CartConfig;
  isLoggedIn: boolean;
  selectedLang: string;
  checkoutData: CheckoutData;
  items: CartItemWithProduct[];
  hasVariants: boolean;
  cartResourceType: 'cart' | 'cart-summary';
  user: unknown;
}

Default endpoints

import { DEFAULT_CART_ENDPOINTS } from '@weareconceptstudio/cart';

// {
//   getCart: 'cart',
//   addItem: 'toggle-cart-item',
//   removeItem: 'delete-cart-item',
//   clearCart: 'clear-cart',
//   guestCart: 'guest-cart',
//   mergeCart: 'merge-cart',
//   syncCart: 'sync-cart',
//   checkout: 'checkout',
//   reorder: 'reorder',
// }

Standard shop (no integration)

Defaults work out of the box if your backend matches the contract below.

Custom fields (generic shop)

import {
  defineCartIntegration,
  defaultBuildAddItemRequest,
  defaultBuildCookieItem,
  pickCartFields,
} from '@weareconceptstudio/cart';

export const myCartIntegration = defineCartIntegration({
  guest: {
    buildCookieItem(params, existing, hasVariants) {
      return {
        ...defaultBuildCookieItem(params, existing, hasVariants),
        ...pickCartFields(params, ['engraving_text']),
      };
    },
  },
  buildAddItemRequest(params, ctx, matched) {
    return {
      ...defaultBuildAddItemRequest(params, ctx, matched),
      engraving_text: params.engraving_text,
    };
  },
});

Tour / booking project (Silk Road pattern)

Keep all tour-specific logic in the project file — not in the cart package:

import {
  defineCartIntegration,
  defaultBuildAddItemRequest,
  defaultBuildCookieItem,
  defaultMatchCookieItems,
  pickCartFields,
} from '@weareconceptstudio/cart';

const GUEST_KEYS = ['tour_id', 'adults', 'children', 'start_date', 'addons', 'clientId'] as const;

export const silkCartIntegration = defineCartIntegration({
  guest: {
    buildCookieItem(params, existing, hasVariants) {
      const base = defaultBuildCookieItem(params, existing, hasVariants);
      return {
        ...base,
        ...pickCartFields(params, GUEST_KEYS),
        clientId: params.itemId ?? params.clientId ?? Date.now(),
        tour_id: params.tour_id ?? params.productId,
      };
    },
    matchCookieItems(item1, item2, hasVariants) {
      const id1 = item1.clientId ?? item1.itemId;
      const id2 = item2.clientId ?? item2.itemId;
      if (id1 != null && id2 != null) return id1 == id2;
      if (!hasVariants) return item1.productId === item2.productId;
      return defaultMatchCookieItems(item1, item2, hasVariants);
    },
  },
  buildAddItemRequest(params, ctx, matched) {
    return {
      ...defaultBuildAddItemRequest(params, ctx, matched),
      tour_id: params.tour_id ?? params.productId,
    };
  },
  findCartItem(params, items) {
    if (params.itemId != null) {
      return items.find((i) => i.id === params.itemId || i.itemId === params.itemId);
    }
    return items.find((i) => Number(i.product?.id) === Number(params.productId));
  },
});

Wire it:

// account config / AppProviders
cartIntegration: silkCartIntegration,
globalSettings: { supportsGuestCart: true, ... },

Backward compatibility

  • guestCart prop on AccountProvider → treated as integration.guest
  • GuestCartHooks type → alias for GuestCartIntegration

Backend API contract

Default integration expects these endpoints (all under your API base + language prefix from @weareconceptstudio/core).

GET cart

Query: checkout-related fields + cartResourceType.

Response (typical):

{
  "items": [{ "id": 1, "product": { "id": 10, "name": "..." }, "qty": 2 }],
  "itemsCount": 1,
  "subtotal": 100,
  "total": 100,
  "formatted_total": "$100",
  "currency": "USD",
  "utensils": [],
  "messages": []
}

POST toggle-cart-item

Body: productId, qty, optional variantId / optionId / itemId, cartResourceType, checkout fields.

Returns updated cart (same shape as GET).

POST delete-cart-item

Body: productId, optional ids, cartResourceType, checkout fields.

POST guest-cart

Body: { items: CookieCartItem[], cart: CookieCartItem[], ...checkoutData }.

Cookie items are minimal lines (productId, qty, + project fields from guest.buildCookieItem).

Returns cart with priced items (same shape as logged-in cart).

POST merge-cart

Body: { items: CookieCartItem[] }. Called after login.

POST checkout

Body: CheckoutData fields + optional items for guests.

Response: { redirect_url? } or { url, form } (Idram) or success payload.

POST clear-cart · POST sync-cart · POST reorder

As used by store actions above.

Override paths via integration.endpoints if your backend differs.


Types

Cookie / request items

interface BaseCartItem {
  productId: number;
  qty: number;
  [key: string]: unknown; // project-specific guest fields
}

interface VariantCartItem extends BaseCartItem {
  variantId: number;
  optionId: number;
  itemId?: number;
}

type CartItem = BaseCartItem | VariantCartItem;

API cart line

interface CartItemWithProduct {
  id?: number;
  product: CartProduct;
  qty: number;
  variantId?: number;
  optionId?: number;
  itemId?: number;
  is_gift?: boolean;
  appliedPromotion?: unknown;
  // stock / qty adjustment fields...
}

Checkout data (persisted in sessionStorage)

interface CheckoutData {
  addressId: number | null;
  billingAddressId?: number | null;
  shippingAddressId?: number | null;
  useBalance: number | string | null;
  note: string;
  paymentType: string | number;
  gifts: GiftItem[];
  card_id: number | null;
  excludedPromotions: number[];
  promotion_code: string | null;
  promotion_error: string | null;
  checkout_error?: string | null;
  utensils: UtensilItem[];
  fulfillmentType?: 'delivery' | 'takeaway' | null;
  fulfillmentBranchId?: number | null;
  scheduledDate?: string | null;
  scheduledTime?: string | null;
}

Projects may store extra keys in checkoutData via fillCheckoutData (e.g. tour UI state); they are sent to the API when included in buildAddItemRequest / checkout unless stripped by cleanObject.


Utilities & defaults

Exported from @weareconceptstudio/cart:

| Export | Purpose | |--------|---------| | resolveCartIntegration(config) | Merge config + defaults into resolved hooks | | defineCartIntegration(overrides) | Declare project integration | | DEFAULT_CART_ENDPOINTS | Default API paths | | defaultBuildAddItemRequest | Standard add payload | | defaultBuildRemoveItemRequest | Standard remove payload | | defaultBuildCookieItem | Standard guest cookie line | | defaultBuildRemoveCookieTarget | Standard guest remove target | | defaultFindCartItem | Standard server line matching | | defaultMatchCookieItems | Match by itemId/clientId or product/variant | | defaultNormalizeCartResponse | itemsCount / items_qty normalization | | pickCartFields(source, keys) | Pick params for cookies/API | | getCartCookieName(config) | Resolve cookie name | | parseCookieCart / serializeCartToCookie | Cookie JSON helpers | | itemsMatch / addOrUpdateItem / removeItem | Low-level cookie array ops | | cleanObject | Remove null/empty from request bodies |


Examples

Mini cart

import { useCart } from '@weareconceptstudio/cart';

function MiniCart() {
  const { itemsCount, formatted_total, loading } = useCart();
  if (loading) return null;
  return <span>{itemsCount} · {formatted_total}</span>;
}

Checkout with terms gate

import { useCart } from '@weareconceptstudio/cart';

function CheckoutButton({ termsAccepted }) {
  const { checkout, checkoutBtnDisabled, itemsCount } = useCart();
  const disabled = checkoutBtnDisabled || !termsAccepted;

  return (
    <button
      disabled={disabled}
      onClick={() => checkout()}
    >
      Pay ({itemsCount})
    </button>
  );
}

Pass extra params from product page

const cart = useCart();

await cart.addItem({
  productId: tourId,
  qty: 1,
  tour_id: tourId,
  adults: 2,
  children: 0,
  start_date: '2026-07-01',
  addons: { lunch: true, tickets: false, guide_id: 5 },
});

Tour fields are persisted only if cartIntegration.guest.buildCookieItem and buildAddItemRequest include them (see Silk example).


Package structure

packages/cart/src/
├── index.ts              # Public exports
├── hooks/
│   ├── useCart.ts          # useCart, useSimpleCart, useVariantCart, useCheckout
│   └── index.ts
├── store/
│   ├── cartStore.ts        # Zustand store + initCartStore
│   ├── helpers.ts          # sessionStorage, cartResourceType, checkout cleanup
│   └── index.ts
├── integration/
│   ├── types.ts            # CartIntegration, endpoints, context
│   ├── defaults.ts         # Default hooks + DEFAULT_CART_ENDPOINTS
│   ├── resolve.ts          # resolveCartIntegration, defineCartIntegration
│   └── index.ts
├── types/
│   └── index.ts            # CartItem, CartState, CartConfig, …
└── utils/
    ├── helpers.ts          # Cookie/item array utilities
    └── guestCart.ts        # Deprecated shims

Migration from Context API

| Context API | Zustand | |-------------|---------| | toggleCartItem({ productId, qty }) | useSimpleCart().addProduct(id, qty) or useCart().toggleCartItem(...) | | deleteCartItem(...) | useCart().deleteCartItem(...) | | fillCheckoutData(...) | useCart().fillCheckoutData(...) | | Context provider | initCartStore / AccountProvider |


License

ISC © Concept Studio