← back to All Designerwallcoverings
Astek-viewer alignment: accordion filter sections + CARD FIELDS toggles (image-only wall) + header count pill + --fs type scale + Basic Auth gate (admin/DW2024!, env-overridable, healthz open)
96f3c154411252634ea4bafeea548cd2d79e9d34 · 2026-07-02 17:09:30 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit 96f3c154411252634ea4bafeea548cd2d79e9d34
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 17:09:30 2026 -0700
Astek-viewer alignment: accordion filter sections + CARD FIELDS toggles (image-only wall) + header count pill + --fs type scale + Basic Auth gate (admin/DW2024!, env-overridable, healthz open)
---
public/index.html | 107 +++++++++++++++++++++++++++++++++++++++---------------
server.js | 10 +++++
2 files changed, 88 insertions(+), 29 deletions(-)
diff --git a/public/index.html b/public/index.html
index 5b37d37..2d35ae3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>All Products — Designer Wallcoverings</title>
<style>
- :root { --cols: 6; --bg:#0d0d0f; --card:#17171b; --line:#2a2a30; --txt:#e8e8ea; --mut:#9a9aa2; --accent:#c8a96a; --rail:260px; }
+ :root { --cols: 6; --fs: 1; --bg:#0d0d0f; --card:#17171b; --line:#2a2a30; --txt:#e8e8ea; --mut:#9a9aa2; --accent:#c8a96a; --rail:230px; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--txt); font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
@@ -15,6 +15,7 @@
h1 { margin:0; font-size:17px; font-weight:600; letter-spacing:.02em; white-space:nowrap; }
h1 small { color:var(--mut); font-weight:400; font-size:12px; margin-left:8px; }
h1 a { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; text-decoration:none; border-bottom:1px dotted var(--mut); }
+ .pill { background:var(--accent); color:#1a1407; border-radius:20px; padding:2px 10px; font-size:11.5px; font-weight:700; vertical-align:2px; }
select, input[type=search], input[type=text] { background:var(--card); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:7px 10px; font-size:13px; }
input[type=search] { flex:1 1 200px; min-width:180px; max-width:420px; }
.bar { display:flex; align-items:center; gap:8px; color:var(--mut); font-size:12px; white-space:nowrap; }
@@ -28,10 +29,17 @@
/* ── layout: Amazon-style left rail + content ── */
.wrap { display:flex; align-items:flex-start; }
aside { width:var(--rail); flex:0 0 var(--rail); position:sticky; top:59px; max-height:calc(100vh - 59px);
- overflow-y:auto; padding:14px 14px 60px; border-right:1px solid var(--line); }
- aside h4 { margin:16px 0 7px; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--mut);
- border-top:1px solid var(--line); padding-top:12px; }
- aside h4:first-of-type { border-top:none; margin-top:2px; padding-top:0; }
+ overflow-y:auto; padding:10px 10px 60px; border-right:1px solid var(--line); }
+ /* Astek-style collapsed accordion sections per field */
+ details.sec { border:1px solid var(--line); border-radius:7px; margin-bottom:7px; background:var(--card); }
+ details.sec > summary { list-style:none; cursor:pointer; user-select:none; padding:8px 10px; font-size:10.5px;
+ letter-spacing:.09em; text-transform:uppercase; color:var(--mut); display:flex; align-items:center; gap:7px; }
+ details.sec > summary::-webkit-details-marker { display:none; }
+ details.sec > summary::before { content:'▸'; color:var(--accent); font-size:10px; }
+ details.sec[open] > summary::before { content:'▾'; }
+ details.sec > summary .n { margin-left:auto; background:var(--accent); color:#1a1407; border-radius:20px;
+ padding:0 7px; font-size:10px; font-weight:700; }
+ .fbody { padding:2px 8px 9px; }
.fitem { display:flex; align-items:center; gap:7px; padding:2.5px 2px; cursor:pointer; font-size:12.5px; color:var(--txt); border-radius:5px; }
.fitem:hover { background:var(--card); }
.fitem .box { width:13px; height:13px; flex:0 0 13px; border:1px solid var(--mut); border-radius:3px; display:inline-flex; align-items:center; justify-content:center; font-size:10px; }
@@ -49,23 +57,23 @@
main { flex:1; min-width:0; }
- /* ── grid view ── */
- .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:10px; padding:14px 16px 30px; }
- .card { background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
+ /* ── grid view (tight 1px cards, type scale rides --fs from the density slider) ── */
+ .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:8px; padding:12px 14px 30px; }
+ .card { background:var(--card); border:1px solid var(--line); border-radius:4px; overflow:hidden; display:flex; flex-direction:column; }
.thumb { aspect-ratio:1/1; background:#000 center/cover no-repeat; position:relative; }
.thumb.empty { display:flex; align-items:center; justify-content:center; color:#444; font-size:11px; }
.badge { position:absolute; top:6px; left:6px; font-size:10px; font-weight:700; letter-spacing:.04em; padding:2px 7px; border-radius:20px; text-transform:uppercase; }
.badge.st-ARCHIVED { background:#5a3d3d; color:#e8c9c9; }
.badge.st-DRAFT { background:#3d4a5a; color:#c9dae8; }
- .meta { padding:8px 10px; }
- .sku { font-weight:700; font-size:13px; color:var(--accent); word-break:break-all; }
- .ttl { color:var(--txt); font-size:11.5px; margin-top:3px; max-height:2.8em; overflow:hidden; }
- .vt { color:var(--mut); font-size:10.5px; margin-top:3px; }
- .fac { margin-top:4px; font-size:10.5px; color:var(--accent); }
- .price { color:#e8e8ea; font-size:12.5px; font-weight:600; margin-top:4px; display:block; }
- /* dense grids: shed card text progressively so 20-across stays a clean texture wall */
- .grid.d1 .ttl, .grid.d1 .fac { display:none; }
- .grid.d2 .meta { display:none; }
+ .meta { padding:calc(7px*var(--fs)) calc(9px*var(--fs)); }
+ .sku { font-weight:700; font-size:calc(13px*var(--fs)); color:var(--accent); word-break:break-all; }
+ .ttl { color:var(--txt); font-size:calc(11.5px*var(--fs)); margin-top:3px; max-height:2.8em; overflow:hidden; }
+ .vt { color:var(--mut); font-size:calc(10.5px*var(--fs)); margin-top:3px; }
+ .fac { margin-top:4px; font-size:calc(10.5px*var(--fs)); color:var(--accent); }
+ .price { color:#e8e8ea; font-size:calc(12.5px*var(--fs)); font-weight:600; margin-top:4px; display:block; }
+ /* CARD FIELDS visibility toggles — all off = pure image-only texture wall */
+ .grid.h-sku .sku, .grid.h-ttl .ttl, .grid.h-vt .vt, .grid.h-fac .fac, .grid.h-price .price { display:none; }
+ .grid.h-sku.h-ttl.h-vt.h-fac.h-price .meta { display:none; }
/* ── list view ── */
.listwrap { padding:0 16px 30px; overflow-x:auto; }
@@ -95,7 +103,7 @@
</head>
<body>
<header>
- <h1>All Products <small id="sub">loading…</small><a href="/vendors.html">vendor directory →</a></h1>
+ <h1>All Products <span class="pill" id="pillN">…</span> <small id="sub">loading…</small><a href="/vendors.html">vendor directory →</a></h1>
<button class="dirbtn" id="railToggle">☰ Filters</button>
<input type="search" id="q" placeholder="Search anything — red, damask, DWKR, Thibaut…">
<span class="bar">Sort
@@ -123,14 +131,15 @@
<div class="wrap">
<aside id="rail">
<div class="active" id="activeRow"></div>
- <h4>Vendor</h4><div id="vendorRow"></div>
- <h4>Type</h4><div id="typeRow"></div>
- <h4>Color</h4><div id="colorRow"></div>
- <h4>Style</h4><div id="styleRow"></div>
- <h4>Material</h4><div id="matRow"></div>
- <h4>Price</h4><div id="priceRow"></div>
- <h4>Status</h4><div id="statusRow"></div>
- <h4>Image</h4><div id="imgRow"></div>
+ <details class="sec" data-k="fields"><summary>Card fields<span class="n" id="nFields"></span></summary><div class="fbody" id="fieldsRow"></div></details>
+ <details class="sec" data-k="vendor"><summary>Vendor<span class="n" id="nVendor"></span></summary><div class="fbody" id="vendorRow"></div></details>
+ <details class="sec" data-k="type"><summary>Type<span class="n" id="nType"></span></summary><div class="fbody" id="typeRow"></div></details>
+ <details class="sec" data-k="color"><summary>Color<span class="n" id="nColor"></span></summary><div class="fbody" id="colorRow"></div></details>
+ <details class="sec" data-k="style"><summary>Style<span class="n" id="nStyle"></span></summary><div class="fbody" id="styleRow"></div></details>
+ <details class="sec" data-k="material"><summary>Material<span class="n" id="nMaterial"></span></summary><div class="fbody" id="matRow"></div></details>
+ <details class="sec" data-k="price"><summary>Price<span class="n" id="nPrice"></span></summary><div class="fbody" id="priceRow"></div></details>
+ <details class="sec" data-k="status"><summary>Status<span class="n" id="nStatus"></span></summary><div class="fbody" id="statusRow"></div></details>
+ <details class="sec" data-k="image"><summary>Image<span class="n" id="nImage"></span></summary><div class="fbody" id="imgRow"></div></details>
</aside>
<main>
@@ -174,11 +183,45 @@ const rowUI = {
/* ── top-bar wiring ── */
const dens = document.getElementById('dens'), densN = document.getElementById('densN');
dens.value = localStorage.getItem('all_cols') || 6;
-const setCols = () => { document.documentElement.style.setProperty('--cols', dens.value); densN.textContent = dens.value;
- grid.classList.toggle('d1', dens.value >= 9); grid.classList.toggle('d2', dens.value >= 13); };
+const setCols = () => {
+ document.documentElement.style.setProperty('--cols', dens.value); densN.textContent = dens.value;
+ // type scale rides the density slider (Astek --fs pattern): 6-across = 1.0, shrinking toward .62 at 20
+ document.documentElement.style.setProperty('--fs', Math.max(.62, Math.min(1.15, 6 / dens.value)).toFixed(2));
+};
setCols();
dens.oninput = () => { setCols(); localStorage.setItem('all_cols', dens.value); };
+/* accordion sections: persist open/closed per section (collapsed by default, Astek-style) */
+document.querySelectorAll('details.sec').forEach((d) => {
+ d.open = localStorage.getItem('all_sec_' + d.dataset.k) === '1';
+ d.ontoggle = () => localStorage.setItem('all_sec_' + d.dataset.k, d.open ? '1' : '0');
+});
+
+/* CARD FIELDS: toggle which card elements render — all off = image-only texture wall */
+const FIELD_DEFS = [['sku', 'SKU'], ['ttl', 'Title'], ['vt', 'Vendor · Type'], ['fac', 'Color / Style / Material'], ['price', 'Price']];
+let hiddenFields = loadSet('all_hidden_fields');
+function drawFieldToggles() {
+ const row = document.getElementById('fieldsRow'); row.innerHTML = '';
+ FIELD_DEFS.forEach(([k, label]) => {
+ const on = !hiddenFields.has(k);
+ const el = document.createElement('div');
+ el.className = 'fitem' + (on ? ' on' : '');
+ el.innerHTML = `<span class="box">${on ? '✓' : ''}</span><span class="lbl">${label}</span>`;
+ el.onclick = () => {
+ hiddenFields.has(k) ? hiddenFields.delete(k) : hiddenFields.add(k);
+ localStorage.setItem('all_hidden_fields', JSON.stringify([...hiddenFields]));
+ applyFieldClasses(); drawFieldToggles();
+ };
+ row.appendChild(el);
+ });
+ document.getElementById('nFields').textContent = hiddenFields.size ? `${FIELD_DEFS.length - hiddenFields.size}/${FIELD_DEFS.length}` : '';
+ document.getElementById('nFields').style.display = hiddenFields.size ? '' : 'none';
+}
+function applyFieldClasses() {
+ FIELD_DEFS.forEach(([k]) => grid.classList.toggle('h-' + k, hiddenFields.has(k)));
+}
+applyFieldClasses(); drawFieldToggles();
+
const sortSel = document.getElementById('sort'), dirBtn = document.getElementById('dir');
sortSel.value = ['price_asc','price_desc'].includes(state.sort) ? 'price' : state.sort;
state.sort = sortSel.value;
@@ -306,10 +349,15 @@ function renderFacets(f) {
renderActive();
}
+const SECN = { Vendor: 'nVendor', Type: 'nType', Color: 'nColor', Style: 'nStyle', Material: 'nMaterial', Price: 'nPrice', Status: 'nStatus', Image: 'nImage' };
function renderActive() {
const row = document.getElementById('activeRow'); row.innerHTML = '';
const dims = [['Vendor', state.vendors], ['Type', state.types], ['Color', state.colors], ['Style', state.styles],
['Material', state.materials], ['Price', state.prices], ['Status', state.statuses], ['Image', state.images]];
+ for (const [label, set] of dims) { // selection-count badge on each collapsed section header
+ const b = document.getElementById(SECN[label]);
+ if (b) { b.textContent = set.size || ''; b.style.display = set.size ? '' : 'none'; }
+ }
let n = 0;
for (const [label, set] of dims) {
set.forEach((v) => {
@@ -380,7 +428,8 @@ async function load() {
(state.view === 'list' ? tbody : grid).appendChild(frag);
state.offset += data.rows.length;
state.done = state.offset >= data.total || data.rows.length === 0;
- subEl.textContent = `${data.catalog.toLocaleString()} products · ${data.total.toLocaleString()} matching · showing ${state.offset.toLocaleString()}`;
+ document.getElementById('pillN').textContent = data.total.toLocaleString();
+ subEl.textContent = `of ${data.catalog.toLocaleString()} products · showing ${state.offset.toLocaleString()}`;
statusEl.textContent = data.total === 0 ? '— no matching products —'
: (state.done ? `— end · ${state.total.toLocaleString()} matching —` : 'scroll for more…');
} catch (e) {
diff --git a/server.js b/server.js
index 8eda2af..3739b8f 100644
--- a/server.js
+++ b/server.js
@@ -190,9 +190,19 @@ function loadVendors() {
return vendorsCache;
}
+// Basic Auth gate (Steve 2026-07-02: all. is published but un/pw-gated).
+// Override with BASIC_AUTH=user:pass in .env; /healthz stays open for probes.
+const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
+const AUTH_OK = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
+
const server = http.createServer((req, res) => {
const u = new URL(req.url, `http://localhost:${PORT}`);
+ if (u.pathname !== '/healthz' && req.headers.authorization !== AUTH_OK) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Designer Wallcoverings - All Products"', 'Content-Type': 'text/plain' });
+ return res.end('Authentication required');
+ }
+
if (u.pathname === '/api/products') {
const f = parseFilters(u);
const sort = u.searchParams.get('sort') || 'newest';
← bb41e31 amazon-style left filter rail + grid/list toggle + sortable
·
back to All Designerwallcoverings
·
all.: add live *.designerwallcoverings.com microsite crawl ( 260257f →