← 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
M viewer-local/public/index.html
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 →