← back to NationalPaperHangers
v0.4 #5: load theme-toggle.js + /find↔/map filter parity + tablet/phone breakpoints
9bcbde81b8cf09053eb39a80ac33a23620e68c72 · 2026-05-06 13:21:40 -0700 · SteveStudio2
- public/css/public.css: 1024/769 tablet (3-col filter, 2-col how-grid) + 768 phone breakpoints (full single-column collapse, smaller padding, .display-sm shrink, map-controls grid)
- views/partials/head.ejs: <script src=/js/theme-toggle.js defer> — the file existed but was never loaded (regression)
- views/public/find.ejs: 'View on map' button in result-bar
- views/public/map.ejs: 'View as list' button + read q/segment/material from URL on load + sync list-link href on filter change
- routes/public.js: build mapHref preserving q/segment/material and pass to find.ejs
26/26 tests pass.
Files touched
M public/css/public.cssM routes/public.jsM views/partials/head.ejsM views/public/find.ejsM views/public/map.ejs
Diff
commit 9bcbde81b8cf09053eb39a80ac33a23620e68c72
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 6 13:21:40 2026 -0700
v0.4 #5: load theme-toggle.js + /find↔/map filter parity + tablet/phone breakpoints
- public/css/public.css: 1024/769 tablet (3-col filter, 2-col how-grid) + 768 phone breakpoints (full single-column collapse, smaller padding, .display-sm shrink, map-controls grid)
- views/partials/head.ejs: <script src=/js/theme-toggle.js defer> — the file existed but was never loaded (regression)
- views/public/find.ejs: 'View on map' button in result-bar
- views/public/map.ejs: 'View as list' button + read q/segment/material from URL on load + sync list-link href on filter change
- routes/public.js: build mapHref preserving q/segment/material and pass to find.ejs
26/26 tests pass.
---
public/css/public.css | 27 +++++++++++++++++++++++++--
routes/public.js | 10 +++++++++-
views/partials/head.ejs | 1 +
views/public/find.ejs | 9 ++++++---
views/public/map.ejs | 25 +++++++++++++++++++++++++
5 files changed, 66 insertions(+), 6 deletions(-)
diff --git a/public/css/public.css b/public/css/public.css
index 6327daa..31f79e0 100644
--- a/public/css/public.css
+++ b/public/css/public.css
@@ -156,10 +156,33 @@
}
.claim-next-steps li strong { display: block; margin-bottom: 4px; font-size: 15px; font-weight: 500; }
-@media (max-width: 900px) {
- .footer-inner, .trust-inner, .how-grid, .profile-body, .filter-row, .book-layout { grid-template-columns: 1fr; gap: 24px; }
+/* Tablet — keeps two-up grids but tightens gaps + collapses 4-up to 2-up */
+@media (max-width: 1024px) and (min-width: 769px) {
+ .trust-inner { grid-template-columns: repeat(2, 1fr); gap: 24px; }
+ .how-grid { grid-template-columns: repeat(2, 1fr); gap: 24px; }
+ .filter-row { grid-template-columns: repeat(3, 1fr); gap: 12px; }
+ .filter-row > button[type="submit"] { grid-column: 1 / -1; }
+ .profile-body { grid-template-columns: 3fr 2fr; gap: 32px; }
+ .row3 { grid-template-columns: repeat(2, 1fr); }
+ .map-controls { flex-wrap: wrap; gap: 8px; }
+ .map-controls > * { flex: 1 1 200px; }
+}
+
+/* Phone — full single-column collapse */
+@media (max-width: 768px) {
+ .footer-inner, .trust-inner, .how-grid, .profile-body, .filter-row, .book-layout { grid-template-columns: 1fr; gap: 16px; }
.footer-fineprint { flex-direction: column; }
.row3 { grid-template-columns: 1fr; }
+ .map-head { flex-direction: column; align-items: stretch; gap: 12px; }
+ .map-controls { display: grid; grid-template-columns: 1fr; gap: 8px; }
+ .map-controls > * { width: 100%; box-sizing: border-box; }
+ .reserve-banner-inner { flex-direction: column; align-items: flex-start; }
+ .reserve-banner .reserve-cta { width: 100%; }
+ .primary-nav { gap: 12px; flex-wrap: wrap; }
+ .header-actions { gap: 8px; }
+ .find-page, .featured, .how-it-works, .for-installers-cta, .installer-profile, .long-form, .booking-confirmation { padding: 32px 16px; }
+ .display-sm { font-size: 28px; line-height: 1.15; }
+ .installer-grid { grid-template-columns: 1fr; gap: 16px; }
}
/* PD imagery for cards (tick 4) */
diff --git a/routes/public.js b/routes/public.js
index 8cf1935..8ee34f0 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -117,11 +117,19 @@ router.get('/find', async (req, res, next) => {
if (show !== 'all') _filterParams.set('show', 'all');
const toggleHref = '/find' + (_filterParams.toString() ? '?' + _filterParams.toString() : '');
+ // Cross-link to /map preserving the shared filter params (q/segment/material).
+ // ZIP/state aren't yet in the map UI; they get dropped silently.
+ const _mapParams = new URLSearchParams();
+ if (q) _mapParams.set('q', q);
+ if (segment) _mapParams.set('segment', segment);
+ if (material) _mapParams.set('material', material);
+ const mapHref = '/map' + (_mapParams.toString() ? '?' + _mapParams.toString() : '');
+
res.render('public/find', {
title, metaDescription,
canonicalPath: '/find',
installers,
- q, segment, material, zip, state, show, toggleHref
+ q, segment, material, zip, state, show, toggleHref, mapHref
});
} catch (err) { next(err); }
});
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
index 00c5b38..4736ee6 100644
--- a/views/partials/head.ejs
+++ b/views/partials/head.ejs
@@ -49,5 +49,6 @@
} catch (e) {}
})();
</script>
+ <script src="/js/theme-toggle.js" defer></script>
</head>
<body class="<%= typeof bodyClass !== 'undefined' ? bodyClass : '' %>">
diff --git a/views/public/find.ejs b/views/public/find.ejs
index 9a97556..f117d21 100644
--- a/views/public/find.ejs
+++ b/views/public/find.ejs
@@ -48,9 +48,12 @@
<%= 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>
- <a href="<%= toggleHref %>" class="btn btn-ghost btn-sm" style="font-size:13px">
- <%= show === 'all' ? 'Hide directory listings' : 'Show directory listings' %>
- </a>
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+ <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' %>
+ </a>
+ </div>
</div>
<div class="installer-grid">
diff --git a/views/public/map.ejs b/views/public/map.ejs
index 2a9c12c..168fb06 100644
--- a/views/public/map.ejs
+++ b/views/public/map.ejs
@@ -85,6 +85,7 @@
<option value="vinyl">Vinyl</option>
<option value="metallic_leaf">Metallic leaf</option>
</select>
+ <a id="map-list-link" href="/find" class="btn btn-ghost btn-sm" style="font-size:13px;align-self:center;text-decoration:none" aria-label="View these results as a list">View as list</a>
</div>
</div>
<div id="map" role="application" aria-label="Map of installer studios"></div>
@@ -202,12 +203,36 @@
var bounds = L.latLngBounds(allMarkers.map(function(r){ return r.marker.getLatLng(); }));
map.fitBounds(bounds, { padding: [40,40], maxZoom: 7 });
}
+ // Pre-fill controls from URL query string so /find→/map hand-off keeps state.
+ var urlParams = new URLSearchParams(window.location.search);
+ var qParam = urlParams.get('q'); if (qParam) document.getElementById('map-search').value = qParam;
+ var segParam = urlParams.get('segment'); if (segParam) document.getElementById('map-segment').value = segParam;
+ var matParam = urlParams.get('material'); if (matParam) document.getElementById('map-material').value = matParam;
+
+ function syncListLink(){
+ var p = new URLSearchParams();
+ var q = document.getElementById('map-search').value.trim();
+ var s = document.getElementById('map-segment').value;
+ var m = document.getElementById('map-material').value;
+ if (q) p.set('q', q);
+ if (s) p.set('segment', s);
+ if (m) p.set('material', m);
+ var link = document.getElementById('map-list-link');
+ if (link) link.href = '/find' + (p.toString() ? '?' + p.toString() : '');
+ }
+
['map-search','map-segment','map-material'].forEach(function(id){
var el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', applyFilters);
+ el.addEventListener('input', syncListLink);
el.addEventListener('change', applyFilters);
+ el.addEventListener('change', syncListLink);
});
+
+ // Apply pre-filled filters + initial link sync.
+ if (qParam || segParam || matParam) applyFilters();
+ syncListLink();
})
.catch(function(e){ console.error('[map] load failed', e); });
})();
← 939d55b watch: linkedin embed support + industry company-page strip
·
back to NationalPaperHangers
·
v0.4 #5 follow-up: hide brand-name + tighten header actions de5a034 →