← back to Nationalrealestate
CSV export on rankings (/api/markets.csv, type-synced download button) — analyst data pull
58c461c754547c5f9a95e866b576549a2c0b6ad1 · 2026-07-21 15:55:38 -0700 · Steve Abrams
Files touched
M public/markets.htmlM src/server/index.ts
Diff
commit 58c461c754547c5f9a95e866b576549a2c0b6ad1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 21 15:55:38 2026 -0700
CSV export on rankings (/api/markets.csv, type-synced download button) — analyst data pull
---
public/markets.html | 4 ++++
src/server/index.ts | 30 ++++++++++++++++++++++++++++++
2 files changed, 34 insertions(+)
diff --git a/public/markets.html b/public/markets.html
index 9fcb9ae..70892a1 100644
--- a/public/markets.html
+++ b/public/markets.html
@@ -47,6 +47,9 @@
<button data-type="metro">Metros</button>
<button data-type="state">States</button>
</span>
+ <a id="exportcsv" href="/api/markets.csv?type=county" download
+ style="font-size:12px;color:var(--muted);border:1px solid var(--line);border-radius:8px;
+ padding:5px 12px;text-decoration:none;white-space:nowrap;">⭳ CSV</a>
</div>
<div class="cg-shell">
<div class="cg-rail" id="cgRail"></div>
@@ -101,6 +104,7 @@ function visibleRows(){ return showThinEl.checked ? allRows : allRows.filter(r =
async function load(type){
TYPE = type;
localStorage.setItem('usreMkt.type', type);
+ document.getElementById('exportcsv').href = '/api/markets.csv?type=' + type;
document.querySelectorAll('#typeSeg button').forEach(b => b.classList.toggle('active', b.dataset.type === type));
const d = await fetch('/api/markets?type=' + type).then(r => r.json());
allRows = d.rows || [];
diff --git a/src/server/index.ts b/src/server/index.ts
index de4785c..44b956d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -308,6 +308,36 @@ app.get('/api/firm/:id', async (req, res) => {
}
});
+// CSV export of the ranked markets table (analyst pull) — reuses the same
+// /api/markets cache-backed dataset via an internal fetch so the shape never drifts.
+app.get('/api/markets.csv', async (req, res) => {
+ try {
+ const type = String(req.query.type || 'county');
+ if (type !== 'county' && type !== 'metro' && type !== 'state') return res.status(400).send('bad type');
+ const r = await fetch(`http://127.0.0.1:${PORT}/api/markets?type=${type}`, {
+ headers: { authorization: req.headers.authorization || '' },
+ });
+ const { rows } = await r.json() as { rows: Record<string, unknown>[] };
+ if (!rows?.length) return res.status(404).send('no data');
+ const cols = ['rank', 'name', 'state', 'opportunity_score', 'data_completeness', 'thin_market',
+ 'zhvi', 'zhvi_yoy', 'median_sale_price', 'median_sale_price_yoy', 'inventory', 'dom',
+ 'months_of_supply', 'sale_to_list', 'median_hh_income', 'rent_yield', 'affordability', 'population'];
+ const esc = (v: unknown) => {
+ if (v == null) return '';
+ const s = String(v);
+ return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
+ };
+ const csv = [cols.join(',')]
+ .concat(rows.map(row => cols.map(c => esc(row[c])).join(',')))
+ .join('\n');
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', `attachment; filename="usrealestate-${type}-markets.csv"`);
+ res.send(csv);
+ } catch (e: any) {
+ res.status(500).send('error: ' + String(e.message || e));
+ }
+});
+
mountWatchlistAdmin(app);
mountProperty(app);
← ae4b420 States tab on rankings (51 states, raw metrics), national in
·
back to Nationalrealestate
·
auto-save: 2026-07-21T16:08:25 (5 files) — public/market.htm a435237 →