← back to Costa Rica
public/region.html
321 lines
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loading… — Costa Rica Directory</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<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;--cols:4}
[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}
.hero-wrap{position:relative;width:100%;height:42vh;min-height:340px;max-height:520px;overflow:hidden;background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 35%, var(--card)), color-mix(in srgb, var(--accent-2) 30%, var(--card)))}
.hero-wrap img{width:100%;height:100%;object-fit:cover;display:block;filter:saturate(1.05)}
.hero-overlay{position:absolute;inset:0;background:linear-gradient(to bottom, rgba(0,0,0,.05) 0%, rgba(0,0,0,.7) 100%);display:flex;align-items:flex-end;padding:36px 22px}
.hero-inner{max-width:1320px;margin:0 auto;width:100%;color:#fff}
.hero-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.85;margin-bottom:6px}
.hero-title{font-size:clamp(36px,6vw,68px);letter-spacing:-.025em;line-height:1.05;margin:0 0 6px;font-weight:800}
.hero-sub{font-size:16px;opacity:.92;max-width:760px}
.hero-credit{position:absolute;bottom:8px;right:14px;background:rgba(0,0,0,.5);color:#fff;font-size:11px;padding:4px 10px;border-radius:999px}
.hero-credit a{color:#fff;text-decoration:underline;text-decoration-color:rgba(255,255,255,.5)}
main{max-width:1320px;margin:0 auto;padding:24px 22px 80px}
.crumbs{font-size:13px;color:var(--muted);margin-bottom:16px}
.stats-row{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:18px}
.stat{padding:10px 14px;border:1px solid var(--card-line);border-radius:12px;background:var(--card);font-size:13px;color:var(--muted)}
.stat .n{color:var(--fg);font-weight:600;font-size:18px;margin-right:6px;font-variant-numeric:tabular-nums}
.layout{display:grid;grid-template-columns:1fr 360px;gap:32px;margin-top:8px}
@media(max-width:980px){.layout{grid-template-columns:1fr}}
.filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.pill{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border:1px solid var(--line);border-radius:999px;background:var(--card);color:var(--fg);font-size:13px;cursor:pointer}
.pill.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
.pill .n{font-variant-numeric:tabular-nums;color:var(--muted);font-size:12px;margin-left:4px}
.pill.active .n{color:color-mix(in srgb,var(--bg) 70%, transparent)}
.controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:6px 0 14px}
.search{flex:1;min-width:240px;display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--line);border-radius:12px;background:var(--card)}
.search input{flex:1;border:0;outline:0;background:transparent;color:inherit;font-size:15px}
.ctrl{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}
.ctrl select{appearance:none;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:6px 10px;font-size:13px}
.ctrl input[type=range]{padding:0;width:120px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:16px}
@media(max-width:760px){.grid{grid-template-columns:repeat(2,1fr)}}
.card{background:var(--card);border:1px solid var(--card-line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;transition:transform .15s,box-shadow .15s}
.card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(20,20,20,.08)}
.card .img{aspect-ratio:16/10;background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 35%, var(--card)), color-mix(in srgb, var(--accent-2) 30%, var(--card)));display:flex;align-items:center;justify-content:center;font-size:24px;color:color-mix(in srgb,var(--fg) 35%,transparent)}
.card .img img{width:100%;height:100%;object-fit:cover;display:block}
.card .body{padding:12px 14px 14px}
.cat{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-weight:600}
.name{font-weight:600;letter-spacing:-.01em;margin-top:4px}
.where{font-size:12px;color:var(--muted);margin-top:2px}
.side .card{padding:18px;margin-bottom:14px}
.label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;margin-bottom:10px}
#region-map{aspect-ratio:1/1;border-radius:12px;border:1px solid var(--card-line);overflow:hidden;background:color-mix(in srgb,var(--fg) 4%,transparent)}
.vert-list{display:flex;flex-direction:column;gap:6px}
.vert-list a{display:flex;justify-content:space-between;padding:7px 10px;border-radius:8px;font-size:13px}
.vert-list a:hover{background:color-mix(in srgb,var(--fg) 5%,transparent)}
.vert-list .n{color:var(--muted);font-variant-numeric:tabular-nums}
.nearby{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.nearby a{display:flex;align-items:center;gap:8px;padding:6px;border:1px solid var(--card-line);border-radius:10px;font-size:12px}
.nearby a:hover{border-color:var(--accent)}
.nearby .pic{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 35%,var(--card)),color-mix(in srgb,var(--accent-2) 30%,var(--card)));background-size:cover;background-position:center;flex-shrink:0}
.meta{font-size:13px;color:var(--muted);margin:6px 0 14px}
footer{max-width:1320px;margin:0 auto;padding:30px 22px 60px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);margin-top:30px}
.leaflet-container{background:color-mix(in srgb,var(--fg) 4%,transparent)!important}
</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="/?category=tourism">Tourism</a>
<a href="/?category=rentals">Rentals</a>
<a href="/?category=service">Local services</a>
</nav>
<div class="ml-auto" style="display:flex;gap:8px;align-items:center">
<button class="icon-btn" id="theme-toggle" title="Toggle theme">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
</button>
</div>
</div>
</header>
<section class="hero-wrap" id="hero-wrap">
<img id="hero-img" alt="" />
<div class="hero-overlay">
<div class="hero-inner">
<div class="hero-eyebrow" id="hero-eyebrow">Costa Rica</div>
<h1 class="hero-title" id="hero-title">Loading…</h1>
<p class="hero-sub" id="hero-sub"></p>
</div>
</div>
<div class="hero-credit" id="hero-credit" style="display:none"></div>
</section>
<main>
<div class="crumbs"><a href="/">Directory</a> · <span id="crumb-region">Region</span></div>
<div class="stats-row">
<div class="stat"><span class="n" id="s-count">—</span> listings</div>
<div class="stat"><span class="n" id="s-tourism">—</span> tourism</div>
<div class="stat"><span class="n" id="s-rentals">—</span> rentals</div>
<div class="stat"><span class="n" id="s-service">—</span> local services</div>
</div>
<div class="layout">
<section>
<div class="filter-bar" id="vertical-bar"></div>
<div class="controls">
<div class="search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="q" placeholder="Search businesses in this region…" autocomplete="off" />
</div>
<div class="ctrl">
<label for="sort">Sort</label>
<select id="sort">
<option value="newest">Newest</option>
<option value="name">Name A→Z</option>
<option value="rating">Rating</option>
<option value="verified">Verified first</option>
</select>
</div>
<div class="ctrl">
<label for="cols">Density</label>
<input id="cols" type="range" min="2" max="6" step="1" value="4" />
</div>
</div>
<div class="meta" id="meta">Loading places…</div>
<div class="grid" id="grid"></div>
</section>
<aside class="side">
<div class="card">
<div class="label">On the map</div>
<div id="region-map"></div>
</div>
<div class="card">
<div class="label">By vertical</div>
<div class="vert-list" id="vert-list"></div>
</div>
<div class="card" id="nearby-card" style="display:none">
<div class="label">Nearby in province</div>
<div class="nearby" id="nearby"></div>
</div>
</aside>
</div>
</main>
<footer>© Costa Rica Directory. Aggregated from public records (MEIC, ICT, municipal patentes) and local web directories. Hero photos via Wikipedia/Wikimedia Commons. Map data © OpenStreetMap contributors. Owners can correct or claim a listing at <a href="mailto:info@agentabrams.com">info@agentabrams.com</a>.</footer>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
document.documentElement.style.setProperty('--cols', localStorage.getItem('cr-cols') || 4);
$('#theme-toggle').addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-theme') || 'light';
const nxt = cur === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', nxt);
try { localStorage.setItem('cr-theme', nxt); } catch(e){}
});
const slug = location.pathname.replace(/^\/r\//,'');
const state = { vertical: '', q: '', sort: 'newest' };
function pluralize(n, s) { return n === 1 ? s : s + 's'; }
function headline(region, count) {
const variants = [
`${count.toLocaleString()} ${pluralize(count,'business')} in ${region.name}`,
`${region.name} — every business, in one place`,
`Find ${pluralize(count,'business')} across ${region.name}`,
`The directory of ${region.name}`,
];
return variants[Math.abs(region.id) % variants.length] || variants[0];
}
function subline(region, byVert) {
const top = (byVert||[]).slice(0,3).map(v => v.vertical.replace(/_/g,' ')).join(' · ');
const where = region.province ? `${region.province} province` : 'Costa Rica';
return `${region.region_type === 'city' ? 'City of' : ''} ${region.name}, ${where}. ${top || 'Tourism, rentals, and local services'}.`.replace(/^\s+/,'');
}
Promise.all([
fetch('/api/regions/' + encodeURIComponent(slug)).then(r => r.ok ? r.json() : Promise.reject(r)),
]).then(([region]) => {
document.title = region.name + ' — Costa Rica Directory';
$('#crumb-region').textContent = region.name;
const ld=document.createElement('script');ld.type='application/ld+json';
ld.textContent=JSON.stringify({"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"Home","item":"https://costarica.agentabrams.com/"},
{"@type":"ListItem","position":2,"name":region.province||"Provinces","item":"https://costarica.agentabrams.com/provinces"},
{"@type":"ListItem","position":3,"name":region.name,"item":`https://costarica.agentabrams.com/r/${region.slug}`}
]});document.head.appendChild(ld);
// Hero
if (region.image_url) { $('#hero-img').src = region.image_url; $('#hero-img').alt = region.name; }
else { $('#hero-img').style.display = 'none'; }
$('#hero-eyebrow').textContent = region.province ? region.province + ' · Costa Rica' : 'Costa Rica';
$('#hero-title').textContent = headline(region, region.place_count);
$('#hero-sub').textContent = subline(region, region.by_vertical);
if (region.image_credit && region.image_source_url) {
$('#hero-credit').innerHTML = '📷 <a href="'+region.image_source_url+'" target="_blank" rel="noopener nofollow">'+region.image_credit+'</a>';
$('#hero-credit').style.display = '';
}
// Stats
const byCat = {};
for (const v of (region.by_vertical||[])) {
const cat = v.vertical.startsWith('tourism_') ? 'tourism' : v.vertical.startsWith('rentals_') ? 'rentals' : 'service';
byCat[cat] = (byCat[cat] || 0) + v.n;
}
$('#s-count').textContent = region.place_count.toLocaleString();
$('#s-tourism').textContent = (byCat.tourism||0).toLocaleString();
$('#s-rentals').textContent = (byCat.rentals||0).toLocaleString();
$('#s-service').textContent = (byCat.service||0).toLocaleString();
// Vertical pill bar (top 8) + sidebar list (all)
const vbar = $('#vertical-bar');
const allPill = document.createElement('button');
allPill.className = 'pill active'; allPill.dataset.vert='';
allPill.innerHTML = `All<span class="n">${region.place_count}</span>`;
vbar.appendChild(allPill);
(region.by_vertical||[]).slice(0,8).forEach(v => {
const p = document.createElement('button');
p.className = 'pill'; p.dataset.vert = v.vertical;
p.innerHTML = `${v.vertical.replace(/_/g,' ')}<span class="n">${v.n}</span>`;
vbar.appendChild(p);
});
vbar.addEventListener('click', e => {
const t = e.target.closest('.pill'); if (!t) return;
$$('.pill', vbar).forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.vertical = t.dataset.vert;
loadGrid();
});
const vlist = $('#vert-list');
(region.by_vertical||[]).forEach(v => {
const a = document.createElement('a');
a.href = '#'; a.dataset.vert = v.vertical;
a.innerHTML = `<span>${v.vertical.replace(/_/g,' ')}</span><span class="n">${v.n}</span>`;
a.addEventListener('click', e => { e.preventDefault(); state.vertical=v.vertical; $$('.pill', vbar).forEach(x=>x.classList.remove('active')); loadGrid(); window.scrollTo({top:0,behavior:'smooth'}); });
vlist.appendChild(a);
});
// Nearby in province
if (region.nearby_in_province && region.nearby_in_province.length) {
$('#nearby-card').style.display = '';
const wrap = $('#nearby');
region.nearby_in_province.forEach(n => {
const a = document.createElement('a');
a.href = '/r/' + n.slug;
a.innerHTML = `<div class="pic"${n.image_url ? ' style="background-image:url('+n.image_url+')"' : ''}></div><div><div>${n.name}</div><div style="color:var(--muted);font-size:11px">${n.n} listings</div></div>`;
wrap.appendChild(a);
});
}
// Region map (Leaflet) — pin region center, fetch place markers async
if (region.lat && region.lng) {
const map = L.map('region-map', { zoomControl: true, scrollWheelZoom: false }).setView([+region.lat, +region.lng], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a>' }).addTo(map);
L.circleMarker([+region.lat, +region.lng], { radius: 8, color: '#0a7a3a', fillColor:'#0a7a3a', fillOpacity:.7 }).addTo(map).bindPopup('<strong>'+region.name+'</strong><br>'+region.place_count+' listings');
} else {
$('#region-map').textContent = 'No coordinates on file.';
$('#region-map').style.cssText = 'aspect-ratio:auto;padding:20px;color:var(--muted);font-size:13px;display:flex;align-items:center;justify-content:center';
}
loadGrid();
}).catch(() => {
$('#hero-title').textContent = 'Region not found';
});
function card(p){
const wrap = document.createElement('a');
wrap.className = 'card'; wrap.href = '/p/' + encodeURIComponent(p.slug);
const heroSrc = p.effective_image_url || p.image_url;
wrap.innerHTML = `${heroSrc ? '<div class="img"><img loading="lazy" src="'+heroSrc+'" alt=""></div>' : '<div class="img">⛰</div>'}<div class="body"><div class="cat">${(p.vertical||'').replace(/_/g,' ')}</div><div class="name">${p.name}</div><div class="where">${p.address || p.region_name || ''}</div></div>`;
return wrap;
}
let qDebounce;
$('#q').addEventListener('input', e => { clearTimeout(qDebounce); qDebounce=setTimeout(()=>{ state.q=e.target.value.trim(); loadGrid(); },220); });
$('#sort').addEventListener('change', e => { state.sort=e.target.value; loadGrid(); });
$('#cols').addEventListener('input', e => { document.documentElement.style.setProperty('--cols', e.target.value); try{localStorage.setItem('cr-cols',e.target.value)}catch(e){} });
function loadGrid() {
const params = new URLSearchParams();
params.set('region', slug);
if (state.vertical) params.set('vertical', state.vertical);
if (state.q) params.set('q', state.q);
if (state.sort) params.set('sort', state.sort);
params.set('limit', '120');
$('#meta').textContent = 'Loading…';
$('#grid').innerHTML = '';
fetch('/api/places?' + params).then(r => r.json()).then(j => {
$('#meta').textContent = `${(j.total||0).toLocaleString()} matches` + (state.vertical ? ` · ${state.vertical.replace(/_/g,' ')}` : '');
(j.places||[]).forEach(p => $('#grid').appendChild(card(p)));
});
}
</script>
</body>
</html>