← 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
A public/admin/theme-gallery.htmlM src/admin.js
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 #</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/<id>/<slug></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 →