[object Object]

← back to Sample Box

initial scaffold (sample-box) via web-dev accelerator

6cfcf4efbcaf63d7711f86f022310fc791bd6274 · 2026-07-15 16:53:28 -0700 · Steve

Files touched

Diff

commit 6cfcf4efbcaf63d7711f86f022310fc791bd6274
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 15 16:53:28 2026 -0700

    initial scaffold (sample-box) via web-dev accelerator
---
 .gitignore         |  8 ++++++++
 BRIEF.md           |  5 +++++
 README.md          | 19 +++++++++++++++++++
 data/products.json |  6 ++++++
 public/index.html  | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js          | 41 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 134 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/BRIEF.md b/BRIEF.md
new file mode 100644
index 0000000..2107538
--- /dev/null
+++ b/BRIEF.md
@@ -0,0 +1,5 @@
+# sample-box — brief
+- Client:
+- Outcome / "high value":
+- Constraints (brand, domain, deadline):
+- Reference / competitor URLs:
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9c6f5b4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# Project
+
+Scaffolded by the Claude web-dev accelerator. Storefront starter with a
+server-side-sorted product grid + density slider (both persist in localStorage).
+
+## Run
+```bash
+PORT=3900 node server.js                                # open http://localhost:3900
+BASIC_AUTH="admin:DW2024!" PORT=3900 node server.js     # gated
+```
+
+## Fill it in
+- `data/products.json` — array of `{ title, sku, price, hex, image }`.
+- `server.js` `sortProducts()` — add sort modes as needed.
+- `public/index.html` — the grid + controls.
+
+## Preview publicly
+From the accelerator dir: `scripts/preview-tunnel.sh <slug> <port>` stands up
+`<slug>.agentabrams.com` (basic-auth gated). The DNS step is Steve-gated.
diff --git a/data/products.json b/data/products.json
new file mode 100644
index 0000000..8eee5f2
--- /dev/null
+++ b/data/products.json
@@ -0,0 +1,6 @@
+[
+  { "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": "" }
+]
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..ceb7ab1
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,55 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1"><title>Storefront starter</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>
+</header>
+<div class="grid" id="grid"><div class="empty">Loading…</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);}
+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>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..57f5df5
--- /dev/null
+++ b/server.js
@@ -0,0 +1,41 @@
+// 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; }
+}
+// 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 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)); }
+  res.writeHead(404); res.end('not found');
+}).listen(PORT, function () { console.log('[' + path.basename(DIR) + '] http://localhost:' + this.address().port); });

(oldest)  ·  back to Sample Box  ·  sample-box v1: vibe-picker funnel + swatch grid + TEST check ac3f15d →