[object Object]

← 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 →