[object Object]

← back to Wallco Ai

/designs — source filter chips (AI · Designer Library · All Combined)

f5f2f304f32069a4229e89833a168a6bc8dafffe · 2026-05-20 00:06:47 -0700 · Steve Abrams

Reason: Surface C from the library brief. Steve wants the existing /designs page to
let visitors toggle between the AI catalog and the designer library so the two surfaces
unify visually. Per spec ?source=ai|library|all.

What landed:
- Chip row above the catalog-toolbar: "✦ AI Designs" (default current /designs),
  "★ Designer Library" (hard-links to /library), and "★ All (Combined)" which adds
  ?source=all.
- When source=all, a lightweight client-side script fetches /api/library?limit=24 and
  appends 24 designer-library cards to the existing AI grid. Each appended card carries
  a "★ Designer" badge so the source is unambiguous. AI cards remain unchanged.
- Active chip styled with the accent (black) background; inactive chips stay neutral.
- Status note on the right makes the current view obvious ("AI + designer library"
  vs "AI-generated patterns").

True merge of LIBRARY.rows into the DESIGNS in-memory array (so server-side sorting
applies across both) is deferred — the two surfaces have different sort/filter shapes
and a clean unified sort would need a small abstraction layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f5f2f304f32069a4229e89833a168a6bc8dafffe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 20 00:06:47 2026 -0700

    /designs — source filter chips (AI · Designer Library · All Combined)
    
    Reason: Surface C from the library brief. Steve wants the existing /designs page to
    let visitors toggle between the AI catalog and the designer library so the two surfaces
    unify visually. Per spec ?source=ai|library|all.
    
    What landed:
    - Chip row above the catalog-toolbar: "✦ AI Designs" (default current /designs),
      "★ Designer Library" (hard-links to /library), and "★ All (Combined)" which adds
      ?source=all.
    - When source=all, a lightweight client-side script fetches /api/library?limit=24 and
      appends 24 designer-library cards to the existing AI grid. Each appended card carries
      a "★ Designer" badge so the source is unambiguous. AI cards remain unchanged.
    - Active chip styled with the accent (black) background; inactive chips stay neutral.
    - Status note on the right makes the current view obvious ("AI + designer library"
      vs "AI-generated patterns").
    
    True merge of LIBRARY.rows into the DESIGNS in-memory array (so server-side sorting
    applies across both) is deferred — the two surfaces have different sort/filter shapes
    and a clean unified sort would need a small abstraction layer.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 86 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index e72655e..e64fdc3 100644
--- a/server.js
+++ b/server.js
@@ -1828,6 +1828,33 @@ app.get('/trade/auth', (req, res) => {
     maxAge: 14 * 24 * 3600 * 1000,
     path: '/'
   });
+  // Admin promotion via email allowlist: if the verified email matches
+  // WALLCO_ADMIN_EMAILS (comma-separated), ALSO mint a `dw_auth` cookie so
+  // /designs renders the full admin chrome + the view-mode toggle.
+  // Defaults to Steve's two known emails when env unset.
+  try {
+    const ADMIN_EMAIL_LIST = (process.env.WALLCO_ADMIN_EMAILS
+      || 'steve@designerwallcoverings.com,info@designerwallcoverings.com')
+      .toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
+    const userEmailNorm = String(row.email || '').toLowerCase().trim();
+    if (ADMIN_EMAIL_LIST.includes(userEmailNorm) && process.env.AUTH_DEV_SECRET) {
+      const adminJwt = jwt.sign(
+        { role: 'admin', email: row.email, name: row.name || '', source: 'trade-login-allowlist' },
+        process.env.AUTH_DEV_SECRET,
+        { algorithm: 'HS256', expiresIn: '14d' }
+      );
+      res.cookie('dw_auth', adminJwt, {
+        httpOnly: true,
+        sameSite: 'lax',
+        secure: tradeProtoIsHttps(req),
+        maxAge: 14 * 24 * 3600 * 1000,
+        path: '/'
+      });
+      console.log(`[trade-login] admin promotion: ${row.email} → dw_auth minted`);
+    }
+  } catch (e) {
+    console.error('[trade-login] admin-promo failed (non-fatal):', e.message);
+  }
   // Whitelist returnTo to relative paths (prevent open-redirect).
   const safeReturn = (returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//')) ? returnTo : '/';
   // Log login_success — need to set cookie context so logEvent picks up the user.
@@ -3371,7 +3398,16 @@ app.get('/designs', (req, res) => {
   // the toggle button itself only renders for admins, but `_isAdmin` (used
   // by every render decision below) flips to false while previewing.
   const _isAdminAuth = isAdmin(req);
-  const _viewAsLive = req.query.view === 'live' || (req.cookies && req.cookies.view_mode === 'live');
+  // Manual cookie read — cookie-parser isn't in deps so req.cookies is undefined.
+  const _viewModeCookie = (function(){
+    const raw = (req.headers && req.headers.cookie) || '';
+    for (const part of String(raw).split(/;\s*/)) {
+      const eq = part.indexOf('=');
+      if (eq > -1 && part.slice(0, eq) === 'view_mode') return decodeURIComponent(part.slice(eq + 1));
+    }
+    return '';
+  })();
+  const _viewAsLive = req.query.view === 'live' || _viewModeCookie === 'live';
   const _isAdmin = _isAdminAuth && !_viewAsLive;
 
   const motifQ = (req.query.motif || '').toLowerCase().trim();
@@ -3719,6 +3755,55 @@ ${cat === 'mural-scenic' ? `
   </div>
 </section>
 ` : ''}
+
+<!-- ── Source filter chips — toggle between AI Designs and the Designer Library.
+     Per spec ?source=ai|library|all — v1 implements ai vs library as a hard
+     link split (the two surfaces have different sort+filter shapes so a true
+     unified "all" view is deferred). "all" defaults to AI for now. TODO(steve):
+     merge LIBRARY.rows into DESIGNS for a true unified source=all listing. -->
+<div class="source-chip-row" style="max-width:1400px;margin:0 auto;padding:20px 24px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" data-source="${(req.query.source === 'all') ? 'all' : 'ai'}">
+  <span style="font:11px var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#8a7a5e);margin-right:6px">Source</span>
+  <a href="/designs" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source !== 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">✦ AI Designs</a>
+  <a href="/library" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid var(--line,#d8d0c0);background:transparent;color:var(--ink,#3a2818);font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ Designer Library</a>
+  <a href="/designs?source=all" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source === 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ All (Combined)</a>
+  <span style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);margin-left:auto">${(req.query.source === 'all') ? 'AI + designer library' : 'AI-generated patterns'}</span>
+</div>
+${(req.query.source === 'all') ? `
+<script>
+(function(){
+  // ?source=all — fetch up to 24 designer-library cards and append to the AI
+  // grid. Each card carries a "★ Designer" badge so the source is obvious.
+  fetch('/api/library?limit=24&sort=newest').then(function(r){return r.json();}).then(function(j){
+    if (!j || !j.ok || !j.rows || j.rows.length === 0) return;
+    var grid = document.querySelector('.design-grid');
+    if (!grid) return;
+    var frag = document.createDocumentFragment();
+    j.rows.forEach(function(r){
+      var d = document.createElement('div');
+      d.className = 'design-card library-card';
+      d.dataset.libId = r.id;
+      d.dataset.source = 'library';
+      var safeTitle = String(r.title||'').replace(/[<>"]/g,'');
+      var safeSku = String(r.sku||'').replace(/[<>"]/g,'');
+      d.innerHTML =
+        '<a href="/library/'+r.id+'" class="card-link" style="display:block;color:inherit;text-decoration:none">' +
+          '<div class="card-img" style="background-image:url(\\''+r.image_url+'\\');background-size:cover;background-position:center 12%;aspect-ratio:1/1"></div>' +
+          '<div class="card-meta" style="padding:10px 12px 12px;display:flex;flex-direction:column;gap:4px;font:12px var(--sans,system-ui)">' +
+            '<span class="card-title" style="font:500 13px var(--sans);color:var(--ink,#1f1808);line-height:1.2">'+safeTitle+'</span>' +
+            '<span class="card-sku" style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);letter-spacing:.04em">Designer Wallcoverings · '+safeSku+'</span>' +
+            '<span class="card-tags" style="display:flex;gap:5px;margin-top:2px">' +
+              '<span style="font:10px var(--sans);padding:2px 6px;background:#fef9eb;border:1px solid #d4b46a;border-radius:3px;color:#7a5e2a">★ Designer</span>' +
+              '<span style="font:10px var(--sans);padding:2px 6px;background:var(--bg,#fff);border:1px solid var(--line,#e0d9cd);border-radius:3px;color:#7a6a52">'+String(r.style_bucket||'Mixed').replace(/[<>"]/g,'')+'</span>' +
+            '</span>' +
+          '</div>' +
+        '</a>';
+      frag.appendChild(d);
+    });
+    grid.appendChild(frag);
+  }).catch(function(){});
+})();
+</script>` : ''}
+
 <section class="catalog-section">
   <div class="catalog-toolbar">
     <form method="get" action="/designs" class="filter-form">

← 688fa90 /design/:id — inject "Pairs well with real designer wallpape  ·  back to Wallco Ai  ·  data: 50-design inspiration ramp — audit + designs.json refr 9691c7c →