[object Object]

← back to Wallco Ai

Add admin Theme Preview Gallery (/admin/theme-gallery) — all 11 PDP theme variants side-by-side per design, viewport toggle, created-date chip

d65186a2629e32befb60840b2eb96cd3c910c8eb · 2026-05-29 09:50:28 -0700 · Steve

Files touched

Diff

commit d65186a2629e32befb60840b2eb96cd3c910c8eb
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri May 29 09:50:28 2026 -0700

    Add admin Theme Preview Gallery (/admin/theme-gallery) — all 11 PDP theme variants side-by-side per design, viewport toggle, created-date chip
---
 public/admin/theme-gallery.html | 207 ++++++++++++++++++++++++++++++++++++++++
 src/admin.js                    |   4 +
 2 files changed, 211 insertions(+)

diff --git a/public/admin/theme-gallery.html b/public/admin/theme-gallery.html
new file mode 100644
index 0000000..0e9da4c
--- /dev/null
+++ b/public/admin/theme-gallery.html
@@ -0,0 +1,207 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Theme Preview Gallery — wallco.ai admin</title>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+  :root { --bg:#f7f5f1; --ink:#1c1a17; --mut:#8a837a; --line:#e3ddd3; --card:#fff; --accent:#16203c; }
+  * { box-sizing:border-box; }
+  body { margin:0; background:var(--bg); color:var(--ink);
+         font:15px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
+  header.tg { position:sticky; top:0; z-index:10; background:rgba(247,245,241,.96);
+              backdrop-filter:blur(8px); border-bottom:1px solid var(--line); padding:14px 22px; }
+  .tg-row { display:flex; flex-wrap:wrap; gap:14px 22px; align-items:center; }
+  h1 { font-size:18px; margin:0; font-weight:650; letter-spacing:.2px; }
+  .sub { color:var(--mut); font-size:13px; }
+  .picker { display:flex; gap:8px; align-items:center; }
+  input[type=number] { width:120px; padding:7px 9px; border:1px solid var(--line); border-radius:8px;
+                       font:inherit; background:#fff; }
+  button, a.btn { padding:7px 13px; border:1px solid var(--line); border-radius:8px; background:#fff;
+                  color:var(--ink); font:inherit; cursor:pointer; text-decoration:none; }
+  button.primary { background:var(--accent); color:#fff; border-color:var(--accent); }
+  button:hover, a.btn:hover { border-color:var(--accent); }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { border:0; border-right:1px solid var(--line); border-radius:0; }
+  .seg button:last-child { border-right:0; }
+  .seg button.on { background:var(--accent); color:#fff; }
+  .chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--mut);
+          border:1px solid var(--line); border-radius:999px; padding:4px 10px; background:#fff; }
+  .meta-thumb { width:34px; height:34px; border-radius:6px; object-fit:cover; border:1px solid var(--line); }
+  main { padding:22px; }
+  .grid { display:grid; gap:22px; grid-template-columns:repeat(auto-fill, minmax(var(--cardw,420px), 1fr)); }
+  .tcard { background:var(--card); border:1px solid var(--line); border-radius:14px; overflow:hidden;
+           box-shadow:0 1px 3px rgba(0,0,0,.04); display:flex; flex-direction:column; }
+  .tcard h2 { font-size:13px; margin:0; padding:11px 14px; border-bottom:1px solid var(--line);
+              display:flex; justify-content:space-between; align-items:center; font-weight:600; }
+  .tcard h2 .slug { color:var(--mut); font-weight:400; font-size:11px; }
+  .frame { position:relative; width:100%; background:#fff; overflow:hidden; }
+  .frame iframe { border:0; transform-origin:0 0; background:#fff; }
+  .frame .scrim { position:absolute; inset:0; cursor:pointer; }
+  footer.tg { color:var(--mut); font-size:12px; padding:8px 22px 30px; }
+</style>
+</head>
+<body data-admin="true">
+<header class="tg">
+  <div class="tg-row">
+    <h1>Theme Preview Gallery</h1>
+    <div class="picker">
+      <span class="sub">Design&nbsp;#</span>
+      <input type="number" id="idIn" placeholder="id" min="1">
+      <button id="loadBtn" class="primary">Load</button>
+      <button id="newestBtn">Newest</button>
+    </div>
+    <img id="thumb" class="meta-thumb" alt="" hidden>
+    <span class="sub" id="title">—</span>
+    <span class="chip" id="when" title="" hidden>🕓 <span id="whenTxt"></span></span>
+    <span style="flex:1"></span>
+    <span class="sub">Viewport</span>
+    <span class="seg" id="seg">
+      <button data-w="1280" class="on">Desktop</button>
+      <button data-w="834">Tablet</button>
+      <button data-w="390">Mobile</button>
+    </span>
+  </div>
+</header>
+<main>
+  <div class="grid" id="grid"></div>
+</main>
+<footer class="tg">
+  All 11 PDP theme variants render live from <code>/design/&lt;id&gt;/&lt;slug&gt;</code>. Click any preview to open it full-screen.
+  Source designs in the Figma file <em>Wallco theme variants</em>.
+</footer>
+
+<script>
+const VARIANTS = [
+  ['compact',  'V1 · Two-Column Compact'],
+  ['bento',    'V2 · Bento Grid'],
+  ['tabs',     'V3 · Sticky Hero + Tabs'],
+  ['hero',     'V4 · Hero-First Storefront'],
+  ['vertical', 'V5 · Mobile-Up Vertical'],
+  ['drawer',   'V6 · Drawer Heavy'],
+  ['editorial','V7 · Editorial Magazine'],
+  ['spec',     'V8 · Compact Spec-Sheet'],
+  ['config',   'V9 · Configurator-First'],
+  ['stack',    'V10 · Card-Stack'],
+  ['best',     'V11 · Best (synthesis)'],
+];
+const LOGICAL_H = { 1280:1700, 834:1180, 390:760 }; // logical iframe height per width
+const $ = s => document.querySelector(s);
+let curId = null;
+let logicalW = +(localStorage.getItem('tg-vw') || 1280);
+
+function fmtWhen(iso){
+  if(!iso) return null;
+  const d = new Date(iso);
+  if (isNaN(d)) return null;
+  return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
+}
+
+function buildGrid(id){
+  curId = id;
+  const grid = $('#grid'); grid.innerHTML = '';
+  VARIANTS.forEach(([slug,label]) => {
+    const card = document.createElement('div'); card.className = 'tcard';
+    const url = `/design/${id}/${slug}`;
+    card.innerHTML =
+      `<h2><span>${label} <span class="slug">/${slug}</span></span>`+
+      `<a class="btn" href="${url}" target="_blank" rel="noopener" style="padding:3px 9px;font-size:11px">open ↗</a></h2>`+
+      `<div class="frame" data-url="${url}"></div>`;
+    grid.appendChild(card);
+  });
+  rescale();
+  // lazy-mount iframes as cards scroll into view
+  const io = new IntersectionObserver((ents,obs)=>{
+    ents.forEach(e=>{ if(e.isIntersecting){ mountFrame(e.target); obs.unobserve(e.target);} });
+  },{rootMargin:'400px'});
+  document.querySelectorAll('.frame').forEach(f=>io.observe(f));
+}
+
+function mountFrame(frame){
+  if (frame.dataset.mounted) return;
+  frame.dataset.mounted = '1';
+  const url = frame.dataset.url;
+  const ifr = document.createElement('iframe');
+  ifr.src = url; ifr.loading = 'lazy'; ifr.title = url;
+  frame.appendChild(ifr);
+  const scrim = document.createElement('a');
+  scrim.className='scrim'; scrim.href=url; scrim.target='_blank'; scrim.rel='noopener';
+  frame.appendChild(scrim);
+  sizeFrame(frame);
+}
+
+function sizeFrame(frame){
+  const ifr = frame.querySelector('iframe'); if(!ifr) return;
+  const w = frame.clientWidth || 400;
+  const s = w / logicalW;
+  const h = LOGICAL_H[logicalW] || 1600;
+  ifr.style.width = logicalW + 'px';
+  ifr.style.height = h + 'px';
+  ifr.style.transform = `scale(${s})`;
+  frame.style.height = (h * s) + 'px';
+}
+
+function rescale(){ document.querySelectorAll('.frame').forEach(sizeFrame); }
+
+async function loadMeta(id){
+  try {
+    const r = await fetch(`/api/design/${id}`);
+    if(!r.ok) throw 0;
+    const j = await r.json();
+    const d = j.design || j.item || j;
+    $('#title').textContent = (d.title || ('Design #'+id)) + (d.category ? ' · '+d.category : '');
+    const when = fmtWhen(d.created_at);
+    const chip = $('#when');
+    if(when){ $('#whenTxt').textContent = when; chip.title = d.created_at; chip.hidden=false; }
+    else chip.hidden = true;
+    const img = d.image_url || ('/designs/img/by-id/'+id);
+    const t = $('#thumb'); t.src = img; t.hidden = false;
+  } catch(e){
+    $('#title').textContent = 'Design #'+id; $('#when').hidden=true; $('#thumb').hidden=true;
+  }
+}
+
+function go(id){
+  id = parseInt(id,10);
+  if(!id) return;
+  localStorage.setItem('tg-id', id);
+  $('#idIn').value = id;
+  const u = new URL(location); u.searchParams.set('id', id); history.replaceState(null,'',u);
+  loadMeta(id);
+  buildGrid(id);
+}
+
+async function newest(){
+  try {
+    const r = await fetch('/api/designs?limit=1');
+    const j = await r.json();
+    const id = (j.items && j.items[0] && j.items[0].id);
+    if(id) go(id);
+  } catch(e){}
+}
+
+// viewport segmented control
+$('#seg').addEventListener('click', e=>{
+  const b = e.target.closest('button[data-w]'); if(!b) return;
+  document.querySelectorAll('#seg button').forEach(x=>x.classList.remove('on'));
+  b.classList.add('on');
+  logicalW = +b.dataset.w; localStorage.setItem('tg-vw', logicalW);
+  rescale();
+});
+document.querySelectorAll('#seg button').forEach(b=>b.classList.toggle('on', +b.dataset.w===logicalW));
+$('#loadBtn').addEventListener('click', ()=>go($('#idIn').value));
+$('#idIn').addEventListener('keydown', e=>{ if(e.key==='Enter') go($('#idIn').value); });
+$('#newestBtn').addEventListener('click', newest);
+window.addEventListener('resize', ()=>{ clearTimeout(window._rt); window._rt=setTimeout(rescale,150); });
+
+// init: ?id= > last-used > newest
+(function init(){
+  const q = new URLSearchParams(location.search).get('id');
+  const last = localStorage.getItem('tg-id');
+  if(q) go(q);
+  else if(last) go(last);
+  else newest();
+})();
+</script>
+</body>
+</html>
diff --git a/src/admin.js b/src/admin.js
index 69b6a8c..1e4e59f 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -174,6 +174,10 @@ function adminHeader(active) {
 }
 
 function mount(app) {
+  // Theme Preview Gallery — all 11 PDP theme variants side-by-side for one design.
+  app.get('/admin/theme-gallery', gate, (req, res) =>
+    res.sendFile(require('path').join(__dirname, '..', 'public', 'admin', 'theme-gallery.html')));
+
   ensureInspirationSchema();
 
   // ── Landing: vendor cards

← 61fa817 audit: unpublish 7 designs caught by haiku-weekend goods-smo  ·  back to Wallco Ai  ·  Add Theme Gallery link to admin-nav hamburger (sibling of Ro 2c77613 →