← back to Stars of Design
starsofdesign: sort dropdown on /firms + /clients (per standing sort+density rule)
10f3ae0c7d9cc86258fc428bde4166db9dd8dbb3 · 2026-05-12 15:34:57 -0700 · Steve Abrams
Files touched
M lib/clients.jsM lib/firms.jsM routes/public.jsM views/public/clients.ejsM views/public/firms.ejs
Diff
commit 10f3ae0c7d9cc86258fc428bde4166db9dd8dbb3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 15:34:57 2026 -0700
starsofdesign: sort dropdown on /firms + /clients (per standing sort+density rule)
---
lib/clients.js | 16 +++++++++++++---
lib/firms.js | 23 ++++++++++++++++++-----
routes/public.js | 10 +++++++---
views/public/clients.ejs | 11 +++++++++++
views/public/firms.ejs | 12 ++++++++++++
5 files changed, 61 insertions(+), 11 deletions(-)
diff --git a/lib/clients.js b/lib/clients.js
index 31c8ed0..e7b4c7e 100644
--- a/lib/clients.js
+++ b/lib/clients.js
@@ -20,7 +20,17 @@ const pool = new Pool({
max: 4,
});
-async function listClients({ limit = 200, q = null, area = null } = {}) {
+// Per Steve's standing rule: every product/listing grid needs sort + density.
+const SORT_KEYS = {
+ default: `(d.premium_tier IS NOT NULL) DESC, d.full_name`,
+ name_az: `d.full_name`,
+ name_za: `d.full_name DESC`,
+ city: `COALESCE(f.city, d.city) NULLS LAST, d.full_name`,
+ firm: `f.name NULLS LAST, d.full_name`,
+ claimed: `(d.claimed_at IS NOT NULL) DESC, d.full_name`,
+};
+
+async function listClients({ limit = 200, q = null, area = null, sort = 'default' } = {}) {
const params = [];
let where = `(d.role IS NULL OR d.role <> 'Manufacturer Rep')`;
if (q) {
@@ -58,7 +68,7 @@ async function listClients({ limit = 200, q = null, area = null } = {}) {
LEFT JOIN sod_designer_firm df ON df.designer_id = d.id AND df.is_current = true
LEFT JOIN sod_firms f ON f.id = df.firm_id
WHERE ${where}
- ORDER BY (d.premium_tier IS NOT NULL) DESC, d.full_name
+ ORDER BY ${SORT_KEYS[sort] || SORT_KEYS.default}
LIMIT $${params.length}`;
const r = await pool.query(sql, params);
return r.rows.map(row => {
@@ -136,4 +146,4 @@ async function totalCount() {
return parseInt(r.rows[0].n, 10);
}
-module.exports = { listClients, getClient, totalCount };
+module.exports = { listClients, getClient, totalCount, SORT_KEYS };
diff --git a/lib/firms.js b/lib/firms.js
index 990d79b..63a76bb 100644
--- a/lib/firms.js
+++ b/lib/firms.js
@@ -13,7 +13,22 @@ const pool = new Pool({
max: 4,
});
-async function listFirms({ q = null, limit = 300 } = {}) {
+// Per Steve's standing rule: every product/listing grid needs sort + density.
+// SORT_KEYS controls the firm-tier sort dropdown — claimed-first is default
+// because verified profiles deserve top-of-feed.
+const SORT_KEYS = {
+ claimed: `(f.claimed_at IS NOT NULL) DESC, f.size_band DESC NULLS LAST, f.name`,
+ name_az: `f.name`,
+ name_za: `f.name DESC`,
+ founded_new:`f.founded_year DESC NULLS LAST, f.name`,
+ founded_old:`f.founded_year ASC NULLS LAST, f.name`,
+ city: `f.city NULLS LAST, f.name`,
+ size: `f.size_band DESC NULLS LAST, f.name`,
+};
+
+async function listFirms({ q = null, sort = 'claimed', limit = 300 } = {}) {
+ const sortKey = SORT_KEYS[sort] ? sort : 'claimed';
+ const orderBy = SORT_KEYS[sortKey];
const params = [];
let where = `1=1`;
if (q) {
@@ -33,9 +48,7 @@ async function listFirms({ q = null, limit = 300 } = {}) {
) AS links
FROM sod_firms f
WHERE ${where}
- ORDER BY (f.claimed_at IS NOT NULL) DESC,
- f.size_band DESC NULLS LAST,
- f.name
+ ORDER BY ${orderBy}
LIMIT $${params.length}`;
const r = await pool.query(sql, params);
return r.rows.map(row => {
@@ -104,4 +117,4 @@ async function totalCount() {
return parseInt(r.rows[0].n, 10);
}
-module.exports = { listFirms, getFirm, totalCount };
+module.exports = { listFirms, getFirm, totalCount, SORT_KEYS };
diff --git a/routes/public.js b/routes/public.js
index 2e24ad8..86ea5a7 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -80,8 +80,9 @@ router.get('/clients', async (req, res, next) => {
try {
const q = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q : null;
const area = typeof req.query.area === 'string' && req.query.area.length < 80 ? req.query.area : null;
+ const sort = clientsLib.SORT_KEYS[req.query.sort] ? req.query.sort : 'default';
const [clients, total] = await Promise.all([
- clientsLib.listClients({ q, area, limit: 300 }),
+ clientsLib.listClients({ q, area, sort, limit: 300 }),
clientsLib.totalCount(),
]);
res.render('public/clients', {
@@ -89,6 +90,7 @@ router.get('/clients', async (req, res, next) => {
meta_desc_override: 'The active list of interior designers and architecture firms who buy wallcoverings from Designer Wallcoverings. Pulled from order history, trade signups, and sample requests. City + state + areas served per profile (no street addresses).',
clients,
total,
+ sortKey: sort,
filter: { q, area },
});
} catch (e) { next(e); }
@@ -115,9 +117,10 @@ router.get('/clients/:slug', async (req, res, next) => {
// auto-materialized from sig-extract. Keyed on sod_firms not sod_designers.
router.get('/firms', async (req, res, next) => {
try {
- const q = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q : null;
+ const q = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q : null;
+ const sort = firmsLib.SORT_KEYS[req.query.sort] ? req.query.sort : 'claimed';
const [firms, total] = await Promise.all([
- firmsLib.listFirms({ q, limit: 400 }),
+ firmsLib.listFirms({ q, sort, limit: 400 }),
firmsLib.totalCount(),
]);
res.render('public/firms', {
@@ -125,6 +128,7 @@ router.get('/firms', async (req, res, next) => {
meta_desc_override: 'Top US architecture and interior design firms — Gensler, HOK, Perkins&Will, Studio Sofield, Kelly Wearstler, Studio McGee, and more. Public-data seed; firms may claim and edit their profile.',
firms,
total,
+ sortKey: sort,
filter: { q },
});
} catch (e) { next(e); }
diff --git a/views/public/clients.ejs b/views/public/clients.ejs
index 1cfed11..f1b4091 100644
--- a/views/public/clients.ejs
+++ b/views/public/clients.ejs
@@ -26,6 +26,17 @@
<button type="submit" class="btn ghost" style="margin-left:8px">Search</button>
</div>
<div class="grid-controls-right">
+ <label class="control">
+ <span>Sort</span>
+ <select id="sort-select" name="sort" data-sort onchange="this.form.submit()">
+ <option value="default" <%= sortKey==='default' ? 'selected' : '' %>>Premium first</option>
+ <option value="name_az" <%= sortKey==='name_az' ? 'selected' : '' %>>Name A→Z</option>
+ <option value="name_za" <%= sortKey==='name_za' ? 'selected' : '' %>>Name Z→A</option>
+ <option value="firm" <%= sortKey==='firm' ? 'selected' : '' %>>By firm</option>
+ <option value="city" <%= sortKey==='city' ? 'selected' : '' %>>By city</option>
+ <option value="claimed" <%= sortKey==='claimed' ? 'selected' : '' %>>Claimed first</option>
+ </select>
+ </label>
<label class="control">
<span>Density</span>
<input id="density-slider" type="range" min="220" max="440" step="20" value="320" data-density>
diff --git a/views/public/firms.ejs b/views/public/firms.ejs
index 9a032c7..44aa7c6 100644
--- a/views/public/firms.ejs
+++ b/views/public/firms.ejs
@@ -27,6 +27,18 @@
<button type="submit" class="btn ghost" style="margin-left:8px">Search</button>
</div>
<div class="grid-controls-right">
+ <label class="control">
+ <span>Sort</span>
+ <select id="sort-select" name="sort" data-sort onchange="this.form.submit()">
+ <option value="claimed" <%= sortKey==='claimed' ? 'selected' : '' %>>Claimed first</option>
+ <option value="name_az" <%= sortKey==='name_az' ? 'selected' : '' %>>Name A→Z</option>
+ <option value="name_za" <%= sortKey==='name_za' ? 'selected' : '' %>>Name Z→A</option>
+ <option value="founded_new" <%= sortKey==='founded_new' ? 'selected' : '' %>>Founded — newest</option>
+ <option value="founded_old" <%= sortKey==='founded_old' ? 'selected' : '' %>>Founded — oldest</option>
+ <option value="city" <%= sortKey==='city' ? 'selected' : '' %>>By city</option>
+ <option value="size" <%= sortKey==='size' ? 'selected' : '' %>>By size</option>
+ </select>
+ </label>
<label class="control">
<span>Density</span>
<input id="density-slider" type="range" min="220" max="440" step="20" value="320" data-density>
← f55b3a7 feat(starsofdesign): sitemap.xml now includes /firms/:slug ×
·
back to Stars of Design
·
starsofdesign: +40 firms (20 arch, 20 interior) — Selldorf, cca9c68 →