[object Object]

← back to Japan Enrich

viewer: live JPY→USD pricing onload, colorway sibling chips (other colors of each pattern), per-category All toggle to clear chip filters

4a7ec640349008b22433797a0d80a40014144c0c · 2026-06-30 12:11:46 -0700 · Steve

Files touched

Diff

commit 4a7ec640349008b22433797a0d80a40014144c0c
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 12:11:46 2026 -0700

    viewer: live JPY→USD pricing onload, colorway sibling chips (other colors of each pattern), per-category All toggle to clear chip filters
---
 viewer-local/public/index.html | 51 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 47 insertions(+), 4 deletions(-)

diff --git a/viewer-local/public/index.html b/viewer-local/public/index.html
index 8f47ca1..46e6629 100644
--- a/viewer-local/public/index.html
+++ b/viewer-local/public/index.html
@@ -36,6 +36,8 @@
   .chip.on { background:var(--accent); color:#1a1407; border-color:var(--accent); font-weight:600; }
   .chip .ct { opacity:.65; margin-left:4px; font-size:10px; }
   .chip.zero { opacity:.3; }
+  .chip.allchip { font-weight:600; border-style:dashed; }
+  .chip.allchip.on { border-style:solid; }
   .active { display:flex; flex-wrap:wrap; gap:6px; align-items:center; padding-top:2px; }
   .afilter { background:#23211a; border:1px solid var(--accent); color:var(--txt); border-radius:20px; padding:3px 9px; font-size:11px; cursor:pointer; }
   .afilter b { color:var(--accent); font-weight:600; }
@@ -59,6 +61,12 @@
   .fam { font-size:10px; color:var(--mut); }
   .sm { margin-top:4px; font-size:10.5px; color:var(--accent); }
   .un { font-size:9.5px; color:#6a6a72; border:1px dashed #3a3a40; border-radius:10px; padding:1px 6px; }
+  /* colorway siblings — the "other colors of this pattern", vendor-style chips */
+  .colorways { display:flex; align-items:center; gap:3px; margin-top:5px; flex-wrap:wrap; }
+  .colorways .cw { width:13px; height:13px; border-radius:3px; display:inline-block; box-shadow:0 0 0 1px rgba(255,255,255,.18) inset; background:#3a3a40; }
+  .colorways .cw.pend { background:repeating-linear-gradient(45deg,#33333a,#33333a 2px,#26262b 2px,#26262b 4px); }
+  .colorways .cwmore { font-size:9.5px; color:#8a8a92; }
+  .colorways .cwlbl { font-size:9.5px; color:#6a6a72; margin-left:3px; }
   .priceline { margin-top:4px; }
   .price { color:#e8e8ea; font-size:12.5px; font-weight:600; }
   .price i { color:#777; font-weight:400; font-style:normal; font-size:10px; margin-left:2px; }
@@ -131,6 +139,25 @@ const FAMILY_DOT = {
 const DEFAULT_FAMILY_ORDER = Object.keys(FAMILY_DOT);
 const loadSet = (k) => { try { return new Set(JSON.parse(localStorage.getItem(k) || '[]')); } catch { return new Set(); } };
 
+// --- live JPY→USD exchange rate, fetched fresh on load (no API key) ---
+let FX = { rate: null, asOf: null, src: null };
+async function loadFX() {
+  try {
+    const r = await fetch('https://open.er-api.com/v6/latest/JPY');
+    const d = await r.json();
+    if (d && d.result === 'success' && d.rates && d.rates.USD) {
+      FX = { rate: d.rates.USD, asOf: (d.time_last_update_utc || '').replace(/ \d\d:.*/, '') || 'today', src: 'live' };
+      return;
+    }
+  } catch (e) { /* fall through */ }
+  FX = { rate: 0.0064, asOf: 'approx', src: 'fallback' }; // safety net so prices still show in $
+}
+function usd(yen) {
+  if (!yen || !FX.rate) return null;
+  const v = yen * FX.rate;
+  return '$' + (v >= 100 ? Math.round(v).toLocaleString() : v.toFixed(2));
+}
+
 let state = {
   source: localStorage.getItem('dw_src') || 'all',
   sort: localStorage.getItem('dw_sort') || 'newest',
@@ -192,10 +219,23 @@ document.getElementById('enrSeg').onclick = (e) => {
 // --- facet rendering ---
 function toggleSet(set, val) { set.has(val) ? set.delete(val) : set.add(val); applyFilterChange(); }
 
+// "All" toggle for a chip category — highlighted when nothing is selected (showing all);
+// click it to switch that whole category OFF (clear its selections). Lets the user toggle
+// off any/all categories without hunting down each active chip.
+function allChip(set) {
+  const el = document.createElement('span');
+  el.className = 'chip allchip' + (set.size === 0 ? ' on' : '');
+  el.textContent = 'All';
+  el.title = set.size ? 'Clear this category — show all' : 'Showing all';
+  el.onclick = () => { if (set.size) { set.clear(); applyFilterChange(); } };
+  return el;
+}
+
 function renderFacets(f) {
   const order = (f.family_order && f.family_order.length) ? f.family_order : DEFAULT_FAMILY_ORDER;
   // color swatches
   const colorRow = document.getElementById('colorRow'); colorRow.innerHTML = '';
+  colorRow.appendChild(allChip(state.colors));
   order.forEach(fam => {
     const ct = (f.color_family && f.color_family[fam]) || 0;
     const on = state.colors.has(fam);
@@ -209,7 +249,10 @@ function renderFacets(f) {
   // style + material chips (sorted by count desc)
   const chipRow = (rowId, counts, set) => {
     const row = document.getElementById(rowId); row.innerHTML = '';
-    Object.entries(counts || {}).sort((a, b) => b[1] - a[1]).forEach(([val, ct]) => {
+    row.appendChild(allChip(set));
+    const entries = Object.entries(counts || {}).sort((a, b) => b[1] - a[1]);
+    if (!entries.length) { const s = document.createElement('span'); s.className = 'un'; s.textContent = 'none yet'; row.appendChild(s); return; }
+    entries.forEach(([val, ct]) => {
       const on = set.has(val);
       const el = document.createElement('span');
       el.className = 'chip' + (on ? ' on' : '') + (ct ? '' : ' zero');
@@ -217,7 +260,6 @@ function renderFacets(f) {
       el.onclick = () => toggleSet(set, val);
       row.appendChild(el);
     });
-    if (!Object.keys(counts || {}).length) row.innerHTML = '<span class="un">none yet</span>';
   };
   chipRow('styleRow', f.style, state.styles);
   chipRow('matRow', f.material, state.materials);
@@ -308,14 +350,15 @@ async function load() {
   state.offset += data.rows.length;
   state.done = state.offset >= data.total || data.rows.length === 0;
   state.loading = false;
-  subEl.textContent = `staged offline · ${data.counts.all.toLocaleString()} SKUs (Lily ${data.counts.lilycolor.toLocaleString()} / Sangetsu ${data.counts.sangetsu.toLocaleString()}) · showing ${state.offset.toLocaleString()}/${state.total.toLocaleString()}`;
+  const fxNote = FX.rate ? ` · ¥→$ ${FX.rate.toFixed(5)} ${FX.src === 'live' ? 'live' : 'approx'} (${FX.asOf})` : ' · ¥→$ …';
+  subEl.textContent = `staged offline · ${data.counts.all.toLocaleString()} SKUs (Lily ${data.counts.lilycolor.toLocaleString()} / Sangetsu ${data.counts.sangetsu.toLocaleString()}) · showing ${state.offset.toLocaleString()}/${state.total.toLocaleString()}${fxNote}`;
   statusEl.textContent = state.done ? `— end · ${state.total.toLocaleString()} matching SKUs —` : 'scroll for more…';
 }
 
 // infinite scroll
 new IntersectionObserver((es) => { if (es[0].isIntersecting) load(); }, { rootMargin: '600px' }).observe(document.getElementById('sentinel'));
 refreshFacets();
-load();
+loadFX().finally(load);   // pull the live JPY→USD rate first so the first cards render in $ (load runs regardless)
 </script>
 </body>
 </html>

← ef33cc0 auto-save: 2026-06-30T12:10:12 (6 files) — __pycache__/enric  ·  back to Japan Enrich  ·  chore: lint (load() try/finally guard), refactor (siblingChi 5e316b7 →