← back to Wallco Ai
public/admin/inspirations.html
337 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Inspirations — wallco.ai admin</title>
<style>
:root {
--ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
--bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
--serif:'Playfair Display','Didot',Georgia,serif;
}
body { margin:0; background:var(--bg); color:var(--ink); font:14px var(--sans); }
header {
padding:18px 28px; border-bottom:1px solid var(--line); background:#fff;
display:flex; gap:16px; align-items:baseline;
position:sticky; top:0; z-index:30;
}
header h1 { font:400 24px/1.2 var(--serif); margin:0; }
header .meta { color:var(--faint); font-size:12px; letter-spacing:.04em; }
header .right { margin-left:auto; display:flex; gap:14px; align-items:center; }
header a, header button {
color:var(--ink); text-decoration:none; font:600 11px var(--sans);
letter-spacing:.1em; text-transform:uppercase;
padding:7px 14px; border:1px solid var(--line); border-radius:18px;
background:#fff; cursor:pointer;
}
header button.primary { background:var(--ink); color:#f4ece0; border-color:var(--ink); }
main { max-width:1380px; margin:0 auto; padding:24px 22px 64px; }
/* Sort + density controls (Steve's standing rule) — pinned below the
sticky header so the board chrome stays fixed while the pairs scroll. */
.controls {
display:flex; gap:18px; align-items:center; padding:10px 14px;
background:#fff; border:1px solid var(--line); border-radius:8px;
margin-bottom:18px; flex-wrap:wrap;
position:sticky; top:65px; z-index:29;
box-shadow:0 4px 12px rgba(31,24,8,.05);
}
.controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
text-transform:uppercase; color:var(--faint); }
.controls select { padding:6px 10px; border:1px solid var(--line);
border-radius:4px; background:#fff; font:13px var(--sans); }
.controls input[type=range] { vertical-align:middle; width:200px; }
.controls .density-val { font:600 12px ui-monospace,Menlo,monospace;
color:var(--faint); min-width:32px; display:inline-block; }
.controls .stat { margin-left:auto; font:600 11px var(--sans);
letter-spacing:.08em; color:var(--faint); }
/* Pair card layout */
.grid {
display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--card-min,520px),1fr));
gap:20px;
}
.pair {
background:var(--card); border:1px solid var(--line); border-radius:10px;
padding:14px; display:flex; flex-direction:column; gap:12px;
transition:box-shadow .2s;
}
.pair:hover { box-shadow:0 6px 24px rgba(0,0,0,.08); }
.pair-row {
display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:start;
}
.pane {
display:flex; flex-direction:column; gap:6px; min-width:0;
}
.pane h4 {
margin:0; font:600 10.5px var(--sans); letter-spacing:.18em;
text-transform:uppercase; color:var(--faint);
}
.pane .label-seed { color:#a04a1f; }
.pane .label-out { color:#1f6a2c; }
.pane .img-wrap {
aspect-ratio:1; background:#f3eee2; border-radius:6px; overflow:hidden;
display:flex; align-items:center; justify-content:center;
border:1px solid var(--line);
}
.pane .img-wrap img { width:100%; height:100%; object-fit:cover; display:block; }
.pane .img-wrap.no-img { color:#bba; font-size:11px; }
.pane .meta-row { font-size:11.5px; color:var(--ink); line-height:1.5; word-break:break-word; }
.pane .meta-row strong { color:var(--faint); font-weight:600; }
.pane .when { margin-top:3px; color:var(--faint); font-size:10.5px;
font-variant-numeric:tabular-nums; }
.pane .vendor-tag {
display:inline-block; padding:2px 7px; background:#fff4d8;
border:1px solid #e1c674; border-radius:3px;
font:600 10.5px var(--sans); letter-spacing:.04em; color:#7a5a16;
}
.pane .swatches { display:flex; gap:4px; margin-top:4px; flex-wrap:wrap; }
.pane .swatch { width:18px; height:18px; border-radius:3px;
border:1px solid rgba(0,0,0,.15); }
.pair-foot {
display:flex; gap:10px; align-items:center; padding-top:8px;
border-top:1px solid var(--line); font-size:11.5px; color:var(--faint);
}
.pair-foot .design-link {
color:var(--ink); text-decoration:none; font-weight:600;
padding:6px 12px; border:1px solid var(--ink); border-radius:14px;
}
.pair-foot .regen-btn {
padding:6px 12px; border:1px solid var(--gold); color:#7a5a16;
background:#fff; border-radius:14px; cursor:pointer; font:600 11px var(--sans);
letter-spacing:.06em; text-transform:uppercase;
}
.pair-foot .regen-btn:hover { background:#fff4d8; }
.pair-foot .published-pill {
margin-left:auto; padding:3px 8px; border-radius:3px;
font:600 10px var(--sans); letter-spacing:.08em; text-transform:uppercase;
}
.published-pill.yes { background:#d8f0d8; color:#1f5a1f; }
.published-pill.no { background:#f5d8d8; color:#7a1f1f; }
.empty { padding:60px 20px; text-align:center; color:var(--faint); font:14px var(--sans); }
.loading { padding:60px 20px; text-align:center; color:var(--faint); }
</style>
</head>
<body>
<header>
<h1>Inspirations</h1>
<span class="meta">Real archival seeds × wallco AI-generated originals</span>
<div class="right">
<a href="/admin">← admin home</a>
<a href="/designs">designs grid</a>
</div>
</header>
<main>
<div class="controls">
<label for="sort">Sort</label>
<select id="sort">
<option value="newest">Newest</option>
<option value="vendor">Vendor A→Z</option>
<option value="sku">SKU A→Z</option>
<option value="published">Published first</option>
<option value="unpublished">Unpublished first</option>
</select>
<label for="density">Density</label>
<input id="density" type="range" min="380" max="780" step="20" value="520">
<span class="density-val" id="density-val">520</span>
<span class="stat" id="count-stat">loading…</span>
</div>
<div id="grid" class="grid"><div class="loading">Loading inspirations…</div></div>
</main>
<script>
(function(){
// ── Persist sort + density per Steve's standing rule ────────────────────
var LS_SORT = 'wallco.admin.inspirations.sort';
var LS_DENSITY = 'wallco.admin.inspirations.density';
var sortEl = document.getElementById('sort');
var densityEl = document.getElementById('density');
var densityVal= document.getElementById('density-val');
var gridEl = document.getElementById('grid');
var stat = document.getElementById('count-stat');
sortEl.value = localStorage.getItem(LS_SORT) || 'newest';
densityEl.value = localStorage.getItem(LS_DENSITY) || '520';
applyDensity();
function applyDensity() {
densityVal.textContent = densityEl.value;
gridEl.style.setProperty('--card-min', densityEl.value + 'px');
}
densityEl.addEventListener('input', function(){
applyDensity();
localStorage.setItem(LS_DENSITY, densityEl.value);
});
sortEl.addEventListener('change', function(){
localStorage.setItem(LS_SORT, sortEl.value);
render();
});
// ── State ──────────────────────────────────────────────────────────────
var ROWS = [];
function escAttr(s){ return String(s == null ? '' : s).replace(/"/g,'"').replace(/</g,'<'); }
function escHtml(s){ return String(s == null ? '' : s).replace(/[<>&]/g, function(c){ return c=='<'?'<':c=='>'?'>':'&'; }); }
function fmtDate(iso){
if(!iso) return '—';
var d = new Date(iso); if(isNaN(d)) return '—';
return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
}
function paletteOf(row) {
// First try server-side hex_codes jsonb, then fall back to palette jsonb
var hexes = [];
if (Array.isArray(row.seed_hex_codes)) {
hexes = row.seed_hex_codes.map(function(h){
if (typeof h === 'string') return h;
if (h && h.hex) return h.hex;
return null;
}).filter(Boolean);
}
return hexes.slice(0, 5);
}
function outPaletteOf(row) {
if (!Array.isArray(row.palette)) return [];
return row.palette.map(function(p){ return p && p.hex; }).filter(Boolean).slice(0, 5);
}
function pairCard(row) {
var seedHexes = paletteOf(row);
var outHexes = outPaletteOf(row);
var seedImgHtml = row.seed_image_url
? '<div class="img-wrap"><img src="' + escAttr(row.seed_image_url) + '" alt="archival seed reference" loading="lazy" referrerpolicy="no-referrer" onerror="this.parentNode.classList.add(\'no-img\');this.replaceWith(document.createTextNode(\'image unavailable\'));"></div>'
: '<div class="img-wrap no-img">no seed image</div>';
var outImgHtml = '<div class="img-wrap"><img src="' + escAttr(row.image_url || ('/designs/img/by-id/' + row.id)) + '" alt="wallco AI-generated original" loading="lazy"></div>';
var seedPaletteHtml = seedHexes.length
? '<div class="swatches">' + seedHexes.map(function(h){ return '<span class="swatch" style="background:' + escAttr(h) + '" title="' + escAttr(h) + '"></span>'; }).join('') + '</div>'
: '';
var outPaletteHtml = outHexes.length
? '<div class="swatches">' + outHexes.map(function(h){ return '<span class="swatch" style="background:' + escAttr(h) + '" title="' + escAttr(h) + '"></span>'; }).join('') + '</div>'
: '';
var pubPill = row.is_published
? '<span class="published-pill yes">Published</span>'
: '<span class="published-pill no">Unpublished</span>';
return ''
+ '<div class="pair">'
+ '<div class="pair-row">'
+ '<div class="pane">'
+ '<h4 class="label-seed">Archival seed (admin-only)</h4>'
+ seedImgHtml
+ '<div class="meta-row"><span class="vendor-tag">' + escHtml(row.inspired_by_vendor || '(no vendor)') + '</span></div>'
+ '<div class="meta-row"><strong>SKU:</strong> ' + escHtml(row.inspired_by_sku || '') + '</div>'
+ '<div class="meta-row"><strong>Title:</strong> ' + escHtml(row.seed_title || '(unknown)') + '</div>'
+ (row.seed_era ? '<div class="meta-row"><strong>Era:</strong> ' + escHtml(row.seed_era) + '</div>' : '')
+ seedPaletteHtml
+ '</div>'
+ '<div class="pane">'
+ '<h4 class="label-out">wallco AI original</h4>'
+ outImgHtml
+ '<div class="meta-row"><strong>Design #</strong> ' + row.id + '</div>'
+ '<div class="meta-row"><strong>Dominant hex:</strong> ' + escHtml(row.dominant_hex || '—') + '</div>'
+ '<div class="when" title="created ' + escAttr(row.created_at || '') + '">🕓 ' + escHtml(fmtDate(row.created_at)) + '</div>'
+ outPaletteHtml
+ '</div>'
+ '</div>'
+ '<div class="pair-foot">'
+ '<a href="/design/' + row.id + '" class="design-link">Open /design/' + row.id + ' →</a>'
+ '<button class="regen-btn" data-sku="' + escAttr(row.inspired_by_sku || '') + '">Regenerate from same seed</button>'
+ pubPill
+ '</div>'
+ '</div>';
}
function sortRows(rows, mode) {
var arr = rows.slice();
switch (mode) {
case 'vendor':
arr.sort(function(a,b){ return String(a.inspired_by_vendor||'').localeCompare(String(b.inspired_by_vendor||'')); }); break;
case 'sku':
arr.sort(function(a,b){ return String(a.inspired_by_sku||'').localeCompare(String(b.inspired_by_sku||'')); }); break;
case 'published':
arr.sort(function(a,b){ return (b.is_published?1:0) - (a.is_published?1:0); }); break;
case 'unpublished':
arr.sort(function(a,b){ return (a.is_published?1:0) - (b.is_published?1:0); }); break;
case 'newest':
default:
arr.sort(function(a,b){ return b.id - a.id; }); break;
}
return arr;
}
function render() {
if (!ROWS.length) {
gridEl.innerHTML = '<div class="empty">No inspired-by-seed designs yet. Run<br><code style="background:#fff;padding:4px 8px;border:1px solid var(--line);border-radius:4px;font-family:ui-monospace,Menlo,monospace">node scripts/inspiration_generator.js --n 5</code><br>to seed this page.</div>';
stat.textContent = '0 pairs';
return;
}
var sorted = sortRows(ROWS, sortEl.value);
gridEl.innerHTML = sorted.map(pairCard).join('');
stat.textContent = sorted.length + ' pair' + (sorted.length === 1 ? '' : 's');
}
gridEl.addEventListener('click', function(e){
var btn = e.target.closest('.regen-btn');
if (!btn) return;
var sku = btn.getAttribute('data-sku');
if (!sku) return;
btn.disabled = true;
btn.textContent = 'queued…';
fetch('/api/admin/inspirations/regenerate', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ sku: sku }), credentials:'same-origin'
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d && d.ok) {
btn.textContent = 'queued ✓';
setTimeout(function(){ btn.textContent = 'Regenerate from same seed'; btn.disabled = false; }, 5000);
} else {
btn.textContent = 'failed — ' + (d.error || 'try again');
btn.disabled = false;
}
})
.catch(function(){ btn.textContent = 'network error'; btn.disabled = false; });
});
function load() {
fetch('/api/admin/inspirations/list?limit=200', { credentials:'same-origin' })
.then(function(r){
if (r.status === 404) throw new Error('not_admin');
return r.json();
})
.then(function(d){
ROWS = (d && d.rows) || [];
render();
})
.catch(function(err){
if (String(err.message) === 'not_admin') {
gridEl.innerHTML = '<div class="empty">Admin gate failed. Hit <code>/_devlogin</code> first or include <code>?admin=<ADMIN_TOKEN></code>.</div>';
} else {
gridEl.innerHTML = '<div class="empty">Failed to load: ' + escHtml(err.message) + '</div>';
}
});
}
load();
})();
</script>
<script src="/admin/admin-modal-kit.js" defer></script>
</body>
</html>