← back to Costa Rica
yolo tick 3: /stats dashboard with KPIs, by-vertical/category/region tables, filter inputs, theme toggle
72b08a16c8266b476479bb3839283e38d7aa3d6c · 2026-05-08 08:21:06 -0700 · Steve
Files touched
A public/stats.htmlM server.js
Diff
commit 72b08a16c8266b476479bb3839283e38d7aa3d6c
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 08:21:06 2026 -0700
yolo tick 3: /stats dashboard with KPIs, by-vertical/category/region tables, filter inputs, theme toggle
---
public/stats.html | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 2 +
2 files changed, 109 insertions(+)
diff --git a/public/stats.html b/public/stats.html
new file mode 100644
index 0000000..5bad144
--- /dev/null
+++ b/public/stats.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<html lang="en" data-theme="light">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Stats — Costa Rica Directory</title>
+ <script>(function(){try{var t=localStorage.getItem('cr-theme');if(t)document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
+ <style>
+ :root{--bg:#fafaf7;--fg:#0e1015;--muted:#5a6270;--line:#e3e0d6;--card:#fff;--card-line:#ebe7da;--accent:#0a7a3a;--accent-2:#c2a85a}
+ [data-theme="dark"]{--bg:#0e1015;--fg:#f3f1ea;--muted:#9aa3b2;--line:#23262d;--card:#15181f;--card-line:#262932;--accent:#3ec07a;--accent-2:#d8c074}
+ *{box-sizing:border-box}
+ body{margin:0;background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',Roboto,sans-serif;line-height:1.55}
+ a{color:inherit;text-decoration:none} a:hover{color:var(--accent)}
+ .nav{position:sticky;top:0;z-index:30;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:saturate(160%) blur(10px);border-bottom:1px solid var(--line)}
+ .nav-in{max-width:1320px;margin:0 auto;padding:14px 22px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
+ .brand{font-weight:700;font-size:18px} .brand .pin{color:var(--accent)}
+ .nav-links{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:14px}
+ .nav-links a.active{color:var(--fg);font-weight:600}
+ .ml-auto{margin-left:auto} .icon-btn{appearance:none;background:transparent;color:inherit;border:1px solid var(--line);border-radius:999px;width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
+ main{max-width:1320px;margin:0 auto;padding:28px 22px}
+ h1{font-size:38px;letter-spacing:-.02em;margin:6px 0 4px}
+ .sub{color:var(--muted);margin-bottom:22px}
+ .row{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px}
+ @media (max-width:760px){.row{grid-template-columns:repeat(2,1fr)}}
+ .stat{background:var(--card);border:1px solid var(--card-line);border-radius:14px;padding:16px}
+ .stat-n{font-size:30px;font-weight:800;letter-spacing:-.02em}
+ .stat-l{color:var(--muted);font-size:13px;text-transform:uppercase;letter-spacing:.06em;margin-top:2px}
+ .grid2{display:grid;grid-template-columns:1.2fr 1fr;gap:18px}
+ @media (max-width:900px){.grid2{grid-template-columns:1fr}}
+ .panel{background:var(--card);border:1px solid var(--card-line);border-radius:14px;padding:18px}
+ .panel h2{margin:0 0 12px;font-size:18px;letter-spacing:-.01em}
+ table{width:100%;border-collapse:collapse;font-size:14px}
+ th,td{padding:8px 6px;border-bottom:1px solid var(--line);text-align:left}
+ th{color:var(--muted);font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.05em}
+ .bar{position:relative;background:color-mix(in srgb,var(--accent) 14%,transparent);height:8px;border-radius:999px;overflow:hidden}
+ .bar > span{display:block;height:100%;background:var(--accent);border-radius:999px}
+ .num{text-align:right;font-variant-numeric:tabular-nums}
+ .filter{display:flex;gap:8px;align-items:center;margin-bottom:10px}
+ .filter input{flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px}
+ .scroll{max-height:520px;overflow:auto}
+ </style>
+</head>
+<body>
+ <header class="nav"><div class="nav-in">
+ <a class="brand" href="/"><span class="pin">●</span> Costa Rica Directory</a>
+ <nav class="nav-links">
+ <a href="/">Browse</a><a href="/regions">Regions</a><a href="/stats" class="active">Stats</a>
+ </nav>
+ <div class="ml-auto"><button class="icon-btn" id="themeBtn" aria-label="Theme">◐</button></div>
+ </div></header>
+
+ <main>
+ <h1>Directory stats</h1>
+ <div class="sub" id="sub">Live counts across the Costa Rica catalog.</div>
+
+ <div class="row" id="kpis"></div>
+
+ <div class="grid2">
+ <section class="panel">
+ <h2>By vertical</h2>
+ <div class="filter"><input id="vfilter" placeholder="filter…" /></div>
+ <div class="scroll"><table id="vtable"><thead><tr><th>Vertical</th><th class="num">Count</th><th>Share</th></tr></thead><tbody></tbody></table></div>
+ </section>
+ <section class="panel">
+ <h2>By category</h2>
+ <table id="ctable"><thead><tr><th>Category</th><th class="num">Count</th><th>Share</th></tr></thead><tbody></tbody></table>
+ </section>
+ </div>
+
+ <section class="panel" style="margin-top:18px">
+ <h2>By region (top 30 by listings)</h2>
+ <div class="filter"><input id="rfilter" placeholder="filter region…" /></div>
+ <div class="scroll"><table id="rtable"><thead><tr><th>Region</th><th class="num">Count</th><th>Share</th></tr></thead><tbody></tbody></table></div>
+ </section>
+ </main>
+
+ <script>
+ const themeBtn=document.getElementById('themeBtn');
+ themeBtn.addEventListener('click',()=>{const cur=document.documentElement.getAttribute('data-theme')||'light';const nx=cur==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',nx);try{localStorage.setItem('cr-theme',nx);}catch{}});
+
+ function bar(pct){return `<div class="bar"><span style="width:${pct.toFixed(1)}%"></span></div>`;}
+ function row(label,n,total){const pct=total?(n*100/total):0;return `<tr><td>${label}</td><td class="num">${n.toLocaleString()}</td><td style="width:38%">${bar(pct)}</td></tr>`;}
+
+ fetch('/api/stats').then(r=>r.json()).then(s=>{
+ const total=s.total||0;
+ document.getElementById('kpis').innerHTML=[
+ ['Total listings',total],
+ ['Verticals',s.by_vertical.length],
+ ['Regions w/ listings',s.by_region.filter(r=>r.n>0).length],
+ ['Categories',s.by_category.length]
+ ].map(([l,n])=>`<div class="stat"><div class="stat-n">${n.toLocaleString()}</div><div class="stat-l">${l}</div></div>`).join('');
+
+ const vbody=document.querySelector('#vtable tbody');
+ const cbody=document.querySelector('#ctable tbody');
+ const rbody=document.querySelector('#rtable tbody');
+
+ function renderV(filter=''){const f=filter.trim().toLowerCase();vbody.innerHTML=s.by_vertical.filter(v=>!f||v.vertical.toLowerCase().includes(f)).map(v=>row(v.vertical,v.n,total)).join('')||`<tr><td colspan="3" style="color:var(--muted);text-align:center;padding:18px">no matches</td></tr>`;}
+ function renderC(){cbody.innerHTML=s.by_category.map(c=>row(c.category,c.n,total)).join('');}
+ function renderR(filter=''){const f=filter.trim().toLowerCase();const items=s.by_region.filter(r=>r.n>0&&(!f||r.name.toLowerCase().includes(f))).slice(0,30);rbody.innerHTML=items.map(r=>row(`<a href="/r/${r.slug}">${r.name}</a>`,r.n,total)).join('')||`<tr><td colspan="3" style="color:var(--muted);text-align:center;padding:18px">no matches</td></tr>`;}
+
+ renderV();renderC();renderR();
+ document.getElementById('vfilter').addEventListener('input',e=>renderV(e.target.value));
+ document.getElementById('rfilter').addEventListener('input',e=>renderR(e.target.value));
+ }).catch(e=>{document.getElementById('sub').textContent='Could not load stats: '+e.message;});
+ </script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 862bbac..ddeca29 100644
--- a/server.js
+++ b/server.js
@@ -387,6 +387,8 @@ app.get('/p/:slug', async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); }
});
+app.get('/stats', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'stats.html')));
+
app.use(express.static(path.join(__dirname, 'public')));
app.listen(PORT, '0.0.0.0', () => {
← cfab471 yolo tick 2: dedicated region landing page (/r/:slug) with h
·
back to Costa Rica
·
track YOLO loop progress + flag prod-deploy + fallback-scrip a3854c7 →