← back to Muralsource Curator

index.html

121 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mural Source — Settlement Curator (228)</title>
<style>
  :root{--cols:5;}
  *{box-sizing:border-box}
  body{margin:0;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0e0f12;color:#e7e7ea}
  header{position:sticky;top:0;z-index:10;background:#15171c;border-bottom:1px solid #262a32;padding:10px 16px;display:flex;flex-wrap:wrap;gap:12px;align-items:center}
  h1{font-size:15px;margin:0;font-weight:600;letter-spacing:.02em}
  .muted{color:#8b90a0}
  input,select,button{font:inherit;background:#1c1f26;color:#e7e7ea;border:1px solid #2c313b;border-radius:8px;padding:7px 10px}
  button{cursor:pointer}
  button.primary{background:#2f6df6;border-color:#2f6df6;color:#fff;font-weight:600}
  button.ghost{background:transparent}
  .controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
  .count{font-weight:700}
  #grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:16px}
  .card{background:#15171c;border:1px solid #262a32;border-radius:12px;overflow:hidden;position:relative;transition:border-color .15s}
  .card.sel{border-color:#2f6df6;box-shadow:0 0 0 2px #2f6df6 inset}
  .card .imgwrap{aspect-ratio:1/1.1;background:#0a0b0d;overflow:hidden;display:flex;align-items:center;justify-content:center}
  .card img{width:100%;height:100%;object-fit:cover;display:block}
  .card .meta{padding:8px 10px}
  .card .pat{font-weight:600;font-size:13px;line-height:1.25}
  .card .sub{color:#9aa0b0;font-size:11px;margin-top:2px}
  .card .tags{margin-top:6px;font-size:10px;color:#c9b98a;line-height:1.35;max-height:2.7em;overflow:hidden}
  .card .when{margin-top:6px;font-size:10px;color:#6f7688}
  .card .price{position:absolute;top:8px;right:8px;background:#0e0f12cc;border:1px solid #2c313b;border-radius:6px;padding:2px 6px;font-size:11px;font-weight:700}
  .card .chk{position:absolute;top:8px;left:8px;width:22px;height:22px;accent-color:#2f6df6;cursor:pointer}
  .card .sku{position:absolute;bottom:8px;left:10px;font-size:10px;color:#6f7688;font-family:ui-monospace,monospace}
  #export{position:fixed;bottom:0;left:0;right:0;background:#15171c;border-top:1px solid #262a32;padding:10px 16px;display:none;gap:10px;align-items:center}
  #export.show{display:flex}
  textarea{width:100%;height:70px;background:#0a0b0d;color:#9fe6a0;border:1px solid #2c313b;border-radius:8px;font-family:ui-monospace,monospace;font-size:11px;padding:8px}
  a{color:#6ea8ff}
</style>
</head>
<body>
<header>
  <h1>Mural Source — Settlement Curator</h1>
  <span class="muted">Check the murals that <b>PASS</b> the settlement (no bananas/grapes/birds/butterflies + directional foliage w/ open space). <span class="count" id="selN">0</span> selected / <span id="totN">0</span></span>
  <div class="controls" style="margin-left:auto">
    <input id="q" placeholder="filter pattern / color / tag" size="22">
    <select id="sort">
      <option value="pattern">Pattern A→Z</option>
      <option value="color">Color</option>
      <option value="price_asc">Price ↑</option>
      <option value="price_desc">Price ↓</option>
      <option value="sku">SKU</option>
    </select>
    <label class="muted">density <input id="dens" type="range" min="3" max="8" value="5"></label>
    <button class="ghost" id="selAll">select all shown</button>
    <button class="ghost" id="selNone">clear</button>
    <button class="primary" id="exp">Export selection ▸</button>
  </div>
</header>
<div id="grid"></div>
<div id="export">
  <div style="flex:1">
    <div class="muted" style="margin-bottom:4px">Selected DW SKUs — copy this back to Claude to push (settlement-PASS set):</div>
    <textarea id="out" readonly></textarea>
  </div>
  <button class="primary" id="copy">Copy</button>
  <button class="ghost" id="close">×</button>
</div>
<script>
const LS='ms_curator_sel', LSD='ms_curator_dens', LSS='ms_curator_sort';
let items=[], sel=new Set(JSON.parse(localStorage.getItem(LS)||'[]'));
const grid=document.getElementById('grid'), q=document.getElementById('q'),
      sortEl=document.getElementById('sort'), dens=document.getElementById('dens');
dens.value=localStorage.getItem(LSD)||5; document.documentElement.style.setProperty('--cols',dens.value);
sortEl.value=localStorage.getItem(LSS)||'pattern';
function tagList(s){try{return JSON.parse(s)}catch{return[]}}
function render(){
  const term=q.value.trim().toLowerCase();
  let list=items.filter(it=>!term|| (it.pattern_name+' '+it.color_name+' '+it.ai_tags).toLowerCase().includes(term));
  const s=sortEl.value;
  list.sort((a,b)=> s==='price_asc'? a.price_retail-b.price_retail :
    s==='price_desc'? b.price_retail-a.price_retail :
    s==='color'? (a.color_name||'').localeCompare(b.color_name||'') :
    s==='sku'? a.dw_sku.localeCompare(b.dw_sku) :
    (a.pattern_name||'').localeCompare(b.pattern_name||''));
  grid.innerHTML='';
  for(const it of list){
    const c=document.createElement('div'); c.className='card'+(sel.has(it.dw_sku)?' sel':'');
    const tags=tagList(it.ai_tags).slice(0,6).join(' · ');
    c.innerHTML=`<div class="imgwrap"><img loading="lazy" src="${it.image_url}" alt="${it.pattern_name}"
        onerror="this.style.opacity=.2;this.alt='no image'"></div>
      <input type="checkbox" class="chk" ${sel.has(it.dw_sku)?'checked':''}>
      <span class="price">$${Number(it.price_retail).toFixed(0)}</span>
      <span class="sku">${it.dw_sku}</span>
      <div class="meta"><div class="pat">${it.pattern_name}</div>
      <div class="sub">${it.color_name||''} · ${it.product_type||''}</div>
      <div class="tags">${tags}</div>
      <div class="when" title="scraped ${it.refreshed_at}">🕓 ${it.created_at} · <a href="${it.product_url}" target="_blank" rel="noopener noreferrer">vendor ↗</a></div></div>`;
    const toggle=()=>{ sel.has(it.dw_sku)?sel.delete(it.dw_sku):sel.add(it.dw_sku);
      c.classList.toggle('sel'); c.querySelector('.chk').checked=sel.has(it.dw_sku); save(); };
    c.querySelector('.chk').addEventListener('click',e=>{e.stopPropagation();toggle();});
    c.querySelector('.imgwrap').addEventListener('click',toggle);
    grid.appendChild(c);
  }
  document.getElementById('totN').textContent=items.length;
  document.getElementById('selN').textContent=sel.size;
}
function save(){localStorage.setItem(LS,JSON.stringify([...sel]));document.getElementById('selN').textContent=sel.size;}
q.oninput=render;
sortEl.onchange=()=>{localStorage.setItem(LSS,sortEl.value);render();};
dens.oninput=()=>{document.documentElement.style.setProperty('--cols',dens.value);localStorage.setItem(LSD,dens.value);};
document.getElementById('selAll').onclick=()=>{document.querySelectorAll('#grid .card').forEach((c,i)=>{});
  const term=q.value.trim().toLowerCase();
  items.filter(it=>!term||(it.pattern_name+' '+it.color_name+' '+it.ai_tags).toLowerCase().includes(term)).forEach(it=>sel.add(it.dw_sku));save();render();};
document.getElementById('selNone').onclick=()=>{sel.clear();save();render();};
document.getElementById('exp').onclick=()=>{
  document.getElementById('out').value=[...sel].sort().join(',');
  document.getElementById('export').classList.add('show');};
document.getElementById('close').onclick=()=>document.getElementById('export').classList.remove('show');
document.getElementById('copy').onclick=()=>{const t=document.getElementById('out');t.select();document.execCommand('copy');};
fetch('items.json').then(r=>r.json()).then(d=>{items=d;render();});
</script>
</body></html>