← 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>