← 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 →