← back to Dw Hero Admin
fleet: honor _shared/data/dw-tossed-domains.json — exclude tossed from sidebar
a2f7a8c8d97632aea6424fad457a57a670df27bc · 2026-05-14 09:06:46 -0700 · Steve
Files touched
M public/index.htmlM server.js
Diff
commit a2f7a8c8d97632aea6424fad457a57a670df27bc
Author: Steve <steve@designerwallcoverings.com>
Date: Thu May 14 09:06:46 2026 -0700
fleet: honor _shared/data/dw-tossed-domains.json — exclude tossed from sidebar
---
public/index.html | 59 ++++++++++++++++++++++++++++++++++++++++++++++++-------
server.js | 9 +++++++++
2 files changed, 61 insertions(+), 7 deletions(-)
diff --git a/public/index.html b/public/index.html
index 2ae840f..abb8be8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -59,10 +59,14 @@
.toolbar button.primary:hover { background:#c0b090; }
/* ─ products grid ─ */
.products-wrap { flex:1; overflow-y:auto; padding:18px 24px; }
- .products-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
+ .products-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; gap:12px; flex-wrap:wrap; }
.products-header h3 { margin:0; font-size:12px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted); font-weight:500; }
- .product-search input { padding:6px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; outline:none; border-radius:3px; width:260px; font-size:12px; }
- .products { display:grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap:10px; }
+ .products-controls { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
+ .products-controls label { display:inline-flex; align-items:center; gap:6px; font-size:11px; letter-spacing:0.1em; text-transform:uppercase; color:var(--muted); }
+ .products-controls select { padding:5px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; font-size:12px; outline:none; border-radius:3px; }
+ .products-controls input[type=range] { width:120px; accent-color:var(--accent); }
+ .product-search input { padding:6px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; outline:none; border-radius:3px; width:220px; font-size:12px; }
+ .products { display:grid; grid-template-columns: repeat(auto-fill, minmax(var(--card-w, 140px), 1fr)); gap:10px; }
.product { position:relative; aspect-ratio:1/1.15; background:var(--slot-bg); border:1px solid var(--line); overflow:hidden; cursor:grab; transition:border-color .14s; }
.product:active { cursor:grabbing; }
.product:hover { border-color:var(--accent); }
@@ -130,7 +134,16 @@
<script>
(function () {
'use strict';
- var state = { site: null, products: [], grid: null, filter: '' };
+ var LS = 'dw-hero-admin.';
+ var state = {
+ site: null,
+ products: [],
+ grid: null,
+ filter: '',
+ sort: localStorage.getItem(LS + 'sort') || 'newest',
+ cardW: parseInt(localStorage.getItem(LS + 'cardW') || '140', 10),
+ };
+ document.documentElement.style.setProperty('--card-w', state.cardW + 'px');
var els = {
siteList: document.getElementById('siteList'),
siteSearch: document.getElementById('siteSearch'),
@@ -257,8 +270,21 @@
function renderProductsHeader() {
els.productsWrap.innerHTML = '\
<div class="products-header">\
- <h3 id="prodCount">All products — newest first</h3>\
- <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(state.filter || '') + '"></div>\
+ <h3 id="prodCount">All products</h3>\
+ <div class="products-controls">\
+ <label>Sort\
+ <select id="prodSort">\
+ <option value="newest">Newest first</option>\
+ <option value="title">Title A→Z</option>\
+ <option value="sku">SKU A→Z</option>\
+ <option value="vendor">Vendor A→Z</option>\
+ </select>\
+ </label>\
+ <label>Size\
+ <input id="prodDensity" type="range" min="90" max="320" step="10" value="' + state.cardW + '">\
+ </label>\
+ <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(state.filter || '') + '"></div>\
+ </div>\
</div>\
<div class="products" id="prodGrid"></div>\
';
@@ -267,6 +293,19 @@
state.filter = pf.value;
renderProductsGrid();
});
+ var ps = document.getElementById('prodSort');
+ ps.value = state.sort;
+ ps.addEventListener('change', function () {
+ state.sort = ps.value;
+ localStorage.setItem(LS + 'sort', state.sort);
+ renderProductsGrid();
+ });
+ var pd = document.getElementById('prodDensity');
+ pd.addEventListener('input', function () {
+ state.cardW = parseInt(pd.value, 10);
+ localStorage.setItem(LS + 'cardW', state.cardW);
+ document.documentElement.style.setProperty('--card-w', state.cardW + 'px');
+ });
}
// Only re-renders the grid + count — never touches the header DOM, so
@@ -277,8 +316,14 @@
if (!q) return true;
return ((p.title || '') + ' ' + (p.sku || '') + ' ' + (p.vendor || '') + ' ' + (p.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
});
+ var cmp = function (a, b, key) { return (a[key] || '').localeCompare(b[key] || ''); };
+ if (state.sort === 'title') items = items.slice().sort(function (a, b) { return cmp(a, b, 'title'); });
+ else if (state.sort === 'sku') items = items.slice().sort(function (a, b) { return cmp(a, b, 'sku'); });
+ else if (state.sort === 'vendor') items = items.slice().sort(function (a, b) { return cmp(a, b, 'vendor') || cmp(a, b, 'title'); });
+ // newest = leave api ordering (server already reversed products.json)
+ var label = ({ newest:'newest first', title:'title A→Z', sku:'SKU A→Z', vendor:'vendor A→Z' })[state.sort] || 'newest first';
var count = document.getElementById('prodCount');
- if (count) count.textContent = 'All products — newest first (' + items.length + ')';
+ if (count) count.textContent = 'All products — ' + label + ' (' + items.length + ')';
var grid = document.getElementById('prodGrid');
if (!grid) return;
grid.innerHTML = items.slice(0, 400).map(function (p) {
diff --git a/server.js b/server.js
index f942154..3dd4a41 100644
--- a/server.js
+++ b/server.js
@@ -22,11 +22,20 @@ const REMOTE_HOST = process.env.REMOTE_HOST || 'root@45.61.58.125';
// Canonical 53-site list — same source as /tmp/dw-sites.txt (auto-discovers
// every project under ~/Projects/ whose public/index.html has nsContactOpen
// or .ns-modal markers — i.e. the DW family).
+function loadTossedDomains() {
+ try {
+ const buf = fs.readFileSync(path.join(PROJECTS_DIR, '_shared', 'data', 'dw-tossed-domains.json'), 'utf8');
+ return new Set((JSON.parse(buf).tossed || []).map(t => t.domain.replace(/\.com$/, '')));
+ } catch (e) { return new Set(); }
+}
+
function discoverSites() {
+ const tossed = loadTossedDomains();
const sites = [];
const entries = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true });
for (const e of entries) {
if (!e.isDirectory()) continue;
+ if (tossed.has(e.name)) continue; // honor toss list
const idx = path.join(PROJECTS_DIR, e.name, 'public', 'index.html');
if (!fs.existsSync(idx)) continue;
try {
← 6224e99 fix(mobile): products grid pushed off-screen below the 2x2 h
·
back to Dw Hero Admin
·
feat: per-site fashion-house style guide picker with color d 74b3917 →