[object Object]

← back to Commercialrealestate

sfv-pool: add Cards/Map view toggle — Leaflet map w/ clustered markers colored by pool, photo popups, respects filters

8d764718399653fb7590e713ec564b93104df6a8 · 2026-08-26 09:42:11 -0700 · Steve Abrams

Files touched

Diff

commit 8d764718399653fb7590e713ec564b93104df6a8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 26 09:42:11 2026 -0700

    sfv-pool: add Cards/Map view toggle — Leaflet map w/ clustered markers colored by pool, photo popups, respects filters
---
 public/sfv-pool.html | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 69 insertions(+), 2 deletions(-)

diff --git a/public/sfv-pool.html b/public/sfv-pool.html
index 688e2d7..3bf5d9e 100644
--- a/public/sfv-pool.html
+++ b/public/sfv-pool.html
@@ -70,6 +70,22 @@
   .x { background:none; border:0; color:#8b949e; font-size:22px; cursor:pointer; line-height:1; }
 </style>
   <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
+  <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
+  <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
+  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
+  <script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
+  <style>
+    #map { height:70vh; min-height:460px; border:1px solid #2a313c; border-radius:10px; display:none; }
+    body.mapview #grid, body.mapview #foot { display:none; }
+    body.mapview #map { display:block; }
+    .leaflet-popup-content-wrapper, .leaflet-popup-tip { background:#161b22; color:#e6edf3; }
+    .leaflet-popup-content { margin:10px 12px; font-size:13px; }
+    .mpop img { width:100%; height:120px; object-fit:cover; border-radius:6px; margin-bottom:6px; }
+    .mpop .p { font-size:16px; font-weight:700; }
+    .mpop a { color:#58a6ff; }
+    .viewseg button.on[data-view="map"] { background:#1f6feb; }
+  </style>
 </head>
 <body>
 <div class="wrap">
@@ -81,6 +97,12 @@
   <div class="stats" id="stats"></div>
 
   <div class="controls">
+    <div><label>View</label>
+      <span class="seg viewseg" id="viewSeg">
+        <button data-view="cards" class="on">Cards</button>
+        <button data-view="map">🗺️ Map</button>
+      </span>
+    </div>
     <div><label>Pool</label>
       <span class="seg" id="poolSeg">
         <button data-pool="all" class="on">All</button>
@@ -108,6 +130,7 @@
   </div>
 
   <div class="grid" id="grid"><div class="empty">Loading…</div></div>
+  <div id="map"></div>
   <div class="foot" id="foot"></div>
 </div>
 
@@ -118,7 +141,7 @@
 <script>
 const KEY = 'sfvpool';
 const st = { pool:'all', city:'', sort: localStorage.getItem(KEY+':sort')||'price_desc',
-             cols:+(localStorage.getItem(KEY+':cols')||3) };
+             cols:+(localStorage.getItem(KEY+':cols')||3), view: localStorage.getItem(KEY+':view')||'cards' };
 let DATA = { withPool:[], withoutPool:[], unknown:[] };
 const $ = s => document.querySelector(s);
 const money = n => n>=1000 ? '$'+(+n).toLocaleString() : '—';
@@ -165,6 +188,14 @@ function render() {
     </div>`;
   }).join('');
   $('#foot').textContent = `${rows.length} shown · pool status enriched from Redfin listing detail (structured POOL_FEATURES). "Pending" = detail not yet checked or field absent.`;
+  if (st.view === 'map' && typeof renderMap === 'function') renderMap();
+}
+// current filtered set (shared by cards + map)
+function currentRows() {
+  let rows = allRows();
+  if (st.pool !== 'all') rows = rows.filter(r => r._pool === st.pool);
+  if (st.city) rows = rows.filter(r => r.city === st.city);
+  return rows;
 }
 
 function paintStats() {
@@ -240,7 +271,43 @@ $('#grid').addEventListener('click', e => { const c = e.target.closest('.card');
 $('#backdrop').addEventListener('click', e => { if (e.target === $('#backdrop')) closeModal(); });
 document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
 
-load();
+// ---- map view (Leaflet + markercluster, matches CRCP's CARTO tiles) ----
+let _map=null, _cluster=null;
+function renderMap() {
+  if (!window.L) return;
+  if (!_map) {
+    _map = L.map('map', { scrollWheelZoom:true }).setView([34.19,-118.44], 11); // SFV center
+    L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+      { attribution:'© OpenStreetMap © CARTO', maxZoom:19 }).addTo(_map);
+    _cluster = L.markerClusterGroup({ maxClusterRadius:45 }); _map.addLayer(_cluster);
+  }
+  _cluster.clearLayers();
+  const color = t => t==='true' ? '#3fb950' : t==='false' ? '#58a6ff' : '#8b949e';
+  const pts = [];
+  currentRows().filter(r=>r.lat && r.lng).forEach(r => {
+    const m = L.circleMarker([r.lat, r.lng], { radius:7, weight:1, color:'#0e1116', fillColor:color(r._pool), fillOpacity:.92 });
+    const pp = r.ppsf ? '$'+(+r.ppsf).toLocaleString()+'/sqft' : '';
+    const lot = r.lot_size ? String(r.lot_size).replace(/ square feet/i,' sf') : '';
+    const img = r.photo_url ? `<img src="${r.photo_url}">` : '';
+    m.bindPopup(`<div class="mpop">${img}<div class="p">${money(r.price)} ${pp?'<span style="font-size:12px;color:#8b949e">· '+pp+'</span>':''}</div>`
+      + `<div>${r.address||''}, ${r.city}</div>`
+      + `<div style="color:#8b949e;font-size:12px">${r.beds||'?'}bd/${r.baths||'?'}ba · ${r.sqft?(+r.sqft).toLocaleString()+' sqft':''}${lot?' · lot '+lot:''}</div>`
+      + `<div style="margin:4px 0">${poolBadge(r._pool)}</div>`
+      + `<a href="${r.source||'#'}" target="_blank" rel="noopener noreferrer">View on Redfin →</a></div>`);
+    _cluster.addLayer(m); pts.push([r.lat, r.lng]);
+  });
+  if (pts.length) _map.fitBounds(pts, { padding:[30,30], maxZoom:14 });
+  setTimeout(()=>_map.invalidateSize(), 60); // tiles render correctly after the div un-hides
+}
+function setView(v) {
+  st.view = v; localStorage.setItem(KEY+':view', v);
+  document.body.classList.toggle('mapview', v==='map');
+  [...$('#viewSeg').children].forEach(b=>b.classList.toggle('on', b.dataset.view===v));
+  if (v==='map') renderMap();
+}
+$('#viewSeg').addEventListener('click', e => { if (e.target.tagName==='BUTTON') setView(e.target.dataset.view); });
+
+load().then(()=>{ if (st.view==='map') setView('map'); });
 </script>
   <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
 </body>

← 84c2a7c Apply Nav Agent to CRCP (9 pages incl. sfv-pool): field togg  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-26T09:38:32 (3 data files) — dat c87ea0c →