Skip to contentVibraUI
Dashboards

Shopper's cart

The sample shopper's cart, read one way for every band that sells: the lines, their prices and the stock that caps them, the countries the shop delivers to, and the moment a checkout calls now.

Server-only, since it reads the shelf: a section reads the cart in its own data file and hands the lines to its island, which totals them with storefront-ecommerce's checkout.ts as the shopper changes them. The store keeps no carts, so shopperCart is a rule over the shelf: the three bestsellers under CART_CEILING_CENTS ($30) from three departments, one of each — a first order that sits under the free-delivery line, so a band can show it being crossed. Each line carries the stock on hand, which caps its quantity. Replace shopperCart with your cart store and every band that sells follows. shipsTo lists the countries the shop has customers in, A to Z — a checkout's country list — and checkoutNow hands a checkout REFERENCE_DATE as the moment an order is placed (an ISO string an island can take, for its delivery dates) and the month a card must not have run out before; nextOrderNumber is the number the next order would be given, one past the book's last.

Install

npx shadcn@latest add @vibra/cart-ecommerce

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
shopperCart() => CartLine[]—The sample shopper's cart: { productId, name, category, href, unitCents, qty, stock, stockState } per line.
CART_CEILING_CENTSnumber3000The most a product in the sample cart costs.
shipsTo() => string[]—Every country the shop has a customer in, A to Z.
checkoutNow() => { placedAt: string; month: { year: number; month: number } }—REFERENCE_DATE as the moment an order is placed, and the month a card must not have expired before.
nextOrderNumber() => string—The number the next order would be given: ORD- and one past the book's last.

Dependencies

Source

lib/dashboards/ecommerce/cart.ts
/**
 * The shopper's cart, read one way for every band that sells — a cart page,
 * a drawer, a checkout, the count on the store's header — so the lines, the
 * quantities and the totals agree wherever they are shown.
 *
 * The store keeps no carts, so the sample shopper's is a rule over the shelf:
 * the shop's three bestsellers under `CART_CEILING_CENTS`, each from a
 * different department, one of each. A small first order that sits under the
 * free-delivery line, so a band can show the line being crossed. Replace
 * `shopperCart` with your cart store and every band follows.
 *
 * Server-only, because it reads the shelf: a section hands the lines to its
 * island as props, and the island totals them with `checkout.ts`.
 */
import type { YearMonth } from "@/lib/dashboards/ecommerce/checkout"
import { storeShelf, type StockState } from "@/lib/dashboards/ecommerce/storefront"
import { REFERENCE_DATE, db } from "@/lib/sample-data"

/** The most a product in the sample cart costs, in cents. */
export const CART_CEILING_CENTS = 3_000

/** A line of the cart: the product as the cart shows it, its price, how many, and the most it can hold. */
export type CartLine = {
  productId: string
  name: string
  category: string
  href: string
  /** What one costs, in cents: the catalogue's list price. */
  unitCents: number
  qty: number
  /** Units on hand: a line never holds more. */
  stock: number
  stockState: StockState
}

/**
 * The sample shopper's cart: the bestselling products in stock at or under
 * `CART_CEILING_CENTS`, one per department until there are three, one of each,
 * in the shelf's order.
 */
export function shopperCart(): CartLine[] {
  const departments = new Set<string>()
  const lines: CartLine[] = []
  for (const product of storeShelf()) {
    if (lines.length === 3) break
    if (product.priceCents > CART_CEILING_CENTS || product.stock === 0 || departments.has(product.category)) continue
    departments.add(product.category)
    lines.push({
      productId: product.id,
      name: product.name,
      category: product.category,
      href: product.href,
      unitCents: product.priceCents,
      qty: 1,
      stock: product.stock,
      stockState: product.stockState,
    })
  }
  return lines
}

/** Every country the shop has a customer in, A to Z: the places it delivers to. */
export function shipsTo(): string[] {
  return [...new Set(db.customers.all().map((customer) => customer.country))].sort((a, b) => a.localeCompare(b))
}

/**
 * "Now", as a checkout needs it: the instant an order placed now would be
 * placed at — `REFERENCE_DATE`, as an ISO string an island can take — and the
 * month a card must not have run out before.
 */
export function checkoutNow(): { placedAt: string; month: YearMonth } {
  return {
    placedAt: REFERENCE_DATE.toISOString(),
    month: { year: REFERENCE_DATE.getUTCFullYear(), month: REFERENCE_DATE.getUTCMonth() + 1 },
  }
}

/** The number the next order would be given: one past the book's last, in its own series. */
export function nextOrderNumber(): string {
  return `ORD-${100_000 + db.orders.all().length + 1}`
}