← back to Interiordesignershowroom
admin: brand-level line selector — hide/show a whole brand across every advertiser
dcba584c958c3cf8f0edf3126210926a04502ad6 · 2026-08-03 09:10:11 -0700 · Steve Abrams
Files touched
Diff
commit dcba584c958c3cf8f0edf3126210926a04502ad6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 09:10:11 2026 -0700
admin: brand-level line selector — hide/show a whole brand across every advertiser
---
routes/admin.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 105 insertions(+), 1 deletion(-)
diff --git a/routes/admin.js b/routes/admin.js
index 5348807..41919b6 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -127,7 +127,7 @@ a.ezjoin:hover{background:#6f5c40}
.why{color:#6b6259;font-size:.82rem}
</style></head><body>
<header><h1>Interior Designer’s Showroom — Admin</h1>
-<nav><a href="/admin">Dashboard</a> · <a href="/admin/affiliates">Affiliates</a> · <a href="/">View site ↗</a></nav></header>
+<nav><a href="/admin">Dashboard</a> · <a href="/admin/brands">Brands</a> · <a href="/admin/affiliates">Affiliates</a> · <a href="/">View site ↗</a></nav></header>
<main>${body}</main></body></html>`;
router.get('/admin', async (req, res, next) => {
@@ -138,6 +138,8 @@ router.get('/admin', async (req, res, next) => {
(SELECT count(*) FROM products) AS products,
(SELECT count(*) FROM products WHERE featured) AS featured,
(SELECT count(*) FROM products WHERE suppressed) AS suppressed,
+ (SELECT count(DISTINCT brand) FROM products WHERE brand IS NOT NULL AND brand <> '') AS brands,
+ (SELECT count(DISTINCT brand) FROM products WHERE suppressed AND brand IS NOT NULL AND brand <> '') AS brands_hidden,
(SELECT count(*) FROM guides WHERE published) AS guides,
(SELECT count(*) FROM clicks) AS clicks,
(SELECT count(*) FROM clicks WHERE clicked_at > now() - interval '7 days') AS clicks7,
@@ -155,6 +157,7 @@ router.get('/admin', async (req, res, next) => {
<div class="stat"><b>${c.products}</b><span>Products</span></div>
<div class="stat"><b>${c.featured}</b><span>Featured</span></div>
<div class="stat"><b>${c.suppressed}</b><span>Suppressed${Number(c.suppressed) ? ' · hidden' : ''}</span></div>
+ <a class="stat" href="/admin/brands" style="text-decoration:none;color:inherit"><b>${c.brands}</b><span>Brands${Number(c.brands_hidden) ? ` · <span style="color:#b1483c">${c.brands_hidden} hidden</span>` : ' →'}</span></a>
<div class="stat"><b>${c.guides}</b><span>Published guides</span></div>
<div class="stat"><b>${c.clicks}</b><span>Total clicks</span></div>
<div class="stat"><b>${c.clicks7}</b><span>Clicks · 7 days</span></div>
@@ -218,6 +221,107 @@ router.post('/admin/guides/:id/published', async (req, res, next) => {
catch (e) { next(e); }
});
+// --- Brands: select a whole BRAND LINE and hide/show it across every advertiser --
+// The fourth join key. network/advertiser toggles (affiliate_settings) hide by SOURCE;
+// the per-product Hide flips ONE row. A brand, though, can be carried by several
+// advertisers at once — e.g. "Honiture" sold by the Honiture merchant AND by resellers
+// (AUKRO, BSC). Killing the merchant leaves the reseller-carried items live. This page
+// flips `products.suppressed` for EVERY row of a brand in one click, so a cross-advertiser
+// line is switched off (or back on) as a unit. Reversible; nothing is deleted.
+function brandBtn(brand, action, label, disabled) {
+ if (disabled) return `<button type="button" disabled style="opacity:.4;cursor:not-allowed">${label}</button>`;
+ return `<form method="post" action="/admin/brands/suppress" style="display:inline;margin:0 3px 0 0">
+ <input type="hidden" name="brand" value="${esc(brand)}">
+ <input type="hidden" name="action" value="${action}">
+ <button>${label}</button></form>`;
+}
+
+router.get('/admin/brands', async (req, res, next) => {
+ try {
+ const q = req.query.q ? String(req.query.q).slice(0, 60) : '';
+ const params = [];
+ let filter = `brand IS NOT NULL AND brand <> ''`;
+ if (q) { params.push('%' + q + '%'); filter += ` AND brand ILIKE $${params.length}`; }
+ const { rows } = await db.query(`SELECT brand,
+ count(*) AS products,
+ count(*) FILTER (WHERE in_stock) AS in_stock,
+ count(*) FILTER (WHERE suppressed) AS suppressed,
+ count(DISTINCT advertiser) AS advertisers,
+ string_agg(DISTINCT advertiser, ', ' ORDER BY advertiser) AS advertiser_list,
+ max(created_at) AS latest
+ FROM products WHERE ${filter}
+ GROUP BY brand
+ ORDER BY (count(*) FILTER (WHERE suppressed) > 0) DESC, count(*) DESC
+ LIMIT 300`, params);
+
+ const onOff = (on) => `<span class="pill ${on ? 'on' : 'off'}">${on ? '● ON' : '○ OFF'}</span>`;
+ const hiddenBrands = rows.filter((r) => Number(r.suppressed) > 0).length;
+
+ const brandRows = rows.map((r) => {
+ const total = Number(r.products), hid = Number(r.suppressed), advs = Number(r.advertisers);
+ const allHidden = hid === total, allVisible = hid === 0;
+ // status pill: fully visible / partially hidden / fully hidden
+ const status = allVisible ? onOff(true)
+ : allHidden ? onOff(false)
+ : `<span class="pill" style="background:#faf1e0;border-color:#e5cf9e;color:#9a6a1c">◐ ${hid}/${total} hidden</span>`;
+ // cross-advertiser badge — the Honiture case: one brand, many merchants
+ const spread = advs > 1
+ ? `<span class="pill" title="${esc(r.advertiser_list || '')}" style="background:#eef2f7;border-color:#c6d3e2;color:#3a5578;margin-left:6px">spans ${advs} advertisers</span>`
+ : '';
+ // action buttons: Hide all (disabled if already fully hidden), Unhide all (disabled if none hidden)
+ const actions = brandBtn(r.brand, 'hide', 'Hide brand', allHidden)
+ + brandBtn(r.brand, 'unhide', 'Unhide brand', allVisible);
+ return `<tr class="${allHidden ? 'row-off' : ''}">
+ <td><a href="/shop?q=${encodeURIComponent(r.brand)}" target="_blank" rel="noopener" style="color:var(--ink);font-weight:600;text-decoration:none">${esc(r.brand)}</a> ${spread}</td>
+ <td>${advs}${r.advertiser_list ? ` <span class="subtle" title="${esc(r.advertiser_list)}">›</span>` : ''}</td>
+ <td>${r.in_stock} <span class="subtle">/ ${total}</span></td>
+ <td>${hid ? `<b style="color:#b1483c">${hid}</b>` : '0'}</td>
+ <td>${when(r.latest)}</td>
+ <td>${status}</td>
+ <td style="white-space:nowrap">${actions}</td>
+ </tr>`;
+ }).join('');
+
+ const intro = `<p class="subtle" style="margin:0 0 10px">
+ Select a whole <b>brand line</b> and switch it off across <i>every</i> advertiser at once —
+ the reversible "check it off as no" at the brand level. Hiding a brand flips it out of the
+ storefront, room builder, and brand filters everywhere it appears; it stays in the database and
+ returns the moment you un-hide it. Use this for a brand carried by several merchants (a
+ <b>spans N advertisers</b> badge marks those) where a single advertiser switch wouldn't catch it all.
+ ${hiddenBrands ? `<b style="color:#b1483c"> ${hiddenBrands} brand${hiddenBrands === 1 ? '' : 's'} currently hidden (fully or partly).</b>` : ''}</p>`;
+
+ const searchForm = `<form method="get" action="/admin/brands" style="margin:0 0 14px">
+ <input name="q" value="${esc(q)}" placeholder="Search brands…" style="padding:6px 10px;border:1px solid var(--line);border-radius:5px;font-size:.85rem;min-width:240px">
+ <button style="padding:6px 12px">Search</button>
+ ${q ? ` <a href="/admin/brands" class="subtle" style="margin-left:6px">clear</a>` : ''}</form>`;
+
+ res.send(shell(`
+ <h1 style="font-size:1.15rem;margin:0 0 4px">Brands — hide or show a whole line</h1>
+ ${intro}
+ ${searchForm}
+ <table>
+ <tr><th>Brand</th><th>Advertisers</th><th>In-stock / total</th><th>Hidden</th><th>Newest item</th><th>Status</th><th>Action</th></tr>
+ ${brandRows || `<tr><td colspan="7" class="subtle">No brands${q ? ` matching “${esc(q)}”` : ''}.</td></tr>`}
+ </table>
+ ${rows.length >= 300 ? `<p class="subtle">Showing the first 300 brands — narrow with search.</p>` : ''}
+ `));
+ } catch (e) { next(e); }
+});
+
+// Flip `suppressed` for EVERY product of one brand. action=hide → TRUE, unhide → FALSE.
+// Guarded: the brand must actually exist in the catalog. Reversible, kept in DB.
+router.post('/admin/brands/suppress', async (req, res, next) => {
+ try {
+ const brand = String(req.body.brand || '').trim().slice(0, 200);
+ const hide = req.body.action !== 'unhide'; // default = hide
+ if (!brand) return res.status(400).send('Missing brand.');
+ const exists = await db.query('SELECT 1 FROM products WHERE brand=$1 LIMIT 1', [brand]);
+ if (!exists.rowCount) return res.status(404).send('Unknown brand.');
+ await db.query('UPDATE products SET suppressed=$2, updated_at=now() WHERE brand=$1', [brand, hide]);
+ res.redirect('/admin/brands');
+ } catch (e) { next(e); }
+});
+
// --- Affiliates: see every link source & switch it on/off -----------------
// An affiliate is either a whole NETWORK (cj/amazon/...) or a specific ADVERTISER
// (merchant) on that network. OFF here => that source's products vanish from the
← 99f7508 auto-save: 2026-08-03T08:52:19 (7 files) — db/schema.sql lib
·
back to Interiordesignershowroom
·
admin: keyword line-hider with preview — catch a line across 770915b →