← back to Whatsmystyle
public/brands.html
199 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Brands — WhatsMyStyle</title>
<link rel="stylesheet" href="/css/app.css" />
<style>
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #faf7f2; color: #1d1d1f; max-width: 1100px; margin: 40px auto; padding: 0 24px; }
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
.sub { color: #707070; margin-bottom: 18px; font-size: 14px; }
.nav { margin-bottom: 18px; }
.nav a { color: #707070; text-decoration: none; font-size: 14px; }
.nav a:hover { color: #1d1d1f; }
.controls {
display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
margin-bottom: 14px; padding: 12px 14px;
background: #fff; border: 1px solid #e6e1d8; border-radius: 12px;
}
.controls label { font-size: 12px; color: #707070; display: inline-flex; align-items: center; gap: 8px; }
.controls select {
font-family: inherit; font-size: 13px; padding: 6px 10px;
border: 1px solid #c8c1b3; border-radius: 8px; background: #faf7f2; color: #1d1d1f;
}
.controls input[type=range] { width: 140px; }
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.filter-pill {
background: #fff; border: 1px solid #e6e1d8; border-radius: 999px;
padding: 7px 14px; font-size: 13px; cursor: pointer; color: #1d1d1f;
font-family: inherit; letter-spacing: -0.005em; transition: all 0.12s;
}
.filter-pill:hover { border-color: #c8c1b3; }
.filter-pill.is-active { background: #1d1d1f; color: #fff; border-color: #1d1d1f; }
.filter-pill .pill-count { color: #999; font-variant-numeric: tabular-nums; margin-left: 4px; font-size: 11px; }
.filter-pill.is-active .pill-count { color: #aaa; }
.brand-grid {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 220px), 1fr));
}
.brand-card {
background: #fff; border: 1px solid #e6e1d8; border-radius: 16px;
padding: 18px 22px; text-decoration: none; color: inherit;
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
transition: background 0.15s;
}
.brand-card:hover { background: #f9f5ed; border-color: #c8c1b3; }
.brand-card .name { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.brand-card .count { font-size: 12px; color: #707070; font-variant-numeric: tabular-nums; }
.brand-card .tier {
font-size: 11px; padding: 2px 8px; border-radius: 999px;
background: #f3eee2; color: #6e4f0d; margin-top: 4px; display: inline-block;
}
.brand-card .tier-5 { background: #d4f4d4; color: #1d5e1d; }
.brand-card .tier-1 { background: #fbe4e4; color: #7a1717; }
.empty { color: #999; font-size: 14px; padding: 24px 0; text-align: center; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
</style>
</head>
<body>
<div class="nav"><a href="/">← back to app</a></div>
<h1>Brands</h1>
<p class="sub" id="sub">Every brand in our catalog.</p>
<div class="controls" id="controls" hidden>
<label style="flex: 1; min-width: 180px;">
<span class="sr-only">Filter brands</span>
<input id="search" type="search" placeholder="Filter brands…" autocomplete="off" spellcheck="false"
style="font-family: inherit; font-size: 13px; padding: 6px 12px; border: 1px solid #c8c1b3; border-radius: 8px; background: #faf7f2; color: #1d1d1f; width: 100%;" />
</label>
<label>Sort
<select id="sort">
<option value="pieces">Most pieces</option>
<option value="az">A → Z</option>
<option value="sustain">Sustainability ↓</option>
</select>
</label>
<label>Density
<input id="density" type="range" min="160" max="320" step="20" value="220" aria-label="Card density" />
<span id="density-val" style="font-variant-numeric: tabular-nums; color: #999;">220px</span>
</label>
</div>
<div id="filters" class="filter-row" hidden></div>
<div id="grid" class="brand-grid"></div>
<script>
const $ = s => document.querySelector(s);
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
const FILTER_KEY = 'wms.brands.sustain_filter';
const SORT_KEY = 'wms.brands.sort';
const DENSITY_KEY= 'wms.brands.density';
let ALL = [];
let SEARCH = '';
function currentFilter() { return localStorage.getItem(FILTER_KEY) || 'all'; }
function currentSort() { return localStorage.getItem(SORT_KEY) || 'pieces'; }
function currentDensity() { return Number(localStorage.getItem(DENSITY_KEY) || 220); }
function setFilter(v) { localStorage.setItem(FILTER_KEY, v); render(); }
function setSort(v) { localStorage.setItem(SORT_KEY, v); render(); }
function setDensity(n) {
localStorage.setItem(DENSITY_KEY, String(n));
document.documentElement.style.setProperty('--card-min', `${n}px`);
$('#density-val').textContent = `${n}px`;
}
function matches(b, f) {
if (f === 'all') return true;
if (f === 'none') return !b.sustain_tier;
if (f === '5') return b.sustain_tier === 5;
if (f === '4') return b.sustain_tier === 4;
if (f === '3plus') return (b.sustain_tier || 0) >= 3;
return true;
}
function sortList(list, mode) {
const arr = list.slice();
if (mode === 'az') arr.sort((a, b) => a.brand.localeCompare(b.brand));
else if (mode === 'sustain') arr.sort((a, b) => (b.sustain_tier || 0) - (a.sustain_tier || 0) || b.item_count - a.item_count);
else arr.sort((a, b) => b.item_count - a.item_count); // 'pieces'
return arr;
}
function render() {
const f = currentFilter();
const s = currentSort();
const counts = {
all: ALL.length,
'5': ALL.filter(b => b.sustain_tier === 5).length,
'4': ALL.filter(b => b.sustain_tier === 4).length,
'3plus': ALL.filter(b => (b.sustain_tier || 0) >= 3).length,
none: ALL.filter(b => !b.sustain_tier).length,
};
const pills = [
['all', 'All', counts.all],
['5', '♻ 5', counts['5']],
['4', '♻ 4', counts['4']],
['3plus', '♻ 3+', counts['3plus']],
['none', 'No tier', counts.none],
];
const fr = $('#filters');
fr.hidden = false;
fr.innerHTML = pills.map(([v, label, n]) => `
<button type="button" class="filter-pill ${f === v ? 'is-active' : ''}" data-filter="${v}">
${label} <span class="pill-count">${n}</span>
</button>
`).join('');
$('#sort').value = s;
const q = SEARCH.trim().toLowerCase();
const searched = q ? ALL.filter(b => b.brand.toLowerCase().includes(q)) : ALL;
const filtered = sortList(searched.filter(b => matches(b, f)), s);
const sortLabel = { pieces: 'most pieces', az: 'A→Z', sustain: 'sustainability' }[s];
const note = q ? ` · matching “${q}”` : '';
$('#sub').textContent = `${filtered.length} of ${ALL.length} brand${ALL.length === 1 ? '' : 's'} · sorted by ${sortLabel}${note}.`;
if (!filtered.length) {
$('#grid').innerHTML = '<div class="empty">No brands match this filter.</div>';
return;
}
$('#grid').innerHTML = filtered.map(b => `
<a class="brand-card" href="/?brand=${encodeURIComponent(b.brand)}" onclick="event.preventDefault();const u=new URL(location.origin);u.searchParams.set('brand',this.dataset.brand||'${escapeHtml(b.brand)}');location.href=u.toString();" data-brand="${escapeHtml(b.brand)}">
<div>
<div class="name">${escapeHtml(b.brand)}</div>
${b.sustain_tier ? `<span class="tier tier-${b.sustain_tier}">♻ ${b.sustain_tier}/5</span>` : ''}
</div>
<div class="count">${b.item_count} pcs</div>
</a>
`).join('');
}
document.addEventListener('click', e => {
const pill = e.target.closest('.filter-pill');
if (!pill) return;
setFilter(pill.dataset.filter);
});
(async function () {
const r = await fetch('/api/brands');
const j = await r.json();
ALL = j.brands || [];
$('#controls').hidden = false;
// Hydrate sort + density BEFORE first render (standing rule M-6/M-7).
const d = currentDensity();
document.documentElement.style.setProperty('--card-min', `${d}px`);
$('#density').value = d;
$('#density-val').textContent = `${d}px`;
$('#sort').value = currentSort();
$('#sort').addEventListener('change', () => setSort($('#sort').value));
$('#density').addEventListener('input', () => setDensity(Number($('#density').value)));
$('#search').addEventListener('input', () => { SEARCH = $('#search').value; render(); });
render();
})();
</script>
</body>
</html>