← back to Commercialrealestate
public/recities.html
157 lines
<!doctype html>
<html lang="en">
<head>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRCP · Cities</title>
<style>
:root{
--bg:#0f1419; --panel:#1a2029; --panel2:#232b36; --edge:#2f3946;
--ink:#e8edf2; --mut:#93a1b0; --accent:#4ea1ff; --accent2:#2b6fd6;
}
/* LIGHT mode surface flips — keep the bespoke blue ACCENT, only re-point generic surfaces
to light so day mode has no dark islands (Cycle-6 lesson). Accents unchanged. */
html[data-theme="light"]{
--bg:#f4f7fb; --panel:#ffffff; --panel2:#eef3f9; --edge:#d5dee8;
--ink:#131a22; --mut:#586472;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;min-height:100vh}
.wrap{max-width:1080px;margin:0 auto;padding:40px 20px 80px}
header.top{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:6px}
header.top h1{font-size:26px;font-weight:700;margin:0;letter-spacing:.2px}
header.top .tag{color:var(--mut);font-size:14px}
.sub{color:var(--mut);margin:0 0 26px}
.panel{background:var(--panel);border:1px solid var(--edge);border-radius:14px;padding:22px;box-shadow:0 8px 30px rgba(0,0,0,.25)}
.pickrow{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
label.lbl{color:var(--mut);font-size:13px;text-transform:uppercase;letter-spacing:.6px;display:block;margin-bottom:8px}
select,button,input[type=search]{font:inherit}
select#city{flex:1 1 320px;min-width:240px;background:var(--panel2);color:var(--ink);border:1px solid var(--edge);
border-radius:10px;padding:13px 14px;appearance:none;
background-image:linear-gradient(45deg,transparent 50%,var(--mut) 50%),linear-gradient(135deg,var(--mut) 50%,transparent 50%);
background-position:calc(100% - 20px) 20px,calc(100% - 14px) 20px;background-size:6px 6px,6px 6px;background-repeat:no-repeat}
button.go{background:var(--accent2);color:#fff;border:1px solid var(--accent);border-radius:10px;padding:13px 22px;cursor:pointer;font-weight:600}
button.go:hover{background:var(--accent)}
.row2{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:16px}
.count{color:var(--mut);font-size:13px}
.toggle{color:var(--mut);font-size:13px;display:flex;gap:7px;align-items:center;cursor:pointer;user-select:none}
input#filter{width:100%;background:var(--panel2);color:var(--ink);border:1px solid var(--edge);border-radius:10px;padding:12px 14px;margin-top:22px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-top:14px}
a.city{display:flex;justify-content:space-between;align-items:center;gap:10px;text-decoration:none;color:var(--ink);
background:var(--panel2);border:1px solid var(--edge);border-radius:10px;padding:12px 14px;transition:.12s}
a.city:hover{border-color:var(--accent);background:var(--panel);transform:translateY(-1px)}
a.city.empty{opacity:.5}
a.city .nm{font-weight:600}
a.city .host{display:block;color:var(--mut);font-size:12px;margin-top:2px;font-weight:400}
a.city .n{flex:0 0 auto;font-size:12px;color:var(--mut);background:var(--bg);border:1px solid var(--edge);border-radius:20px;padding:2px 9px}
a.city .n.has{color:#bfe0ff;border-color:#2f5da3}
.empty-note{color:var(--mut);padding:20px 4px}
footer{color:var(--mut);font-size:12px;margin-top:34px}
</style>
<link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
<link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<div class="wrap">
<header class="top">
<h1>CRCP · Cities</h1>
<span class="tag">Commercial Real Estate — pick a market</span>
</header>
<p class="sub">Markets with active listings. Choose one to open its scoped dashboard.</p>
<div class="panel">
<label class="lbl" for="city">Jump to a city</label>
<div class="pickrow">
<select id="city" aria-label="Select a city"></select>
<button class="go" id="go">Open →</button>
</div>
<input id="filter" type="search" placeholder="Filter cities…" autocomplete="off">
<div class="row2">
<span class="count" id="count"></span>
<label class="toggle"><input type="checkbox" id="showEmpty"> Show empty markets</label>
</div>
<div class="grid" id="grid"></div>
</div>
<footer id="foot"></footer>
</div>
<script src="/crcp-cities.js"></script>
<script>
var CITIES = window.CRCP_CITIES.CITIES.slice(); // from the shared cities module
var sameCity = window.CRCP_CITIES.sameCity;
var counts = {}; // slug -> listing count (from ranked.json)
var loaded = false;
var urlOf = function (c) { return "https://" + c.host + "/"; };
var sel = document.getElementById('city'), grid = document.getElementById('grid'),
count = document.getElementById('count'), filter = document.getElementById('filter'),
showEmpty = document.getElementById('showEmpty'), foot = document.getElementById('foot');
// Count listings per city from the same snapshot the app uses. Best-effort: if it fails, show all.
fetch('/data/ranked.json').then(function (r) { return r.json(); }).then(function (d) {
var arr = (d && d.ranked) || [];
CITIES.forEach(function (c) {
counts[c.slug] = arr.filter(function (p) { return sameCity(p.city, c); }).length;
});
loaded = true; render();
}).catch(function () { loaded = true; render(); }); // no counts -> treat all as shown
function visible() {
var withData = CITIES.filter(function (c) { return (counts[c.slug] || 0) > 0; });
// Default: hide empty markets (Steve's choice). Toggle reveals all 50. Before counts load, show all.
var base = (!loaded || showEmpty.checked || withData.length === 0) ? CITIES : withData;
return base.slice().sort(function (a, b) {
var na = counts[a.slug] || 0, nb = counts[b.slug] || 0;
if (nb !== na) return nb - na; // most inventory first
return a.label.localeCompare(b.label);
});
}
function fillSelect() {
var list = visible(); sel.innerHTML = '';
list.forEach(function (c) {
var o = document.createElement('option'); o.value = c.slug;
o.textContent = c.label + (loaded ? ' (' + (counts[c.slug] || 0) + ')' : '');
sel.appendChild(o);
});
}
function render() {
fillSelect();
var q = (filter.value || '').trim().toLowerCase();
var list = visible().filter(function (c) { return !q || c.label.toLowerCase().indexOf(q) >= 0 || c.slug.indexOf(q) >= 0; });
grid.innerHTML = '';
if (!list.length) { grid.innerHTML = '<div class="empty-note">No cities match “' + q + '”.</div>'; }
list.forEach(function (c) {
var n = counts[c.slug] || 0;
var a = document.createElement('a');
a.className = 'city' + (loaded && n === 0 ? ' empty' : '');
a.href = urlOf(c);
a.innerHTML = '<span><span class="nm">' + c.label + '</span><span class="host">' + c.host + '</span></span>' +
'<span class="n' + (n ? ' has' : '') + '">' + (loaded ? n : '·') + '</span>';
grid.appendChild(a);
});
var shownCities = visible();
var withData = CITIES.filter(function (c) { return (counts[c.slug] || 0) > 0; }).length;
count.textContent = loaded
? (shownCities.length + ' markets shown · ' + withData + ' with listings · ' + CITIES.length + ' total')
: (CITIES.length + ' city markets');
foot.textContent = 'CRCP · recities.crcp.agentabrams.com';
}
function open() { var c = CITIES.find(function (x) { return x.slug === sel.value; }); if (c) window.location.href = urlOf(c); }
document.getElementById('go').addEventListener('click', open);
sel.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(); });
filter.addEventListener('input', render);
showEmpty.addEventListener('change', render);
render();
</script>
<script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
<script src="/corner-nav.js" defer></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>