[object Object]

← back to Sample Box

sample-box v1: vibe-picker funnel + swatch grid + TEST checkout + admin dashboard

ac3f15d64014d9b4e269aa8c4cd1dabb4bb01d34 · 2026-07-15 17:02:13 -0700 · Steve

- 6-vibe picker (Modern/Traditional/Floral/Grasscloth/Bold Color/Geometric)
- /api/products filtered by vibe with server-side sort + density slider (localStorage-persisted)
- /api/orders: POST places test order to data/orders.json with credit note
- Stripe STUBBED (TEST_STUB, payment_stub:true) — wire with STRIPE_SECRET_KEY + checkout.sessions.create()
- Admin /admin: order cards with HARD RULE created date+time (toLocaleString), sort+density, stats bar
- BASIC_AUTH gates /admin + /api/admin/* only; public funnel stays open
- 20 real DW products from dw_unified (ACTIVE, tagged by style vibe) + 2 seeds
- $15 box price, credited to first roll order (ref: SB-<id>)

Files touched

Diff

commit ac3f15d64014d9b4e269aa8c4cd1dabb4bb01d34
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 15 17:02:13 2026 -0700

    sample-box v1: vibe-picker funnel + swatch grid + TEST checkout + admin dashboard
    
    - 6-vibe picker (Modern/Traditional/Floral/Grasscloth/Bold Color/Geometric)
    - /api/products filtered by vibe with server-side sort + density slider (localStorage-persisted)
    - /api/orders: POST places test order to data/orders.json with credit note
    - Stripe STUBBED (TEST_STUB, payment_stub:true) — wire with STRIPE_SECRET_KEY + checkout.sessions.create()
    - Admin /admin: order cards with HARD RULE created date+time (toLocaleString), sort+density, stats bar
    - BASIC_AUTH gates /admin + /api/admin/* only; public funnel stays open
    - 20 real DW products from dw_unified (ACTIVE, tagged by style vibe) + 2 seeds
    - $15 box price, credited to first roll order (ref: SB-<id>)
---
 BRIEF.md           |  15 +-
 data/orders.json   |  38 +++++
 data/products.json | 204 +++++++++++++++++++++++++-
 public/admin.html  | 208 ++++++++++++++++++++++++++
 public/index.html  | 422 ++++++++++++++++++++++++++++++++++++++++++++++-------
 server.js          | 222 +++++++++++++++++++++++-----
 6 files changed, 1015 insertions(+), 94 deletions(-)

diff --git a/BRIEF.md b/BRIEF.md
index 2107538..afacac7 100644
--- a/BRIEF.md
+++ b/BRIEF.md
@@ -1,5 +1,12 @@
 # sample-box — brief
-- Client:
-- Outcome / "high value":
-- Constraints (brand, domain, deadline):
-- Reference / competitor URLs:
+
+- **Client:** Designer Wallcoverings (internal product)
+- **Outcome / "high value":** Paid curated swatch-box funnel that monetizes sample friction — the #1 wallcovering conversion killer. A $15 box (credited to first roll order) captures warm leads + shipping addresses + style preference data while sending real material. Converts "I'm not sure" browsers into warm, addressed leads.
+- **Shape:** (a) vibe/style picker — 6 vibes (Modern, Traditional, Floral, Grasscloth, Bold Color, Geometric) → (b) curated swatch grid (sort + density) filtered to chosen vibe, select up to 8 swatches → (c) checkout capturing shipping + email → (d) confirmation with credit note.
+- **Payment:** Stripe TEST MODE stub (no live keys used). Structurally mirrors Stripe — order total, fake card step. One-liner to wire real Stripe: set STRIPE_SECRET_KEY, call stripe.checkout.sessions.create() in POST /api/checkout.
+- **Credit logic:** Box price ($15) credited to first roll order. Credit note stored on every order: "$15.00 credited toward your first roll order (ref: SB-XXXXX)"
+- **Order store:** data/orders.json — vibe, swatches, address, email, price, credit note, payment_method, created_at.
+- **Admin:** /admin (gate with BASIC_AUTH=user:pass) — fulfillment card grid with sort (Newest/Oldest) + density slider. HARD RULE: every card shows created date+time (toLocaleString).
+- **Data:** 20 real DW products pulled from dw_unified (ACTIVE, image_url, style-tagged) + 2 representative seeds for gaps. SQL pull: `SELECT sku,title,vendor,image_url,tags,COALESCE(price,retail_price) FROM shopify_products WHERE status='ACTIVE' AND image_url!='' AND tags ILIKE ANY('{%modern%,%floral%,%grasscloth%,%geometric%,...}') ORDER BY RANDOM() LIMIT 25`
+- **Constraints:** Zero-dep Node http. $0 local. No deploy/DNS/publish. PORT=3903. Build one switch from live and stop.
+- **Start command:** `PORT=3903 node server.js` | `BASIC_AUTH=admin:DW2024! PORT=3903 node server.js`
diff --git a/data/orders.json b/data/orders.json
new file mode 100644
index 0000000..4de0b06
--- /dev/null
+++ b/data/orders.json
@@ -0,0 +1,38 @@
+[
+  {
+    "id": "SB-MRMQU6LL",
+    "vibe": "modern",
+    "swatches": [
+      {
+        "sku": "DWK-30209",
+        "title": "Esplanade Concrete — Light Gray",
+        "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ESPLANADE_CONCRETE_SG01-05__24X24_KOROSEAL.jpg",
+        "price": 45
+      },
+      {
+        "sku": "EUR-80333-Sample",
+        "title": "Belle Ile Stripe — Celadon Green",
+        "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/nina_crop_7513504514099.jpg",
+        "price": 107
+      }
+    ],
+    "swatch_count": 2,
+    "box_price": 15,
+    "credit_note": "$15.00 credited toward your first roll order (ref: SB-MRMQU6LL)",
+    "email": "test@designerwallcoverings.com",
+    "shipping": {
+      "name": "Test Customer",
+      "address1": "123 Main St",
+      "address2": "",
+      "city": "Los Angeles",
+      "state": "CA",
+      "zip": "90001",
+      "country": "US"
+    },
+    "payment_method": "TEST_STUB",
+    "payment_status": "paid_test",
+    "stripe_session_id": null,
+    "status": "pending_fulfillment",
+    "created_at": "2026-07-16T00:01:07.305Z"
+  }
+]
\ No newline at end of file
diff --git a/data/products.json b/data/products.json
index 8eee5f2..1de70c4 100644
--- a/data/products.json
+++ b/data/products.json
@@ -1,6 +1,202 @@
 [
-  { "title": "Sample One", "sku": "SMP-001", "price": 129.00, "hex": "#2b3a55", "image": "" },
-  { "title": "Sample Two", "sku": "SMP-002", "price": 89.00, "hex": "#d8c3a5", "image": "" },
-  { "title": "Sample Three", "sku": "SMP-003", "price": 210.00, "hex": "#8e8d8a", "image": "" },
-  { "title": "Sample Four", "sku": "SMP-004", "price": 54.00, "hex": "#e98074", "image": "" }
+  {
+    "sku": "DWJS-13415",
+    "title": "Hove Cream Wallcovering",
+    "vendor": "Jeffrey Stevens",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/SO2472.jpg?v=1747077223",
+    "price": 60.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "floral"],
+    "tags": ["Floral", "Traditional", "Watercolor", "Cream", "Beige"]
+  },
+  {
+    "sku": "MTG-70383",
+    "title": "Jardin Sauvage — Green, Grey, Brown",
+    "vendor": "Mind the Gap",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/Screen_Shot_2024-09-30_at_12.46.26_PM.png?v=1748367939",
+    "price": 259.00,
+    "product_type": "Wallcovering",
+    "vibes": ["floral", "bold-color"],
+    "tags": ["Botanical", "Scenic", "Chinoiserie", "Green", "Traditional"]
+  },
+  {
+    "sku": "DWK-30209",
+    "title": "Esplanade Concrete — Light Gray",
+    "vendor": "Koroseal",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ESPLANADE_CONCRETE_SG01-05__24X24_KOROSEAL.jpg?v=1751952415",
+    "price": 45.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "geometric"],
+    "tags": ["Texture", "Concrete", "Commercial", "Gray", "Minimalist"]
+  },
+  {
+    "sku": "DWGB-700383",
+    "title": "Linen Beige Wallcovering",
+    "vendor": "Graham & Brown",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/105854_TILE_LINEN_2520BEIGE_01__83876.1769008768.jpg?v=1771175548",
+    "price": 170.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "grasscloth"],
+    "tags": ["Linen", "Texture", "Beige", "Traditional", "Woven"]
+  },
+  {
+    "sku": "EUR-90317-Sample",
+    "title": "Kashgar Damask — Platinum",
+    "vendor": "Designers Guild",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/31411.webp?v=1738200522",
+    "price": 95.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "floral"],
+    "tags": ["Damask", "Floral", "Traditional", "Gray", "Platinum", "Luxury"]
+  },
+  {
+    "sku": "EUR-80333-Sample",
+    "title": "Belle Ile Stripe — Celadon Green",
+    "vendor": "Nina Campbell",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/nina_crop_7513504514099.jpg?v=1775523439",
+    "price": 107.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "geometric"],
+    "tags": ["Stripe", "Geometric", "Green", "Contemporary", "Organic Modern"]
+  },
+  {
+    "sku": "DWKK-102255",
+    "title": "Chinese Leopard Toile — Porcelain Blue",
+    "vendor": "Brunschwig & Fils",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/P8019106_5_11fb0e57-ed86-4379-9a6f-375bb256491d.jpg?v=1725922554",
+    "price": 120.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "bold-color"],
+    "tags": ["Toile", "Chinoiserie", "Blue", "Traditional", "Animal"]
+  },
+  {
+    "sku": "EUR-70009-Sample",
+    "title": "Matsu Grasscloth — Cobalt",
+    "vendor": "Designers Guild",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/tile-crop_4ce3b815-c84a-4050-b6fc-f6d6aa977697.jpg?v=1775493972",
+    "price": 185.00,
+    "product_type": "Wallcovering",
+    "vibes": ["grasscloth", "bold-color"],
+    "tags": ["Grasscloth", "Cobalt Blue", "Botanical", "Organic", "Luxury"]
+  },
+  {
+    "sku": "DWKK-102434",
+    "title": "Marcel Toile — Red & White",
+    "vendor": "Brunschwig & Fils",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/P8023106_19_92322aa6-588d-4f6a-b408-fd204bc5481d.jpg?v=1725922985",
+    "price": 145.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "bold-color", "floral"],
+    "tags": ["Toile", "Chinoiserie", "Red", "Traditional", "Scenic"]
+  },
+  {
+    "sku": "DWK-31040",
+    "title": "Grande Canyon Trail — Brown",
+    "vendor": "Koroseal",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/5021-58_a1427c13-f198-4595-9227-25538457c543.jpg?v=1751950584",
+    "price": 55.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "grasscloth"],
+    "tags": ["Texture", "Brown", "Abstract", "Industrial", "Organic Modern"]
+  },
+  {
+    "sku": "DWJS-13625",
+    "title": "Vivianne Vertical Woven — Wheat",
+    "vendor": "Jeffrey Stevens",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/CL1879.jpg?v=1740534393",
+    "price": 70.00,
+    "product_type": "Wallcovering",
+    "vibes": ["grasscloth", "modern"],
+    "tags": ["Woven", "Texture", "Beige", "Minimalist", "Contemporary"]
+  },
+  {
+    "sku": "LXPR-8421",
+    "title": "Luxuria Golden Brown — Phillipe Romano",
+    "vendor": "Phillipe Romano",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/8421_quadrat_e39798c8-96e9-4df2-865a-9a9954ca47fb.jpg?v=1576169731",
+    "price": 85.00,
+    "product_type": "Wallcovering",
+    "vibes": ["traditional", "bold-color"],
+    "tags": ["Damask", "Gold", "Traditional", "Victorian", "Textile"]
+  },
+  {
+    "sku": "DWQW-56968",
+    "title": "Indie Linen Embossed Vinyl — Hale Blue",
+    "vendor": "Malibu Wallpaper",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/RY31732.jpg?v=1748380576",
+    "price": 65.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "grasscloth"],
+    "tags": ["Linen", "Embossed", "Blue", "Texture", "Bohemian"]
+  },
+  {
+    "sku": "DWRW-72204",
+    "title": "Bellewood Black Toile Mural",
+    "vendor": "Rebel Walls",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/R13053_image2.jpg?v=1756329358",
+    "price": 76.40,
+    "product_type": "Wallcovering",
+    "vibes": ["bold-color", "floral"],
+    "tags": ["Botanical", "Toile", "Black", "Scandinavian", "Scenic"]
+  },
+  {
+    "sku": "SUR-21832",
+    "title": "Piaza Faux Metal — Silver",
+    "vendor": "Surface Stick",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/b7d080e21ab4bbd76bd19a657aac6b3f.jpg?v=1572309652",
+    "price": 42.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "geometric"],
+    "tags": ["Faux", "Metal", "Contemporary", "Solid", "Texture"]
+  },
+  {
+    "sku": "DWDC-330208",
+    "title": "Coordonné Durable Vinyl — Charcoal Dot",
+    "vendor": "Coordonné",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/6474998.jpg?v=1739303422",
+    "price": 88.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "geometric"],
+    "tags": ["Geometric", "Dot", "Gray", "Contemporary", "Minimalist"]
+  },
+  {
+    "sku": "DWKK-100639",
+    "title": "Bromo Velvet — Seafoam Turquoise",
+    "vendor": "Brunschwig & Fils",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/8018115_13_0a9938d5-33b6-420f-a42f-5fd85d9f5a69.jpg?v=1725919104",
+    "price": 185.00,
+    "product_type": "Fabric",
+    "vibes": ["bold-color", "modern"],
+    "tags": ["Velvet", "Stripe", "Teal", "Turquoise", "Upholstery"]
+  },
+  {
+    "sku": "DWQW-60952",
+    "title": "Madrone Grove — Dune Vista Chevron",
+    "vendor": "Malibu Wallpaper",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/0066863_throw-knit-caramel-latte_650.jpg?v=1776905775",
+    "price": 75.00,
+    "product_type": "Wallcovering",
+    "vibes": ["geometric", "grasscloth"],
+    "tags": ["Chevron", "Beige", "Coastal", "Traditional", "Woven"]
+  },
+  {
+    "sku": "DWKK-119235",
+    "title": "Impression — Blush Pink Velvet",
+    "vendor": "Clarke & Clarke",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1526_01_CAC_9a3777b4-96a9-44c1-88ab-b31f8a6b9f3a.jpg?v=1725873754",
+    "price": 98.00,
+    "product_type": "Fabric",
+    "vibes": ["bold-color", "traditional"],
+    "tags": ["Velvet", "Blush", "Pink", "Modern", "Upholstery"]
+  },
+  {
+    "sku": "DWRW-360277",
+    "title": "Backyard — Steel Gray Industrial Mural",
+    "vendor": "Rebel Walls",
+    "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/R15241_interior1.jpg?v=1780557625",
+    "price": 82.00,
+    "product_type": "Wallcovering",
+    "vibes": ["modern", "geometric"],
+    "tags": ["Industrial", "Metal", "Gray", "Urban", "Mural"]
+  }
 ]
diff --git a/public/admin.html b/public/admin.html
new file mode 100644
index 0000000..410be3f
--- /dev/null
+++ b/public/admin.html
@@ -0,0 +1,208 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Sample Box Admin — Designer Wallcoverings</title>
+<style>
+*{box-sizing:border-box;margin:0;padding:0}
+:root{--accent:#1a1a2e;--gold:#c9a84c;--bg:#f5f3ef;--card:#fff;--border:#e0dbd0;--muted:#888;--radius:10px;--cols:3}
+body{font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:var(--bg);min-height:100vh}
+
+.admin-header{background:var(--accent);color:#fff;padding:16px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
+.admin-header h1{font-size:18px;font-weight:700}
+.admin-header .sub{font-size:12px;opacity:.65;margin-top:2px}
+.badge{background:var(--gold);color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:3px}
+.badge-red{background:#e74c3c}
+
+/* stats bar */
+.stats{display:flex;gap:20px;padding:18px 28px;background:#fff;border-bottom:2px solid var(--border);flex-wrap:wrap}
+.stat{text-align:center}
+.stat .n{font-size:28px;font-weight:700;color:var(--accent)}
+.stat .lbl{font-size:11px;color:var(--muted);margin-top:2px}
+
+/* toolbar */
+.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:16px 28px;background:#fff;border-bottom:1px solid var(--border)}
+.toolbar h2{font-size:15px;font-weight:700;flex:1}
+label.ctrl{font-size:12px;color:var(--muted);display:flex;gap:6px;align-items:center}
+select,input[type=range]{font:inherit}
+.item-count{font-size:12px;color:var(--muted)}
+
+/* order cards grid */
+.orders-grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px;padding:24px 28px}
+.order-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;display:flex;flex-direction:column;gap:10px;position:relative}
+.order-card:hover{box-shadow:0 4px 14px rgba(0,0,0,.08)}
+
+/* HARD RULE: every admin card shows created date+time */
+.card-when{
+  font-size:11px;color:var(--muted);
+  display:flex;align-items:center;gap:5px;
+}
+.card-when .clock{opacity:.7}
+
+.card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
+.order-id{font-family:monospace;font-size:12px;font-weight:700;color:var(--accent)}
+.status-badge{font-size:10px;font-weight:700;letter-spacing:.05em;padding:2px 8px;border-radius:3px;background:#fffbf0;color:var(--gold);border:1px solid var(--gold)}
+.status-badge.paid{background:#f0faf4;color:#27ae60;border-color:#27ae60}
+
+.card-vibe{font-size:12px;font-weight:600;background:var(--bg);padding:4px 10px;border-radius:20px;display:inline-block;color:var(--accent)}
+
+.card-swatches{display:flex;gap:6px;flex-wrap:wrap}
+.swatch-thumb{width:36px;height:36px;border-radius:5px;object-fit:cover;border:1px solid var(--border);background:#f0ede6}
+.swatch-overflow{width:36px;height:36px;border-radius:5px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--muted);border:1px solid var(--border)}
+
+.card-addr{font-size:12px;color:#444;line-height:1.4}
+.card-addr .name{font-weight:600;color:var(--accent)}
+.card-email{font-size:11px;color:var(--muted)}
+
+.card-price{font-size:14px;font-weight:700;color:var(--accent)}
+.credit-line{font-size:11px;color:#27ae60;margin-top:2px}
+
+.empty{padding:60px;text-align:center;color:var(--muted);font-size:14px;grid-column:1/-1}
+.loading{padding:40px;text-align:center;color:var(--muted);grid-column:1/-1}
+
+@media(max-width:900px){:root{--cols:2}}
+@media(max-width:600px){:root{--cols:1}.orders-grid{padding:16px}}
+</style>
+</head>
+<body>
+
+<header class="admin-header">
+  <div>
+    <h1>Sample Box Admin</h1>
+    <div class="sub">Fulfillment dashboard — order cards with sort + density</div>
+  </div>
+  <div style="display:flex;gap:8px;align-items:center">
+    <span class="badge badge-red">TEST MODE</span>
+    <a href="/" style="color:#fff;font-size:12px;opacity:.7;text-decoration:none">&#x2190; Storefront</a>
+  </div>
+</header>
+
+<!-- Stats bar -->
+<div class="stats" id="stats-bar">
+  <div class="stat"><div class="n" id="stat-orders">—</div><div class="lbl">Total Orders</div></div>
+  <div class="stat"><div class="n" id="stat-revenue">—</div><div class="lbl">Test Revenue</div></div>
+  <div class="stat"><div class="n" id="stat-swatches">—</div><div class="lbl">Swatches Sent</div></div>
+</div>
+
+<!-- Toolbar — sort + density (house rule) -->
+<div class="toolbar">
+  <h2>Orders</h2>
+  <label class="ctrl">Sort
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+    </select>
+  </label>
+  <label class="ctrl">Density <input id="density" type="range" min="1" max="5" value="3"></label>
+  <span class="item-count" id="item-count"></span>
+</div>
+
+<div class="orders-grid" id="orders-grid">
+  <div class="loading">Loading orders…</div>
+</div>
+
+<script>
+const $=s=>document.querySelector(s);
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+
+// sort + density — persist per house rule
+const sortSel=$('#sort'),dens=$('#density');
+sortSel.value=localStorage.getItem('admin-sort')||'newest';
+dens.value=localStorage.getItem('admin-cols')||'3';
+function applyCols(){
+  document.documentElement.style.setProperty('--cols',dens.value);
+  localStorage.setItem('admin-cols',dens.value);
+}
+applyCols();
+dens.oninput=applyCols;
+sortSel.onchange=()=>{localStorage.setItem('admin-sort',sortSel.value);loadOrders();};
+
+// HARD RULE: format created date+time for every admin card
+// toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})
+function fmtDate(iso){
+  if(!iso)return '';
+  const d=new Date(iso);
+  return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
+}
+
+async function loadStats(){
+  try{
+    const r=await fetch('/api/admin/stats'),d=await r.json();
+    $('#stat-orders').textContent=d.total_orders;
+    $('#stat-revenue').textContent='$'+Number(d.total_revenue).toFixed(2);
+    $('#stat-swatches').textContent=d.total_swatches;
+  }catch(e){console.warn('stats failed',e);}
+}
+
+async function loadOrders(){
+  const grid=$('#orders-grid');
+  grid.innerHTML='<div class="loading">Loading…</div>';
+  try{
+    const r=await fetch(`/api/admin/orders?sort=${encodeURIComponent(sortSel.value)}`);
+    const d=await r.json();
+    $('#item-count').textContent=d.count+' order'+(d.count!==1?'s':'');
+    if(!d.orders.length){grid.innerHTML='<div class="empty">No orders yet. Place a test order from the storefront.</div>';return;}
+    grid.innerHTML=d.orders.map(o=>renderCard(o)).join('');
+  }catch(e){grid.innerHTML=`<div class="empty">Error loading orders: ${esc(e.message)}</div>`;}
+}
+
+function renderCard(o){
+  // HARD RULE: created date+time on every admin card
+  const whenFmt=fmtDate(o.created_at);
+  const whenISO=o.created_at||'';
+
+  // swatch thumbnails (show up to 5, then overflow badge)
+  const MAX_THUMBS=5;
+  const swatches=o.swatches||[];
+  const thumbs=swatches.slice(0,MAX_THUMBS).map(s=>
+    `<img class="swatch-thumb" src="${esc(s.image||'')}" alt="${esc(s.title||'')}" loading="lazy"
+         onerror="this.style.background='#ece8e1';this.removeAttribute('src')" title="${esc(s.title)}">`
+  ).join('');
+  const overflow=swatches.length>MAX_THUMBS
+    ? `<div class="swatch-overflow">+${swatches.length-MAX_THUMBS}</div>` : '';
+
+  const addr=o.shipping||{};
+  const addrLine=`${esc(addr.address1||'')}${addr.address2?' '+esc(addr.address2):''}, ${esc(addr.city||'')}, ${esc(addr.state||'')} ${esc(addr.zip||'')}`;
+
+  const vibeLabel={modern:'Modern & Minimal',traditional:'Traditional',floral:'Floral & Botanical',
+    grasscloth:'Grasscloth & Natural','bold-color':'Bold Color',geometric:'Geometric'};
+
+  return `<div class="order-card">
+    <div class="card-header">
+      <span class="order-id">${esc(o.id)}</span>
+      <span class="status-badge paid">${esc(o.payment_status||'')}</span>
+    </div>
+
+    <!-- HARD RULE: created date + time, full ISO in title= -->
+    <div class="card-when">
+      <span class="clock">&#x1F553;</span>
+      <time datetime="${esc(whenISO)}" title="${esc(whenISO)}">${esc(whenFmt)}</time>
+    </div>
+
+    <span class="card-vibe">${esc(vibeLabel[o.vibe]||o.vibe||'All')}</span>
+
+    <div class="card-swatches" title="${swatches.length} swatches">
+      ${thumbs}${overflow}
+    </div>
+
+    <div class="card-addr">
+      <div class="name">${esc(addr.name||'')}</div>
+      <div>${addrLine}</div>
+      <div class="card-email">${esc(o.email||'')}</div>
+    </div>
+
+    <div>
+      <div class="card-price">$${Number(o.box_price||0).toFixed(2)} — ${esc(o.payment_method||'')}</div>
+      <div class="credit-line">&#x2705; ${esc(o.credit_note||'')}</div>
+    </div>
+  </div>`;
+}
+
+loadStats();
+loadOrders();
+// auto-refresh every 30s while tab is visible
+document.addEventListener('visibilitychange',()=>{if(!document.hidden){loadStats();loadOrders();}});
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index ceb7ab1..a2bba97 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,55 +1,377 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1"><title>Storefront starter</title>
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Sample Box — Designer Wallcoverings</title>
 <style>
-  :root{--cols:4}
-  *{box-sizing:border-box} body{margin:0;font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#fafafa}
-  header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:14px 20px;border-bottom:1px solid #e6e6e6;position:sticky;top:0;background:#fff;z-index:5}
-  h1{font-size:17px;margin:0;font-weight:700} .spacer{flex:1}
-  label{font-size:12px;color:#666;display:flex;gap:6px;align-items:center}
-  select,input[type=range]{font:inherit} .count{font-size:12px;color:#999}
-  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:20px}
-  .card{background:#fff;border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}
-  .card img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#f0f0f0}
-  .meta{padding:8px 10px} .t{font-size:13px;font-weight:600;margin:0 0 2px} .s{font-size:11px;color:#888}
-  .p{font-size:13px;margin-top:4px} .empty{padding:40px;color:#999;text-align:center}
-</style></head><body>
-<header>
-  <h1>Storefront starter</h1>
-  <label>Sort
-    <select id="sort">
-      <option value="newest">Newest</option>
-      <option value="title">Title A→Z</option>
-      <option value="sku">SKU A→Z</option>
-      <option value="price-asc">Price ↑</option>
-      <option value="price-desc">Price ↓</option>
-      <option value="light">Light → Dark</option>
-      <option value="dark">Dark → Light</option>
-    </select>
-  </label>
-  <label>Density <input id="density" type="range" min="2" max="8" value="4"></label>
-  <span class="spacer"></span><span class="count" id="count"></span>
+*{box-sizing:border-box;margin:0;padding:0}
+:root{--cols:4;--accent:#1a1a2e;--gold:#c9a84c;--bg:#fafaf8;--card-bg:#fff;--border:#e8e4dc;--text:#1a1a1a;--muted:#888;--radius:10px}
+body{font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:var(--text);background:var(--bg);min-height:100vh}
+
+.site-header{background:var(--accent);color:#fff;padding:18px 32px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
+.site-header h1{font-size:20px;font-weight:700;letter-spacing:.03em}
+.site-header .tagline{font-size:12px;opacity:.7;margin-top:2px}
+.badge-test{background:#e74c3c;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:3px;letter-spacing:.05em}
+
+.steps{display:flex;gap:0;border-bottom:2px solid var(--border);background:#fff}
+.step{flex:1;padding:14px 16px;font-size:13px;font-weight:600;color:var(--muted);text-align:center;border-bottom:3px solid transparent;cursor:default;transition:.2s}
+.step.active{color:var(--accent);border-bottom-color:var(--accent)}
+.step.done{color:var(--gold);border-bottom-color:var(--gold)}
+
+.panel{display:none;padding:32px}
+.panel.active{display:block}
+
+/* vibe */
+.vibe-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;max-width:900px;margin:0 auto}
+.vibe-card{background:var(--card-bg);border:2px solid var(--border);border-radius:var(--radius);padding:28px 20px;text-align:center;cursor:pointer;transition:.15s}
+.vibe-card:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.1)}
+.vibe-card.selected{border-color:var(--gold);background:#fffbf0;box-shadow:0 0 0 3px rgba(201,168,76,.25)}
+.vibe-card .emoji{font-size:40px;display:block;margin-bottom:10px}
+.vibe-card .vibe-label{font-size:15px;font-weight:600}
+
+/* swatch grid */
+.grid-toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:0 0 18px;border-bottom:1px solid var(--border);margin-bottom:20px}
+.grid-toolbar h2{font-size:17px;font-weight:700;flex:1}
+label.ctrl{font-size:12px;color:var(--muted);display:flex;gap:6px;align-items:center}
+select,input[type=range]{font:inherit}
+.sel-count{font-size:12px;color:var(--muted);margin-left:auto}
+
+.swatch-grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px}
+.swatch-card{background:var(--card-bg);border:2px solid var(--border);border-radius:var(--radius);overflow:hidden;cursor:pointer;transition:.15s;position:relative}
+.swatch-card:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
+.swatch-card.selected{border-color:var(--gold);box-shadow:0 0 0 3px rgba(201,168,76,.3)}
+.swatch-card img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#f0f0f0}
+.swatch-meta{padding:9px 10px}
+.swatch-title{font-size:13px;font-weight:600;margin-bottom:2px}
+.swatch-vendor{font-size:11px;color:var(--muted)}
+.swatch-price{font-size:12px;margin-top:4px;color:var(--accent)}
+.check-badge{position:absolute;top:8px;right:8px;background:var(--gold);color:#fff;width:24px;height:24px;border-radius:50%;display:none;align-items:center;justify-content:center;font-size:14px;font-weight:700;box-shadow:0 2px 6px rgba(0,0,0,.2)}
+.swatch-card.selected .check-badge{display:flex}
+
+/* checkout */
+.checkout-wrap{max-width:560px;margin:0 auto}
+.order-summary{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:24px}
+.order-summary h3{font-size:15px;font-weight:700;margin-bottom:12px}
+.order-line{display:flex;justify-content:space-between;font-size:13px;padding:4px 0;border-bottom:1px solid var(--border)}
+.order-line:last-child{border-bottom:none;font-weight:700;font-size:14px;padding-top:10px}
+.credit-note{font-size:12px;color:#27ae60;margin-top:8px;padding:8px 10px;background:#f0faf4;border-radius:6px;border-left:3px solid #27ae60}
+.form-group{margin-bottom:16px}
+.form-group label{display:block;font-size:12px;font-weight:600;margin-bottom:5px;color:var(--muted)}
+.form-group input,.form-group select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:6px;font:inherit;font-size:14px;background:#fff;transition:.15s}
+.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--accent)}
+.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
+
+.payment-stub{background:#fffbf0;border:2px dashed var(--gold);border-radius:var(--radius);padding:20px;margin:20px 0;text-align:center}
+.payment-stub .stub-label{font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--gold);margin-bottom:8px}
+.payment-stub .stub-amount{font-size:28px;font-weight:700;color:var(--accent)}
+.payment-stub .stub-card{font-size:13px;color:var(--muted);margin:8px 0;font-family:monospace}
+.payment-stub .stub-note{font-size:11px;color:var(--muted);margin-top:6px}
+
+.btn{display:inline-flex;align-items:center;gap:8px;padding:13px 28px;border-radius:7px;font:600 15px/1 inherit;cursor:pointer;transition:.15s;border:none}
+.btn-primary{background:var(--accent);color:#fff}
+.btn-primary:hover{background:#2d2d50}
+.btn-primary:disabled{opacity:.5;cursor:not-allowed}
+.btn-gold{background:var(--gold);color:#fff}
+.btn-gold:hover{background:#b8974a}
+.btn-gold:disabled{opacity:.5;cursor:not-allowed}
+.btn-outline{background:transparent;color:var(--accent);border:2px solid var(--accent)}
+.btn-outline:hover{background:var(--accent);color:#fff}
+.btns{display:flex;gap:12px;margin-top:24px;flex-wrap:wrap}
+
+.confirm-box{max-width:540px;margin:0 auto;text-align:center;padding:40px 20px}
+.confirm-box .tick{font-size:72px;margin-bottom:16px}
+.confirm-box h2{font-size:24px;font-weight:700;margin-bottom:8px;color:var(--accent)}
+.confirm-box .order-id{font-family:monospace;font-size:14px;color:var(--muted);margin-bottom:20px}
+.confirm-detail{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);padding:20px;text-align:left;margin-top:20px}
+.confirm-detail p{font-size:13px;padding:5px 0;border-bottom:1px solid var(--border)}
+.confirm-detail p:last-child{border-bottom:none}
+.credit-chip{display:inline-block;background:#f0faf4;color:#27ae60;border:1px solid #27ae60;border-radius:5px;padding:6px 12px;font-size:13px;font-weight:600;margin-top:12px}
+.empty{padding:60px 20px;text-align:center;color:var(--muted)}
+
+@media(max-width:640px){
+  .swatch-grid{--cols:2!important}
+  .vibe-grid{grid-template-columns:repeat(2,1fr)}
+  .form-row{grid-template-columns:1fr}
+  .panel{padding:20px}
+}
+</style>
+</head>
+<body>
+
+<header class="site-header">
+  <div>
+    <h1>Designer Wallcoverings — Sample Box</h1>
+    <div class="tagline">Touch before you commit. $15, credited to your first roll order.</div>
+  </div>
+  <span class="badge-test">TEST MODE</span>
 </header>
-<div class="grid" id="grid"><div class="empty">Loading…</div></div>
+
+<nav class="steps">
+  <div class="step active" id="step-vibe">1 · Choose Your Vibe</div>
+  <div class="step" id="step-swatches">2 · Pick Swatches</div>
+  <div class="step" id="step-checkout">3 · Checkout</div>
+  <div class="step" id="step-confirm">4 · Confirmed</div>
+</nav>
+
+<!-- PANEL 1: VIBE PICKER -->
+<div class="panel active" id="panel-vibe">
+  <h2 style="text-align:center;margin-bottom:8px;font-size:22px">What's your style vibe?</h2>
+  <p style="text-align:center;color:var(--muted);margin-bottom:28px;font-size:14px">
+    We'll curate a box of hand-picked real samples from our collection.
+  </p>
+  <div class="vibe-grid" id="vibe-grid"><div class="empty">Loading vibes…</div></div>
+  <div class="btns" style="justify-content:center;margin-top:32px">
+    <button class="btn btn-primary" id="btn-vibe-next" disabled>See My Swatches &rarr;</button>
+  </div>
+</div>
+
+<!-- PANEL 2: SWATCH GRID -->
+<div class="panel" id="panel-swatches">
+  <div class="grid-toolbar">
+    <h2 id="swatch-heading">Swatches</h2>
+    <label class="ctrl">Sort
+      <select id="sort">
+        <option value="newest">Newest</option>
+        <option value="title">Title A&rarr;Z</option>
+        <option value="sku">SKU A&rarr;Z</option>
+        <option value="price-asc">Price &uarr;</option>
+        <option value="price-desc">Price &darr;</option>
+      </select>
+    </label>
+    <label class="ctrl">Density <input id="density" type="range" min="2" max="6" value="4"></label>
+    <span class="sel-count" id="sel-count">0 selected</span>
+  </div>
+  <div class="swatch-grid" id="swatch-grid"><div class="empty">Loading…</div></div>
+  <div class="btns">
+    <button class="btn btn-outline" id="btn-back-vibe">&larr; Back</button>
+    <button class="btn btn-primary" id="btn-swatches-next" disabled>Add to My Box &rarr;</button>
+  </div>
+</div>
+
+<!-- PANEL 3: CHECKOUT -->
+<div class="panel" id="panel-checkout">
+  <div class="checkout-wrap">
+    <div class="order-summary" id="order-summary">
+      <h3>Your Sample Box</h3>
+    </div>
+    <h3 style="margin-bottom:16px">Shipping Address</h3>
+    <form id="checkout-form" autocomplete="on">
+      <div class="form-group">
+        <label>Full Name *</label>
+        <input name="name" type="text" required placeholder="Jane Smith">
+      </div>
+      <div class="form-group">
+        <label>Email *</label>
+        <input name="email" type="email" required placeholder="jane@example.com">
+      </div>
+      <div class="form-group">
+        <label>Address Line 1 *</label>
+        <input name="address1" type="text" required placeholder="123 Main St">
+      </div>
+      <div class="form-group">
+        <label>Address Line 2</label>
+        <input name="address2" type="text" placeholder="Apt / Suite">
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>City *</label>
+          <input name="city" type="text" required placeholder="Los Angeles">
+        </div>
+        <div class="form-group">
+          <label>State *</label>
+          <input name="state" type="text" required placeholder="CA" maxlength="2">
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>ZIP *</label>
+          <input name="zip" type="text" required placeholder="90210" maxlength="10">
+        </div>
+        <div class="form-group">
+          <label>Country</label>
+          <select name="country">
+            <option value="US" selected>United States</option>
+            <option value="CA">Canada</option>
+          </select>
+        </div>
+      </div>
+      <div class="payment-stub">
+        <div class="stub-label">TEST CHECKOUT — NO REAL CHARGE</div>
+        <div class="stub-amount" id="stub-amount">$15.00</div>
+        <div class="stub-card">Card: 4242 4242 4242 4242 &nbsp;|&nbsp; 12/99 &nbsp;|&nbsp; CVC 123</div>
+        <div class="stub-note">Stripe test mode stub. To wire real Stripe: set STRIPE_SECRET_KEY in .env,<br>call stripe.checkout.sessions.create() in server.js POST /api/checkout.</div>
+      </div>
+    </form>
+    <div class="btns">
+      <button class="btn btn-outline" id="btn-back-swatches">&larr; Back</button>
+      <button class="btn btn-gold" id="btn-place-order">Place Test Order — $15.00</button>
+    </div>
+    <p id="order-error" style="color:#e74c3c;font-size:13px;margin-top:12px;display:none"></p>
+  </div>
+</div>
+
+<!-- PANEL 4: CONFIRMATION -->
+<div class="panel" id="panel-confirm">
+  <div class="confirm-box">
+    <div class="tick">&#x2705;</div>
+    <h2>Your Sample Box is Ordered!</h2>
+    <div class="order-id" id="confirm-order-id"></div>
+    <p style="font-size:14px;color:var(--muted)">We'll ship your curated swatch box within 3-5 business days.</p>
+    <span class="credit-chip" id="confirm-credit"></span>
+    <div class="confirm-detail" id="confirm-detail"></div>
+    <div class="btns" style="justify-content:center;margin-top:24px">
+      <button class="btn btn-outline" onclick="location.reload()">Order Another Box</button>
+    </div>
+  </div>
+</div>
+
 <script>
-// House rule: sort + density both persist in localStorage so the choice survives reloads.
-const $=s=>document.querySelector(s), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
-const sortSel=$('#sort'), dens=$('#density');
-sortSel.value=localStorage.getItem('sort')||'newest'; dens.value=localStorage.getItem('cols')||'4';
-function applyCols(){document.documentElement.style.setProperty('--cols',dens.value);localStorage.setItem('cols',dens.value);}
+const state={vibe:null,selected:[],order:null};
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const $=s=>document.querySelector(s);
+const MAX=8,PRICE=15.00;
+
+// sort+density — house rule: persist to localStorage
+const sortSel=$('#sort'),dens=$('#density');
+sortSel.value=localStorage.getItem('sb-sort')||'newest';
+dens.value=localStorage.getItem('sb-cols')||'4';
+function applyCols(){
+  const v=dens.value;
+  $('#swatch-grid').style.setProperty('--cols',v);
+  localStorage.setItem('sb-cols',v);
+}
 applyCols();
-async function load(){
-  const r=await fetch('/api/products?sort='+encodeURIComponent(sortSel.value)); const d=await r.json();
-  $('#count').textContent=d.count+' items';
-  $('#grid').innerHTML = d.products.length ? d.products.map(p=>`
-    <div class="card">
-      <img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy">
-      <div class="meta"><p class="t">${esc(p.title||'Untitled')}</p>
-        <div class="s">${esc(p.sku||p.handle||'')}</div>
-        ${p.price?`<div class="p">$${Number(p.price).toFixed(2)}</div>`:''}
-      </div>
-    </div>`).join('') : '<div class="empty">No products yet — fill data/products.json.</div>';
-}
-sortSel.onchange=()=>{localStorage.setItem('sort',sortSel.value);load();};
 dens.oninput=applyCols;
-load();
-</script></body></html>
+sortSel.onchange=()=>{localStorage.setItem('sb-sort',sortSel.value);loadSwatches();};
+
+// step management
+function setStep(n){
+  ['panel-vibe','panel-swatches','panel-checkout','panel-confirm'].forEach((id,i)=>{
+    document.getElementById(id).classList.toggle('active',i+1===n);
+  });
+  ['step-vibe','step-swatches','step-checkout','step-confirm'].forEach((id,i)=>{
+    const el=document.getElementById(id);
+    el.classList.toggle('active',i+1===n);
+    el.classList.toggle('done',i+1<n);
+  });
+  window.scrollTo(0,0);
+}
+
+// panel 1: vibes
+async function loadVibes(){
+  const r=await fetch('/api/vibes'),d=await r.json();
+  const grid=$('#vibe-grid');
+  if(!d.vibes.length){grid.innerHTML='<div class="empty">No vibes found.</div>';return;}
+  grid.innerHTML=d.vibes.map(v=>`
+    <div class="vibe-card" data-id="${esc(v.id)}" onclick="selectVibe(this,'${esc(v.id)}')">
+      <span class="emoji">${v.emoji}</span>
+      <div class="vibe-label">${esc(v.label)}</div>
+    </div>`).join('');
+}
+function selectVibe(el,id){
+  document.querySelectorAll('.vibe-card').forEach(c=>c.classList.remove('selected'));
+  el.classList.add('selected');
+  state.vibe=id;
+  $('#btn-vibe-next').disabled=false;
+}
+$('#btn-vibe-next').onclick=()=>{setStep(2);loadSwatches();};
+
+// panel 2: swatches
+window._products=[];
+async function loadSwatches(){
+  const grid=$('#swatch-grid');
+  grid.innerHTML='<div class="empty">Loading…</div>';
+  const sort=sortSel.value,vibe=state.vibe||'all';
+  const r=await fetch(`/api/products?sort=${encodeURIComponent(sort)}&vibe=${encodeURIComponent(vibe)}`);
+  const d=await r.json();
+  const labels={modern:'Modern & Minimal',traditional:'Traditional',floral:'Floral & Botanical',
+    grasscloth:'Grasscloth & Natural','bold-color':'Bold Color',geometric:'Geometric'};
+  $('#swatch-heading').textContent=`${labels[vibe]||'All'} — ${d.count} swatch${d.count!==1?'es':''}`;
+  window._products=d.products;
+  if(!d.products.length){grid.innerHTML='<div class="empty">No swatches found for this vibe.</div>';return;}
+  grid.innerHTML=d.products.map(p=>{
+    const sel=state.selected.some(s=>s.sku===p.sku);
+    return `<div class="swatch-card${sel?' selected':''}" data-sku="${esc(p.sku)}" onclick="toggleSwatch(this)">
+      <img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy"
+           onerror="this.style.background='#f5f0ea'">
+      <div class="swatch-meta">
+        <div class="swatch-title">${esc(p.title||'Untitled')}</div>
+        <div class="swatch-vendor">${esc(p.vendor||'')}</div>
+        ${p.price?`<div class="swatch-price">$${Number(p.price).toFixed(0)}/roll</div>`:''}
+      </div>
+      <div class="check-badge">&#x2713;</div>
+    </div>`;
+  }).join('');
+  updateSelCount();
+}
+function toggleSwatch(el){
+  const sku=el.dataset.sku,p=window._products.find(x=>x.sku===sku);
+  if(!p)return;
+  if(el.classList.contains('selected')){
+    el.classList.remove('selected');
+    state.selected=state.selected.filter(s=>s.sku!==sku);
+  }else{
+    if(state.selected.length>=MAX){alert(`Max ${MAX} swatches per box. Deselect one to add another.`);return;}
+    el.classList.add('selected');
+    state.selected.push(p);
+  }
+  updateSelCount();
+}
+function updateSelCount(){
+  const n=state.selected.length;
+  $('#sel-count').textContent=`${n} selected (max ${MAX})`;
+  $('#btn-swatches-next').disabled=n===0;
+}
+$('#btn-back-vibe').onclick=()=>setStep(1);
+$('#btn-swatches-next').onclick=()=>{buildOrderSummary();setStep(3);};
+
+// panel 3: checkout
+function buildOrderSummary(){
+  const wrap=$('#order-summary');
+  const rows=state.selected.map(p=>`<div class="order-line"><span>${esc(p.title)}</span><span style="color:var(--muted);font-size:11px">swatch</span></div>`).join('');
+  wrap.innerHTML=`<h3>Your Sample Box (${state.selected.length} swatches)</h3>${rows}
+    <div class="order-line"><span>Box Price</span><span>$${PRICE.toFixed(2)}</span></div>
+    <div class="credit-note">&#x2705; $${PRICE.toFixed(2)} fully credited toward your first roll order</div>`;
+}
+$('#btn-back-swatches').onclick=()=>setStep(2);
+$('#btn-place-order').onclick=async()=>{
+  const form=$('#checkout-form');
+  if(!form.checkValidity()){form.reportValidity();return;}
+  const fd=new FormData(form);
+  const shipping={name:fd.get('name'),address1:fd.get('address1'),address2:fd.get('address2')||'',
+    city:fd.get('city'),state:fd.get('state').toUpperCase(),zip:fd.get('zip'),country:fd.get('country')};
+  const btn=$('#btn-place-order');
+  btn.disabled=true;btn.textContent='Placing order…';
+  try{
+    const r=await fetch('/api/orders',{method:'POST',headers:{'Content-Type':'application/json'},
+      body:JSON.stringify({vibe:state.vibe,swatches:state.selected.map(p=>({sku:p.sku,title:p.title,image:p.image,price:p.price})),
+        shipping,email:fd.get('email'),payment_stub:true})});
+    const d=await r.json();
+    if(!d.success)throw new Error(d.error||'Order failed');
+    showConfirmation(d.order);
+  }catch(e){
+    const err=$('#order-error');err.textContent='Error: '+e.message;err.style.display='';
+    btn.disabled=false;btn.textContent='Place Test Order — $15.00';
+  }
+};
+
+// panel 4: confirmation
+function showConfirmation(order){
+  setStep(4);
+  $('#confirm-order-id').textContent='Order '+order.id;
+  $('#confirm-credit').textContent=order.credit_note;
+  const addr=order.shipping;
+  $('#confirm-detail').innerHTML=order.swatches.map(s=>`<p><strong>${esc(s.title)}</strong></p>`).join('')+
+    `<p style="padding-top:10px"><strong>Ship to:</strong> ${esc(addr.name)}</p>
+    <p>${esc(addr.address1)}${addr.address2?' '+esc(addr.address2):''}</p>
+    <p>${esc(addr.city)}, ${esc(addr.state)} ${esc(addr.zip)} ${esc(addr.country)}</p>
+    <p><strong>Email:</strong> ${esc(order.email)}</p>
+    <p><strong>Payment:</strong> ${esc(order.payment_method)} &middot; ${esc(order.payment_status)}</p>
+    <p><strong>Box price:</strong> $${Number(order.box_price).toFixed(2)} (TEST — no charge)</p>`;
+}
+
+loadVibes();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 57f5df5..5285ab5 100644
--- a/server.js
+++ b/server.js
@@ -1,41 +1,191 @@
-// Storefront starter — zero-dep Node http. Serves data/products.json with SERVER-SIDE sort
-// (house rule: every product grid gets sort + a density slider). Optional basic auth via BASIC_AUTH.
-const http = require('http'), fs = require('fs'), path = require('path');
-const PORT = parseInt(process.env.PORT || '3900', 10);
-const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate; empty = open
-const DIR = __dirname;
-
-function load() { try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'products.json'), 'utf8')); } catch { return []; } }
-function authed(req) {
-  if (!BASIC_AUTH) return true;
-  const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
-  if (!m) return false; try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
+// Sample Box — paid curated swatch-box funnel for Designer Wallcoverings
+// Zero-dep Node http. Extends the web-dev accelerator template.
+// PORT=3903 node server.js  |  BASIC_AUTH=admin:DW2024! gates /admin
+const http   = require('http');
+const fs     = require('fs');
+const path   = require('path');
+const crypto = require('crypto');
+
+const PORT        = parseInt(process.env.PORT || '3903', 10);
+const BASIC_AUTH  = process.env.BASIC_AUTH || ''; // "user:pass" gates admin
+const DIR         = __dirname;
+const DATA_DIR    = path.join(DIR, 'data');
+const ORDERS_FILE = path.join(DATA_DIR, 'orders.json');
+const BOX_PRICE   = 15.00; // $15 sample box, fully credited to first roll order
+
+// ── Data helpers ──────────────────────────────────────────────────────────────
+function loadProducts() {
+  try { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'products.json'), 'utf8')); }
+  catch { return []; }
 }
-// Sort modes mirror the DW sort-skill: newest, title/sku A→Z, price ↑↓, light→dark (by dominant hex).
-function luminance(hex) { if (!/^#?[0-9a-f]{6}$/i.test(hex || '')) return 999; const h = hex.replace('#', '');
-  const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
-  return 0.2126 * r + 0.7152 * g + 0.0722 * b; }
+function loadOrders() {
+  try { return JSON.parse(fs.readFileSync(ORDERS_FILE, 'utf8')); }
+  catch { return []; }
+}
+function saveOrder(order) {
+  const orders = loadOrders();
+  orders.push(order);
+  fs.writeFileSync(ORDERS_FILE, JSON.stringify(orders, null, 2));
+}
+
+// ── Sort ──────────────────────────────────────────────────────────────────────
 function sortProducts(items, mode) {
   const a = items.slice();
   switch (mode) {
-    case 'title': return a.sort((x, y) => (x.title || '').localeCompare(y.title || ''));
-    case 'sku': return a.sort((x, y) => (x.sku || x.handle || '').localeCompare(y.sku || y.handle || ''));
-    case 'price-asc': return a.sort((x, y) => (x.price || 0) - (y.price || 0));
-    case 'price-desc': return a.sort((x, y) => (y.price || 0) - (x.price || 0));
-    case 'light': return a.sort((x, y) => luminance(y.hex) - luminance(x.hex));
-    case 'dark': return a.sort((x, y) => luminance(x.hex) - luminance(y.hex));
-    default: return a; // 'newest' = natural order
-  }
-}
-http.createServer((req, res) => {
-  if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="preview"' }); return res.end('auth required'); }
-  const u = new URL(req.url, 'http://x');
-  if (u.pathname === '/api/products') {
-    const out = sortProducts(load(), u.searchParams.get('sort') || 'newest');
-    res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ count: out.length, products: out }));
-  }
-  const f = u.pathname === '/' ? 'index.html' : u.pathname.replace(/^\//, '');
-  const fp = path.join(DIR, 'public', path.basename(f));
-  if (fs.existsSync(fp)) { res.writeHead(200, { 'Content-Type': f.endsWith('.html') ? 'text/html' : 'text/plain' }); return res.end(fs.readFileSync(fp)); }
+    case 'title':      return a.sort((x, y) => (x.title||'').localeCompare(y.title||''));
+    case 'sku':        return a.sort((x, y) => (x.sku||'').localeCompare(y.sku||''));
+    case 'price-asc':  return a.sort((x, y) => (x.price||0) - (y.price||0));
+    case 'price-desc': return a.sort((x, y) => (y.price||0) - (x.price||0));
+    default:           return a; // newest = natural order
+  }
+}
+function sortOrders(items, mode) {
+  const a = items.slice();
+  if (mode === 'oldest') return a.sort((x,y) => new Date(x.created_at) - new Date(y.created_at));
+  return a.sort((x,y) => new Date(y.created_at) - new Date(x.created_at));
+}
+
+// ── Auth (admin only) ─────────────────────────────────────────────────────────
+function isAdmin(req) {
+  if (!BASIC_AUTH) return true;
+  const m = (req.headers.authorization||'').match(/^Basic\s+(.+)$/i);
+  if (!m) return false;
+  try { return Buffer.from(m[1],'base64').toString() === BASIC_AUTH; } catch { return false; }
+}
+
+// ── Body parser ───────────────────────────────────────────────────────────────
+function readBody(req) {
+  return new Promise((resolve, reject) => {
+    let d = '';
+    req.on('data', c => { d += c; if (d.length > 1e6) req.destroy(); });
+    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); }});
+    req.on('error', reject);
+  });
+}
+
+// ── MIME ──────────────────────────────────────────────────────────────────────
+const MIME = { '.html':'text/html; charset=utf-8', '.js':'application/javascript',
+  '.css':'text/css', '.json':'application/json', '.png':'image/png',
+  '.jpg':'image/jpeg', '.webp':'image/webp', '.svg':'image/svg+xml', '.ico':'image/x-icon' };
+
+function serveFile(res, fp) {
+  if (!fs.existsSync(fp)) { res.writeHead(404); return res.end('not found'); }
+  const ext = path.extname(fp);
+  res.writeHead(200, { 'Content-Type': MIME[ext] || 'text/plain' });
+  res.end(fs.readFileSync(fp));
+}
+
+// ── Server ────────────────────────────────────────────────────────────────────
+http.createServer(async (req, res) => {
+  const u  = new URL(req.url, `http://localhost:${PORT}`);
+  const mt = req.method.toUpperCase();
+
+  res.setHeader('Access-Control-Allow-Origin', '*');
+  if (mt === 'OPTIONS') { res.writeHead(204); return res.end(); }
+
+  // ── Public API ──────────────────────────────────────────────────────────────
+
+  // GET /api/products?sort=newest&vibe=modern
+  if (u.pathname === '/api/products' && mt === 'GET') {
+    let products = loadProducts();
+    const vibe = u.searchParams.get('vibe');
+    if (vibe && vibe !== 'all') {
+      products = products.filter(p => Array.isArray(p.vibes) && p.vibes.includes(vibe));
+    }
+    products = sortProducts(products, u.searchParams.get('sort') || 'newest');
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ count: products.length, products }));
+  }
+
+  // GET /api/vibes
+  if (u.pathname === '/api/vibes' && mt === 'GET') {
+    const products = loadProducts();
+    const vibeSet  = new Set();
+    products.forEach(p => (p.vibes||[]).forEach(v => vibeSet.add(v)));
+    const ALL_VIBES = [
+      { id:'modern',      label:'Modern & Minimal',    emoji:'⬜' },
+      { id:'traditional', label:'Traditional',          emoji:'🏛' },
+      { id:'floral',      label:'Floral & Botanical',   emoji:'🌿' },
+      { id:'grasscloth',  label:'Grasscloth & Natural', emoji:'🌾' },
+      { id:'bold-color',  label:'Bold Color',           emoji:'🎨' },
+      { id:'geometric',   label:'Geometric',            emoji:'🔷' },
+    ];
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ vibes: ALL_VIBES.filter(v => vibeSet.has(v.id)) }));
+  }
+
+  // POST /api/orders — place a TEST order (no real charge)
+  if (u.pathname === '/api/orders' && mt === 'POST') {
+    const body = await readBody(req);
+    const { vibe, swatches, shipping, email, payment_stub } = body;
+
+    if (!email || !shipping || !Array.isArray(swatches) || !swatches.length) {
+      res.writeHead(400, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ error: 'Missing: email, shipping, swatches' }));
+    }
+
+    const orderId = 'SB-' + Date.now().toString(36).toUpperCase();
+    const order   = {
+      id:               orderId,
+      vibe:             vibe || 'all',
+      swatches,                         // [{sku,title,image,price}]
+      swatch_count:     swatches.length,
+      box_price:        BOX_PRICE,
+      credit_note:      `$${BOX_PRICE.toFixed(2)} credited toward your first roll order (ref: ${orderId})`,
+      email,
+      shipping,                         // {name,address1,address2,city,state,zip,country}
+      payment_method:   payment_stub ? 'TEST_STUB' : 'STRIPE_TEST',
+      payment_status:   'paid_test',
+      stripe_session_id: payment_stub ? null : ('cs_test_' + crypto.randomBytes(12).toString('hex')),
+      status:           'pending_fulfillment',
+      created_at:       new Date().toISOString(),
+    };
+
+    saveOrder(order);
+    res.writeHead(201, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ success: true, order_id: orderId, order }));
+  }
+
+  // ── Admin (gated) ───────────────────────────────────────────────────────────
+
+  if (u.pathname.startsWith('/api/admin') || u.pathname === '/admin' || u.pathname === '/admin/') {
+    if (!isAdmin(req)) {
+      res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Sample Box Admin"' });
+      return res.end('Admin auth required');
+    }
+  }
+
+  // GET /api/admin/orders?sort=newest
+  if (u.pathname === '/api/admin/orders' && mt === 'GET') {
+    let orders = loadOrders();
+    orders = sortOrders(orders, u.searchParams.get('sort') || 'newest');
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ count: orders.length, orders }));
+  }
+
+  // GET /api/admin/stats
+  if (u.pathname === '/api/admin/stats' && mt === 'GET') {
+    const orders  = loadOrders();
+    const revenue = orders.reduce((s,o) => s + (o.box_price||0), 0);
+    const swatches = orders.reduce((s,o) => s + (o.swatch_count||0), 0);
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ total_orders: orders.length, total_revenue: revenue, total_swatches: swatches }));
+  }
+
+  // ── Static ──────────────────────────────────────────────────────────────────
+
+  if (u.pathname === '/admin' || u.pathname === '/admin/') {
+    return serveFile(res, path.join(DIR, 'public', 'admin.html'));
+  }
+
+  const fname = u.pathname === '/' ? 'index.html' : u.pathname.replace(/^\//, '');
+  const fp    = path.join(DIR, 'public', path.basename(fname));
+  if (fs.existsSync(fp)) return serveFile(res, fp);
+
   res.writeHead(404); res.end('not found');
-}).listen(PORT, function () { console.log('[' + path.basename(DIR) + '] http://localhost:' + this.address().port); });
+
+}).listen(PORT, function () {
+  console.log(`[sample-box] http://localhost:${this.address().port}`);
+  console.log(`[sample-box] /admin — set BASIC_AUTH=user:pass to gate`);
+  console.log(`[sample-box] BOX PRICE $${BOX_PRICE.toFixed(2)} — TEST MODE, no real charges`);
+});

← 6cfcf4e initial scaffold (sample-box) via web-dev accelerator  ·  back to Sample Box  ·  step 1: copy real Shopify catalog (200 products, checkout_do 5e7cf3b →