← back to Dw Photo Capture
public/vendor-photos.html
79 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Vendor Photos · DW</title>
<style>
:root{--bg:#0f0e0c;--card:#1b1916;--ink:#f3efe7;--muted:#9a9184;--line:#2e2a25;--gold:#c8a24a;--cardw:220px}
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.4 -apple-system,system-ui,sans-serif}
header{position:sticky;top:0;z-index:5;background:var(--bg);border-bottom:1px solid var(--line);padding:12px 16px}
h1{margin:0 0 10px;font-size:18px} h1 a{color:var(--muted);font-size:13px;text-decoration:none;margin-left:8px}
.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
select,input[type=search]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:14px}
.dens{color:var(--muted);font-size:13px;display:flex;align-items:center;gap:6px}
.vendors{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.vendors button{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:999px;padding:5px 11px;font-size:13px;cursor:pointer}
.vendors button.on{border-color:var(--gold);color:var(--gold)}
.vendors b{color:var(--muted);font-weight:400;margin-left:4px}
main{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--cardw),1fr));gap:10px;padding:14px 16px}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}
.card img{width:100%;aspect-ratio:1;object-fit:cover;display:block;background:#000}
.card .meta{padding:8px 10px;font-size:12px;display:grid;gap:3px}
.card .sku{font-weight:600;font-size:13px}
.card .ven,.card .src{color:var(--muted)}
.when{color:var(--gold)}
.empty{padding:40px 16px;color:var(--muted);text-align:center}
</style></head>
<body>
<header>
<h1>Vendor Photos <span id="count" style="color:var(--muted);font-weight:400;font-size:14px"></span><a href="/app">← capture</a></h1>
<div class="controls">
<input type="search" id="q" placeholder="Search SKU or vendor">
<select id="sort">
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="vendor">Vendor A→Z</option>
<option value="sku">SKU A→Z</option>
</select>
<label class="dens">Size <input type="range" id="dens" min="120" max="420" step="20" value="220"></label>
</div>
<div class="vendors" id="vendors"></div>
</header>
<main id="grid"></main>
<script>
const $ = s => document.querySelector(s);
const ls = { get(k,d){ try{ return localStorage.getItem(k) ?? d; }catch(e){ return d; } }, set(k,v){ try{ localStorage.setItem(k,v); }catch(e){} } };
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
const fmtDate = iso => { const d = new Date(iso); return isNaN(d) ? '' : d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); };
let all = [], vendor = ls.get('vp.vendor',''), sort = ls.get('vp.sort','newest');
$('#sort').value = sort; $('#dens').value = ls.get('vp.dens','220');
document.documentElement.style.setProperty('--cardw', $('#dens').value + 'px');
function render(){
const q = $('#q').value.trim().toLowerCase();
let rows = all.filter(r => (!vendor || r.vendor === vendor) && (!q || (r.dw_sku||'').toLowerCase().includes(q) || r.vendor.toLowerCase().includes(q)));
const by = { newest:(a,b)=>b.at.localeCompare(a.at), oldest:(a,b)=>a.at.localeCompare(b.at),
vendor:(a,b)=>a.vendor.localeCompare(b.vendor)||b.at.localeCompare(a.at), sku:(a,b)=>(a.dw_sku||'').localeCompare(b.dw_sku||'') };
rows.sort(by[sort] || by.newest);
$('#count').textContent = rows.length + ' of ' + all.length;
$('#grid').innerHTML = rows.length ? rows.map(r => `<div class="card">
<a href="${esc(r.path)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="${esc(r.path)}" alt="${esc(r.dw_sku||'')}"></a>
<div class="meta"><div class="sku">${esc(r.dw_sku || '—')}</div><div class="ven">${esc(r.vendor)}</div>
<div class="when" title="${esc(r.at)}">🕓 ${esc(fmtDate(r.at))}</div><div class="src">${esc(r.source||'')}</div></div></div>`).join('')
: '<div class="empty">No photos yet.</div>';
}
function renderVendors(counts){
const names = Object.keys(counts).sort((a,b)=>a.localeCompare(b));
if (vendor && !counts[vendor]) vendor = '';
$('#vendors').innerHTML = `<button data-v="" class="${vendor?'':'on'}">All<b>${all.length}</b></button>` +
names.map(v => `<button data-v="${esc(v)}" class="${v===vendor?'on':''}">${esc(v)}<b>${counts[v]}</b></button>`).join('');
}
$('#vendors').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return;
vendor = b.dataset.v; ls.set('vp.vendor', vendor);
document.querySelectorAll('#vendors button').forEach(x => x.classList.toggle('on', x === b)); render(); });
$('#sort').addEventListener('change', e => { sort = e.target.value; ls.set('vp.sort', sort); render(); });
$('#dens').addEventListener('input', e => { document.documentElement.style.setProperty('--cardw', e.target.value + 'px'); ls.set('vp.dens', e.target.value); });
$('#q').addEventListener('input', render);
fetch(location.origin + '/api/vendor-photos').then(r => r.json()).then(d => { all = d.photos || []; renderVendors(d.vendors || {}); render(); })
.catch(() => { $('#grid').innerHTML = '<div class="empty">Could not load photos.</div>'; });
</script>
</body></html>