← 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>&nbsp;Costa&nbsp;Rica&nbsp;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>