[object Object]

← back to Commercialrealestate

CRCP: recities.crcp hub — city launcher dropdown of all 50 markets, host-routed

7f18f17bbf97406e6a33dd9a0a0a8e520fad72da · 2026-08-20 12:38:24 -0700 · Steve Abrams

Files touched

Diff

commit 7f18f17bbf97406e6a33dd9a0a0a8e520fad72da
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 12:38:24 2026 -0700

    CRCP: recities.crcp hub — city launcher dropdown of all 50 markets, host-routed
---
 public/recities.html | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/serve.js     |   4 +-
 2 files changed, 125 insertions(+), 1 deletion(-)

diff --git a/public/recities.html b/public/recities.html
new file mode 100644
index 0000000..db5f854
--- /dev/null
+++ b/public/recities.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<html lang="en">
+<head>
+<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;
+  }
+  *{box-sizing:border-box}
+  body{margin:0;background:linear-gradient(180deg,#0f1419,#121924);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)}
+  .count{color:var(--mut);font-size:13px;margin:16px 0 10px}
+  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(190px,1fr));gap:10px;margin-top:14px}
+  a.city{display:block;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:#26313f;transform:translateY(-1px)}
+  a.city .nm{font-weight:600}
+  a.city .host{display:block;color:var(--mut);font-size:12px;margin-top:2px}
+  .empty{color:var(--mut);padding:20px 4px}
+  footer{color:var(--mut);font-size:12px;margin-top:34px}
+</style>
+</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">Every city market we serve. Choose one to open its 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="count" id="count"></div>
+    <div class="grid" id="grid"></div>
+  </div>
+
+  <footer id="foot"></footer>
+</div>
+
+<script>
+// The set of city markets served under *.crcp.agentabrams.com (each resolves + serves the CRCP app).
+// To add/remove a city: add its subdomain vhost + DNS, then add it here. slug = subdomain label.
+const LABELS = {
+  belair:"Bel-Air", beverlyhills:"Beverly Hills", canyondam:"Canyon Dam", chinesecamp:"Chinese Camp",
+  crescentmills:"Crescent Mills", echolake:"Echo Lake", forestranch:"Forest Ranch", grassvalley:"Grass Valley",
+  grizzlyflats:"Grizzly Flats", klamathriver:"Klamath River", lakeelsinore:"Lake Elsinore", northfork:"North Fork",
+  pacificpalisades:"Pacific Palisades", paynescreek:"Paynes Creek", sanjuancapistrano:"San Juan Capistrano",
+  santamonica:"Santa Monica", shermanoaks:"Sherman Oaks", sierramadre:"Sierra Madre", southlaketahoe:"South Lake Tahoe",
+  studiocity:"Studio City", twinbridges:"Twin Bridges", woffordheights:"Wofford Heights"
+};
+const SLUGS = ["altadena","belair","beverlyhills","caliente","camarillo","canyondam","chester","chico",
+ "chinesecamp","clearlake","cohasset","colfax","crescentmills","doyle","echolake","encino","foresthill",
+ "forestranch","georgetown","grassvalley","greenville","grizzlyflats","havilah","hemet","janesville",
+ "klamathriver","lakeelsinore","lakehead","malibu","mariposa","mineral","northfork","pacificpalisades",
+ "pasadena","paynescreek","redding","sanjuancapistrano","santamonica","shermanoaks","sierramadre","somerset",
+ "somis","southlaketahoe","studiocity","topanga","twinbridges","ukiah","weed","woffordheights","wrightwood"];
+
+const title = s => s.replace(/(^|\s)\S/g, c => c.toUpperCase());
+const CITIES = SLUGS.map(slug => ({
+  slug, label: LABELS[slug] || title(slug),
+  host: slug + ".crcp.agentabrams.com"
+})).sort((a,b) => a.label.localeCompare(b.label));
+const urlOf = c => "https://" + c.host + "/";
+
+const sel = document.getElementById('city');
+CITIES.forEach(c => { const o=document.createElement('option'); o.value=c.slug; o.textContent=c.label; sel.appendChild(o); });
+
+const grid = document.getElementById('grid'), count = document.getElementById('count'),
+      filter = document.getElementById('filter'), foot = document.getElementById('foot');
+
+function draw(q){
+  q = (q||'').trim().toLowerCase();
+  const shown = CITIES.filter(c => !q || c.label.toLowerCase().includes(q) || c.slug.includes(q));
+  grid.innerHTML = '';
+  if(!shown.length){ grid.innerHTML = '<div class="empty">No cities match “'+q+'”.</div>'; }
+  shown.forEach(c => {
+    const a = document.createElement('a');
+    a.className='city'; a.href=urlOf(c);
+    a.innerHTML = '<span class="nm">'+c.label+'</span><span class="host">'+c.host+'</span>';
+    grid.appendChild(a);
+  });
+  count.textContent = shown.length + (shown.length===CITIES.length ? '' : ' of '+CITIES.length) + ' cities';
+}
+function open(){ const c = CITIES.find(x=>x.slug===sel.value); if(c) window.location.href = urlOf(c); }
+
+document.getElementById('go').addEventListener('click', open);
+sel.addEventListener('keydown', e => { if(e.key==='Enter') open(); });
+filter.addEventListener('input', e => draw(e.target.value));
+draw('');
+foot.textContent = CITIES.length + ' city markets · CRCP · recities.crcp.agentabrams.com';
+</script>
+</body>
+</html>
diff --git a/scripts/serve.js b/scripts/serve.js
index 3a3e470..a783f69 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -68,7 +68,9 @@ app.use((req, res, next) => {
 const PUB_DIR = path.join(ROOT, 'public');
 const BADGE_TAG = '<script src="/user-badge.js" defer></script>';
 app.get(/(?:^\/$|\.html$)/, (req, res, next) => {
-  const rel = req.path === '/' ? 'index.html' : req.path.replace(/^\/+/, '');
+  // recities.crcp.agentabrams.com serves the city-launcher hub at '/'; every other host gets the app.
+  const isRecities = (req.hostname || '').toLowerCase() === 'recities.crcp.agentabrams.com';
+  const rel = req.path === '/' ? (isRecities ? 'recities.html' : 'index.html') : req.path.replace(/^\/+/, '');
   if (rel === 'login.html') return next();                 // sign-in page stays badge-free
   const file = path.join(PUB_DIR, rel);
   if (!file.startsWith(PUB_DIR + path.sep)) return next();  // path-traversal guard

← 1b7ba1f CRCP: resolve popup double-fire — ONE row-expand panel, serv  ·  back to Commercialrealestate  ·  loan-officers: seed 29 CA officers from firm public rosters a337f9a →