[object Object]

← 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

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 →