← back to Wallco Ai
public/joint-fix-gallery.html
140 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Joint-Fix Gallery — Source vs Fix</title>
<style>
:root { --bg:#fafaf8; --fg:#1a1a1a; --muted:#6a6a6a; --card:#fff; --border:#e8e6e1; --accent:#8b1f1f; --gold:#c9a14b; --card-min:360px; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; }
header { padding:24px 32px 16px; border-bottom:1px solid var(--border); background:var(--card); position:sticky; top:0; z-index:10; }
header h1 { margin:0 0 4px; font-size:20px; font-weight:600; letter-spacing:-0.01em; }
header .stats { color:var(--muted); font-size:13px; }
header .stats b { color:var(--fg); font-weight:600; }
.filters { margin-top:12px; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.filters select, .filters input { padding:6px 10px; border:1px solid var(--border); border-radius:4px; background:var(--card); font:inherit; }
.filters label { font-size:12px; color:var(--muted); margin-right:4px; }
main { padding:24px 32px 64px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--card-min), 1fr)); gap:24px; }
.filters input[type=range] { accent-color:var(--gold); vertical-align:middle; padding:0; }
.pair { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.pair-imgs { display:grid; grid-template-columns:1fr 1fr; gap:0; }
.pair-imgs > div { position:relative; aspect-ratio:1; background:#f0eee8; overflow:hidden; }
.pair-imgs img { width:100%; height:100%; object-fit:cover; display:block; }
.pair-imgs .label { position:absolute; top:6px; left:6px; padding:2px 6px; background:rgba(0,0,0,0.7); color:#fff; font-size:10px; letter-spacing:0.06em; text-transform:uppercase; border-radius:2px; }
.pair-imgs .label.fix { background:rgba(40,140,90,0.85); }
.pair-meta { padding:10px 12px; display:flex; justify-content:space-between; align-items:center; font-size:12px; }
.pair-meta .cat { color:var(--muted); }
.pair-meta .ids a { color:var(--accent); text-decoration:none; font-weight:600; }
.pair-meta .ids a:hover { text-decoration:underline; }
.arrow { color:var(--muted); margin:0 4px; }
.badge { display:inline-block; padding:1px 6px; font-size:10px; font-weight:600; border-radius:10px; background:#e8f4ec; color:#1d6b3a; margin-left:6px; }
.empty { padding:40px; text-align:center; color:var(--muted); }
</style>
</head>
<body>
<header>
<h1>Joint-Fix Gallery <span class="badge" id="count">…</span></h1>
<div class="stats">Source design (left) → joint-fix output (right). 95% of source preserved, edges repainted to tile seamlessly. Gemini-2.5-flash-image-edit · avg <b>7.6s</b> per fix.</div>
<div class="filters">
<label>Category</label>
<select id="catFilter"><option value="">All categories</option></select>
<label>Sort</label>
<select id="sortBy">
<option value="newest">Newest first</option>
<option value="src">Source ID ↑</option>
<option value="cat">Category</option>
</select>
<label>Show</label>
<select id="limit">
<option value="60">60</option>
<option value="120">120</option>
<option value="240">240</option>
<option value="0">All</option>
</select>
<label>Density</label>
<input type="range" id="density" min="320" max="680" step="20" title="pair card size">
</div>
</header>
<main>
<div class="grid" id="grid"><div class="empty">Loading…</div></div>
</main>
<script>
async function loadData() {
const r = await fetch('/api/joint-fix-gallery');
if (!r.ok) throw new Error('fetch failed: ' + r.status);
return r.json();
}
function categoryBase(c) {
if (!c) return '';
return c.split('·')[0].trim();
}
function render(items, allCats) {
const grid = document.getElementById('grid');
document.getElementById('count').textContent = items.length;
if (!items.length) { grid.innerHTML = '<div class="empty">No fixes match filters.</div>'; return; }
grid.innerHTML = items.map(p => `
<div class="pair">
<div class="pair-imgs">
<div>
<span class="label">Source #${p.src_id}</span>
<img loading="lazy" src="/designs/img/by-id/${p.src_id}?asis=1" alt="src ${p.src_id}">
</div>
<div>
<span class="label fix">Fix #${p.new_id}</span>
<img loading="lazy" src="/designs/img/by-id/${p.new_id}?asis=1" alt="fix ${p.new_id}">
</div>
</div>
<div class="pair-meta">
<div class="cat">${p.src_category || '?'}</div>
<div class="ids">
<a href="/design/${p.src_id}?asis=1">${p.src_id}</a>
<span class="arrow">→</span>
<a href="/design/${p.new_id}">${p.new_id}</a>
</div>
</div>
</div>
`).join('');
}
let DATA = [];
// sort + density persist (Steve standing rule: every product grid; both persist)
document.getElementById('sortBy').value = localStorage.getItem('jointFix_sort') || 'newest';
document.getElementById('density').value = localStorage.getItem('jointFix_density') || '360';
document.documentElement.style.setProperty('--card-min', document.getElementById('density').value + 'px');
document.getElementById('density').addEventListener('input', () => {
localStorage.setItem('jointFix_density', document.getElementById('density').value);
document.documentElement.style.setProperty('--card-min', document.getElementById('density').value + 'px');
});
function apply() {
const cat = document.getElementById('catFilter').value;
const sort = document.getElementById('sortBy').value;
const limit = parseInt(document.getElementById('limit').value, 10);
let items = DATA.slice();
if (cat) items = items.filter(p => categoryBase(p.src_category) === cat);
if (sort === 'newest') items.sort((a,b) => b.new_id - a.new_id);
else if (sort === 'src') items.sort((a,b) => a.src_id - b.src_id);
else if (sort === 'cat') items.sort((a,b) => (categoryBase(a.src_category)||'').localeCompare(categoryBase(b.src_category)||''));
if (limit > 0) items = items.slice(0, limit);
render(items);
}
loadData().then(data => {
DATA = data.items || [];
const cats = [...new Set(DATA.map(p => categoryBase(p.src_category)).filter(Boolean))].sort();
const sel = document.getElementById('catFilter');
for (const c of cats) {
const o = document.createElement('option');
o.value = c; o.textContent = c + ' (' + DATA.filter(p => categoryBase(p.src_category) === c).length + ')';
sel.appendChild(o);
}
document.getElementById('catFilter').addEventListener('change', apply);
document.getElementById('sortBy').addEventListener('change', () => { localStorage.setItem('jointFix_sort', document.getElementById('sortBy').value); apply(); });
document.getElementById('limit').addEventListener('change', apply);
apply();
}).catch(e => {
document.getElementById('grid').innerHTML = '<div class="empty">Failed to load: ' + e.message + '</div>';
});
</script>
</body>
</html>