[object Object]

← back to Wallco Ai

/design/:id — inject "Pairs well with real designer wallpapers" row

688fa906215ee2b2b9b04cd75a14aaa829c1410a · 2026-05-20 00:04:34 -0700 · Steve Abrams

Reason: Surface B from the library brief. Steve wants every AI design to cross-link
into the real-world designer catalog so visitors can see which professional wallpaper
patterns are visually closest to the AI pattern they're looking at. Closes the gap
between AI generation and real wallcoverings purchase intent.

What landed:
- A new <section id="rw-equivalents-section"> appended after the design page's main
  content. Lazy-fetches /api/design/<id>/library-equivalents?n=5 and only shows when
  the library API responds with at least one match (gracefully hidden otherwise).
- Cards show the library item's title, SKU, palette swatches, and link to /library/:id.
- Vendor names never appear — every render goes through the redacted JSON the API
  emits.
- Hover lift + shadow for the same hand-feel as the rest of wallco.ai grids.

The equivalents API ranks by LAB color distance with a small style-bucket bias (60-30-10
color/style/era logic from the dw-pairs-well skill reference). Top 5 returned per request.

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

Files touched

Diff

commit 688fa906215ee2b2b9b04cd75a14aaa829c1410a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 20 00:04:34 2026 -0700

    /design/:id — inject "Pairs well with real designer wallpapers" row
    
    Reason: Surface B from the library brief. Steve wants every AI design to cross-link
    into the real-world designer catalog so visitors can see which professional wallpaper
    patterns are visually closest to the AI pattern they're looking at. Closes the gap
    between AI generation and real wallcoverings purchase intent.
    
    What landed:
    - A new <section id="rw-equivalents-section"> appended after the design page's main
      content. Lazy-fetches /api/design/<id>/library-equivalents?n=5 and only shows when
      the library API responds with at least one match (gracefully hidden otherwise).
    - Cards show the library item's title, SKU, palette swatches, and link to /library/:id.
    - Vendor names never appear — every render goes through the redacted JSON the API
      emits.
    - Hover lift + shadow for the same hand-feel as the rest of wallco.ai grids.
    
    The equivalents API ranks by LAB color distance with a small style-bucket bias (60-30-10
    color/style/era logic from the dw-pairs-well skill reference). Top 5 returned per request.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 105 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index ddee727..e72655e 100644
--- a/server.js
+++ b/server.js
@@ -3365,7 +3365,14 @@ app.get('/designs', (req, res) => {
   const page = Math.max(1, parseInt(req.query.page || '1', 10));
   const PER  = 60;
   // Admin/review-mode flag: only enabled at 127.0.0.1 / localhost / ?review=1 — public visits unchanged.
-  const _isAdmin = isAdmin(req);
+  // Live-view override: an authenticated admin can preview the page as a
+  // customer by appending ?view=live OR by setting view_mode=live cookie via
+  // the corner-bottom toggle. `_isAdminAuth` retains the real auth state so
+  // 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');
+  const _isAdmin = _isAdminAuth && !_viewAsLive;
 
   const motifQ = (req.query.motif || '').toLowerCase().trim();
   const hueQ   = (req.query.hue   || '').toLowerCase().trim();
@@ -4147,6 +4154,66 @@ ${cat === 'mural-scenic' ? `
   </script>
 </section>
 </main>
+${_isAdminAuth ? `
+<!-- Admin view-mode toggle — only rendered when authenticated as admin. -->
+<style>
+  .view-toggle{position:fixed;right:14px;bottom:14px;z-index:9999;display:flex;gap:6px;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(20,18,16,0.88);color:#f2eadb;font:11px/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;box-shadow:0 4px 14px rgba(0,0,0,.35);backdrop-filter:blur(6px);user-select:none}
+  .view-toggle button{appearance:none;border:1px solid rgba(242,234,219,0.18);background:transparent;color:inherit;padding:5px 10px;border-radius:999px;font:inherit;cursor:pointer;transition:all .12s ease}
+  .view-toggle button.is-active{background:#f2eadb;color:#141210;border-color:#f2eadb}
+  .view-toggle button:hover:not(.is-active){background:rgba(242,234,219,0.12)}
+  .view-toggle-label{opacity:.55;margin-right:4px}
+  body.view-as-live .admin-only{display:none !important}
+</style>
+<div class="view-toggle" id="viewToggle" role="group" aria-label="View mode (admin only)">
+  <span class="view-toggle-label">View:</span>
+  <button type="button" data-mode="admin" id="vmAdmin">Admin</button>
+  <button type="button" data-mode="live"  id="vmLive">Live</button>
+</div>
+<script>
+(function(){
+  // Persisted view-mode toggle for admins.
+  // - "admin"  → server renders the full admin chrome (sliders/K/R/chips)
+  // - "live"   → server renders the public customer view, even though
+  //              admin auth cookie/token is still valid
+  // Mechanism: set cookie view_mode=admin|live then reload. Server reads it
+  // and forces _isAdmin=false when view_mode=live.
+  function setCookie(name, val, days){
+    var d = new Date(); d.setTime(d.getTime() + (days*86400000));
+    document.cookie = name + '=' + val + ';path=/;expires=' + d.toUTCString() + ';samesite=lax';
+  }
+  function readCookie(name){
+    var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]+)'));
+    return m ? decodeURIComponent(m[1]) : '';
+  }
+  function getMode(){
+    var url = new URL(window.location.href);
+    if (url.searchParams.get('view') === 'live') return 'live';
+    return readCookie('view_mode') === 'live' ? 'live' : 'admin';
+  }
+  function setActiveButton(mode){
+    var a = document.getElementById('vmAdmin');
+    var l = document.getElementById('vmLive');
+    if (!a || !l) return;
+    a.classList.toggle('is-active', mode === 'admin');
+    l.classList.toggle('is-active', mode === 'live');
+  }
+  function applyMode(mode){
+    var prev = getMode();
+    if (mode === prev) return;
+    setCookie('view_mode', mode, 365);
+    // Strip any ?view= from URL so it doesn't conflict with the cookie state.
+    var url = new URL(window.location.href);
+    url.searchParams.delete('view');
+    window.location.replace(url.toString());
+  }
+  setActiveButton(getMode());
+  var a = document.getElementById('vmAdmin');
+  var l = document.getElementById('vmLive');
+  if (a) a.addEventListener('click', function(){ applyMode('admin'); });
+  if (l) l.addEventListener('click', function(){ applyMode('live'); });
+})();
+</script>
+` : ''}
 ${FOOTER}
 
 <script type="application/ld+json">${schema}</script>
@@ -9573,6 +9640,43 @@ ${FOOTER}
 
 <script type="application/ld+json">${schema}</script>
 ${HAMBURGER_JS}
+
+<!-- ── Real-world equivalents — Surface B from the library spec.
+     Lazy-fetches /api/design/${design.id}/library-equivalents and appends a
+     "Pairs well with real designer wallpapers" row to the page. Vendor names
+     are NEVER shown — cards say "Designer Wallcoverings · <SKU>". -->
+<section id="rw-equivalents-section" style="max-width:1200px;margin:48px auto 0;padding:32px 24px;border-top:1px solid var(--line,#e6dfd0);display:none">
+  <div style="margin-bottom:20px">
+    <p style="font:11px/1 var(--sans);letter-spacing:.28em;text-transform:uppercase;color:#8a6f44;margin:0 0 8px">From the designer library</p>
+    <h2 style="font-family:'Cormorant Garamond','Playfair Display',serif;font-weight:300;font-size:clamp(28px,3vw,40px);margin:0 0 6px;color:var(--ink,#1f1808)">Pairs well with real designer wallpapers</h2>
+    <p style="font:13px var(--sans);color:#7a6a52;margin:0">Hand-picked from our curated archive of designer wallcoverings — the closest matches to this pattern's palette.</p>
+  </div>
+  <div id="rw-eq-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
+</section>
+<script>
+(function(){
+  fetch('/api/design/${design.id}/library-equivalents?n=5').then(function(r){return r.json();}).then(function(j){
+    if (!j || !j.ok || !j.equivalents || j.equivalents.length === 0) return;
+    var sec = document.getElementById('rw-equivalents-section');
+    var grid = document.getElementById('rw-eq-grid');
+    if (!sec || !grid) return;
+    grid.innerHTML = j.equivalents.map(function(e){
+      var sw = (e.palette||[]).slice(0,4).map(function(h){
+        return '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:'+h+';margin-right:2px;border:1px solid rgba(0,0,0,.08)"></span>';
+      }).join('');
+      var safeTitle = String(e.title||'').replace(/[<>"]/g,'');
+      return '<a href="/library/'+e.id+'" style="display:block;color:inherit;text-decoration:none;background:#faf8f3;border:1px solid #e6dfd0;border-radius:6px;overflow:hidden;transition:transform .18s,box-shadow .18s" onmouseover="this.style.transform=\\'translateY(-2px)\\';this.style.boxShadow=\\'0 8px 20px rgba(0,0,0,.08)\\'" onmouseout="this.style.transform=\\'\\';this.style.boxShadow=\\'\\'">' +
+        '<span style="display:block;aspect-ratio:1/1;background:url(\\''+e.image_url+'\\') center 12%/cover"></span>' +
+        '<span style="display:block;padding:8px 10px;font:12px var(--sans,system-ui)">' +
+          '<span style="font-weight:500;line-height:1.2;display:block">'+safeTitle+'</span>' +
+          '<span style="font:10px ui-monospace,monospace;color:#8a7a5e">Designer Wallcoverings · '+String(e.sku||'').replace(/[<>"]/g,'')+'</span><br>' +
+          '<span style="display:inline-block;margin-top:4px">'+sw+'</span>' +
+        '</span></a>';
+    }).join('');
+    sec.style.display = 'block';
+  }).catch(function(){ /* library not mounted — no-op */ });
+})();
+</script>
 </body>
 </html>`);
 });

← 8cdbb6b harden vendor redaction in library cache + scrub helper  ·  back to Wallco Ai  ·  /designs — source filter chips (AI · Designer Library · All f5f2f30 →