[object Object]

← back to NEW SKU Viewer

viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer

b7a59b5c797c047834e41490a673ac5341c384b7 · 2026-08-12 08:53:17 -0700 · Steve

Files touched

Diff

commit b7a59b5c797c047834e41490a673ac5341c384b7
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 08:53:17 2026 -0700

    viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer
---
 public/index.html | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 101 insertions(+), 5 deletions(-)

diff --git a/public/index.html b/public/index.html
index 8ac7379..6a16c8c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -356,19 +356,49 @@
                 flex-direction: column;
                 text-align: center;
             }
-            
+
             .title h1 {
                 font-size: 2rem;
             }
-            
+
             .vendor-grid {
                 grid-template-columns: 1fr;
             }
-            
+
             .stats {
                 justify-content: center;
             }
         }
+
+        /* ── Grid / List toggle ── */
+        .vtoggle { display: flex; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; overflow: hidden; }
+        .vtoggle button { background: rgba(255,255,255,0.1); color: white; border: none; padding: 8px 16px; font-size: 13px; cursor: pointer; font-weight: 500; }
+        .vtoggle button.on { background: rgba(255,255,255,0.9); color: #667eea; font-weight: 700; }
+
+        /* ── List view ── */
+        .vendor-grid.list { display: block; }
+        .vendor-grid.list .vendor-card { display: flex; flex-direction: row; align-items: center; gap: 18px; margin-bottom: 10px; border-left-width: 4px; border-radius: 8px; padding: 12px 18px; }
+        .vendor-grid.list .vendor-card .vendor-header { flex: 0 0 220px; margin-bottom: 0; }
+        .vendor-grid.list .vendor-card .product-count { font-size: 1.4rem; margin: 0; }
+        .vendor-grid.list .vendor-card .vendor-url { flex: 1; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+        .vendor-grid.list .vendor-card .vendor-actions { margin-top: 0; flex: none; }
+
+        /* ── Fields menu ── */
+        .fieldmenu { position: relative; }
+        .fbtn { padding: 8px 16px; border: 1px solid rgba(255,255,255,0.3); background: rgba(255,255,255,0.1); color: white; font-size: 12px; cursor: pointer; border-radius: 8px; }
+        .fbtn:hover { border-color: rgba(255,255,255,0.7); }
+        .fpop { position: absolute; top: calc(100% + 8px); right: 0; background: white; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 12px 30px rgba(0,0,0,0.2); padding: 8px 4px; z-index: 30; min-width: 190px; }
+        .fpop[hidden] { display: none; }
+        .fpop label { display: flex; align-items: center; gap: 10px; padding: 7px 14px; font-size: 12px; color: #4a5568; cursor: pointer; white-space: nowrap; }
+        .fpop label:hover { background: #f7fafc; }
+        .fpop input { accent-color: #667eea; }
+
+        /* ── Field-hide rules (grid mode only) ── */
+        .vendor-grid:not(.list).hf-count .vendor-card .product-count { display: none; }
+        .vendor-grid:not(.list).hf-url   .vendor-card .vendor-url    { display: none; }
+        .vendor-grid:not(.list).hf-scraper .vendor-card .nsk-scraper  { display: none; }
+        .vendor-grid:not(.list).hf-error  .vendor-card .error-message { display: none; }
+        .vendor-grid:not(.list).hf-actions .vendor-card .vendor-actions { display: none; }
     </style>
 </head>
 <body>
@@ -433,10 +463,25 @@
                     <option value="sku">SKU A→Z</option>
                     <option value="title">Title A→Z</option>
                     <option value="status">Status (working first)</option>
+                    <option value="errors">Errors 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 class="vtoggle" style="margin-left:8px;">
+                    <button id="vGrid" class="on" onclick="setNSKView('grid')">▦ Grid</button>
+                    <button id="vList" onclick="setNSKView('list')">☰ List</button>
+                </div>
+                <div class="fieldmenu" style="margin-left:8px;">
+                    <button class="fbtn" onclick="toggleNSKFieldsMenu(event)">Fields ▾</button>
+                    <div class="fpop" id="fpop" hidden>
+                        <label><input type="checkbox" data-nf="count" checked> Product count</label>
+                        <label><input type="checkbox" data-nf="url" checked> Vendor URL</label>
+                        <label><input type="checkbox" data-nf="scraper" checked> Scraper used</label>
+                        <label><input type="checkbox" data-nf="error" checked> Error message</label>
+                        <label><input type="checkbox" data-nf="actions" checked> Action buttons</label>
+                    </div>
+                </div>
             </div>
 
             <div id="content">
@@ -587,8 +632,8 @@
                         ${vendor.url ? vendor.url.substring(0, 50) + '...' : 'No URL'}
                     </div>
                     
-                    ${vendor.scraperUsed ? 
-                        `<div style="font-size: 0.8rem; color: #666; margin-top: 5px;">
+                    ${vendor.scraperUsed ?
+                        `<div class="nsk-scraper" style="font-size: 0.8rem; color: #666; margin-top: 5px;">
                             <i class="fas fa-robot"></i> ${vendor.scraperUsed}
                         </div>` : ''}
                     
@@ -875,9 +920,60 @@
             updateElement.textContent = `Last updated: ${timeStr}`;
         }
 
+        // ── Grid / List view toggle ──
+        const NSK_LS_VIEW = 'newSkuViewer.grid.view';
+        const NSK_LS_FIELDS = 'newSkuViewer.grid.fields';
+        const NSK_FIELD_DEFAULTS = { count: true, url: true, scraper: true, error: true, actions: true };
+
+        function setNSKView(v) {
+            const g = document.getElementById('content');
+            const vg = document.querySelector('.vendor-grid');
+            if (vg) vg.classList.toggle('list', v === 'list');
+            document.getElementById('vGrid').classList.toggle('on', v !== 'list');
+            document.getElementById('vList').classList.toggle('on', v === 'list');
+            try { localStorage.setItem(NSK_LS_VIEW, v); } catch(e) {}
+        }
+
+        function getNSKFields() {
+            try { return Object.assign({}, NSK_FIELD_DEFAULTS, JSON.parse(localStorage.getItem(NSK_LS_FIELDS) || '{}')); }
+            catch(e) { return Object.assign({}, NSK_FIELD_DEFAULTS); }
+        }
+        function applyNSKFields(prefs) {
+            const vg = document.querySelector('.vendor-grid');
+            if (!vg) return;
+            for (const [k, on] of Object.entries(prefs)) vg.classList.toggle('hf-' + k, !on);
+            document.querySelectorAll('#fpop [data-nf]').forEach(cb => { cb.checked = !!prefs[cb.dataset.nf]; });
+        }
+        function toggleNSKFieldsMenu(e) {
+            e.stopPropagation();
+            const p = document.getElementById('fpop');
+            p.hidden = !p.hidden;
+        }
+        document.addEventListener('click', () => { const p = document.getElementById('fpop'); if (p) p.hidden = true; });
+        document.querySelectorAll('#fpop [data-nf]').forEach(cb => {
+            cb.addEventListener('change', () => {
+                const prefs = getNSKFields();
+                prefs[cb.dataset.nf] = cb.checked;
+                try { localStorage.setItem(NSK_LS_FIELDS, JSON.stringify(prefs)); } catch(e) {}
+                applyNSKFields(prefs);
+            });
+        });
+
+        // Re-apply view + fields after every render (renderVendors replaces innerHTML)
+        const _origRenderVendors = renderVendors;
+        function renderVendors(vendors) {
+            _origRenderVendors(vendors);
+            const savedView = (() => { try { return localStorage.getItem(NSK_LS_VIEW) || 'grid'; } catch(e) { return 'grid'; } })();
+            setNSKView(savedView);
+            applyNSKFields(getNSKFields());
+        }
+
         // Initialize the application — hydrate sort/density from localStorage
         // BEFORE the first API call so the initial fetch uses the saved sort.
         hydrateGridControls();
+        // Restore view toggle state in the button bar
+        (function() { try { const v = localStorage.getItem(NSK_LS_VIEW)||'grid'; document.getElementById('vGrid').classList.toggle('on',v!=='list'); document.getElementById('vList').classList.toggle('on',v==='list'); } catch(e){} })();
+        applyNSKFields(getNSKFields());
         loadVendorData();
 
         // Refresh data every 30 seconds

← d99c7a1 fix(server): gate auto-YOLO-mode behind YOLO_MODE_AUTOSTART  ·  back to NEW SKU Viewer  ·  (newest)