Skip to content
~sami-ullah

Concept project · runs in your browserTypeScript · React.js · Next.js · zod

E-commerce Storefront with Test-Mode Checkout Demo

A small shop that works end to end. Filter and search the catalogue, pick a size and colour, fill the cart and check out. The payment step is a clearly marked test mode with three test cards: one succeeds, one is declined and one asks for 3-D Secure. Open the dev panel to see the API calls and SQL behind each step.

Read the case study →

Interactive demo

samiullah.tech/lab/ecommerce-storefront

18 products · filters are saved in the URL

## How it works

The browser only owns the cart: a list of SKUs and quantities. Every price you pay is worked out again on the “server” (api.ts) from the catalogue. Press tamper cart price in the dev panel to set a line to $0.01 in localStorage, then check out: the quote and the order ignore it and say so.

Placing an order is one transaction. It locks the variant rows with SELECT … FOR UPDATE in SKU order, checks stock, decrements it, and inserts the order and its lines. If any line is short, nothing is written and you get 409. Rival buys my item simulates a second shopper taking the last units so you can see that happen.

Payment follows the PaymentIntent pattern. The intent is created with an Idempotency-Key, so a retried request returns the same intent instead of a second charge. The order is only marked paid when the payment_intent.succeeded webhook arrives, and webhook event ids are stored so a duplicate delivery changes nothing.

## View source

The code running in the window above, exactly as deployed.

app/lab/ecommerce-storefront/api.ts506 lines
/**
 * The storefront "server". Each export mirrors one NestJS controller method; the body is what the
 * service layer would do inside PostgreSQL. Handlers are pure: they take the current DB and return
 * the next one, so a failed transaction simply returns the untouched DB (that is the ROLLBACK).
 *
 * Every response carries `sql`: the statements the production service would send, in order.
 */
import { z } from "zod";
import {
  findVariant,
  fromPrice,
  totals,
  totalStock,
  variantLabel,
  type Category,
  type DB,
  type Order,
  type OrderLine,
  type PaymentIntent,
  type Product,
} from "./db";

export type ApiError = { error: string; message: string; fields?: Record<string, string> };
export type Res<T> =
  | { ok: true; status: number; body: T; sql: string[]; headers?: Record<string, string> }
  | { ok: false; status: number; body: ApiError; sql: string[]; headers?: Record<string, string> };
export type Handled<T> = { db: DB; res: Res<T> };

const ok = <T>(db: DB, status: number, body: T, sql: string[], headers?: Record<string, string>): Handled<T> => ({
  db,
  res: { ok: true, status, body, sql, headers },
});
const fail = <T>(db: DB, status: number, error: string, message: string, sql: string[], fields?: Record<string, string>): Handled<T> => ({
  db,
  res: { ok: false, status, body: { error, message, fields }, sql },
});

/* ------------------------------------------------------------------ */
/* GET /api/products                                                   */
/* ------------------------------------------------------------------ */

export type Sort = "featured" | "price-asc" | "price-desc" | "name";
export type ProductQuery = { q: string; category: Category | "all"; min: number | null; max: number | null; sort: Sort; inStock: boolean };

export function listProducts(db: DB, query: ProductQuery): Handled<Product[]> {
  const where: string[] = [];
  const having: string[] = [];
  const params: (string | number)[] = [];
  const p = (v: string | number) => (params.push(v), `$${params.length}`);

  let rows = db.products;
  if (query.category !== "all") {
    where.push(`p.category = ${p(query.category)}`);
    rows = rows.filter((r) => r.category === query.category);
  }
  const q = query.q.trim().toLowerCase();
  if (q) {
    where.push(`p.search @@ websearch_to_tsquery('simple', ${p(q)})`);
    const terms = q.split(/\s+/);
    rows = rows.filter((r) => {
      const hay = `${r.name} ${r.brand} ${r.description} ${r.category}`.toLowerCase();
      return terms.every((t) => hay.includes(t));
    });
  }
  if (query.min !== null) {
    having.push(`min(v.price_cents) >= ${p(query.min * 100)}`);
    rows = rows.filter((r) => fromPrice(r) >= query.min! * 100);
  }
  if (query.max !== null) {
    having.push(`min(v.price_cents) <= ${p(query.max * 100)}`);
    rows = rows.filter((r) => fromPrice(r) <= query.max! * 100);
  }
  if (query.inStock) {
    having.push("sum(v.stock) > 0");
    rows = rows.filter((r) => totalStock(r) > 0);
  }
  const order = {
    featured: "p.rank ASC",
    "price-asc": "from_price ASC, p.id",
    "price-desc": "from_price DESC, p.id",
    name: "p.name ASC",
  }[query.sort];
  rows = [...rows].sort((a, b) => {
    switch (query.sort) {
      case "price-asc":
        return fromPrice(a) - fromPrice(b) || a.id.localeCompare(b.id);
      case "price-desc":
        return fromPrice(b) - fromPrice(a) || a.id.localeCompare(b.id);
      case "name":
        return a.name.localeCompare(b.name);
      default:
        return a.rank - b.rank;
    }
  });

  const sql = [
    [
      "SELECT p.id, p.name, p.brand, min(v.price_cents) AS from_price, sum(v.stock) AS stock",
      "FROM products p JOIN variants v ON v.product_id = p.id",
      where.length ? `WHERE ${where.join("\n  AND ")}` : null,
      "GROUP BY p.id",
      having.length ? `HAVING ${having.join(" AND ")}` : null,
      `ORDER BY ${order}`,
      "LIMIT 24;",
    ]
      .filter(Boolean)
      .join("\n"),
    `-- params: ${JSON.stringify(params)}  (${rows.length} rows)`,
  ];
  return ok(db, 200, rows, sql);
}

/* ------------------------------------------------------------------ */
/* GET /api/products/:id                                               */
/* ------------------------------------------------------------------ */

export function getProduct(db: DB, id: string): Handled<Product> {
  const sql = [
    "SELECT p.*, json_agg(v ORDER BY v.sku) AS variants\nFROM products p JOIN variants v ON v.product_id = p.id\nWHERE p.id = $1 GROUP BY p.id;",
    `-- params: ["${id}"]`,
  ];
  const product = db.products.find((p) => p.id === id);
  if (!product) return fail(db, 404, "not_found", `Product "${id}" does not exist.`, sql);
  return ok(db, 200, product, sql);
}

/* ------------------------------------------------------------------ */
/* POST /api/cart/quote  — price a cart from the catalogue             */
/* ------------------------------------------------------------------ */

/** What the browser sends. `unitCents` is what the browser *thinks* the price is; the server ignores it. */
export type CartLineInput = { sku: string; qty: number; unitCents?: number };

export type QuotedLine = OrderLine & { available: number; clientUnitCents?: number };
export type Quote = { lines: QuotedLine[]; subtotalCents: number; warnings: string[] };

function priceLines(db: DB, input: CartLineInput[]): { lines: QuotedLine[]; missing: string[]; warnings: string[] } {
  const lines: QuotedLine[] = [];
  const missing: string[] = [];
  const warnings: string[] = [];
  for (const l of input) {
    const hit = findVariant(db.products, l.sku);
    if (!hit) {
      missing.push(l.sku);
      continue;
    }
    const { product, variant } = hit;
    if (l.unitCents !== undefined && l.unitCents !== variant.priceCents) {
      warnings.push(
        `Ignored client price ${(l.unitCents / 100).toFixed(2)} for ${variant.sku}; catalogue price is ${(variant.priceCents / 100).toFixed(2)}.`,
      );
    }
    lines.push({
      sku: variant.sku,
      productId: product.id,
      name: product.name,
      variantLabel: variantLabel(variant),
      qty: l.qty,
      unitCents: variant.priceCents,
      available: variant.stock,
      clientUnitCents: l.unitCents,
    });
  }
  return { lines, missing, warnings };
}

export function quoteCart(db: DB, input: CartLineInput[]): Handled<Quote> {
  const { lines, missing, warnings } = priceLines(db, input);
  const sql = [
    "SELECT v.sku, v.price_cents, v.stock, p.name\nFROM variants v JOIN products p ON p.id = v.product_id\nWHERE v.sku = ANY($1);",
    `-- params: [${JSON.stringify(input.map((l) => l.sku))}]`,
    "-- unit prices in the request body are discarded; only sku and qty are read",
  ];
  if (missing.length) warnings.push(`Removed unknown SKU(s): ${missing.join(", ")}.`);
  return ok(db, 200, { lines, subtotalCents: lines.reduce((n, l) => n + l.unitCents * l.qty, 0), warnings }, sql);
}

/* ------------------------------------------------------------------ */
/* POST /api/orders  — validate, reserve stock, create order (one tx)  */
/* ------------------------------------------------------------------ */

export const COUNTRIES = ["United Kingdom", "Ireland", "Germany", "Netherlands", "Pakistan", "United States"] as const;

/** The DTO. In NestJS this is a class with class-validator decorators behind a global ValidationPipe; zod here. */
export const checkoutSchema = z.object({
  email: z.string().trim().email("Enter a valid email address, such as you@example.com."),
  fullName: z.string().trim().min(2, "Enter your full name."),
  address: z.string().trim().min(5, "Enter your street address."),
  city: z.string().trim().min(2, "Enter your town or city."),
  postcode: z
    .string()
    .trim()
    .regex(/^[A-Za-z0-9][A-Za-z0-9 -]{2,9}$/, "Enter a valid postcode (3–10 letters or digits)."),
  country: z.enum(COUNTRIES, { errorMap: () => ({ message: "Choose a country we ship to." }) }),
  shipping: z.enum(["standard", "express"]),
});
export type CheckoutInput = z.infer<typeof checkoutSchema>;

export function fieldErrors(err: z.ZodError): Record<string, string> {
  const out: Record<string, string> = {};
  for (const i of err.issues) {
    const k = String(i.path[0] ?? "form");
    out[k] ??= i.message;
  }
  return out;
}

export function createOrder(
  db: DB,
  input: { customer: unknown; lines: CartLineInput[]; now: number },
): Handled<{ order: Order; warnings: string[] }> {
  const parsed = checkoutSchema.safeParse(input.customer);
  if (!parsed.success) {
    return fail(db, 422, "validation_failed", "Some fields need attention.", ["-- rejected by ValidationPipe: no query was sent"], fieldErrors(parsed.error));
  }
  const cart = input.lines.filter((l) => Number.isInteger(l.qty) && l.qty > 0 && l.qty <= 99);
  if (!cart.length || cart.length !== input.lines.length) {
    return fail(db, 422, "validation_failed", "The cart is empty or has an invalid quantity (1–99 per line).", ["-- rejected by ValidationPipe: no query was sent"]);
  }

  // Work on a copy: nothing reaches `db` unless every statement succeeds.
  const tx = structuredClone(db);
  const skus = [...new Set(cart.map((l) => l.sku))].sort();
  const sql = [
    "BEGIN;",
    `SELECT sku, price_cents, stock FROM variants\nWHERE sku = ANY($1) ORDER BY sku\nFOR UPDATE;  -- row locks; sorted to avoid deadlocks\n-- params: [${JSON.stringify(skus)}]`,
  ];
  const { lines, missing, warnings } = priceLines(tx, cart);
  if (missing.length) {
    sql.push("ROLLBACK;");
    return fail(db, 404, "sku_not_found", `These items no longer exist: ${missing.join(", ")}.`, sql);
  }

  for (const line of lines) {
    const variant = findVariant(tx.products, line.sku)!.variant;
    if (variant.stock < line.qty) {
      sql.push(`-- ${line.sku}: requested ${line.qty}, in stock ${variant.stock}`, "ROLLBACK;");
      return fail(
        db,
        409,
        "insufficient_stock",
        variant.stock === 0
          ? `${line.name} (${line.variantLabel}) just sold out.`
          : `Only ${variant.stock} left of ${line.name} (${line.variantLabel}). Lower the quantity and try again.`,
        sql,
      );
    }
    variant.stock -= line.qty;
    sql.push(`UPDATE variants SET stock = stock - $2 WHERE sku = $1;  -- [${line.sku}, ${line.qty}] → ${variant.stock} left`);
  }

  const c = parsed.data;
  const subtotal = lines.reduce((n, l) => n + l.unitCents * l.qty, 0);
  const t = totals(subtotal, c.shipping);
  const id = tx.nextOrderId++;
  const order: Order = {
    id,
    number: String(1000 + id),
    status: "pending_payment",
    ...c,
    lines: lines.map(({ sku, productId, name, variantLabel, qty, unitCents }) => ({ sku, productId, name, variantLabel, qty, unitCents })),
    ...t,
    createdAt: input.now,
    paidAt: null,
    paymentIntentId: null,
    card: null,
  };
  tx.orders.unshift(order);
  sql.push(
    `INSERT INTO orders (email, full_name, address, shipping, subtotal_cents, shipping_cents, tax_cents, total_cents, status)\nVALUES ($1, $2, $3, '${c.shipping}', ${t.subtotalCents}, ${t.shippingCents}, ${t.taxCents}, ${t.totalCents}, 'pending_payment')\nRETURNING id;  -- ${id}`,
    `INSERT INTO order_lines (order_id, sku, qty, unit_cents) VALUES\n${order.lines.map((l) => `  (${id}, '${l.sku}', ${l.qty}, ${l.unitCents})`).join(",\n")};`,
    "COMMIT;",
  );
  return ok(tx, 201, { order, warnings }, sql);
}

/* ------------------------------------------------------------------ */
/* POST /api/orders/:id/cancel  — release reserved stock               */
/* ------------------------------------------------------------------ */

export function cancelOrder(db: DB, orderId: number): Handled<Order> {
  const sql = ["BEGIN;", `SELECT status FROM orders WHERE id = $1 FOR UPDATE;  -- [${orderId}]`];
  const tx = structuredClone(db);
  const order = tx.orders.find((o) => o.id === orderId);
  if (!order) return fail(db, 404, "not_found", `Order ${orderId} does not exist.`, [...sql, "ROLLBACK;"]);
  if (order.status !== "pending_payment") {
    return fail(db, 409, "invalid_state", `Order #${order.number} is ${order.status.replace("_", " ")} and cannot be cancelled here.`, [...sql, "ROLLBACK;"]);
  }
  for (const l of order.lines) {
    const hit = findVariant(tx.products, l.sku);
    if (hit) hit.variant.stock += l.qty;
  }
  order.status = "cancelled";
  const intent = tx.intents.find((i) => i.orderId === orderId && i.status !== "succeeded");
  if (intent) intent.status = "canceled";
  sql.push(
    "UPDATE variants v SET stock = v.stock + ol.qty\nFROM order_lines ol WHERE ol.order_id = $1 AND ol.sku = v.sku;",
    "UPDATE orders SET status = 'cancelled' WHERE id = $1;",
    ...(intent ? [`UPDATE payment_intents SET status = 'canceled' WHERE id = '${intent.id}';`, `-- → payment provider: POST /v1/payment_intents/${intent.id}/cancel`] : []),
    "COMMIT;",
  );
  return ok(tx, 200, order, sql);
}

/* ------------------------------------------------------------------ */
/* POST /api/payments/intents   (Idempotency-Key header)               */
/* ------------------------------------------------------------------ */

export function createIntent(db: DB, input: { orderId: number; idempotencyKey: string }): Handled<PaymentIntent> {
  const key = input.idempotencyKey;
  const sql = [`SELECT intent_id FROM idempotency_keys WHERE key = $1;  -- ['${key}']`];
  const replay = db.idempotency[key];
  if (replay) {
    const intent = db.intents.find((i) => i.id === replay)!;
    sql.push("-- key seen before: return the stored response, create nothing");
    return ok(db, 200, intent, sql, { "Idempotent-Replayed": "true" });
  }

  const tx = structuredClone(db);
  const order = tx.orders.find((o) => o.id === input.orderId);
  sql.push("BEGIN;", `SELECT id, status, total_cents FROM orders WHERE id = $1 FOR UPDATE;  -- [${input.orderId}]`);
  if (!order) return fail(db, 404, "not_found", `Order ${input.orderId} does not exist.`, [...sql, "ROLLBACK;"]);
  if (order.status !== "pending_payment") {
    return fail(db, 409, "invalid_state", `Order #${order.number} is already ${order.status.replace("_", " ")}.`, [...sql, "ROLLBACK;"]);
  }

  // One open intent per order: a second tab or a retried request reuses it instead of charging twice.
  let intent = tx.intents.find((i) => i.orderId === order.id && i.status !== "canceled");
  if (intent) {
    sql.push(`SELECT id FROM payment_intents WHERE order_id = $1 AND status <> 'canceled';  -- reuse ${intent.id}`);
  } else {
    intent = {
      id: `pi_test_${(order.id * 7919).toString(36)}${tx.intents.length}`,
      orderId: order.id,
      amountCents: order.totalCents, // amount comes from the order row, never from the browser
      status: "requires_payment_method",
      lastError: null,
    };
    tx.intents.push(intent);
    order.paymentIntentId = intent.id;
    sql.push(
      `-- → payment provider: POST /v1/payment_intents amount=${order.totalCents} currency=usd (Idempotency-Key forwarded)`,
      `INSERT INTO payment_intents (id, order_id, amount_cents, status) VALUES ('${intent.id}', ${order.id}, ${order.totalCents}, 'requires_payment_method');`,
      `UPDATE orders SET payment_intent_id = '${intent.id}' WHERE id = ${order.id};`,
    );
  }
  tx.idempotency[key] = intent.id;
  sql.push(`INSERT INTO idempotency_keys (key, intent_id) VALUES ('${key}', '${intent.id}');`, "COMMIT;");
  return ok(tx, 201, intent, sql);
}

/* ------------------------------------------------------------------ */
/* Browser-side tokenisation (stand-in for the provider's JS SDK)      */
/* ------------------------------------------------------------------ */

export const TEST_CARDS = [
  { number: "4242 4242 4242 4242", pm: "pm_card_visa", outcome: "Succeeds" },
  { number: "4000 0000 0000 0002", pm: "pm_card_chargeDeclined", outcome: "Declined" },
  { number: "4000 0000 0000 3220", pm: "pm_card_threeDSecure2Required", outcome: "Asks for 3-D Secure" },
] as const;
export type PaymentMethodId = (typeof TEST_CARDS)[number]["pm"];

export function luhn(digits: string): boolean {
  let sum = 0;
  for (let i = 0; i < digits.length; i++) {
    let d = Number(digits[digits.length - 1 - i]);
    if (i % 2 === 1) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
  }
  return digits.length >= 12 && sum % 10 === 0;
}

/**
 * Turns card fields into a payment-method id. In production this runs inside the provider's iframe,
 * so card numbers never touch our servers. Here it only accepts the published test numbers.
 */
export function tokenizeCard(
  input: { number: string; expiry: string; cvc: string },
  now: Date,
): { ok: true; pm: PaymentMethodId; last4: string } | { ok: false; fields: Record<string, string> } {
  const fields: Record<string, string> = {};
  const digits = input.number.replace(/\D/g, "");
  const card = TEST_CARDS.find((c) => c.number.replace(/\s/g, "") === digits);
  if (!digits) fields.number = "Enter a card number.";
  else if (!luhn(digits)) fields.number = "That card number is not valid.";
  else if (!card) fields.number = "Test mode only accepts the test cards listed above. Never enter a real card here.";

  const m = /^(\d{2})\s*\/\s*(\d{2})$/.exec(input.expiry.trim());
  if (!m) fields.expiry = "Use MM / YY.";
  else {
    const month = Number(m[1]);
    const year = 2000 + Number(m[2]);
    const endOfMonth = new Date(year, month, 1);
    if (month < 1 || month > 12) fields.expiry = "Month must be 01–12.";
    else if (endOfMonth <= now) fields.expiry = "This card has expired.";
  }
  if (!/^\d{3,4}$/.test(input.cvc.trim())) fields.cvc = "Enter the 3 or 4 digit security code.";

  if (Object.keys(fields).length || !card) return { ok: false, fields };
  return { ok: true, pm: card.pm, last4: digits.slice(-4) };
}

/* ------------------------------------------------------------------ */
/* POST /api/payments/intents/:id/confirm                              */
/* ------------------------------------------------------------------ */

export type ConfirmBody = PaymentIntent & { nextAction?: "three_d_secure" };

export function confirmIntent(db: DB, input: { intentId: string; pm: PaymentMethodId; last4: string }): Handled<ConfirmBody> {
  const tx = structuredClone(db);
  const intent = tx.intents.find((i) => i.id === input.intentId);
  const sql = [`-- → payment provider: POST /v1/payment_intents/${input.intentId}/confirm payment_method=${input.pm}`];
  if (!intent) return fail(db, 404, "not_found", "Payment intent not found.", sql);
  if (intent.status === "succeeded") return ok(db, 200, intent, [...sql, "-- already succeeded: nothing to do"]);
  if (intent.status === "canceled") return fail(db, 409, "intent_canceled", "This payment was cancelled. Start checkout again.", sql);

  const order = tx.orders.find((o) => o.id === intent.orderId);
  if (order) order.card = `•••• ${input.last4}`;

  if (input.pm === "pm_card_chargeDeclined") {
    intent.status = "requires_payment_method";
    intent.lastError = "card_declined";
    sql.push(
      "-- ← 402 card_declined (decline_code: generic_decline)",
      `UPDATE payment_intents SET status = 'requires_payment_method', last_error = 'card_declined' WHERE id = '${intent.id}';`,
      "-- order stays pending_payment; reserved stock is held until the reservation expires",
    );
    return { db: tx, res: { ok: false, status: 402, body: { error: "card_declined", message: "Your card was declined. Try a different card." }, sql } };
  }
  if (input.pm === "pm_card_threeDSecure2Required") {
    intent.status = "requires_action";
    sql.push(
      "-- ← 200 status=requires_action next_action=use_stripe_sdk",
      `UPDATE payment_intents SET status = 'requires_action' WHERE id = '${intent.id}';`,
    );
    return ok(tx, 200, { ...intent, nextAction: "three_d_secure" }, sql);
  }
  intent.status = "succeeded";
  intent.lastError = null;
  sql.push(
    "-- ← 200 status=succeeded",
    `UPDATE payment_intents SET status = 'succeeded' WHERE id = '${intent.id}';`,
    "-- the order is NOT marked paid here: the signed webhook is the source of truth",
  );
  return ok(tx, 200, intent, sql);
}

/* ------------------------------------------------------------------ */
/* 3-D Secure challenge result (happens on the provider's page)        */
/* ------------------------------------------------------------------ */

export function completeAuthentication(db: DB, input: { intentId: string; approve: boolean }): Handled<PaymentIntent> {
  const tx = structuredClone(db);
  const intent = tx.intents.find((i) => i.id === input.intentId);
  const sql = [`-- → payment provider: 3-D Secure challenge for ${input.intentId} (${input.approve ? "approved" : "failed"})`];
  if (!intent || intent.status !== "requires_action") return fail(db, 409, "invalid_state", "No authentication is pending.", sql);
  if (!input.approve) {
    intent.status = "requires_payment_method";
    intent.lastError = "authentication_failed";
    sql.push(`UPDATE payment_intents SET status = 'requires_payment_method', last_error = 'authentication_failed' WHERE id = '${intent.id}';`);
    return { db: tx, res: { ok: false, status: 402, body: { error: "authentication_failed", message: "Authentication failed, so the card was not charged." }, sql } };
  }
  intent.status = "succeeded";
  intent.lastError = null;
  sql.push(`UPDATE payment_intents SET status = 'succeeded' WHERE id = '${intent.id}';`);
  return ok(tx, 200, intent, sql);
}

/* ------------------------------------------------------------------ */
/* POST /webhooks/payments  — payment_intent.succeeded                 */
/* ------------------------------------------------------------------ */

export const eventIdFor = (intentId: string) => `evt_${intentId.slice(3)}_succeeded`;

export function handleWebhook(
  db: DB,
  event: { id: string; type: "payment_intent.succeeded"; intentId: string; signatureValid: boolean },
  now: number,
): Handled<{ received: true; duplicate: boolean; orderStatus?: string }> {
  const sql = ["-- verify signature header with the endpoint secret (HMAC-SHA256, 5 min tolerance)"];
  if (!event.signatureValid) return fail(db, 400, "bad_signature", "Signature check failed; event ignored.", sql);

  sql.push("BEGIN;", `INSERT INTO webhook_events (id) VALUES ($1) ON CONFLICT (id) DO NOTHING RETURNING id;  -- ['${event.id}']`);
  if (db.events.includes(event.id)) {
    sql.push("-- 0 rows: event already processed, skip side effects", "COMMIT;");
    return ok(db, 200, { received: true, duplicate: true }, sql);
  }
  const tx = structuredClone(db);
  tx.events.push(event.id);
  const intent = tx.intents.find((i) => i.id === event.intentId);
  const order = intent ? tx.orders.find((o) => o.id === intent.orderId) : undefined;
  if (order && order.status === "pending_payment") {
    order.status = "paid";
    order.paidAt = now;
  }
  sql.push(
    `UPDATE orders SET status = 'paid', paid_at = now()\nWHERE payment_intent_id = $1 AND status = 'pending_payment';  -- ['${event.intentId}'] ${order?.status === "paid" ? "1 row" : "0 rows"}`,
    "-- enqueue: send-confirmation-email (outbox row, same transaction)",
    "COMMIT;",
  );
  return ok(tx, 200, { received: true, duplicate: false, orderStatus: order?.status }, sql);
}
app/lab/ecommerce-storefront/db.ts465 lines
/**
 * In-memory stand-in for the PostgreSQL schema of the storefront.
 * Every table below maps to a real table in the production design:
 *
 *   products (id, name, brand, category, description)
 *   variants (sku PK, product_id FK, colour, size, price_cents, stock CHECK (stock >= 0))
 *   orders (id bigserial, status, email, totals…, payment_intent_id)
 *   order_lines (order_id, sku, qty, unit_cents)          -- price copied at purchase time
 *   payment_intents (id, order_id, amount_cents, status)
 *   idempotency_keys (key PK, intent_id)                   -- replays return the stored response
 *   webhook_events (id PK)                                 -- dedupes provider retries
 */

export type Category = "apparel" | "footwear" | "home" | "accessories";
export const CATEGORIES: { id: Category; label: string }[] = [
  { id: "apparel", label: "Apparel" },
  { id: "footwear", label: "Footwear" },
  { id: "home", label: "Home" },
  { id: "accessories", label: "Accessories" },
];

export type Shape =
  | "tee"
  | "hoodie"
  | "socks"
  | "cap"
  | "beanie"
  | "sneaker"
  | "mug"
  | "lamp"
  | "plant"
  | "blanket"
  | "bag"
  | "bottle"
  | "watch"
  | "notebook"
  | "wallet";

export type Variant = {
  sku: string;
  colour: string;
  hex: string;
  size: string | null;
  priceCents: number;
  stock: number;
};

export type Product = {
  id: string;
  name: string;
  brand: string;
  category: Category;
  description: string;
  shape: Shape;
  /** Position in the "featured" sort. */
  rank: number;
  variants: Variant[];
};

export type OrderLine = {
  sku: string;
  productId: string;
  name: string;
  variantLabel: string;
  qty: number;
  unitCents: number;
};

export type ShippingMethod = "standard" | "express";

export type OrderStatus = "pending_payment" | "paid" | "cancelled";

export type Order = {
  id: number;
  number: string;
  status: OrderStatus;
  email: string;
  fullName: string;
  address: string;
  city: string;
  postcode: string;
  country: string;
  shipping: ShippingMethod;
  lines: OrderLine[];
  subtotalCents: number;
  shippingCents: number;
  taxCents: number;
  totalCents: number;
  createdAt: number;
  paidAt: number | null;
  paymentIntentId: string | null;
  card: string | null;
};

export type IntentStatus = "requires_payment_method" | "requires_action" | "succeeded" | "canceled";

export type PaymentIntent = {
  id: string;
  orderId: number;
  amountCents: number;
  status: IntentStatus;
  lastError: string | null;
};

export type DB = {
  products: Product[];
  orders: Order[];
  intents: PaymentIntent[];
  /** idempotency_keys: key -> payment intent id */
  idempotency: Record<string, string>;
  /** webhook_events: processed event ids */
  events: string[];
  nextOrderId: number;
};

/* ------------------------------------------------------------------ */
/* Seed                                                                */
/* ------------------------------------------------------------------ */

type Colour = [name: string, hex: string];

/** Deterministic pseudo-random stock so the server and client render the same catalogue. */
function stockFor(sku: string): number {
  let h = 7;
  for (const ch of sku) h = (h * 31 + ch.charCodeAt(0)) % 9973;
  const r = h % 20;
  if (r === 0) return 0;
  if (r < 3) return r; // a few nearly sold-out variants (1–2 left)
  return 4 + (h % 23);
}

function product(
  id: string,
  rank: number,
  p: Omit<Product, "id" | "rank" | "variants">,
  colours: Colour[],
  sizes: (string | null)[],
  basePrice: number,
  sizeDelta: Record<string, number> = {},
  colourDelta: Record<string, number> = {},
): Product {
  const prefix = id.toUpperCase().slice(0, 6);
  const variants: Variant[] = [];
  for (const [colour, hex] of colours) {
    for (const size of sizes) {
      const sku = [prefix, colour.slice(0, 3).toUpperCase(), size?.replace(/\s+/g, "").toUpperCase()]
        .filter(Boolean)
        .join("-");
      variants.push({
        sku,
        colour,
        hex,
        size,
        priceCents: basePrice + (size ? (sizeDelta[size] ?? 0) : 0) + (colourDelta[colour] ?? 0),
        stock: stockFor(sku),
      });
    }
  }
  return { id, rank, ...p, variants };
}

const APPAREL = ["S", "M", "L", "XL"];
const SHOES = ["40", "41", "42", "43", "44"];

export function seedProducts(): Product[] {
  return [
    product(
      "cotton-tee",
      1,
      {
        name: "Everyday Cotton Tee",
        brand: "Fieldline",
        category: "apparel",
        shape: "tee",
        description: "Midweight organic cotton with a relaxed fit. Pre-shrunk, so the size you order stays the size you wear.",
      },
      [["Black", "#27272a"], ["White", "#f4f4f5"], ["Sage", "#86a789"]],
      APPAREL,
      2400,
      { XL: 200 },
    ),
    product(
      "heavy-hoodie",
      3,
      {
        name: "Heavyweight Hoodie",
        brand: "Fieldline",
        category: "apparel",
        shape: "hoodie",
        description: "Brushed fleece inside, double-layer hood, and ribbed cuffs that keep their shape after many washes.",
      },
      [["Charcoal", "#3f3f46"], ["Sand", "#d6c4a1"]],
      APPAREL,
      6400,
      { XL: 400 },
    ),
    product(
      "merino-socks",
      9,
      {
        name: "Merino Crew Socks, 3-pack",
        brand: "Fieldline",
        category: "apparel",
        shape: "socks",
        description: "Soft merino blend with a cushioned sole. Three pairs of the same colour per pack.",
      },
      [["Grey", "#9ca3af"], ["Navy", "#1e3a8a"]],
      ["S/M", "L/XL"],
      1800,
    ),
    product(
      "work-cap",
      11,
      {
        name: "Canvas Work Cap",
        brand: "Trailhead Supply",
        category: "accessories",
        shape: "cap",
        description: "Waxed cotton canvas, six panels and a brass buckle at the back. One size fits most.",
      },
      [["Olive", "#6b7a3a"], ["Navy", "#1e3a8a"], ["Rust", "#b45309"]],
      [null],
      2800,
    ),
    product(
      "wool-beanie",
      14,
      {
        name: "Ribbed Wool Beanie",
        brand: "Fieldline",
        category: "apparel",
        shape: "beanie",
        description: "Lambswool rib knit with a deep turn-up. Warm without being itchy.",
      },
      [["Rust", "#b45309"], ["Cream", "#f5f0e1"], ["Charcoal", "#3f3f46"]],
      [null],
      2600,
    ),
    product(
      "court-sneaker",
      2,
      {
        name: "Court Low Sneaker",
        brand: "Stride Lab",
        category: "footwear",
        shape: "sneaker",
        description: "Full-grain leather upper on a stitched rubber cupsole. Runs true to size.",
      },
      [["White", "#f4f4f5"], ["Black", "#27272a"]],
      SHOES,
      8900,
    ),
    product(
      "trail-runner",
      5,
      {
        name: "Trail Runner",
        brand: "Stride Lab",
        category: "footwear",
        shape: "sneaker",
        description: "Lugged outsole, rock plate and a breathable mesh upper for mixed terrain.",
      },
      [["Slate", "#475569"], ["Orange", "#ea580c"]],
      SHOES,
      11900,
    ),
    product(
      "wool-slipper",
      15,
      {
        name: "Felted Wool Slipper",
        brand: "Stride Lab",
        category: "footwear",
        shape: "sneaker",
        description: "Boiled wool with a thin leather sole for indoors and the occasional trip to the bins.",
      },
      [["Grey", "#9ca3af"]],
      ["S", "M", "L"],
      4500,
    ),
    product(
      "stoneware-mug",
      4,
      {
        name: "Stoneware Mug 350 ml",
        brand: "Kiln Studio",
        category: "home",
        shape: "mug",
        description: "Wheel-thrown and glazed by hand, so every mug is slightly different. Dishwasher safe.",
      },
      [["Speckle", "#e7e0d4"], ["Midnight", "#1e293b"], ["Clay", "#c2410c"]],
      [null],
      1600,
    ),
    product(
      "table-lamp",
      7,
      {
        name: "Linen Table Lamp",
        brand: "Lumen Works",
        category: "home",
        shape: "lamp",
        description: "Ceramic base with a linen drum shade. Takes an E27 bulb; a warm 2700 K LED is included.",
      },
      [["Oat", "#e2d6bf"], ["Black", "#27272a"]],
      [null],
      7900,
    ),
    product(
      "plant-pot",
      12,
      {
        name: "Desk Plant Pot",
        brand: "Kiln Studio",
        category: "home",
        shape: "plant",
        description: "Unglazed terracotta or matte white with a drainage hole and matching saucer. Plant not included.",
      },
      [["Terracotta", "#c2410c"], ["White", "#f4f4f5"]],
      ["12 cm", "16 cm"],
      2200,
      { "16 cm": 700 },
    ),
    product(
      "weighted-throw",
      8,
      {
        name: "Weighted Throw Blanket",
        brand: "Harbor & Co",
        category: "home",
        shape: "blanket",
        description: "Glass-bead fill in quilted pockets so the weight stays even. Removable, washable cover.",
      },
      [["Grey", "#9ca3af"], ["Blue", "#3b82f6"]],
      ["5 kg", "7 kg"],
      9500,
      { "7 kg": 2000 },
    ),
    product(
      "rolltop-pack",
      6,
      {
        name: "Roll-top Backpack 20 L",
        brand: "Trailhead Supply",
        category: "accessories",
        shape: "bag",
        description: "Water-resistant recycled nylon, a padded laptop sleeve and a roll-top that expands to 24 L.",
      },
      [["Black", "#27272a"], ["Olive", "#6b7a3a"]],
      [null],
      9800,
    ),
    product(
      "steel-bottle",
      10,
      {
        name: "Insulated Bottle 750 ml",
        brand: "Trailhead Supply",
        category: "accessories",
        shape: "bottle",
        description: "Double-wall steel keeps drinks cold for a day or hot for half of one. Leak-proof lid.",
      },
      [["Steel", "#a1a1aa"], ["Forest", "#166534"], ["Coral", "#f97316"]],
      [null],
      3200,
    ),
    product(
      "field-watch",
      13,
      {
        name: "Field Watch 38 mm",
        brand: "Harbor & Co",
        category: "accessories",
        shape: "watch",
        description: "Quartz movement, sapphire-coated glass and a quick-release strap. Water resistant to 100 m.",
      },
      [["Leather", "#92400e"], ["Canvas", "#6b7a3a"]],
      [null],
      14900,
      {},
      { Canvas: -1000 },
    ),
    product(
      "dot-notebook",
      16,
      {
        name: "Dot-grid Notebook A5",
        brand: "Paper Mill & Co",
        category: "accessories",
        shape: "notebook",
        description: "192 numbered pages of 100 gsm paper that takes fountain pen ink without bleeding through.",
      },
      [["Black", "#27272a"], ["Sand", "#d6c4a1"]],
      [null],
      1400,
    ),
    product(
      "card-holder",
      17,
      {
        name: "Leather Card Holder",
        brand: "Harbor & Co",
        category: "accessories",
        shape: "wallet",
        description: "Vegetable-tanned leather, four card slots and a centre pocket for folded notes.",
      },
      [["Tan", "#b45309"], ["Black", "#27272a"]],
      [null],
      3900,
    ),
    product(
      "pour-over",
      18,
      {
        name: "Pour-over Coffee Set",
        brand: "Kiln Studio",
        category: "home",
        shape: "mug",
        description: "Ceramic dripper, 600 ml carafe and 50 paper filters. Makes two cups at a time.",
      },
      [["White", "#f4f4f5"]],
      [null],
      4800,
    ),
  ];
}

export function seedDb(): DB {
  return { products: seedProducts(), orders: [], intents: [], idempotency: {}, events: [], nextOrderId: 42 };
}

/* ------------------------------------------------------------------ */
/* Helpers shared by the API and the UI                                */
/* ------------------------------------------------------------------ */

export const money = (cents: number) =>
  `$${(cents / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;

export const variantLabel = (v: Variant) => [v.colour, v.size].filter(Boolean).join(" / ");

export function findVariant(products: Product[], sku: string): { product: Product; variant: Variant } | null {
  for (const product of products) {
    const variant = product.variants.find((v) => v.sku === sku);
    if (variant) return { product, variant };
  }
  return null;
}

export const fromPrice = (p: Product) => Math.min(...p.variants.map((v) => v.priceCents));
export const totalStock = (p: Product) => p.variants.reduce((n, v) => n + v.stock, 0);

export const SHIPPING: Record<ShippingMethod, { label: string; eta: string; cents: number }> = {
  standard: { label: "Standard", eta: "3–5 working days", cents: 499 },
  express: { label: "Express", eta: "1–2 working days", cents: 1499 },
};
export const FREE_SHIPPING_FROM = 7500;
export const TAX_RATE = 0.08;

/** The one pricing function. The server calls it with catalogue prices; the browser only uses it for display. */
export function totals(subtotalCents: number, shipping: ShippingMethod) {
  const shippingCents = shipping === "standard" && subtotalCents >= FREE_SHIPPING_FROM ? 0 : SHIPPING[shipping].cents;
  const taxCents = Math.round(subtotalCents * TAX_RATE);
  return { subtotalCents, shippingCents, taxCents, totalCents: subtotalCents + shippingCents + taxCents };
}
app/lab/ecommerce-storefront/Checkout.tsx482 lines
"use client";

import { useEffect, useRef, useState } from "react";
import {
  COUNTRIES,
  TEST_CARDS,
  checkoutSchema,
  completeAuthentication,
  confirmIntent,
  createIntent,
  createOrder,
  eventIdFor,
  fieldErrors,
  handleWebhook,
  quoteCart,
  tokenizeCard,
  type CartLineInput,
  type CheckoutInput,
  type Handled,
  type Quote,
  type Res,
} from "./api";
import { FREE_SHIPPING_FROM, SHIPPING, money, totals, type DB, type Order, type PaymentIntent } from "./db";
import type { LogEntry } from "./DevPanel";

export type Exec = <T>(
  method: LogEntry["method"],
  path: string,
  fn: (db: DB) => Handled<T>,
  extra?: { headers?: Record<string, string>; note?: string },
) => Res<T>;

const input =
  "mt-1 w-full rounded-md border border-border bg-bg-elev px-3 py-2 text-sm outline-none focus:border-accent aria-[invalid=true]:border-danger";
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));

const EMPTY: CheckoutInput = { email: "", fullName: "", address: "", city: "", postcode: "", country: "United Kingdom", shipping: "standard" };
const SAMPLE: CheckoutInput = {
  email: "alex.morgan@example.com",
  fullName: "Alex Morgan",
  address: "14 Example Street",
  city: "Manchester",
  postcode: "M1 2AB",
  country: "United Kingdom",
  shipping: "standard",
};

function Field({ id, label, error, children }: { id: string; label: string; error?: string; children: React.ReactNode }) {
  return (
    <div>
      <label htmlFor={id} className="text-sm font-medium">
        {label}
      </label>
      {children}
      {error && (
        <p id={`${id}-err`} className="mt-1 text-xs text-danger">
          {error}
        </p>
      )}
    </div>
  );
}

function Summary({ lines, shipping, title }: { lines: { name: string; variantLabel: string; qty: number; unitCents: number; sku: string }[]; shipping: CheckoutInput["shipping"]; title: string }) {
  const t = totals(
    lines.reduce((n, l) => n + l.unitCents * l.qty, 0),
    shipping,
  );
  return (
    <aside aria-label={title} className="h-fit rounded-lg border border-border bg-bg-elev p-4 text-sm">
      <h3 className="font-mono text-xs text-fg-muted">{title}</h3>
      <ul className="mt-2 space-y-2">
        {lines.map((l) => (
          <li key={l.sku} className="flex justify-between gap-3">
            <span>
              {l.qty} × {l.name}
              <span className="block text-xs text-fg-muted">{l.variantLabel}</span>
            </span>
            <span className="font-mono">{money(l.unitCents * l.qty)}</span>
          </li>
        ))}
      </ul>
      <dl className="mt-3 space-y-1 border-t border-border pt-3 font-mono text-xs">
        <div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>{money(t.subtotalCents)}</dd></div>
        <div className="flex justify-between"><dt className="text-fg-muted">Shipping ({SHIPPING[shipping].label})</dt><dd>{t.shippingCents ? money(t.shippingCents) : "Free"}</dd></div>
        <div className="flex justify-between"><dt className="text-fg-muted">Tax (8%)</dt><dd>{money(t.taxCents)}</dd></div>
        <div className="flex justify-between text-sm font-semibold"><dt>Total</dt><dd>{money(t.totalCents)}</dd></div>
      </dl>
    </aside>
  );
}

/* ------------------------------------------------------------------ */
/* Step 1: details                                                     */
/* ------------------------------------------------------------------ */

export function DetailsStep({
  cart,
  exec,
  onCreated,
  onBack,
}: {
  cart: CartLineInput[];
  exec: Exec;
  onCreated: (order: Order) => void;
  onBack: () => void;
}) {
  const [form, setForm] = useState<CheckoutInput>(EMPTY);
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [formError, setFormError] = useState<string | null>(null);
  const [quote, setQuote] = useState<Quote | null>(null);
  const [skipClient, setSkipClient] = useState(false);
  const [busy, setBusy] = useState(false);
  const quoted = useRef(false);

  // Price the cart on the "server" once when checkout opens.
  useEffect(() => {
    if (quoted.current) return;
    quoted.current = true;
    const res = exec("POST", "/api/cart/quote", (db) => quoteCart(db, cart), {
      note: cart.some((l) => l.unitCents === 1) ? "request body contains a tampered unit price" : undefined,
    });
    if (res.ok) setQuote(res.body);
  }, [cart, exec]);

  const set = <K extends keyof CheckoutInput>(k: K, v: CheckoutInput[K]) => {
    setForm((f) => ({ ...f, [k]: v }));
    if (errors[k]) setErrors((e) => ({ ...e, [k]: "" }));
  };

  async function submit(e: React.FormEvent) {
    e.preventDefault();
    setFormError(null);
    if (!skipClient) {
      const parsed = checkoutSchema.safeParse(form);
      if (!parsed.success) {
        const fe = fieldErrors(parsed.error);
        setErrors(fe);
        document.getElementById(`co-${Object.keys(fe)[0]}`)?.focus();
        return;
      }
    }
    setBusy(true);
    await wait(450);
    const res = exec("POST", "/api/orders", (db) => createOrder(db, { customer: form, lines: cart, now: Date.now() }));
    setBusy(false);
    if (res.ok) {
      onCreated(res.body.order);
      return;
    }
    if (res.body.fields) {
      setErrors(res.body.fields);
      setFormError(`${res.status}: ${res.body.message}`);
    } else {
      setFormError(`${res.status}: ${res.body.message}`);
    }
  }

  const err = (k: keyof CheckoutInput) => errors[k] || undefined;
  const a = (k: keyof CheckoutInput) => ({
    id: `co-${k}`,
    "aria-invalid": !!err(k),
    "aria-describedby": err(k) ? `co-${k}-err` : undefined,
  });

  return (
    <div className="grid gap-6 p-4 sm:p-6 lg:grid-cols-[1fr_18rem]">
      <form onSubmit={submit} noValidate className="space-y-4">
        <div className="flex flex-wrap items-center justify-between gap-2">
          <h2 className="text-lg font-semibold">Checkout · details</h2>
          <button type="button" onClick={() => { setForm(SAMPLE); setErrors({}); }} className="rounded border border-border px-2 py-1 font-mono text-xs hover:border-accent hover:text-accent">
            fill sample details
          </button>
        </div>
        {quote && quote.warnings.length > 0 && (
          <div role="status" className="rounded-md border border-warn-fg/40 bg-warn-bg p-3 text-sm text-warn-fg">
            <p className="font-medium">The server re-priced your cart from the catalogue.</p>
            <ul className="mt-1 list-disc pl-5 text-xs">
              {quote.warnings.map((w) => (
                <li key={w}>{w}</li>
              ))}
            </ul>
          </div>
        )}
        <div className="grid gap-4 sm:grid-cols-2">
          <Field id="co-email" label="Email" error={err("email")}>
            <input {...a("email")} type="email" autoComplete="email" value={form.email} onChange={(e) => set("email", e.target.value)} className={input} />
          </Field>
          <Field id="co-fullName" label="Full name" error={err("fullName")}>
            <input {...a("fullName")} autoComplete="name" value={form.fullName} onChange={(e) => set("fullName", e.target.value)} className={input} />
          </Field>
          <div className="sm:col-span-2">
            <Field id="co-address" label="Street address" error={err("address")}>
              <input {...a("address")} autoComplete="street-address" value={form.address} onChange={(e) => set("address", e.target.value)} className={input} />
            </Field>
          </div>
          <Field id="co-city" label="Town or city" error={err("city")}>
            <input {...a("city")} autoComplete="address-level2" value={form.city} onChange={(e) => set("city", e.target.value)} className={input} />
          </Field>
          <Field id="co-postcode" label="Postcode" error={err("postcode")}>
            <input {...a("postcode")} autoComplete="postal-code" value={form.postcode} onChange={(e) => set("postcode", e.target.value)} className={input} />
          </Field>
          <Field id="co-country" label="Country" error={err("country")}>
            <select {...a("country")} autoComplete="country-name" value={form.country} onChange={(e) => set("country", e.target.value as CheckoutInput["country"])} className={input}>
              {COUNTRIES.map((c) => (
                <option key={c}>{c}</option>
              ))}
            </select>
          </Field>
        </div>

        <fieldset>
          <legend className="text-sm font-medium">Delivery</legend>
          <div className="mt-2 grid gap-2 sm:grid-cols-2">
            {(Object.keys(SHIPPING) as CheckoutInput["shipping"][]).map((m) => {
              const free = m === "standard" && (quote?.subtotalCents ?? 0) >= FREE_SHIPPING_FROM;
              return (
                <label key={m} className={`flex cursor-pointer items-start gap-3 rounded-md border p-3 text-sm focus-within:ring-2 focus-within:ring-accent ${form.shipping === m ? "border-accent bg-accent-soft/50" : "border-border"}`}>
                  <input type="radio" name="shipping" value={m} checked={form.shipping === m} onChange={() => set("shipping", m)} className="mt-1 accent-[var(--accent)]" />
                  <span className="flex-1">
                    <span className="font-medium">{SHIPPING[m].label}</span>
                    <span className="block text-xs text-fg-muted">{SHIPPING[m].eta}</span>
                  </span>
                  <span className="font-mono text-xs">{free ? "Free" : money(SHIPPING[m].cents)}</span>
                </label>
              );
            })}
          </div>
          <p className="mt-1 text-xs text-fg-muted">Standard delivery is free on orders over {money(FREE_SHIPPING_FROM)}.</p>
        </fieldset>

        <label className="flex items-center gap-2 text-xs text-fg-muted">
          <input type="checkbox" checked={skipClient} onChange={(e) => setSkipClient(e.target.checked)} />
          Skip browser validation (send the form as-is and see the server answer 422)
        </label>

        {formError && (
          <p role="alert" className="rounded-md border border-danger/40 p-3 text-sm text-danger">
            {formError}
          </p>
        )}

        <div className="flex flex-wrap gap-3">
          <button type="button" onClick={onBack} className="rounded-md border border-border px-4 py-2 text-sm hover:border-accent">
            ← Back to shop
          </button>
          <button type="submit" disabled={busy || !cart.length} className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-fg hover:opacity-90 disabled:opacity-50">
            {busy ? "Reserving stock…" : "Continue to payment"}
          </button>
        </div>
      </form>
      {quote ? <Summary lines={quote.lines} shipping={form.shipping} title="Order summary (server prices)" /> : <p className="text-sm text-fg-muted">Pricing your cart…</p>}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Step 2: payment (TEST MODE)                                         */
/* ------------------------------------------------------------------ */

function formatCard(v: string) {
  return v.replace(/\D/g, "").slice(0, 16).replace(/(\d{4})(?=\d)/g, "$1 ");
}
function formatExpiry(v: string) {
  const d = v.replace(/\D/g, "").slice(0, 4);
  return d.length > 2 ? `${d.slice(0, 2)} / ${d.slice(2)}` : d;
}

export function PaymentStep({ order, exec, onPaid, onCancel }: { order: Order; exec: Exec; onPaid: (orderId: number) => void; onCancel: (orderId: number) => void }) {
  const [intent, setIntent] = useState<PaymentIntent | null>(null);
  const [key, setKey] = useState("");
  // Card fields live only in this component's state: never persisted, never logged.
  const [card, setCard] = useState({ number: "", expiry: "", cvc: "" });
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [alert, setAlert] = useState<string | null>(null);
  const [busy, setBusy] = useState<null | "pay" | "3ds">(null);
  const [challenge, setChallenge] = useState(false);
  const started = useRef(false);
  const dialogRef = useRef<HTMLDivElement>(null);
  const payRef = useRef<HTMLButtonElement>(null);

  const startIntent = (k: string) => {
    const res = exec("POST", "/api/payments/intents", (db) => createIntent(db, { orderId: order.id, idempotencyKey: k }), {
      headers: { "Idempotency-Key": k },
    });
    if (res.ok) setIntent(res.body);
    else setAlert(`${res.status}: ${res.body.message}`);
  };

  useEffect(() => {
    if (started.current) return;
    started.current = true;
    const k = `idem_${order.id}_${Math.random().toString(36).slice(2, 10)}`;
    setKey(k);
    startIntent(k);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (challenge) dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
  }, [challenge]);

  async function succeed(intentId: string) {
    await wait(500);
    exec(
      "POST",
      "/webhooks/payments",
      (db) => handleWebhook(db, { id: eventIdFor(intentId), type: "payment_intent.succeeded", intentId, signatureValid: true }, Date.now()),
      { headers: { "Payment-Signature": "t=…,v1=hmac_sha256(…)" }, note: "event payment_intent.succeeded" },
    );
    setCard({ number: "", expiry: "", cvc: "" });
    onPaid(order.id);
  }

  async function pay(e: React.FormEvent) {
    e.preventDefault();
    if (!intent) return;
    setAlert(null);
    const tok = tokenizeCard(card, new Date());
    if (!tok.ok) {
      setErrors(tok.fields);
      document.getElementById(`pay-${Object.keys(tok.fields)[0]}`)?.focus();
      return;
    }
    setErrors({});
    setBusy("pay");
    await wait(700);
    const res = exec("POST", `/api/payments/intents/${intent.id}/confirm`, (db) => confirmIntent(db, { intentId: intent.id, pm: tok.pm, last4: tok.last4 }), {
      note: `payment_method=${tok.pm} (card number stayed in the browser)`,
    });
    setBusy(null);
    if (!res.ok) {
      setAlert(res.status === 402 ? `Payment declined: ${res.body.message} Try 4242 4242 4242 4242.` : `${res.status}: ${res.body.message}`);
      payRef.current?.focus();
      return;
    }
    setIntent(res.body);
    if (res.body.nextAction === "three_d_secure") {
      setChallenge(true);
      return;
    }
    await succeed(intent.id);
  }

  async function authenticate(approve: boolean) {
    if (!intent) return;
    setBusy("3ds");
    await wait(500);
    const res = exec("POST", `/api/payments/intents/${intent.id}/authenticate`, (db) => completeAuthentication(db, { intentId: intent.id, approve }));
    setBusy(null);
    setChallenge(false);
    if (!res.ok) {
      setAlert(`Payment not completed: ${res.body.message}`);
      payRef.current?.focus();
      return;
    }
    setIntent(res.body);
    await succeed(intent.id);
  }

  function trap(e: React.KeyboardEvent) {
    if (e.key === "Escape") {
      e.preventDefault();
      void authenticate(false);
    }
    if (e.key !== "Tab") return;
    const btns = [...(dialogRef.current?.querySelectorAll<HTMLButtonElement>("button") ?? [])];
    const first = btns[0];
    const last = btns.at(-1);
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last?.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first?.focus();
    }
  }

  const a = (k: string) => ({ id: `pay-${k}`, "aria-invalid": !!errors[k], "aria-describedby": errors[k] ? `pay-${k}-err` : undefined });

  return (
    <div className="grid gap-6 p-4 sm:p-6 lg:grid-cols-[1fr_18rem]">
      <div className="space-y-4">
        <h2 className="text-lg font-semibold">Checkout · payment for order #{order.number}</h2>
        <div role="note" className="rounded-md border border-warn-fg/50 bg-warn-bg p-3 text-sm text-warn-fg">
          <p className="font-mono text-xs font-bold tracking-wide">TEST MODE: no real payment, no card data leaves your browser</p>
          <p className="mt-1 text-xs">This is a simulated payment step. Use one of the test cards below with any future expiry and any CVC. Real card numbers are refused.</p>
          <ul className="mt-2 space-y-1">
            {TEST_CARDS.map((c) => (
              <li key={c.number} className="flex flex-wrap items-center gap-2 text-xs">
                <code className="font-mono">{c.number}</code>
                <span>{c.outcome}</span>
                <button
                  type="button"
                  onClick={() => {
                    setCard({ number: c.number, expiry: "12 / 34", cvc: "123" });
                    setErrors({});
                  }}
                  className="rounded border border-warn-fg/50 px-1.5 font-mono hover:bg-warn-fg/10"
                  aria-label={`Use test card ${c.number} (${c.outcome})`}
                >
                  use
                </button>
              </li>
            ))}
          </ul>
        </div>

        <form onSubmit={pay} noValidate className="space-y-4 rounded-lg border border-border p-4">
          <Field id="pay-number" label="Card number" error={errors.number}>
            <input
              {...a("number")}
              inputMode="numeric"
              autoComplete="off"
              placeholder="4242 4242 4242 4242"
              value={card.number}
              onChange={(e) => setCard((c) => ({ ...c, number: formatCard(e.target.value) }))}
              className={`${input} font-mono`}
            />
          </Field>
          <div className="grid grid-cols-2 gap-4">
            <Field id="pay-expiry" label="Expiry (MM / YY)" error={errors.expiry}>
              <input {...a("expiry")} inputMode="numeric" autoComplete="off" placeholder="12 / 34" value={card.expiry} onChange={(e) => setCard((c) => ({ ...c, expiry: formatExpiry(e.target.value) }))} className={`${input} font-mono`} />
            </Field>
            <Field id="pay-cvc" label="CVC" error={errors.cvc}>
              <input {...a("cvc")} inputMode="numeric" autoComplete="off" placeholder="123" maxLength={4} value={card.cvc} onChange={(e) => setCard((c) => ({ ...c, cvc: e.target.value.replace(/\D/g, "") }))} className={`${input} font-mono`} />
            </Field>
          </div>
          <div aria-live="assertive">
            {alert && (
              <p role="alert" className="rounded-md border border-danger/40 p-3 text-sm text-danger">
                {alert}
              </p>
            )}
          </div>
          <button ref={payRef} type="submit" disabled={!intent || busy !== null} className="w-full rounded-md bg-accent px-4 py-2.5 font-medium text-accent-fg hover:opacity-90 disabled:opacity-50">
            {busy === "pay" ? "Processing…" : `Pay ${money(order.totalCents)} (test)`}
          </button>
        </form>

        <div className="space-y-2 rounded-md border border-border bg-bg-muted p-3 font-mono text-xs text-fg-muted">
          <p>
            intent: <span className="text-fg">{intent?.id ?? "creating…"}</span> · status: <span className="text-accent">{intent?.status ?? "…"}</span>
          </p>
          <p className="break-all">Idempotency-Key: {key || "…"}</p>
          <div className="flex flex-wrap gap-2">
            <button type="button" onClick={() => startIntent(key)} disabled={!key} className="rounded border border-border px-2 py-1 hover:border-accent hover:text-accent">
              resend create-intent (same key)
            </button>
            <button type="button" onClick={() => onCancel(order.id)} className="rounded border border-border px-2 py-1 hover:border-danger hover:text-danger">
              cancel order &amp; release stock
            </button>
          </div>
        </div>
      </div>
      <Summary lines={order.lines} shipping={order.shipping} title={`Order #${order.number}`} />

      {challenge && (
        <div className="fixed inset-0 z-50 grid place-items-center bg-black/60 p-4">
          <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="tds-title" onKeyDown={trap} className="w-full max-w-sm rounded-lg border border-border bg-bg p-5 shadow-xl">
            <p className="font-mono text-[10px] font-bold tracking-wide text-warn-fg">TEST MODE · SIMULATED BANK PAGE</p>
            <h3 id="tds-title" className="mt-2 text-lg font-semibold">
              Confirm this payment
            </h3>
            <p className="mt-2 text-sm text-fg-muted">
              Card 4000 0000 0000 3220 requires 3-D Secure. A real bank would ask you to approve {money(order.totalCents)} in its app. Choose an outcome:
            </p>
            <div className="mt-4 flex flex-wrap gap-2">
              <button type="button" disabled={busy !== null} onClick={() => void authenticate(true)} className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-fg hover:opacity-90">
                Complete authentication
              </button>
              <button type="button" disabled={busy !== null} onClick={() => void authenticate(false)} className="rounded-md border border-border px-4 py-2 text-sm hover:border-danger hover:text-danger">
                Fail authentication
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
app/lab/ecommerce-storefront/Demo.tsx881 lines
"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cancelOrder, createOrder, eventIdFor, getProduct, handleWebhook, listProducts, type Handled, type ProductQuery, type Res, type Sort } from "./api";
import { DetailsStep, PaymentStep, type Exec } from "./Checkout";
import { CATEGORIES, findVariant, fromPrice, money, seedDb, totalStock, variantLabel, type Category, type DB, type Order, type Product } from "./db";
import { DevPanel, type LogEntry } from "./DevPanel";
import { ProductArt } from "./ProductArt";

const KEY = "lab:ecommerce-storefront:v1";
const MAX_QTY = 10;
const RIVAL_EMAIL = "other.shopper@example.com";

/** The cart lives in the browser. `unitCents` is only for display: the server never trusts it. */
type CartLine = { sku: string; qty: number; unitCents: number };
type View =
  | { name: "shop" }
  | { name: "product"; id: string }
  | { name: "checkout" }
  | { name: "pay"; orderId: number }
  | { name: "confirm"; orderId: number }
  | { name: "orders" };

const DEFAULT_QUERY: ProductQuery = { q: "", category: "all", min: null, max: null, sort: "featured", inStock: false };
const SORTS: { id: Sort; label: string }[] = [
  { id: "featured", label: "Featured" },
  { id: "price-asc", label: "Price: low to high" },
  { id: "price-desc", label: "Price: high to low" },
  { id: "name", label: "Name A–Z" },
];

const btn = "rounded-md border border-border px-3 py-1.5 text-sm hover:border-accent hover:text-accent";
const field = "mt-1 w-full rounded-md border border-border bg-bg-elev px-3 py-2 text-sm outline-none focus:border-accent";

function queryFromUrl(): ProductQuery {
  const p = new URLSearchParams(window.location.search);
  const num = (k: string) => (p.get(k) && Number.isFinite(Number(p.get(k))) ? Number(p.get(k)) : null);
  const cat = p.get("category");
  const sort = p.get("sort");
  return {
    q: p.get("q") ?? "",
    category: CATEGORIES.some((c) => c.id === cat) ? (cat as Category) : "all",
    min: num("min"),
    max: num("max"),
    sort: SORTS.some((s) => s.id === sort) ? (sort as Sort) : "featured",
    inStock: p.get("inStock") === "1",
  };
}

function queryToUrl(q: ProductQuery) {
  const p = new URLSearchParams();
  if (q.q) p.set("q", q.q);
  if (q.category !== "all") p.set("category", q.category);
  if (q.min !== null) p.set("min", String(q.min));
  if (q.max !== null) p.set("max", String(q.max));
  if (q.sort !== "featured") p.set("sort", q.sort);
  if (q.inStock) p.set("inStock", "1");
  const s = p.toString();
  window.history.replaceState(null, "", `${window.location.pathname}${s ? `?${s}` : ""}`);
}

function StatusChip({ status }: { status: Order["status"] }) {
  const tone = status === "paid" ? "bg-accent-soft text-accent" : status === "pending_payment" ? "bg-warn-bg text-warn-fg" : "bg-bg-muted text-fg-muted";
  return <span className={`rounded-full px-2 py-0.5 font-mono text-xs ${tone}`}>{status.replace("_", " ")}</span>;
}

export default function Demo() {
  const [db, setDb] = useState<DB>(seedDb);
  const dbRef = useRef(db);
  const [cart, setCart] = useState<CartLine[]>([]);
  const [view, setView] = useState<View>({ name: "shop" });
  const [query, setQuery] = useState<ProductQuery>(DEFAULT_QUERY);
  const [log, setLog] = useState<LogEntry[]>([]);
  const logId = useRef(0);
  const [devOpen, setDevOpen] = useState(false);
  const [cartOpen, setCartOpen] = useState(false);
  const [announce, setAnnounce] = useState("");
  const [loaded, setLoaded] = useState(false);
  const cartBtn = useRef<HTMLButtonElement>(null);
  const searchRef = useRef<HTMLInputElement>(null);

  /** Run a "server" handler: apply its DB (commit) and write the request to the dev panel. */
  const exec: Exec = useCallback(<T,>(method: LogEntry["method"], path: string, fn: (db: DB) => Handled<T>, extra?: { headers?: Record<string, string>; note?: string }): Res<T> => {
    const out = fn(dbRef.current);
    if (out.db !== dbRef.current) {
      dbRef.current = out.db;
      setDb(out.db);
    }
    const entry: LogEntry = {
      id: ++logId.current,
      method,
      path,
      status: out.res.status,
      headers: { ...extra?.headers, ...out.res.headers },
      sql: out.res.sql,
      note: extra?.note ?? (out.res.ok ? undefined : out.res.body.message),
    };
    if (!Object.keys(entry.headers!).length) delete entry.headers;
    setLog((l) => [entry, ...l].slice(0, 60));
    return out.res;
  }, []);

  // Load saved state and URL filters after mount; the server render always uses the seed.
  useEffect(() => {
    try {
      const raw = localStorage.getItem(KEY);
      if (raw) {
        const saved = JSON.parse(raw) as { db?: DB; cart?: CartLine[] };
        if (saved.db?.products?.length) {
          dbRef.current = saved.db;
          setDb(saved.db);
        }
        if (Array.isArray(saved.cart)) setCart(saved.cart);
      }
    } catch {
      /* storage blocked or corrupted: keep the seed */
    }
    setQuery(queryFromUrl());
    setLoaded(true);
  }, []);

  useEffect(() => {
    if (!loaded) return;
    try {
      localStorage.setItem(KEY, JSON.stringify({ db, cart }));
    } catch {
      /* ignore */
    }
  }, [db, cart, loaded]);

  // Filters: keep them in the URL and log the listing request (debounced like a real search box).
  useEffect(() => {
    if (!loaded) return;
    queryToUrl(query);
    const t = setTimeout(() => {
      const qs = new URLSearchParams(
        Object.entries(query).flatMap(([k, v]) => (v === null || v === "" || v === false || v === "all" ? [] : [[k, String(v)]])),
      ).toString();
      exec("GET", `/api/products${qs ? `?${qs}` : ""}`, (d) => listProducts(d, query));
    }, 300);
    return () => clearTimeout(t);
  }, [query, loaded, exec]);

  // "/" focuses search, like most shops.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const t = e.target as HTMLElement;
      if (e.key === "/" && !/INPUT|TEXTAREA|SELECT/.test(t.tagName)) {
        e.preventDefault();
        setView({ name: "shop" });
        setTimeout(() => searchRef.current?.focus(), 0);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const products = useMemo(() => listProducts(db, query).res, [db, query]);
  const rows = products.ok ? products.body : [];
  const cartCount = cart.reduce((n, l) => n + l.qty, 0);
  const myOrders = db.orders.filter((o) => o.email !== RIVAL_EMAIL);

  function addToCart(sku: string, qty: number) {
    const hit = findVariant(dbRef.current.products, sku);
    if (!hit) return;
    const cap = Math.min(MAX_QTY, hit.variant.stock);
    setCart((c) => {
      const existing = c.find((l) => l.sku === sku);
      if (existing) return c.map((l) => (l.sku === sku ? { ...l, qty: Math.min(cap, l.qty + qty) } : l));
      return [...c, { sku, qty: Math.min(cap, qty), unitCents: hit.variant.priceCents }];
    });
    setAnnounce(`Added ${qty} × ${hit.product.name} (${variantLabel(hit.variant)}) to your cart.`);
  }

  function setQty(sku: string, qty: number) {
    setCart((c) => (qty <= 0 ? c.filter((l) => l.sku !== sku) : c.map((l) => (l.sku === sku ? { ...l, qty: Math.min(MAX_QTY, qty) } : l))));
  }

  function openProduct(id: string) {
    exec("GET", `/api/products/${id}`, (d) => getProduct(d, id));
    setView({ name: "product", id });
  }

  function tamper() {
    if (!cart.length) {
      setAnnounce("Add something to the cart first, then tamper with its price.");
      return;
    }
    setCart((c) => c.map((l, i) => (i === 0 ? { ...l, unitCents: 1 } : l)));
    setAnnounce("The first cart line now claims a price of $0.01 in localStorage. Go to checkout and watch the server ignore it.");
  }

  function rival() {
    const line = cart[0];
    const hit = line && findVariant(dbRef.current.products, line.sku);
    if (!line || !hit) {
      setAnnounce("Add something to the cart first.");
      return;
    }
    if (hit.variant.stock === 0) {
      setAnnounce("That variant is already sold out.");
      return;
    }
    const res = exec(
      "POST",
      "/api/orders",
      (d) =>
        createOrder(d, {
          customer: { email: RIVAL_EMAIL, fullName: "Another Shopper", address: "1 Elsewhere Road", city: "Leeds", postcode: "LS1 1AA", country: "United Kingdom", shipping: "standard" },
          lines: [{ sku: line.sku, qty: hit.variant.stock }],
          now: Date.now(),
        }),
      { note: "another shopper (other tab) buys every remaining unit" },
    );
    if (res.ok) setAnnounce(`Another shopper just reserved the last ${hit.variant.stock} × ${hit.product.name}. Try checking out now.`);
  }

  function cancel(orderId: number) {
    const res = exec("POST", `/api/orders/${orderId}/cancel`, (d) => cancelOrder(d, orderId));
    setAnnounce(res.ok ? `Order #${res.body.number} cancelled; its stock is back on the shelf.` : res.body.message);
    if (res.ok) setView({ name: "orders" });
  }

  function reset() {
    try {
      localStorage.removeItem(KEY);
    } catch {
      /* ignore */
    }
    const fresh = seedDb();
    dbRef.current = fresh;
    setDb(fresh);
    setCart([]);
    setLog([]);
    setQuery(DEFAULT_QUERY);
    setView({ name: "shop" });
    setAnnounce("Demo reset: catalogue, stock, cart and orders are back to the start.");
  }

  let main: React.ReactNode;
  switch (view.name) {
    case "product": {
      const p = db.products.find((x) => x.id === view.id);
      main = p ? (
        <ProductDetail key={p.id} product={p} cartQty={(sku) => cart.find((l) => l.sku === sku)?.qty ?? 0} onAdd={addToCart} onBack={() => setView({ name: "shop" })} />
      ) : (
        <p className="p-6 text-sm">Product not found.</p>
      );
      break;
    }
    case "checkout":
      main = cart.length ? (
        <DetailsStep
          cart={cart.map(({ sku, qty, unitCents }) => ({ sku, qty, unitCents }))}
          exec={exec}
          onBack={() => setView({ name: "shop" })}
          onCreated={(o) => {
            setCart([]);
            setView({ name: "pay", orderId: o.id });
          }}
        />
      ) : (
        <EmptyState text="Your cart is empty." onShop={() => setView({ name: "shop" })} />
      );
      break;
    case "pay": {
      const o = db.orders.find((x) => x.id === view.orderId);
      main =
        o && o.status === "pending_payment" ? (
          <PaymentStep key={o.id} order={o} exec={exec} onPaid={(id) => setView({ name: "confirm", orderId: id })} onCancel={cancel} />
        ) : (
          <EmptyState text="This order is not waiting for payment." onShop={() => setView({ name: "orders" })} />
        );
      break;
    }
    case "confirm": {
      const o = db.orders.find((x) => x.id === view.orderId);
      main = o ? <Confirmation order={o} exec={exec} onShop={() => setView({ name: "shop" })} /> : <EmptyState text="Order not found." onShop={() => setView({ name: "shop" })} />;
      break;
    }
    case "orders":
      main = (
        <Orders
          orders={myOrders}
          onView={(id) => setView({ name: "confirm", orderId: id })}
          onPay={(id) => setView({ name: "pay", orderId: id })}
          onCancel={cancel}
          onShop={() => setView({ name: "shop" })}
        />
      );
      break;
    default:
      main = <Catalogue rows={rows} query={query} setQuery={setQuery} searchRef={searchRef} onOpen={openProduct} />;
  }

  return (
    <div>
      <div className="flex flex-wrap items-center gap-2 border-b border-border px-4 py-3 sm:px-6">
        <button type="button" onClick={() => setView({ name: "shop" })} className="mr-auto text-left">
          <span className="block font-semibold tracking-tight">Common Goods</span>
          <span className="block font-mono text-[10px] text-warn-fg">demo store · test mode</span>
        </button>
        <nav aria-label="Store" className="flex flex-wrap gap-2">
          <button type="button" onClick={() => setView({ name: "shop" })} aria-current={view.name === "shop" ? "page" : undefined} className={`${btn} aria-[current=page]:border-accent aria-[current=page]:text-accent`}>
            Shop
          </button>
          <button type="button" onClick={() => setView({ name: "orders" })} aria-current={view.name === "orders" ? "page" : undefined} className={`${btn} aria-[current=page]:border-accent aria-[current=page]:text-accent`}>
            Orders <span className="font-mono text-xs text-fg-muted">{myOrders.length}</span>
          </button>
          <button ref={cartBtn} type="button" onClick={() => setCartOpen(true)} className={btn} aria-label={`Cart, ${cartCount} items`}>
            Cart <span className="ml-1 rounded-full bg-accent px-1.5 font-mono text-xs text-accent-fg">{cartCount}</span>
          </button>
          <button type="button" onClick={() => setDevOpen((d) => !d)} aria-pressed={devOpen} className={`${btn} font-mono aria-pressed:border-accent aria-pressed:text-accent`}>
            Dev panel
          </button>
          <button type="button" onClick={reset} className={`${btn} font-mono`}>
            Reset demo
          </button>
        </nav>
      </div>

      <p aria-live="polite" className={`border-b border-border bg-accent-soft/40 px-4 py-2 text-sm sm:px-6 ${announce ? "" : "sr-only"}`}>
        {announce}
      </p>

      <div className={devOpen ? "grid lg:grid-cols-[minmax(0,1fr)_24rem]" : ""}>
        <div className="min-w-0">{main}</div>
        {devOpen && (
          <div className="border-t border-border lg:border-t-0 lg:border-l">
            <div className="flex flex-wrap gap-2 border-b border-[#27272a] bg-[#0b0b0d] p-3 font-mono text-[11px]">
              <span className="w-full text-[#71717a]">break things on purpose:</span>
              <button type="button" onClick={tamper} className="rounded border border-[#3f3f46] px-2 py-1 text-[#fbbf24] hover:border-[#fbbf24]">
                tamper cart price
              </button>
              <button type="button" onClick={rival} className="rounded border border-[#3f3f46] px-2 py-1 text-[#fbbf24] hover:border-[#fbbf24]">
                rival buys my item
              </button>
            </div>
            <DevPanel log={log} onClear={() => setLog([])} />
          </div>
        )}
      </div>

      {cartOpen && (
        <CartDrawer
          cart={cart}
          products={db.products}
          onQty={setQty}
          onClose={() => {
            setCartOpen(false);
            cartBtn.current?.focus();
          }}
          onCheckout={() => {
            setCartOpen(false);
            setView({ name: "checkout" });
          }}
        />
      )}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Catalogue                                                           */
/* ------------------------------------------------------------------ */

function Catalogue({
  rows,
  query,
  setQuery,
  searchRef,
  onOpen,
}: {
  rows: Product[];
  query: ProductQuery;
  setQuery: React.Dispatch<React.SetStateAction<ProductQuery>>;
  searchRef: React.RefObject<HTMLInputElement | null>;
  onOpen: (id: string) => void;
}) {
  const set = <K extends keyof ProductQuery>(k: K, v: ProductQuery[K]) => setQuery((q) => ({ ...q, [k]: v }));
  const num = (v: string) => (v === "" ? null : Math.max(0, Number(v)));
  const dirty = JSON.stringify(query) !== JSON.stringify(DEFAULT_QUERY);
  const badRange = query.min !== null && query.max !== null && query.min > query.max;

  return (
    <div className="grid gap-6 p-4 sm:p-6 md:grid-cols-[14rem_1fr]">
      <form role="search" aria-label="Filter products" onSubmit={(e) => e.preventDefault()} className="space-y-4">
        <label className="block">
          <span className="text-sm font-medium">
            Search <kbd className="ml-1 rounded border border-border px-1 font-mono text-[10px] text-fg-muted">/</kbd>
          </span>
          <input ref={searchRef} type="search" value={query.q} onChange={(e) => set("q", e.target.value)} placeholder="mug, wool, Stride Lab…" className={field} />
        </label>
        <fieldset>
          <legend className="text-sm font-medium">Category</legend>
          <div className="mt-1 flex flex-wrap gap-1.5">
            {[{ id: "all" as const, label: "All" }, ...CATEGORIES].map((c) => (
              <button
                key={c.id}
                type="button"
                aria-pressed={query.category === c.id}
                onClick={() => set("category", c.id)}
                className="rounded-full border border-border px-2.5 py-1 text-xs hover:border-accent aria-pressed:border-accent aria-pressed:bg-accent aria-pressed:text-accent-fg"
              >
                {c.label}
              </button>
            ))}
          </div>
        </fieldset>
        <fieldset>
          <legend className="text-sm font-medium">Price (USD)</legend>
          <div className="mt-1 grid grid-cols-2 gap-2">
            <label className="text-xs text-fg-muted">
              Min
              <input type="number" min={0} inputMode="numeric" value={query.min ?? ""} onChange={(e) => set("min", num(e.target.value))} className={field} aria-invalid={badRange} />
            </label>
            <label className="text-xs text-fg-muted">
              Max
              <input type="number" min={0} inputMode="numeric" value={query.max ?? ""} onChange={(e) => set("max", num(e.target.value))} className={field} aria-invalid={badRange} />
            </label>
          </div>
          {badRange && <p className="mt-1 text-xs text-danger">Min is higher than max, so nothing can match.</p>}
        </fieldset>
        <label className="flex items-center gap-2 text-sm">
          <input type="checkbox" checked={query.inStock} onChange={(e) => set("inStock", e.target.checked)} />
          In stock only
        </label>
        <label className="block">
          <span className="text-sm font-medium">Sort</span>
          <select value={query.sort} onChange={(e) => set("sort", e.target.value as Sort)} className={field}>
            {SORTS.map((s) => (
              <option key={s.id} value={s.id}>
                {s.label}
              </option>
            ))}
          </select>
        </label>
        {dirty && (
          <button type="button" onClick={() => setQuery(DEFAULT_QUERY)} className="font-mono text-xs text-accent hover:underline">
            clear filters
          </button>
        )}
      </form>

      <section aria-label="Products">
        <p aria-live="polite" className="mb-3 font-mono text-xs text-fg-muted">
          {rows.length} {rows.length === 1 ? "product" : "products"} · filters are saved in the URL
        </p>
        {rows.length === 0 ? (
          <div className="rounded-lg border border-dashed border-border p-8 text-center text-sm text-fg-muted">
            <p>No products match these filters.</p>
            <button type="button" onClick={() => setQuery(DEFAULT_QUERY)} className="mt-3 rounded-md border border-border px-3 py-1.5 hover:border-accent hover:text-accent">
              Clear filters
            </button>
          </div>
        ) : (
          <ul className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-3">
            {rows.map((p) => {
              const stock = totalStock(p);
              const colours = [...new Map(p.variants.map((v) => [v.colour, v.hex])).entries()];
              return (
                <li key={p.id}>
                  <button type="button" onClick={() => onOpen(p.id)} className="group block w-full overflow-hidden rounded-lg border border-border text-left hover:border-accent">
                    <div className="relative aspect-square bg-bg-muted p-4">
                      <ProductArt shape={p.shape} hex={p.variants[0]!.hex} className="h-full w-full transition-transform group-hover:scale-105" />
                      {stock === 0 ? (
                        <span className="absolute top-2 left-2 rounded bg-bg px-1.5 py-0.5 font-mono text-[10px] text-danger">sold out</span>
                      ) : stock < 15 ? (
                        <span className="absolute top-2 left-2 rounded bg-warn-bg px-1.5 py-0.5 font-mono text-[10px] text-warn-fg">low stock</span>
                      ) : null}
                    </div>
                    <div className="space-y-1 p-3">
                      <p className="text-xs text-fg-muted">{p.brand}</p>
                      <p className="text-sm leading-snug font-medium group-hover:text-accent">{p.name}</p>
                      <div className="flex items-center justify-between gap-2">
                        <span className="font-mono text-sm">
                          {p.variants.some((v) => v.priceCents !== fromPrice(p)) ? "from " : ""}
                          {money(fromPrice(p))}
                        </span>
                        <span className="flex gap-1" aria-label={`${colours.length} colours`}>
                          {colours.slice(0, 4).map(([name, hex]) => (
                            <span key={name} title={name} className="size-3 rounded-full border border-border" style={{ background: hex }} />
                          ))}
                        </span>
                      </div>
                    </div>
                  </button>
                </li>
              );
            })}
          </ul>
        )}
      </section>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Product detail                                                      */
/* ------------------------------------------------------------------ */

function ProductDetail({ product, cartQty, onAdd, onBack }: { product: Product; cartQty: (sku: string) => number; onAdd: (sku: string, qty: number) => void; onBack: () => void }) {
  const colours = [...new Map(product.variants.map((v) => [v.colour, v.hex])).entries()];
  const sizes = [...new Set(product.variants.map((v) => v.size))];
  const firstInStock = product.variants.find((v) => v.stock > 0) ?? product.variants[0]!;
  const [colour, setColour] = useState(firstInStock.colour);
  const [size, setSize] = useState<string | null>(firstInStock.size);
  const [qty, setQty] = useState(1);
  const variant = product.variants.find((v) => v.colour === colour && v.size === size) ?? null;
  const inCart = variant ? cartQty(variant.sku) : 0;
  const canAdd = variant ? Math.min(MAX_QTY, variant.stock) - inCart : 0;

  function pickColour(c: string) {
    setColour(c);
    const same = product.variants.find((v) => v.colour === c && v.size === size && v.stock > 0);
    if (!same) setSize(product.variants.find((v) => v.colour === c && v.stock > 0)?.size ?? size);
    setQty(1);
  }

  return (
    <div className="p-4 sm:p-6">
      <button type="button" onClick={onBack} className="font-mono text-xs text-fg-muted hover:text-accent">
        ← all products
      </button>
      <div className="mt-4 grid gap-6 md:grid-cols-2">
        <div className="aspect-square rounded-lg border border-border bg-bg-muted p-8">
          <ProductArt shape={product.shape} hex={variant?.hex ?? colours[0]![1]} className="h-full w-full" />
        </div>
        <div className="space-y-5">
          <div>
            <p className="text-sm text-fg-muted">{product.brand}</p>
            <h2 className="text-2xl font-semibold tracking-tight">{product.name}</h2>
            <p className="mt-2 font-mono text-xl">{variant ? money(variant.priceCents) : "—"}</p>
          </div>
          <p className="text-sm text-fg-muted">{product.description}</p>

          {colours.length > 1 && (
            <fieldset>
              <legend className="text-sm font-medium">
                Colour: <span className="text-fg-muted">{colour}</span>
              </legend>
              <div className="mt-2 flex flex-wrap gap-2">
                {colours.map(([name, hex]) => {
                  const any = product.variants.some((v) => v.colour === name && v.stock > 0);
                  return (
                    <button
                      key={name}
                      type="button"
                      aria-pressed={colour === name}
                      aria-label={`${name}${any ? "" : " (sold out)"}`}
                      onClick={() => pickColour(name)}
                      className={`size-9 rounded-full border-2 ${colour === name ? "border-accent" : "border-border"} ${any ? "" : "opacity-40"}`}
                      style={{ background: hex }}
                    />
                  );
                })}
              </div>
            </fieldset>
          )}

          {sizes[0] !== null && (
            <fieldset>
              <legend className="text-sm font-medium">Size</legend>
              <div className="mt-2 flex flex-wrap gap-2">
                {sizes.map((s) => {
                  const v = product.variants.find((x) => x.colour === colour && x.size === s);
                  const out = !v || v.stock === 0;
                  return (
                    <button
                      key={s}
                      type="button"
                      disabled={out}
                      aria-pressed={size === s}
                      onClick={() => {
                        setSize(s);
                        setQty(1);
                      }}
                      className={`min-w-12 rounded-md border px-3 py-1.5 font-mono text-sm ${size === s ? "border-accent bg-accent-soft text-accent" : "border-border hover:border-accent"} disabled:cursor-not-allowed disabled:line-through disabled:opacity-40`}
                    >
                      {s}
                      {out && <span className="sr-only"> (sold out)</span>}
                    </button>
                  );
                })}
              </div>
            </fieldset>
          )}

          <p className="font-mono text-xs" aria-live="polite">
            {!variant || variant.stock === 0 ? (
              <span className="text-danger">Sold out in this option</span>
            ) : variant.stock <= 3 ? (
              <span className="text-warn-fg">Only {variant.stock} left · SKU {variant.sku}</span>
            ) : (
              <span className="text-fg-muted">
                In stock ({variant.stock}) · SKU {variant.sku}
              </span>
            )}
            {inCart > 0 && <span className="text-fg-muted"> · {inCart} in your cart</span>}
          </p>

          <div className="flex flex-wrap items-end gap-3">
            <label className="text-sm font-medium">
              Quantity
              <select value={qty} onChange={(e) => setQty(Number(e.target.value))} disabled={canAdd <= 0} className={`${field} w-24`}>
                {Array.from({ length: Math.max(1, canAdd) }, (_, i) => i + 1).map((n) => (
                  <option key={n}>{n}</option>
                ))}
              </select>
            </label>
            <button
              type="button"
              disabled={!variant || canAdd <= 0}
              onClick={() => variant && onAdd(variant.sku, Math.min(qty, canAdd))}
              className="rounded-md bg-accent px-5 py-2 font-medium text-accent-fg hover:opacity-90 disabled:opacity-50"
            >
              {!variant || variant.stock === 0 ? "Sold out" : canAdd <= 0 ? "Max in cart" : "Add to cart"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Cart drawer                                                         */
/* ------------------------------------------------------------------ */

function CartDrawer({
  cart,
  products,
  onQty,
  onClose,
  onCheckout,
}: {
  cart: CartLine[];
  products: Product[];
  onQty: (sku: string, qty: number) => void;
  onClose: () => void;
  onCheckout: () => void;
}) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    ref.current?.querySelector<HTMLElement>("button")?.focus();
  }, []);

  function onKey(e: React.KeyboardEvent) {
    if (e.key === "Escape") {
      e.preventDefault();
      onClose();
      return;
    }
    if (e.key !== "Tab") return;
    const items = [...(ref.current?.querySelectorAll<HTMLElement>("button:not([disabled]), a[href], input, select") ?? [])];
    const first = items[0];
    const last = items.at(-1);
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last?.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first?.focus();
    }
  }

  // Display uses the price stored in the browser on purpose: that is what a naive client would show.
  const subtotal = cart.reduce((n, l) => n + l.unitCents * l.qty, 0);

  return (
    <div className="fixed inset-0 z-50 flex justify-end">
      <button type="button" aria-label="Close cart" tabIndex={-1} onClick={onClose} className="absolute inset-0 bg-black/50" />
      <div ref={ref} role="dialog" aria-modal="true" aria-labelledby="cart-title" onKeyDown={onKey} className="relative flex h-full w-full max-w-sm flex-col border-l border-border bg-bg shadow-xl">
        <div className="flex items-center justify-between border-b border-border px-4 py-3">
          <h2 id="cart-title" className="font-semibold">
            Your cart
          </h2>
          <button type="button" onClick={onClose} className="rounded border border-border px-2 py-0.5 font-mono text-xs hover:border-accent" aria-label="Close cart">
            esc ✕
          </button>
        </div>
        {cart.length === 0 ? (
          <p className="p-4 text-sm text-fg-muted">Your cart is empty. Pick something from the shop.</p>
        ) : (
          <ul className="flex-1 space-y-3 overflow-y-auto p-4">
            {cart.map((l) => {
              const hit = findVariant(products, l.sku);
              if (!hit) return null;
              const { product, variant } = hit;
              const short = variant.stock < l.qty;
              return (
                <li key={l.sku} className="flex gap-3 rounded-md border border-border p-2">
                  <div className="size-16 shrink-0 rounded bg-bg-muted p-1">
                    <ProductArt shape={product.shape} hex={variant.hex} className="h-full w-full" />
                  </div>
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-sm font-medium">{product.name}</p>
                    <p className="text-xs text-fg-muted">{variantLabel(variant)}</p>
                    <p className={`font-mono text-xs ${l.unitCents !== variant.priceCents ? "text-warn-fg" : ""}`}>
                      {money(l.unitCents)}
                      {l.unitCents !== variant.priceCents && " (edited in localStorage)"}
                    </p>
                    {short && <p className="text-xs text-danger">Only {variant.stock} left now.</p>}
                    <div className="mt-1 flex items-center gap-1">
                      <button type="button" onClick={() => onQty(l.sku, l.qty - 1)} aria-label={`Decrease ${product.name}`} className="size-7 rounded border border-border font-mono hover:border-accent">
                        −
                      </button>
                      <span className="w-7 text-center font-mono text-sm" aria-label={`Quantity ${l.qty}`}>
                        {l.qty}
                      </span>
                      <button
                        type="button"
                        onClick={() => onQty(l.sku, l.qty + 1)}
                        disabled={l.qty >= Math.min(MAX_QTY, variant.stock)}
                        aria-label={`Increase ${product.name}`}
                        className="size-7 rounded border border-border font-mono hover:border-accent disabled:opacity-40"
                      >
                        +
                      </button>
                      <button type="button" onClick={() => onQty(l.sku, 0)} className="ml-auto font-mono text-xs text-fg-muted hover:text-danger">
                        remove
                      </button>
                    </div>
                  </div>
                </li>
              );
            })}
          </ul>
        )}
        <div className="border-t border-border p-4">
          <div className="flex justify-between font-mono text-sm">
            <span>Subtotal</span>
            <span>{money(subtotal)}</span>
          </div>
          <p className="mt-1 text-xs text-fg-muted">Shipping and tax are calculated by the server at checkout.</p>
          <button type="button" onClick={onCheckout} disabled={!cart.length} className="mt-3 w-full rounded-md bg-accent px-4 py-2 font-medium text-accent-fg hover:opacity-90 disabled:opacity-50">
            Checkout
          </button>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Orders and confirmation                                             */
/* ------------------------------------------------------------------ */

function EmptyState({ text, onShop }: { text: string; onShop: () => void }) {
  return (
    <div className="p-10 text-center text-sm text-fg-muted">
      <p>{text}</p>
      <button type="button" onClick={onShop} className={`${btn} mt-3`}>
        Back
      </button>
    </div>
  );
}

function Orders({ orders, onView, onPay, onCancel, onShop }: { orders: Order[]; onView: (id: number) => void; onPay: (id: number) => void; onCancel: (id: number) => void; onShop: () => void }) {
  if (!orders.length) return <EmptyState text="No orders yet. Buy something with a test card to see it here." onShop={onShop} />;
  return (
    <div className="overflow-x-auto p-4 sm:p-6">
      <table className="w-full min-w-[34rem] text-left text-sm">
        <caption className="mb-2 text-left font-semibold">Order history</caption>
        <thead className="font-mono text-xs text-fg-muted">
          <tr className="border-b border-border">
            <th className="py-2 pr-3 font-normal">order</th>
            <th className="py-2 pr-3 font-normal">placed</th>
            <th className="py-2 pr-3 font-normal">items</th>
            <th className="py-2 pr-3 text-right font-normal">total</th>
            <th className="py-2 pr-3 font-normal">status</th>
            <th className="py-2 font-normal">
              <span className="sr-only">Actions</span>
            </th>
          </tr>
        </thead>
        <tbody>
          {orders.map((o) => (
            <tr key={o.id} className="border-b border-border">
              <td className="py-2 pr-3 font-mono">#{o.number}</td>
              <td className="py-2 pr-3 text-xs text-fg-muted">{new Date(o.createdAt).toLocaleString()}</td>
              <td className="py-2 pr-3">{o.lines.reduce((n, l) => n + l.qty, 0)}</td>
              <td className="py-2 pr-3 text-right font-mono">{money(o.totalCents)}</td>
              <td className="py-2 pr-3">
                <StatusChip status={o.status} />
              </td>
              <td className="space-x-1 py-2 text-right whitespace-nowrap">
                <button type="button" onClick={() => onView(o.id)} className="rounded border border-border px-2 py-0.5 font-mono text-xs hover:border-accent hover:text-accent">
                  view
                </button>
                {o.status === "pending_payment" && (
                  <>
                    <button type="button" onClick={() => onPay(o.id)} className="rounded border border-border px-2 py-0.5 font-mono text-xs hover:border-accent hover:text-accent">
                      pay
                    </button>
                    <button type="button" onClick={() => onCancel(o.id)} aria-label={`Cancel order ${o.number}`} className="rounded border border-border px-2 py-0.5 font-mono text-xs hover:border-danger hover:text-danger">
                      cancel
                    </button>
                  </>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function Confirmation({ order, exec, onShop }: { order: Order; exec: Exec; onShop: () => void }) {
  const [replay, setReplay] = useState<string | null>(null);
  const heading = useRef<HTMLHeadingElement>(null);
  useEffect(() => heading.current?.focus(), []);

  function resend() {
    if (!order.paymentIntentId) return;
    const id = order.paymentIntentId;
    const res = exec("POST", "/webhooks/payments", (d) => handleWebhook(d, { id: eventIdFor(id), type: "payment_intent.succeeded", intentId: id, signatureValid: true }, Date.now()), {
      headers: { "Payment-Signature": "t=…,v1=hmac_sha256(…)" },
      note: "provider retried the same event",
    });
    setReplay(res.ok && res.body.duplicate ? "200 OK, duplicate: the event id was already processed, so nothing changed." : `${res.status}`);
  }

  const paid = order.status === "paid";
  return (
    <div className="mx-auto max-w-2xl space-y-5 p-4 sm:p-6">
      <div>
        <p className="font-mono text-xs text-fg-muted">order #{order.number}</p>
        <h2 ref={heading} tabIndex={-1} className="text-2xl font-semibold tracking-tight outline-none">
          {paid ? "Thank you, your order is confirmed." : order.status === "cancelled" ? "This order was cancelled." : "Waiting for payment."}
        </h2>
        <p className="mt-1 text-sm text-fg-muted">
          {paid
            ? `Paid with test card ${order.card ?? ""}. A confirmation email would go to ${order.email}.`
            : order.status === "cancelled"
              ? "Reserved stock was released back to the catalogue."
              : "The order exists and its stock is reserved, but no successful payment has arrived yet."}
        </p>
      </div>
      <div className="flex items-center gap-2">
        <StatusChip status={order.status} />
        {order.paidAt && <span className="font-mono text-xs text-fg-muted">paid {new Date(order.paidAt).toLocaleTimeString()}</span>}
      </div>
      <ul className="divide-y divide-border rounded-lg border border-border text-sm">
        {order.lines.map((l) => (
          <li key={l.sku} className="flex justify-between gap-3 p-3">
            <span>
              {l.qty} × {l.name} <span className="text-fg-muted">({l.variantLabel})</span>
            </span>
            <span className="font-mono">{money(l.unitCents * l.qty)}</span>
          </li>
        ))}
      </ul>
      <dl className="space-y-1 font-mono text-sm">
        <div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>{money(order.subtotalCents)}</dd></div>
        <div className="flex justify-between"><dt className="text-fg-muted">Shipping</dt><dd>{order.shippingCents ? money(order.shippingCents) : "Free"}</dd></div>
        <div className="flex justify-between"><dt className="text-fg-muted">Tax</dt><dd>{money(order.taxCents)}</dd></div>
        <div className="flex justify-between font-semibold"><dt>Total</dt><dd>{money(order.totalCents)}</dd></div>
      </dl>
      <p className="text-sm text-fg-muted">
        Ship to {order.fullName}, {order.address}, {order.city} {order.postcode}, {order.country}.
      </p>
      <div className="flex flex-wrap gap-2">
        <button type="button" onClick={onShop} className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-fg hover:opacity-90">
          Continue shopping
        </button>
        {paid && (
          <button type="button" onClick={resend} className={`${btn} font-mono text-xs`}>
            replay webhook (duplicate delivery)
          </button>
        )}
      </div>
      <p aria-live="polite" className="font-mono text-xs text-accent">
        {replay}
      </p>
    </div>
  );
}
app/lab/ecommerce-storefront/DevPanel.tsx55 lines
"use client";

export type LogEntry = {
  id: number;
  method: "GET" | "POST";
  path: string;
  status: number;
  headers?: Record<string, string>;
  sql: string[];
  note?: string;
};

/** Always-dark request/SQL log. Fixed colours on purpose: it looks the same in both themes. */
export function DevPanel({ log, onClear }: { log: LogEntry[]; onClear: () => void }) {
  const statusTone = (s: number) => (s >= 500 ? "text-[#f87171]" : s >= 400 ? "text-[#fbbf24]" : "text-[#4ade80]");
  return (
    <section aria-label="Dev panel: API requests and SQL" className="flex h-full flex-col bg-[#0b0b0d] text-[#d4d4d8]">
      <div className="flex items-center justify-between border-b border-[#27272a] px-3 py-2 font-mono text-xs">
        <span className="text-[#a1a1aa]">
          dev panel <span className="text-[#71717a]">· {log.length} requests</span>
        </span>
        <button type="button" onClick={onClear} className="rounded border border-[#3f3f46] px-2 py-0.5 text-[#a1a1aa] hover:border-[#4ade80] hover:text-[#4ade80]">
          clear
        </button>
      </div>
      <ol role="log" aria-live="polite" className="max-h-[36rem] min-h-48 flex-1 space-y-3 overflow-y-auto p-3 font-mono text-[11.5px] leading-relaxed">
        {log.length === 0 && <li className="text-[#71717a]">$ waiting for requests… browse, add to cart or check out.</li>}
        {log.map((e) => (
          <li key={e.id} className="border-b border-[#1f1f23] pb-3 last:border-0">
            <div className="flex flex-wrap gap-x-2">
              <span className="text-[#60a5fa]">{e.method}</span>
              <span className="break-all text-[#e4e4e7]">{e.path}</span>
              <span className={statusTone(e.status)}>→ {e.status}</span>
            </div>
            {e.headers &&
              Object.entries(e.headers).map(([k, v]) => (
                <div key={k} className="text-[#c084fc]">
                  {k}: {v}
                </div>
              ))}
            {e.note && <div className="text-[#fbbf24]">! {e.note}</div>}
            <pre className="mt-1 whitespace-pre-wrap break-words text-[#a1a1aa]">
              {e.sql.map((s, i) => (
                <span key={i} className={`block ${s.startsWith("--") ? "text-[#71717a]" : /^(BEGIN|COMMIT)/.test(s) ? "text-[#4ade80]" : s.startsWith("ROLLBACK") ? "text-[#f87171]" : ""}`}>
                  {s}
                </span>
              ))}
            </pre>
          </li>
        ))}
      </ol>
    </section>
  );
}
app/lab/ecommerce-storefront/ProductArt.tsx159 lines
import type { Shape } from "./db";

/** Simple drawn product images, so the demo needs no photos or external requests. */
export function ProductArt({ shape, hex, className = "" }: { shape: Shape; hex: string; className?: string }) {
  const s = { fill: hex, stroke: "rgba(0,0,0,0.45)", strokeWidth: 1.5, strokeLinejoin: "round" as const };
  const line = { fill: "none", stroke: "rgba(0,0,0,0.35)", strokeWidth: 1.5, strokeLinecap: "round" as const };
  const shade = { fill: "rgba(0,0,0,0.12)" };
  let body: React.ReactNode;
  switch (shape) {
    case "tee":
      body = (
        <>
          <path d="M42 22 L28 30 L16 46 L28 54 L34 48 L34 98 L86 98 L86 48 L92 54 L104 46 L92 30 L78 22 Q60 34 42 22 Z" {...s} />
          <path d="M48 24 Q60 32 72 24" {...line} />
        </>
      );
      break;
    case "hoodie":
      body = (
        <>
          <path d="M40 26 L24 36 L14 80 L26 84 L32 56 L32 100 L88 100 L88 56 L94 84 L106 80 L96 36 L80 26 Q60 8 40 26 Z" {...s} />
          <path d="M46 28 Q60 50 74 28" {...line} />
          <path d="M44 74 L76 74 L72 88 L48 88 Z" {...shade} />
          <path d="M56 42 L54 60 M64 42 L66 60" {...line} />
        </>
      );
      break;
    case "socks":
      body = (
        <>
          <path d="M34 14 L56 14 L56 70 L74 86 Q80 100 66 104 L40 92 Q32 86 34 76 Z" {...s} />
          <path d="M58 22 L80 22 L80 74 L96 90 Q100 104 86 104 L64 96" {...s} />
          <path d="M34 22 L56 22 M58 30 L80 30" {...line} />
        </>
      );
      break;
    case "cap":
      body = (
        <>
          <path d="M24 72 Q24 36 60 34 Q96 36 96 72 Z" {...s} />
          <path d="M20 72 L106 72 Q112 82 96 84 L24 80 Q16 78 20 72 Z" {...s} />
          <path d="M60 34 L60 72 M40 40 Q48 56 46 72 M80 40 Q72 56 74 72" {...line} />
          <circle cx="60" cy="34" r="3" {...s} />
        </>
      );
      break;
    case "beanie":
      body = (
        <>
          <path d="M28 76 Q26 30 60 28 Q94 30 92 76 Z" {...s} />
          <rect x="24" y="72" width="72" height="20" rx="4" {...s} />
          <path d="M36 74 V90 M48 74 V90 M60 74 V90 M72 74 V90 M84 74 V90" {...line} />
          <circle cx="60" cy="24" r="8" {...s} />
        </>
      );
      break;
    case "sneaker":
      body = (
        <>
          <path d="M14 78 L18 52 Q30 50 40 42 L56 46 Q72 62 100 66 Q108 70 106 80 Z" {...s} />
          <path d="M12 78 L108 78 L106 88 L14 88 Z" fill="#f4f4f5" stroke="rgba(0,0,0,0.45)" strokeWidth={1.5} />
          <path d="M44 50 L52 58 M50 48 L58 56 M56 50 L64 58" {...line} />
        </>
      );
      break;
    case "mug":
      body = (
        <>
          <path d="M84 50 Q104 50 102 66 Q100 82 84 80" fill="none" stroke={hex} strokeWidth={9} />
          <path d="M84 50 Q104 50 102 66 Q100 82 84 80" fill="none" stroke="rgba(0,0,0,0.35)" strokeWidth={1.5} />
          <path d="M28 36 L88 36 L84 96 Q84 100 80 100 L36 100 Q32 100 32 96 Z" {...s} />
          <ellipse cx="58" cy="36" rx="30" ry="5" {...s} />
        </>
      );
      break;
    case "lamp":
      body = (
        <>
          <path d="M34 22 L86 22 L96 58 L24 58 Z" {...s} />
          <path d="M58 58 L58 74" {...line} />
          <path d="M44 74 Q40 98 50 102 L70 102 Q80 98 76 74 Z" {...s} />
          <path d="M34 22 L86 22 L96 58 L24 58 Z" fill="rgba(255,255,255,0.18)" />
        </>
      );
      break;
    case "plant":
      body = (
        <>
          <path d="M60 60 Q40 30 26 34 Q36 56 60 60 Z" fill="#4d7c0f" stroke="rgba(0,0,0,0.4)" />
          <path d="M60 60 Q78 22 96 26 Q88 54 60 60 Z" fill="#65a30d" stroke="rgba(0,0,0,0.4)" />
          <path d="M60 60 Q58 30 64 16 Q72 38 60 60 Z" fill="#3f6212" stroke="rgba(0,0,0,0.4)" />
          <path d="M32 60 L88 60 L80 100 L40 100 Z" {...s} />
          <rect x="28" y="56" width="64" height="10" rx="2" {...s} />
        </>
      );
      break;
    case "blanket":
      body = (
        <>
          <rect x="18" y="38" width="84" height="54" rx="6" {...s} />
          <path d="M18 56 H102 M18 74 H102 M46 38 V92 M74 38 V92" {...line} />
          <path d="M18 80 Q60 96 102 80 L102 92 L18 92 Z" {...shade} />
        </>
      );
      break;
    case "bag":
      body = (
        <>
          <path d="M44 32 Q44 16 60 16 Q76 16 76 32" fill="none" stroke="rgba(0,0,0,0.45)" strokeWidth={3} />
          <rect x="30" y="28" width="60" height="78" rx="12" {...s} />
          <rect x="38" y="66" width="44" height="28" rx="6" {...s} />
          <path d="M30 42 H90" {...line} />
        </>
      );
      break;
    case "bottle":
      body = (
        <>
          <rect x="50" y="12" width="20" height="12" rx="3" fill="#3f3f46" />
          <path d="M48 24 L72 24 L78 38 L78 102 Q78 108 72 108 L48 108 Q42 108 42 102 L42 38 Z" {...s} />
          <rect x="42" y="56" width="36" height="22" fill="rgba(255,255,255,0.18)" />
        </>
      );
      break;
    case "watch":
      body = (
        <>
          <rect x="46" y="10" width="28" height="100" rx="6" {...s} />
          <circle cx="60" cy="60" r="24" fill="#e4e4e7" stroke="rgba(0,0,0,0.5)" strokeWidth={3} />
          <path d="M60 60 L60 44 M60 60 L72 64" stroke="#18181b" strokeWidth={2.5} strokeLinecap="round" />
        </>
      );
      break;
    case "notebook":
      body = (
        <>
          <rect x="30" y="16" width="62" height="88" rx="4" {...s} />
          <path d="M40 16 V104" {...line} />
          <path d="M84 16 V104" stroke="#dc2626" strokeWidth={3} />
        </>
      );
      break;
    case "wallet":
      body = (
        <>
          <rect x="22" y="38" width="76" height="50" rx="6" {...s} />
          <path d="M22 52 H98 M22 66 H98" {...line} />
          <rect x="30" y="28" width="40" height="14" rx="2" fill="#60a5fa" stroke="rgba(0,0,0,0.4)" />
        </>
      );
      break;
  }
  return (
    <svg viewBox="0 0 120 120" className={className} aria-hidden="true">
      {body}
    </svg>
  );
}