← back to Muralsource Curator

index.html

190 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 / List toggle */
  .vbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
  .vbtn.on{background:#2f6df6;border-color:#2f6df6;color:#fff}
  /* Fields popover */
  .fieldmenu{position:relative}
  .fbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
  .fbtn:hover{border-color:#5a9af0;color:#e7e7ea}
  .fpop{position:absolute;top:calc(100% + 6px);right:0;background:#1c1f26;border:1px solid #2c313b;border-radius:8px;box-shadow:0 10px 28px rgba(0,0,0,.5);padding:8px 4px;z-index:30;min-width:170px}
  .fpop[hidden]{display:none}
  .fpop label{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#b8bec9;cursor:pointer;white-space:nowrap}
  .fpop label:hover{background:#21252e;color:#e7e7ea}
  .fpop input{accent-color:#2f6df6;width:14px;height:14px;margin:0;padding:0}
  /* Grid view */
  #grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:16px}
  /* List view */
  #grid.list{display:flex;flex-direction:column;gap:6px;padding:16px}
  #grid.list .card{display:flex;flex-direction:row;align-items:stretch;border-radius:8px}
  #grid.list .card .imgwrap{aspect-ratio:1/1;width:72px;min-width:72px;height:72px;border-radius:6px 0 0 6px}
  #grid.list .card .meta{flex:1;display:flex;flex-direction:row;align-items:center;gap:16px;padding:8px 12px;flex-wrap:wrap}
  #grid.list .card .price{position:static;border:0;background:none;font-weight:700;font-size:12px;white-space:nowrap}
  #grid.list .card .chk{position:static;width:18px;height:18px}
  #grid.list .card .sku{position:static;font-size:10px;color:#6f7688}
  /* Field visibility — body class hides tagged elements */
  body.hf-image [data-f="image"]{display:none!important}
  body.hf-title [data-f="title"]{display:none!important}
  body.hf-sub [data-f="sub"]{display:none!important}
  body.hf-tags [data-f="tags"]{display:none!important}
  body.hf-price [data-f="price"]{display:none!important}
  body.hf-sku [data-f="sku"]{display:none!important}
  body.hf-when [data-f="when"]{display:none!important}
  .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="vbtn on" id="vGrid" title="Grid view">⊞ Grid</button>
    <button class="vbtn" id="vList" title="List view">☰ List</button>
    <div class="fieldmenu">
      <button class="fbtn" id="fieldsBtn" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
      <div class="fpop" id="fpop" hidden role="menu"></div>
    </div>
    <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',
      LSV='ms_curator_view', LSF='ms_curator_fields';
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';

// ── Grid / List toggle ──
const MS_FIELDS=[['image','Image'],['title','Title'],['sub','Color / Type'],['tags','Tags'],['price','Price'],['sku','SKU'],['when','Date / Link']];
const MS_FIELDS_DEF=Object.fromEntries(MS_FIELDS.map(([k])=>[k,true]));
let msFieldVis=Object.assign({},MS_FIELDS_DEF,JSON.parse(localStorage.getItem(LSF)||'{}'));
function applyMsFields(){
  MS_FIELDS.forEach(([k])=>document.body.classList.toggle('hf-'+k,!msFieldVis[k]));
  document.querySelectorAll('#fpop input[data-field]').forEach(cb=>cb.checked=!!msFieldVis[cb.dataset.field]);
  localStorage.setItem(LSF,JSON.stringify(msFieldVis));
}
function buildMsFieldMenu(){
  const fpop=document.getElementById('fpop');
  fpop.innerHTML=MS_FIELDS.map(([k,lbl])=>`<label><input type="checkbox" data-field="${k}"${msFieldVis[k]?' checked':''}> ${lbl}</label>`).join('');
  fpop.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{msFieldVis[cb.dataset.field]=cb.checked;localStorage.setItem(LSF,JSON.stringify(msFieldVis));applyMsFields();}));
}
function setMsView(v){
  grid.classList.toggle('list',v==='list');
  document.getElementById('vGrid').classList.toggle('on',v==='grid');
  document.getElementById('vList').classList.toggle('on',v==='list');
  localStorage.setItem(LSV,v);
}
document.getElementById('vGrid').onclick=()=>setMsView('grid');
document.getElementById('vList').onclick=()=>setMsView('list');
setMsView(localStorage.getItem(LSV)||'grid');
buildMsFieldMenu();
applyMsFields();
// Fields popover open/close
(function(){
  const btn=document.getElementById('fieldsBtn'), pop=document.getElementById('fpop');
  btn.addEventListener('click',e=>{e.stopPropagation();const h=pop.hidden;pop.hidden=!h;btn.setAttribute('aria-expanded',String(h));});
  document.addEventListener('click',()=>{pop.hidden=true;btn.setAttribute('aria-expanded','false');});
  pop.addEventListener('click',e=>e.stopPropagation());
})();
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" data-f="image"><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" data-f="price">$${Number(it.price_retail).toFixed(0)}</span>
      <span class="sku" data-f="sku">${it.dw_sku}</span>
      <div class="meta"><div class="pat" data-f="title">${it.pattern_name}</div>
      <div class="sub" data-f="sub">${it.color_name||''} · ${it.product_type||''}</div>
      <div class="tags" data-f="tags">${tags}</div>
      <div class="when" data-f="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>