← 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 →