[object Object]

← back to Commercialrealestate

SFV pool page: click any card -> detail modal with photo, sqft, $/sqft, lot, pool, beds/baths, year, DOM

49751ea163750254e4a82ab9188c5a0bfb6fb4a9 · 2026-08-26 08:48:37 -0700 · Steve Abrams

Files touched

Diff

commit 49751ea163750254e4a82ab9188c5a0bfb6fb4a9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 26 08:48:37 2026 -0700

    SFV pool page: click any card -> detail modal with photo, sqft, $/sqft, lot, pool, beds/baths, year, DOM
---
 public/sfv-pool.html | 73 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 70 insertions(+), 3 deletions(-)

diff --git a/public/sfv-pool.html b/public/sfv-pool.html
index 273e000..445be6a 100644
--- a/public/sfv-pool.html
+++ b/public/sfv-pool.html
@@ -46,6 +46,28 @@
   .feat { font-size:11px; color:#7a828c; }
   .empty { color:#8b949e; padding:30px; text-align:center; }
   .foot { color:#6e7681; font-size:11px; margin-top:20px; border-top:1px solid #2a313c; padding-top:10px; }
+  .card { cursor:pointer; transition:border-color .12s; }
+  .card:hover { border-color:#3d444d; }
+  /* detail modal */
+  .backdrop { position:fixed; inset:0; background:rgba(1,4,9,.72); display:none; align-items:flex-start; justify-content:center; padding:40px 16px; overflow:auto; z-index:50; }
+  .backdrop.open { display:flex; }
+  .modal { background:#161b22; border:1px solid #2a313c; border-radius:14px; max-width:720px; width:100%; overflow:hidden; }
+  .modal .hero { width:100%; height:340px; object-fit:cover; display:block; background:#20262f; }
+  .modal .body { padding:18px 20px 22px; }
+  .modal .top { display:flex; justify-content:space-between; align-items:flex-start; gap:12px; }
+  .modal h2 { font-size:20px; margin:0 0 2px; }
+  .modal .csub { color:#8b949e; font-size:13px; }
+  .modal .mprice { font-size:26px; font-weight:700; text-align:right; white-space:nowrap; }
+  .modal .mprice small { display:block; font-size:12px; color:#8b949e; font-weight:400; }
+  .kv { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; margin:18px 0 6px; }
+  .kv .cell { background:#0e1116; border:1px solid #232a33; border-radius:8px; padding:9px 11px; }
+  .kv .k { font-size:10px; text-transform:uppercase; letter-spacing:.5px; color:#8b949e; }
+  .kv .v { font-size:15px; font-weight:600; margin-top:2px; }
+  .modal .actions { display:flex; gap:10px; margin-top:16px; }
+  .btn { display:inline-block; text-decoration:none; font-size:13px; font-weight:600; padding:9px 16px; border-radius:8px; border:0; cursor:pointer; }
+  .btn.primary { background:#238636; color:#fff; }
+  .btn.ghost { background:#21262d; color:#c9d1d9; border:1px solid #2a313c; }
+  .x { background:none; border:0; color:#8b949e; font-size:22px; cursor:pointer; line-height:1; }
 </style>
 </head>
 <body>
@@ -87,6 +109,10 @@
   <div class="foot" id="foot"></div>
 </div>
 
+<div class="backdrop" id="backdrop">
+  <div class="modal" id="modal" role="dialog" aria-modal="true"></div>
+</div>
+
 <script>
 const KEY = 'sfvpool';
 const st = { pool:'all', city:'', sort: localStorage.getItem(KEY+':sort')||'price_desc',
@@ -124,11 +150,11 @@ function render() {
     const lot = r.lot_size ? String(r.lot_size).replace(/ square feet/i,' sf').replace(/ Sq\.? ?Ft\.?/i,' sf') : '';
     const feat = r._pool==='true' && r.pool_features ? `<div class="feat">${r.pool_features}</div>` : '';
     const photo = r.photo_url
-      ? `<a href="${r.source||'#'}" target="_blank" rel="noopener noreferrer"><img class="photo" src="${r.photo_url}" alt="" loading="lazy"></a>`
+      ? `<img class="photo" src="${r.photo_url}" alt="" loading="lazy">`
       : `<div class="photo ph"></div>`;
-    return `<div class="card">
+    return `<div class="card" data-id="${r.id}">
       ${photo}
-      <div class="addr"><a href="${r.source||'#'}" target="_blank" rel="noopener noreferrer">${r.address||'—'}</a></div>
+      <div class="addr">${r.address||'—'}</div>
       <div class="price">${money(r.price)} <span style="font-size:12px;color:#8b949e;font-weight:400">${pp?'· '+pp:''}</span></div>
       <div class="meta">${r.city} · ${r.beds||'?'} bd / ${r.baths||'?'} ba · ${r.sqft?(+r.sqft).toLocaleString()+' sqft':'—'}${lot?' · lot '+lot:''} · ${r.days_on_market!=null?r.days_on_market+'d':''}</div>
       <div>${poolBadge(r._pool)}</div>${feat}
@@ -147,10 +173,12 @@ function paintStats() {
      <div class="stat pending"><div class="n">${c.u.toLocaleString()}</div><div class="l">Pool Pending</div></div>`;
 }
 
+const byId = new Map();
 async function load() {
   const r = await fetch('/api/sfv-pool');
   const j = await r.json();
   DATA = { withPool:j.withPool||[], withoutPool:j.withoutPool||[], unknown:j.unknown||[] };
+  allRows().forEach(x => byId.set(x.id, x));
   $('#src').textContent = 'source: ' + (j.source||'?');
   // populate city dropdown
   const cities = [...new Set(allRows().map(r=>r.city))].sort();
@@ -169,6 +197,45 @@ $('#city').addEventListener('change', e => { st.city = e.target.value; render();
 $('#sort').addEventListener('change', e => { st.sort = e.target.value; localStorage.setItem(KEY+':sort', st.sort); render(); });
 $('#density').addEventListener('input', e => { st.cols = +e.target.value; localStorage.setItem(KEY+':cols', st.cols); render(); });
 $('#sort').value = st.sort; $('#density').value = st.cols;
+
+// ---- detail modal: click any card to see all info on-page ----
+function cell(k, v) { return v==null||v===''||v==='—' ? '' : `<div class="cell"><div class="k">${k}</div><div class="v">${v}</div></div>`; }
+function openModal(id) {
+  const r = byId.get(id); if (!r) return;
+  const pp = r.ppsf ? '$'+(+r.ppsf).toLocaleString() : (r.sqft>0 ? '$'+Math.round(r.price/r.sqft).toLocaleString() : '—');
+  const lot = r.lot_size ? String(r.lot_size).replace(/ square feet/i,' sq ft').replace(/ Sq\.? ?Ft\.?/i,' sq ft') : '—';
+  const poolTxt = r._pool==='true' ? ('🏊 Yes' + (r.pool_features?' — '+r.pool_features:'')) : r._pool==='false' ? 'No' : 'Pending';
+  const hero = r.photo_url ? `<img class="hero" src="${r.photo_url}" alt="">` : `<div class="hero"></div>`;
+  $('#modal').innerHTML = `${hero}
+    <div class="body">
+      <div class="top">
+        <div><h2>${r.address||'—'}</h2><div class="csub">${r.city}${r.zip?', '+r.zip:''}</div></div>
+        <div><div class="mprice">${money(r.price)}<small>${pp}/sqft</small></div></div>
+        <button class="x" aria-label="close" onclick="closeModal()">×</button>
+      </div>
+      <div class="kv">
+        ${cell('Beds', r.beds)}
+        ${cell('Baths', r.baths)}
+        ${cell('Interior', r.sqft ? (+r.sqft).toLocaleString()+' sqft' : '—')}
+        ${cell('Price / sqft', pp+'/sqft')}
+        ${cell('Lot size', lot)}
+        ${cell('Year built', r.year_built)}
+        ${cell('Days on market', r.days_on_market!=null ? r.days_on_market+'d' : '—')}
+        ${cell('Pool', poolTxt)}
+      </div>
+      <div class="actions">
+        <a class="btn primary" href="${r.source||'#'}" target="_blank" rel="noopener noreferrer">View on Redfin →</a>
+        <button class="btn ghost" onclick="closeModal()">Close</button>
+      </div>
+    </div>`;
+  $('#backdrop').classList.add('open');
+}
+function closeModal(){ $('#backdrop').classList.remove('open'); }
+window.closeModal = closeModal;
+$('#grid').addEventListener('click', e => { const c = e.target.closest('.card'); if (c) openModal(c.dataset.id); });
+$('#backdrop').addEventListener('click', e => { if (e.target === $('#backdrop')) closeModal(); });
+document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
+
 load();
 </script>
 </body>

← c6eda17 SFV pool list: add lot size, price/sqft, photo to enrichment  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-26T09:01:00 (1 data files) — dat 5ed3148 →