← back to NEW SKU Viewer
public: wire sort <select> + density slider with localStorage persistence
098e8b67f5a4ed6953c6dbb4c5aa75650a834204 · 2026-05-19 21:20:29 -0700 · Steve Abrams
Standing rule — every grid page (admin tools included) must have a sort
select + density range slider, both persisted to localStorage, both
hydrated BEFORE the first API call.
- Sort options: Newest (default) / Most Products / Vendor (SKU prefix)
A→Z / Series A→Z / SKU A→Z / Title A→Z / Status (working first).
- Density slider: 220-520px card-min, driving --card-min CSS var on
.vendor-grid's auto-fill minmax.
- hydrateGridControls() runs before loadVendorData(), and the first
fetch is /api/vendors?sort=<saved> so the server returns the
pre-sorted list.
- Removed the hardcoded client-side .sort by products in renderVendors
so the server-side sort actually wins.
Keys: newSkuViewer.grid.sort, newSkuViewer.grid.density.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 098e8b67f5a4ed6953c6dbb4c5aa75650a834204
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 19 21:20:29 2026 -0700
public: wire sort <select> + density slider with localStorage persistence
Standing rule — every grid page (admin tools included) must have a sort
select + density range slider, both persisted to localStorage, both
hydrated BEFORE the first API call.
- Sort options: Newest (default) / Most Products / Vendor (SKU prefix)
A→Z / Series A→Z / SKU A→Z / Title A→Z / Status (working first).
- Density slider: 220-520px card-min, driving --card-min CSS var on
.vendor-grid's auto-fill minmax.
- hydrateGridControls() runs before loadVendorData(), and the first
fetch is /api/vendors?sort=<saved> so the server returns the
pre-sorted list.
- Removed the hardcoded client-side .sort by products in renderVendors
so the server-side sort actually wins.
Keys: newSkuViewer.grid.sort, newSkuViewer.grid.density.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
public/index.html | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 91 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index 6fac2a8..8ac7379 100644
--- a/public/index.html
+++ b/public/index.html
@@ -137,11 +137,42 @@
.vendor-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 350px), 1fr));
gap: 20px;
margin-top: 20px;
}
+ .grid-controls {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+ margin-bottom: 16px;
+ padding: 10px 14px;
+ background: rgba(255,255,255,0.85);
+ backdrop-filter: blur(10px);
+ border-radius: 10px;
+ }
+
+ .grid-controls label {
+ font-size: 0.85rem;
+ color: #4a5568;
+ font-weight: 500;
+ }
+
+ .grid-controls select {
+ padding: 6px 10px;
+ border-radius: 6px;
+ border: 1px solid #cbd5e0;
+ background: #fff;
+ font-size: 0.9rem;
+ color: #2d3748;
+ }
+
+ .grid-controls input[type="range"] {
+ width: 160px;
+ }
+
.vendor-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
@@ -392,6 +423,22 @@
<div class="filter-btn" onclick="setFilter('errors')">Errors</div>
</div>
+ <div class="grid-controls">
+ <label for="sortSelect">Sort:</label>
+ <select id="sortSelect" onchange="onSortChange()">
+ <option value="newest">Newest</option>
+ <option value="products">Most Products</option>
+ <option value="vendor">Vendor (SKU prefix) A→Z</option>
+ <option value="series">Series / Name A→Z</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="title">Title A→Z</option>
+ <option value="status">Status (working first)</option>
+ </select>
+ <label for="densityRange" style="margin-left:12px;">Density:</label>
+ <input type="range" id="densityRange" min="220" max="520" step="10" value="350" oninput="onDensityChange()">
+ <span id="densityVal" style="font-size:0.8rem;color:#666;min-width:48px;">350px</span>
+ </div>
+
<div id="content">
<div class="loading">
<div class="spinner"></div>
@@ -407,9 +454,46 @@
let currentFilter = 'all';
let searchQuery = '';
+ // --- sort + density persistence (rule: hydrate BEFORE first API call) ---
+ const LS_SORT = 'newSkuViewer.grid.sort';
+ const LS_DENSITY = 'newSkuViewer.grid.density';
+
+ function getSavedSort() {
+ try { return localStorage.getItem(LS_SORT) || 'newest'; } catch (e) { return 'newest'; }
+ }
+ function getSavedDensity() {
+ try { return parseInt(localStorage.getItem(LS_DENSITY) || '350', 10) || 350; } catch (e) { return 350; }
+ }
+
+ function hydrateGridControls() {
+ const savedSort = getSavedSort();
+ const savedDensity = getSavedDensity();
+ const sortEl = document.getElementById('sortSelect');
+ const rangeEl = document.getElementById('densityRange');
+ const valEl = document.getElementById('densityVal');
+ if (sortEl) sortEl.value = savedSort;
+ if (rangeEl) rangeEl.value = String(savedDensity);
+ if (valEl) valEl.textContent = savedDensity + 'px';
+ document.documentElement.style.setProperty('--card-min', savedDensity + 'px');
+ }
+
+ function onSortChange() {
+ const v = document.getElementById('sortSelect').value;
+ try { localStorage.setItem(LS_SORT, v); } catch (e) {}
+ loadVendorData();
+ }
+
+ function onDensityChange() {
+ const v = parseInt(document.getElementById('densityRange').value, 10) || 350;
+ try { localStorage.setItem(LS_DENSITY, String(v)); } catch (e) {}
+ document.getElementById('densityVal').textContent = v + 'px';
+ document.documentElement.style.setProperty('--card-min', v + 'px');
+ }
+
async function loadVendorData() {
try {
- const response = await fetch('/api/vendors');
+ const sort = encodeURIComponent(getSavedSort());
+ const response = await fetch('/api/vendors?sort=' + sort);
const data = await response.json();
vendorData = data;
@@ -453,8 +537,8 @@
return;
}
+ // Server already sorted via ?sort= — preserve that order.
const vendorCards = Object.entries(filteredVendors)
- .sort(([,a], [,b]) => (b.products || 0) - (a.products || 0))
.map(([vendorId, vendor]) => createVendorCard(vendorId, vendor))
.join('');
@@ -791,9 +875,11 @@
updateElement.textContent = `Last updated: ${timeStr}`;
}
- // Initialize the application
+ // Initialize the application — hydrate sort/density from localStorage
+ // BEFORE the first API call so the initial fetch uses the saved sort.
+ hydrateGridControls();
loadVendorData();
-
+
// Refresh data every 30 seconds
setInterval(loadVendorData, 30000);
</script>
← b85fbce server: 404-guard snapshot paths + server-side sort on /api/
·
back to NEW SKU Viewer
·
fix(server): gate auto-YOLO-mode behind YOLO_MODE_AUTOSTART d99c7a1 →