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