[object Object]

← back to Wallco Ai

feat(designs grid): admin-only theme1/theme2 toggle on /designs

49dcd87a76b3103ff5efa1e680c2ac9153022352 · 2026-05-29 13:05:51 -0700 · Steve Abrams

Same pattern as /design/:id. Scoped to #grid-main, shares the
'wallco-page-theme' localStorage key so an admin's choice flows
across grid + PDP. Public users see vanilla theme1.

Files touched

Diff

commit 49dcd87a76b3103ff5efa1e680c2ac9153022352
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 13:05:51 2026 -0700

    feat(designs grid): admin-only theme1/theme2 toggle on /designs
    
    Same pattern as /design/:id. Scoped to #grid-main, shares the
    'wallco-page-theme' localStorage key so an admin's choice flows
    across grid + PDP. Public users see vanilla theme1.
---
 server.js | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 109 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index fa7bdb9..b76870f 100644
--- a/server.js
+++ b/server.js
@@ -2004,6 +2004,49 @@ function dropFromIndex(ids) {
   return n;
 }
 
+// Inverse of dropFromIndex — surface a freshly-published design on the public
+// storefront immediately. DESIGNS is filtered to is_published!==false at load
+// (loadDesigns ~L312), so a curator publish that only flips prod PG never shows
+// on /designs or /api/designs until the next Mac2 designs.json deploy. addToIndex
+// pulls the design's CANONICAL entry from data/designs.json (same shape every
+// other DESIGNS entry has — no PG field-mapping bugs), flips is_published, and
+// pushes it in. Designs absent from the snapshot (brand-new prod-only) are
+// skipped — those still need the Mac2 publish pipeline. Cached w/ mtime invalidation.
+let _rawDesignsCache = null, _rawDesignsMtime = 0;
+function _rawDesignsById() {
+  try {
+    const p = path.join(__dirname, 'data', 'designs.json');
+    const mt = fs.statSync(p).mtimeMs;
+    if (!_rawDesignsCache || mt !== _rawDesignsMtime) {
+      const arr = JSON.parse(fs.readFileSync(p, 'utf8'));
+      _rawDesignsCache = new Map(arr.map(d => [d.id, d]));
+      _rawDesignsMtime = mt;
+    }
+  } catch (e) { if (!_rawDesignsCache) _rawDesignsCache = new Map(); }
+  return _rawDesignsCache;
+}
+function addToIndex(ids) {
+  const set = new Set((Array.isArray(ids) ? ids : [ids]).map(n => parseInt(n, 10)).filter(Number.isFinite));
+  if (!set.size) return 0;
+  const have = new Set(DESIGNS.map(d => d.id));
+  const map = _rawDesignsById();
+  let n = 0;
+  for (const id of set) {
+    if (have.has(id)) { const d = DESIGNS.find(x => x.id === id); if (d) d.is_published = true; continue; }
+    const src = map.get(id);
+    if (!src || src.user_removed) continue;
+    // A curator publish is an explicit human override of the load-time aesthetic
+    // gate (passesAestheticGate drops e.g. ≥4-bucket drunk-animals). Tag it
+    // 'curator-force-publish' so the gate exempts it, and DON'T re-gate here —
+    // otherwise the design would publish in PG but never surface in DESIGNS.
+    const entry = Object.assign({}, src, { is_published: true });
+    entry.tags = (Array.isArray(entry.tags) ? entry.tags.filter(t => t !== 'curator-force-publish') : []).concat('curator-force-publish');
+    DESIGNS.push(entry); have.add(id); n++;
+  }
+  if (n) { _byColorCacheVer++; _byColorCache.clear(); }
+  return n;
+}
+
 // Apply one cactus verdict. body { action: 'bad'|'digital'|'fix'|'live'|'unpublish' }.
 function applyCactusDecision(id, action, opts = {}) {
   const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, local_path FROM all_designs WHERE id=${id}) t;`);
@@ -2069,6 +2112,7 @@ function applyCactusDecision(id, action, opts = {}) {
           needs_fixing_at=NULL, digital_file_at=NULL,
           tags = array_remove(array_remove(COALESCE(tags, ARRAY[]::text[]), 'needs-fixing'), 'digital-file')
       WHERE id=${id};`);
+    addToIndex([id]);   // surface on the public storefront immediately (inverse of dropFromIndex)
   } else {
     throw new Error('bad action');
   }
@@ -6277,7 +6321,71 @@ app.get('/designs', (req, res) => {
   })}
 <body>
 ${htmlHeader('/designs')}
-<main>
+<main id="grid-main"${_role === 'admin' ? ' data-page-theme="theme2"' : ''}>
+${_role === 'admin' ? `
+<!-- Theme toggle (theme1 / theme2) — ADMIN-ONLY preview surface
+     (Steve 2026-05-29). Shares localStorage key 'wallco-page-theme' with
+     /design/:id so an admin's choice flows across grid + PDP. -->
+<style id="page-theme-css-grid">
+  #grid-main[data-page-theme="theme2"]{
+    --bg:        #ffffff; --ink:       #0a0a0a;
+    --ink-soft:  #4a4a4a; --ink-faint: #8a8a8a;
+    --line:      #e5e5e5; --card-bg:   #fafafa;
+    --accent:    #c14a2e; --accent-soft:#7a2a18;
+    background: var(--bg); color: var(--ink);
+  }
+  #grid-main[data-page-theme="theme2"] h1,
+  #grid-main[data-page-theme="theme2"] h2,
+  #grid-main[data-page-theme="theme2"] h3{ letter-spacing:-0.01em; font-weight:500; }
+  #grid-main[data-page-theme="theme2"] .card,
+  #grid-main[data-page-theme="theme2"] [class*="card"]{
+    background:var(--card-bg); border:1px solid var(--line); box-shadow:none;
+  }
+  #page-theme-toggle{
+    position:fixed; top:60px; right:14px; z-index:90;
+    display:inline-flex; gap:0; align-items:center;
+    background:rgba(255,255,255,.92); border:1px solid #d8d2c5;
+    border-radius:999px; padding:3px; backdrop-filter:blur(6px);
+    box-shadow:0 2px 10px rgba(0,0,0,.08);
+    font:600 11px ui-sans-serif,system-ui; letter-spacing:.06em; text-transform:uppercase;
+  }
+  #page-theme-toggle button{
+    border:0; background:transparent; color:#5a5048; cursor:pointer;
+    padding:6px 14px; border-radius:999px; font:inherit; transition:all .15s;
+  }
+  #page-theme-toggle button[aria-pressed="true"]{ background:#1a1714; color:#faf7f2; }
+  #page-theme-toggle button:hover:not([aria-pressed="true"]){ color:#1a1714; }
+  @media (prefers-color-scheme: dark){
+    #page-theme-toggle{ background:rgba(20,18,16,.92); border-color:#3a3633; }
+    #page-theme-toggle button{ color:#c8beb2; }
+    #page-theme-toggle button:hover:not([aria-pressed="true"]){ color:#f0ebe3; }
+  }
+</style>
+<div id="page-theme-toggle" role="group" aria-label="Page theme">
+  <button type="button" data-theme="theme1" aria-pressed="false">Theme 1</button>
+  <button type="button" data-theme="theme2" aria-pressed="true">Theme 2</button>
+</div>
+<script>
+(function(){
+  var main = document.getElementById('grid-main');
+  var saved = null;
+  try { saved = localStorage.getItem('wallco-page-theme'); } catch(e){}
+  var theme = saved === 'theme1' ? 'theme1' : 'theme2';
+  main.setAttribute('data-page-theme', theme);
+  var btns = document.querySelectorAll('#page-theme-toggle button');
+  function sync(t){ btns.forEach(function(b){ b.setAttribute('aria-pressed', b.dataset.theme === t ? 'true' : 'false'); }); }
+  sync(theme);
+  btns.forEach(function(b){
+    b.addEventListener('click', function(){
+      var t = b.dataset.theme;
+      main.setAttribute('data-page-theme', t);
+      try { localStorage.setItem('wallco-page-theme', t); } catch(e){}
+      sync(t);
+    });
+  });
+})();
+</script>
+` : ''}
 ${cat === 'drunk-animals' ? `
 <section style="padding:48px 24px 24px;text-align:center;background:linear-gradient(180deg,#1a0f1a 0%,#2a1530 100%);color:#fff;margin-bottom:0">
   <div style="max-width:760px;margin:0 auto">

← bda32cf audit: bulk-publish 246 muybridge-plate designs (force=true  ·  back to Wallco Ai  ·  Add addToIndex(): surface curator-published designs on the s 7e0d667 →