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