← 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>