← back to Interiordesignershowroom
Add faceted filtering (room/style/color/price) to /shop + /rooms
d55b94ca2c466e43c9dcd659146c71f095151c2e · 2026-08-01 11:42:41 -0700 · Steve Abrams
- lib/catalog.js: URL-addressable filters, self-excluding facet counts, active chips
- every facet is a real drillable /shop?... link (data-points-href-to-deeper-data rule)
- verified locally: room/color filters narrow the grid, chips clear individually
Files touched
M public/css/site.cssM server.js
Diff
commit d55b94ca2c466e43c9dcd659146c71f095151c2e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 11:42:41 2026 -0700
Add faceted filtering (room/style/color/price) to /shop + /rooms
- lib/catalog.js: URL-addressable filters, self-excluding facet counts, active chips
- every facet is a real drillable /shop?... link (data-points-href-to-deeper-data rule)
- verified locally: room/color filters narrow the grid, chips clear individually
---
public/css/site.css | 14 ++++++++++++++
server.js | 34 +++++++++++++++++++++++++---------
2 files changed, 39 insertions(+), 9 deletions(-)
diff --git a/public/css/site.css b/public/css/site.css
index 6cbf99f..81083ca 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -72,4 +72,18 @@ h2{font-size:1.4rem;margin:0 0 16px;padding-bottom:8px;border-bottom:1px solid v
.site-footer a{text-decoration:underline}
.site-footer .fine{font-size:.75rem;margin-top:10px;max-width:720px;margin-left:auto;margin-right:auto}
+/* Faceted catalog */
+.catalog{display:grid;grid-template-columns:210px 1fr;gap:26px;align-items:start}
+.facets{position:sticky;top:14px;display:flex;flex-direction:column;gap:16px}
+.facet-group h4{margin:0 0 6px;font-family:var(--sans);font-size:.72rem;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
+.facet{display:flex;justify-content:space-between;align-items:center;padding:3px 8px;border-radius:4px;font-size:.86rem;text-transform:capitalize;color:#3a352e}
+.facet:hover{background:#efe8dd}
+.facet.active{background:var(--ink);color:#fff}
+.facet .fn{font-size:.72rem;color:var(--muted);margin-left:8px}
+.facet.active .fn{color:#cfc4b2}
+.active-filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
+.chip{background:#efe8dd;border:1px solid var(--line);border-radius:20px;padding:3px 11px;font-size:.8rem;text-transform:capitalize}
+.chip:hover{border-color:var(--accent);color:var(--accent)}
+.chip.clear{background:none;font-weight:600}
+@media(max-width:820px){.catalog{grid-template-columns:1fr}.facets{position:static;flex-flow:row wrap}.facet-group{min-width:140px}}
@media(max-width:640px){.site-nav{gap:14px}.brand-word{display:none}.hero h1{font-size:2rem}}
diff --git a/server.js b/server.js
index 0fd8162..94398e3 100644
--- a/server.js
+++ b/server.js
@@ -83,12 +83,28 @@ app.get('/', async (_req, res, next) => {
} catch (e) { next(e); }
});
-app.get('/shop', async (_req, res, next) => {
+// Shared faceted catalog renderer for /shop and /rooms. Filters are URL-addressable
+// so every facet is a real drillable link (Steve's "data points href to deeper data").
+async function renderCatalog(res, { f, basePath, title, description, canonical, activeNav, heading }) {
+ const [products, counts] = await Promise.all([catalog.fetchProducts(f), catalog.facetCounts(f)]);
+ const body = `<section>
+ <h1>${esc(heading)}</h1>
+ <p class="subtle">${products.length} piece${products.length === 1 ? '' : 's'}</p>
+ ${catalog.activeChips(f, basePath)}
+ <div class="catalog">
+ ${catalog.facetRail(f, counts, basePath)}
+ <div class="catalog-main">${gridControls()}${grid(products)}</div>
+ </div>
+ </section><script src="/js/grid.js"></script>`;
+ res.send(layout({ title, description, canonical, body, activeNav }));
+}
+
+app.get('/shop', async (req, res, next) => {
try {
- const { rows } = await db.query(`SELECT * FROM products WHERE in_stock ORDER BY created_at DESC LIMIT 200`);
- const body = `<section><h1>The Showroom</h1><p class="subtle">${rows.length} curated pieces across every room.</p>
- ${gridControls()}${grid(rows)}</section><script src="/js/grid.js"></script>`;
- res.send(layout({ title: 'Shop the Showroom', description: 'Browse every curated piece — sort by color, style, or price.', canonical: `${SITE.url}/shop`, body, activeNav: '/shop' }));
+ const f = catalog.parseFilters(req.query);
+ await renderCatalog(res, { f, basePath: '/shop', heading: 'The Showroom',
+ title: 'Shop the Showroom', description: 'Filter curated designer pieces by room, style, color, or price.',
+ canonical: `${SITE.url}/shop`, activeNav: '/shop' });
} catch (e) { next(e); }
});
@@ -96,10 +112,10 @@ app.get('/rooms/:room', async (req, res, next) => {
try {
const room = req.params.room;
const label = (ROOMS.find(([s]) => s === room) || [null, room])[1];
- const { rows } = await db.query(`SELECT * FROM products WHERE room=$1 AND in_stock ORDER BY created_at DESC LIMIT 200`, [room]);
- const body = `<section><h1>${esc(label)}</h1><p class="subtle">${rows.length} pieces for the ${esc(label.toLowerCase())}.</p>
- ${gridControls()}${grid(rows)}</section><script src="/js/grid.js"></script>`;
- res.send(layout({ title: `${label} Furniture & Decor`, description: `Curated ${label.toLowerCase()} pieces from top design brands.`, canonical: `${SITE.url}/rooms/${room}`, body, activeNav: `/rooms/${room}` }));
+ const f = { ...catalog.parseFilters(req.query), room };
+ await renderCatalog(res, { f, basePath: '/shop', heading: label,
+ title: `${label} Furniture & Decor`, description: `Curated ${label.toLowerCase()} pieces from top design brands.`,
+ canonical: `${SITE.url}/rooms/${room}`, activeNav: `/rooms/${room}` });
} catch (e) { next(e); }
});
← 86a89da auto-save: 2026-08-01T11:36:26 (2 files) — server.js lib/cat
·
back to Interiordesignershowroom
·
Update GOLIVE.md: Kamatera deploy complete; only NS swap + o 4dad839 →