← back to Rigo Line Viewer

public/index.html

133 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>RIGO → Phillipe Romano Vinyls (internal)</title>
<style>
:root { --cols: 5; --bg:#101214; --card:#181b1e; --line:#2a2e33; --fg:#e8e6e1; --dim:#9aa0a6; --teal:#2dd4bf; }
* { box-sizing:border-box; margin:0; }
body { background:var(--bg); color:var(--fg); font:14px/1.45 -apple-system,'IBM Plex Sans',sans-serif; }
header { display:flex; gap:14px; align-items:center; padding:12px 18px; border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--bg); z-index:5; flex-wrap:wrap; }
header h1 { font-size:15px; font-weight:600; letter-spacing:.04em; }
header .pill { font-size:11px; color:var(--dim); border:1px solid var(--line); border-radius:999px; padding:2px 10px; }
main { display:grid; grid-template-columns: 250px 1fr; }
aside { border-right:1px solid var(--line); padding:14px; height:calc(100vh - 55px); overflow:auto; position:sticky; top:55px; }
aside input[type=search] { width:100%; background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:7px 10px; margin-bottom:12px; }
.facet h3 { font-size:11px; text-transform:uppercase; letter-spacing:.08em; color:var(--dim); margin:10px 0 4px; cursor:pointer; user-select:none; }
.facet h3::before { content:'▸ '; } .facet.open h3::before { content:'▾ '; }
.facet table { width:100%; border-collapse:collapse; display:none; }
.facet.open table { display:table; }
.facet td { padding:2px 4px; font-size:12px; cursor:pointer; border-bottom:1px solid var(--line); }
.facet td.n { text-align:right; color:var(--dim); width:36px; }
.facet tr.on td { color:var(--teal); }
.controls { display:flex; gap:10px; align-items:center; }
select { background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:5px 8px; }
#grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:10px; padding:14px; align-content:start; }
.card { background:var(--card); border:1px solid var(--line); border-radius:8px; overflow:hidden; cursor:pointer; }
.card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:#000; }
.card .meta { padding:8px 10px; font-size:calc(10px + (6 - var(--cols)) * 1px); }
.card .pr { font-weight:600; }
.card .internal { color:var(--dim); }
.card .when { color:var(--dim); font-size:.85em; margin-top:3px; }
body.imgonly .meta { display:none; }
.count { color:var(--dim); font-size:12px; }
label.tog { font-size:12px; color:var(--dim); display:flex; gap:5px; align-items:center; cursor:pointer; }
</style>
</head>
<body>
<header>
  <h1>RIGO → Phillipe Romano Vinyls</h1>
  <span class="pill">INTERNAL — real vendor names visible</span>
  <span class="count" id="count"></span>
  <div class="controls" style="margin-left:auto">
    <select id="sort">
      <option value="newest">Newest</option>
      <option value="pattern">Pattern A→Z</option>
      <option value="pr">PR Name A→Z</option>
      <option value="sku">SKU A→Z</option>
      <option value="color">Color</option>
    </select>
    <input type="range" id="density" min="2" max="10" value="5" title="Density">
    <label class="tog"><input type="checkbox" id="imgonly"> texture wall</label>
  </div>
</header>
<main>
  <aside>
    <input type="search" id="q" placeholder="Search pattern / color / SKU…">
    <div id="facets"></div>
  </aside>
  <div id="grid"></div>
</main>
<script>
let ALL = [], filters = {};
const FACET_FIELDS = [
  ['collection','RIGO Collection'], ['pr_collection_name','PR Collection'],
  ['weight_type','Weight'], ['backing','Backing'], ['match_type','Match'],
  ['color_name','Color'], ['status','Status'],
];
const $ = (s) => document.querySelector(s);
const fmtDate = (d) => d ? new Date(d).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) : '';

async function boot() {
  const r = await fetch(location.origin + '/api/products');
  const j = await r.json();
  ALL = j.products;
  $('#count').textContent = j.count + ' SKUs · loaded ' + fmtDate(j.loadedAt);
  buildFacets(); render();
}
function buildFacets() {
  const box = $('#facets'); box.innerHTML = '';
  for (const [field, label] of FACET_FIELDS) {
    const counts = {};
    for (const p of ALL) { const v = p[field] || '(none)'; counts[v] = (counts[v]||0)+1; }
    const div = document.createElement('div'); div.className = 'facet';
    div.innerHTML = `<h3>${label}</h3><table>` +
      Object.entries(counts).sort((a,b)=>b[1]-a[1]).map(([v,n]) =>
        `<tr data-f="${field}" data-v="${v.replace(/"/g,'&quot;')}"><td>${v}</td><td class="n">${n}</td></tr>`).join('') + '</table>';
    div.querySelector('h3').onclick = () => div.classList.toggle('open');
    div.querySelectorAll('tr').forEach(tr => tr.onclick = () => {
      const f = tr.dataset.f, v = tr.dataset.v;
      if (filters[f] === v) delete filters[f]; else filters[f] = v;
      buildSelected(); render();
    });
    box.appendChild(div);
  }
  buildSelected();
}
function buildSelected() {
  document.querySelectorAll('.facet tr').forEach(tr =>
    tr.classList.toggle('on', filters[tr.dataset.f] === tr.dataset.v));
}
function render() {
  const q = $('#q').value.toLowerCase();
  let rows = ALL.filter(p => Object.entries(filters).every(([f,v]) => (p[f]||'(none)') === v));
  if (q) rows = rows.filter(p => [p.pattern_name,p.pr_pattern_name,p.color_name,p.mfr_sku,p.dw_sku].join(' ').toLowerCase().includes(q));
  const s = $('#sort').value;
  rows.sort((a,b) =>
    s==='newest' ? (b.created_at||'').localeCompare(a.created_at||'') :
    s==='pattern' ? (a.pattern_name||'').localeCompare(b.pattern_name||'') :
    s==='pr' ? (a.pr_pattern_name||'').localeCompare(b.pr_pattern_name||'') :
    s==='sku' ? (a.mfr_sku||'').localeCompare(b.mfr_sku||'') :
    (a.color_name||'').localeCompare(b.color_name||''));
  $('#grid').innerHTML = rows.map(p => `
    <div class="card" title="${p.mfr_sku} · created ${p.created_at||''}" onclick="window.open('${p.product_url}','_blank')">
      ${p.img ? `<img loading="lazy" src="${p.img}">` : '<div style="aspect-ratio:1;background:#222"></div>'}
      <div class="meta">
        <div class="pr">${p.pr_pattern_name || '<i>unnamed</i>'} · ${p.color_name||''}</div>
        <div class="internal">${p.pattern_name} · ${p.mfr_sku} · ${p.collection}</div>
        <div class="internal">${p.dw_sku||'no DW SKU'} · ${p.pr_collection_name||''} · ${p.weight_type||''}</div>
        <div class="when">🕓 ${fmtDate(p.created_at)}</div>
      </div>
    </div>`).join('');
  $('#count').textContent = rows.length + ' / ' + ALL.length + ' SKUs';
}
$('#q').oninput = render; $('#sort').onchange = () => { render(); localStorage.setItem('rigo.sort', $('#sort').value); };
$('#density').oninput = (e) => { document.documentElement.style.setProperty('--cols', e.target.value); localStorage.setItem('rigo.cols', e.target.value); };
$('#imgonly').onchange = (e) => document.body.classList.toggle('imgonly', e.target.checked);
if (localStorage.getItem('rigo.cols')) { $('#density').value = localStorage.getItem('rigo.cols'); document.documentElement.style.setProperty('--cols', $('#density').value); }
if (localStorage.getItem('rigo.sort')) $('#sort').value = localStorage.getItem('rigo.sort');
boot();
</script>
</body>
</html>