← back to NationalPaperHangers
find: add sort + density controls per CLAUDE.md standing rule (newest claimed/A-Z/tier/verified/creds + 240-420px card-min slider, both localStorage-persisted)
72e3f64b958e475d6259a95c17ce40cfdffbaca4 · 2026-05-06 18:10:46 -0700 · Steve
Files touched
Diff
commit 72e3f64b958e475d6259a95c17ce40cfdffbaca4
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 18:10:46 2026 -0700
find: add sort + density controls per CLAUDE.md standing rule (newest claimed/A-Z/tier/verified/creds + 240-420px card-min slider, both localStorage-persisted)
---
views/public/find.ejs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 96 insertions(+), 4 deletions(-)
diff --git a/views/public/find.ejs b/views/public/find.ejs
index f117d21..c2aff96 100644
--- a/views/public/find.ejs
+++ b/views/public/find.ejs
@@ -48,7 +48,24 @@
<%= installers.length %> <%= show === 'all' ? '' : 'verified ' %>installer<%= installers.length === 1 ? '' : 's' %>
<% if (show !== 'all') { %><span class="muted" style="font-size:13px;margin-left:8px">· directory listings hidden</span><% } %>
</p>
- <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+ <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
+ <!-- Sort + density per CLAUDE.md global rule: every directory grid has both -->
+ <label style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted)">
+ Sort
+ <select id="sort-select" style="padding:6px 10px;border:1px solid var(--border);background:var(--bg);font-size:13px;border-radius:4px">
+ <option value="default">Default</option>
+ <option value="newest_claimed">Newest claimed</option>
+ <option value="az">A → Z</option>
+ <option value="za">Z → A</option>
+ <option value="tier">Tier (Signature first)</option>
+ <option value="verified_first">Verified first</option>
+ <option value="creds">Most brand-trained creds</option>
+ </select>
+ </label>
+ <label style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted)">
+ Density
+ <input id="density-slider" type="range" min="240" max="420" step="20" value="300" style="width:90px" aria-label="Card density">
+ </label>
<a href="<%= mapHref %>" class="btn btn-ghost btn-sm" style="font-size:13px" aria-label="View these results on the map">View on map</a>
<a href="<%= toggleHref %>" class="btn btn-ghost btn-sm" style="font-size:13px">
<%= show === 'all' ? 'Hide directory listings' : 'Show directory listings' %>
@@ -56,9 +73,22 @@
</div>
</div>
- <div class="installer-grid">
- <% installers.forEach(function(i){ %>
- <a class="installer-card" href="/installer/<%= i.slug %>">
+ <div class="installer-grid" id="installerGrid" style="grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 300px), 1fr))">
+ <% installers.forEach(function(i){
+ // Tier rank for client-side sort: signature/enterprise > pro > basic > unclaimed/null
+ var _tierRank = (i.tier === 'signature' || i.tier === 'enterprise') ? 0
+ : (i.tier === 'pro') ? 1
+ : (i.tier === 'basic') ? 2 : 9;
+ var _verifiedRank = (i.claim_status === 'unclaimed') ? 9 : (i.verified ? 0 : 1);
+ var _claimedAt = i.claimed_at ? new Date(i.claimed_at).getTime() : 0;
+ var _credsCount = i.verified_brands_count || 0;
+ %>
+ <a class="installer-card" href="/installer/<%= i.slug %>"
+ data-name="<%= (i.business_name || '').toLowerCase() %>"
+ data-tier-rank="<%= _tierRank %>"
+ data-verified-rank="<%= _verifiedRank %>"
+ data-claimed-at="<%= _claimedAt %>"
+ data-creds="<%= _credsCount %>">
<% var _pdImg = pickSegmentImage(i); %>
<div class="card-image">
<% if (_pdImg) { %>
@@ -105,3 +135,65 @@
<%- include('../partials/footer') %>
<script src="/js/find-filter.js"></script>
+<script>
+ // Sort + density per CLAUDE.md standing rule. Both persist via localStorage.
+ (function(){
+ var grid = document.getElementById('installerGrid');
+ if (!grid) return;
+ var sortSel = document.getElementById('sort-select');
+ var densSlider = document.getElementById('density-slider');
+
+ // Restore saved values
+ try {
+ var savedSort = localStorage.getItem('nph-find-sort');
+ if (savedSort && sortSel) sortSel.value = savedSort;
+ var savedDens = localStorage.getItem('nph-find-density');
+ if (savedDens && densSlider) { densSlider.value = savedDens; grid.style.setProperty('--card-min', savedDens + 'px'); }
+ } catch (e) {}
+
+ function num(el, attr) { return parseInt(el.getAttribute(attr) || '0', 10) || 0; }
+
+ function applySort() {
+ var mode = sortSel.value;
+ var cards = Array.from(grid.querySelectorAll('.installer-card'));
+ if (mode === 'default') {
+ // Server's natural order — re-sort by original DOM index, which we
+ // store the first time we touch them.
+ cards.forEach(function(c, i){ if (!c.dataset.origIdx) c.dataset.origIdx = i; });
+ cards.sort(function(a,b){ return num(a,'data-orig-idx') - num(b,'data-orig-idx'); });
+ } else if (mode === 'az') {
+ cards.sort(function(a,b){ return (a.dataset.name||'').localeCompare(b.dataset.name||''); });
+ } else if (mode === 'za') {
+ cards.sort(function(a,b){ return (b.dataset.name||'').localeCompare(a.dataset.name||''); });
+ } else if (mode === 'tier') {
+ cards.sort(function(a,b){ return num(a,'data-tier-rank') - num(b,'data-tier-rank'); });
+ } else if (mode === 'verified_first') {
+ cards.sort(function(a,b){ return num(a,'data-verified-rank') - num(b,'data-verified-rank'); });
+ } else if (mode === 'newest_claimed') {
+ cards.sort(function(a,b){ return num(b,'data-claimed-at') - num(a,'data-claimed-at'); });
+ } else if (mode === 'creds') {
+ cards.sort(function(a,b){ return num(b,'data-creds') - num(a,'data-creds'); });
+ }
+ // Stamp original index before reordering
+ cards.forEach(function(c){ if (!c.dataset.origIdx) c.dataset.origIdx = Array.from(grid.children).indexOf(c); });
+ cards.forEach(function(c){ grid.appendChild(c); });
+ }
+
+ if (sortSel) {
+ sortSel.addEventListener('change', function(){
+ try { localStorage.setItem('nph-find-sort', sortSel.value); } catch (e) {}
+ applySort();
+ });
+ // Stamp original DOM index on first paint, BEFORE first sort
+ Array.from(grid.children).forEach(function(c, i){ c.dataset.origIdx = i; });
+ if (sortSel.value !== 'default') applySort();
+ }
+
+ if (densSlider) {
+ densSlider.addEventListener('input', function(){
+ grid.style.setProperty('--card-min', densSlider.value + 'px');
+ try { localStorage.setItem('nph-find-density', densSlider.value); } catch (e) {}
+ });
+ }
+ })();
+</script>
← 7c77afb tests: e2e buyer wizard — 5 steps, conditional brand reveal,
·
back to NationalPaperHangers
·
tests: e2e for /find sort + density controls — 7/7, locks in 0420926 →