← back to Designer Wallcoverings

onboarding/sangetsu-lilycolor/viewer/public/index.html

138 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sangetsu + Lilycolor — Staged SKUs (offline preview)</title>
<style>
  :root { --cols: 6; --bg:#0d0d0f; --card:#17171b; --line:#2a2a30; --txt:#e8e8ea; --mut:#9a9aa2; --accent:#c8a96a; }
  * { box-sizing: border-box; }
  body { margin:0; background:var(--bg); color:var(--txt); font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
  header { position:sticky; top:0; z-index:10; background:rgba(13,13,15,.96); backdrop-filter:blur(8px); border-bottom:1px solid var(--line); padding:14px 20px; }
  h1 { margin:0 0 10px; font-size:18px; font-weight:600; letter-spacing:.02em; }
  h1 small { color:var(--mut); font-weight:400; font-size:13px; margin-left:8px; }
  .controls { display:flex; flex-wrap:wrap; gap:10px 14px; align-items:center; }
  .seg { display:flex; gap:2px; background:var(--card); border:1px solid var(--line); border-radius:8px; padding:2px; }
  .seg button { background:none; border:none; color:var(--mut); padding:6px 12px; border-radius:6px; cursor:pointer; font-size:13px; }
  .seg button.on { background:var(--accent); color:#1a1407; font-weight:600; }
  select, input[type=search] { background:var(--card); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:7px 10px; font-size:13px; }
  input[type=search] { min-width:220px; }
  .dens { display:flex; align-items:center; gap:8px; color:var(--mut); }
  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:12px; padding:18px 20px 60px; }
  .card { background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
  .thumb { aspect-ratio:1/1; background:#000 center/cover no-repeat; position:relative; }
  .thumb.empty { display:flex; align-items:center; justify-content:center; color:#444; font-size:11px; }
  .badge { position:absolute; top:6px; left:6px; font-size:10px; font-weight:700; letter-spacing:.04em; padding:2px 7px; border-radius:20px; text-transform:uppercase; }
  .badge.lilycolor { background:#7c5cff; color:#fff; }
  .badge.sangetsu { background:#1f9e6b; color:#fff; }
  .badge.sample { position:absolute; top:6px; right:6px; left:auto; background:#444; color:#ddd; }
  .meta { padding:8px 10px; }
  .sku { font-weight:700; font-size:13px; color:var(--accent); word-break:break-all; }
  .badge.imgs { background:#2a2a30; color:#9a9aa2; font-size:9px; padding:1px 5px; margin-left:5px; border-radius:10px; vertical-align:middle; }
  .ttl { color:var(--mut); font-size:11.5px; margin-top:3px; max-height:2.8em; overflow:hidden; }
  .priceline { margin-top:4px; }
  .price { color:#e8e8ea; font-size:12.5px; font-weight:600; }
  .price i { color:#777; font-weight:400; font-style:normal; font-size:10px; margin-left:2px; }
  .spec { margin-top:5px; font-size:11px; color:#777; display:flex; gap:8px; flex-wrap:wrap; }
  #sentinel { height:1px; }
  #status { text-align:center; color:var(--mut); padding:16px; font-size:13px; }
</style>
</head>
<body>
<header>
  <h1>Sangetsu + Lilycolor <small id="sub">staged offline · read-only preview</small></h1>
  <div class="controls">
    <div class="seg" id="srcSeg">
      <button data-src="all" class="on">All</button>
      <button data-src="lilycolor">Lilycolor</button>
      <button data-src="sangetsu">Sangetsu</button>
    </div>
    <input type="search" id="q" placeholder="Search SKU or pattern…">
    <label class="dens">Sort
      <select id="sort">
        <option value="newest">Newest</option>
        <option value="sku">SKU A→Z</option>
        <option value="title">Title A→Z</option>
        <option value="width">Width ↑</option>
        <option value="price">Price ↑</option>
        <option value="images">Most images</option>
      </select>
    </label>
    <label class="dens">Density
      <input type="range" id="dens" min="3" max="10" value="6">
    </label>
  </div>
</header>
<div class="grid" id="grid"></div>
<div id="sentinel"></div>
<div id="status">loading…</div>

<script>
const grid = document.getElementById('grid');
const statusEl = document.getElementById('status');
const subEl = document.getElementById('sub');
const LIMIT = 120;
let state = {
  source: localStorage.getItem('dw_src') || 'all',
  sort: localStorage.getItem('dw_sort') || 'newest',
  q: '',
  offset: 0, total: 0, loading: false, done: false,
};

// density persists
const dens = document.getElementById('dens');
dens.value = localStorage.getItem('dw_cols') || 6;
document.documentElement.style.setProperty('--cols', dens.value);
dens.oninput = () => { document.documentElement.style.setProperty('--cols', dens.value); localStorage.setItem('dw_cols', dens.value); };

// restore sort/source UI
document.getElementById('sort').value = state.sort;
document.querySelectorAll('#srcSeg button').forEach(b => b.classList.toggle('on', b.dataset.src === state.source));

function reset() { grid.innerHTML=''; state.offset=0; state.done=false; statusEl.textContent='loading…'; load(); }

document.getElementById('srcSeg').onclick = (e) => {
  const b = e.target.closest('button'); if (!b) return;
  document.querySelectorAll('#srcSeg button').forEach(x => x.classList.remove('on')); b.classList.add('on');
  state.source = b.dataset.src; localStorage.setItem('dw_src', state.source); reset();
};
document.getElementById('sort').onchange = (e) => { state.sort = e.target.value; localStorage.setItem('dw_sort', state.sort); reset(); };
let qt; document.getElementById('q').oninput = (e) => { clearTimeout(qt); qt = setTimeout(() => { state.q = e.target.value.trim(); reset(); }, 250); };

function card(r) {
  const el = document.createElement('div'); el.className='card';
  const tag = r.source==='lilycolor' ? (r.catalog||'Lily') : 'Sangetsu';
  const imgCountBadge = (!r.image && r.image_count) ? `<span class="badge imgs" title="${r.image_count} official images archived (${(r.image_kinds||[]).join('/')}) — bytes not yet pulled">📷 ${r.image_count}</span>` : '';
  const thumb = r.image
    ? `<div class="thumb" style="background-image:url('${r.image}')"><span class="badge ${r.source}">${tag}</span></div>`
    : `<div class="thumb empty"><span class="badge ${r.source}">${tag}</span>${r.image_count?`📷 ${r.image_count} archived`:'no image'}</div>`;
  const price = r.price_yen ? `<span class="price">¥${r.price_yen.toLocaleString()}<i>/m list</i></span>` : '';
  const specs = [r.width, r.repeat, r.fire, r.composition].filter(Boolean).map(s=>`<span>${s}</span>`).join('');
  el.innerHTML = `${thumb}<div class="meta"><div class="sku">${r.sku||'—'}${imgCountBadge}</div><div class="ttl" title="${(r.title||'').replace(/"/g,'&quot;')}">${r.title||''}</div>${price?`<div class="priceline">${price}</div>`:''}<div class="spec">${specs}</div></div>`;
  if (r.url) { el.style.cursor='pointer'; el.onclick=()=>window.open(r.url,'_blank'); }
  return el;
}

async function load() {
  if (state.loading || state.done) return;
  state.loading = true;
  const p = new URLSearchParams({ source:state.source, sort:state.sort, q:state.q, offset:state.offset, limit:LIMIT });
  const res = await fetch('/api/skus?'+p); const data = await res.json();
  state.total = data.total;
  const frag = document.createDocumentFragment();
  data.rows.forEach(r => frag.appendChild(card(r)));
  grid.appendChild(frag);
  state.offset += data.rows.length;
  state.done = state.offset >= data.total;
  state.loading = false;
  subEl.textContent = `staged offline · ${data.counts.all.toLocaleString()} SKUs (Lily ${data.counts.lilycolor.toLocaleString()} / Sangetsu ${data.counts.sangetsu.toLocaleString()}) · showing ${state.offset.toLocaleString()}/${state.total.toLocaleString()}`;
  statusEl.textContent = state.done ? `— end · ${state.total.toLocaleString()} SKUs —` : 'scroll for more…';
}

// infinite scroll
new IntersectionObserver((es)=>{ if (es[0].isIntersecting) load(); }, { rootMargin:'600px' }).observe(document.getElementById('sentinel'));
load();
</script>
</body>
</html>