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