← back to Costa Rica
public/map.html
93 lines
<!doctype html>
<html lang="en"><head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Costa Rica — Places Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"/>
<style>
html,body{margin:0;height:100%;font:14px -apple-system,Segoe UI,Roboto,sans-serif;}
#map{position:absolute;inset:0;}
.panel{position:absolute;top:12px;left:12px;z-index:1000;background:#fff;border-radius:12px;
box-shadow:0 2px 12px rgba(0,0,0,.15);padding:14px 16px;min-width:210px;}
.panel h1{font-size:16px;margin:0 0 8px;color:#0a7d55;}
.panel .count{color:#6b756e;font-size:12px;margin-bottom:10px;}
.chip{display:flex;align-items:center;gap:8px;padding:5px 0;cursor:pointer;user-select:none;}
.chip input{accent-color:#0a7d55;} .dot{width:11px;height:11px;border-radius:50%;display:inline-block;}
.leaflet-popup-content{font:13px -apple-system,sans-serif;} .leaflet-popup-content b{color:#0a7d55;}
</style></head>
<body>
<div id="map"></div>
<div class="panel">
<h1>🇨🇷 Places Map</h1>
<div class="count" id="count">loading…</div>
<div id="filters"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script>
const COLORS = { tourism:'#0a7d55', rentals:'#b8860b', service:'#2b6cb0' };
const map = L.map('map').setView([9.75, -84.0], 8);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{ attribution:'© OpenStreetMap © CARTO', maxZoom:19 }).addTo(map);
const layers = {}; // category -> cluster group
let all = [];
function icon(cat){ const c=COLORS[cat]||'#888';
return L.divIcon({ className:'', iconSize:[12,12],
html:`<span style="width:11px;height:11px;border-radius:50%;background:${c};display:block;border:1.5px solid #fff;box-shadow:0 0 2px rgba(0,0,0,.5)"></span>` }); }
async function load(){
const d = await (await fetch('/api/map')).json();
all = d.places||[];
document.getElementById('count').textContent = `${all.length.toLocaleString()} geocoded places`;
const cats = [...new Set(all.map(p=>p.category))].sort();
const F = document.getElementById('filters');
for (const cat of cats){
const grp = L.markerClusterGroup({ chunkedLoading:true, maxClusterRadius:50 });
for (const p of all.filter(x=>x.category===cat)){
const m = L.marker([p.lat,p.lng], {icon:icon(cat)});
m.bindPopup(`<b>${p.name}</b><br>${String(p.vertical).replace(/_/g,' ')}${p.region?' · '+p.region:''}`
+ (p.rating?`<br>★ ${p.rating}`:'')
+ `<br><a href="/p/${p.slug}" target="_blank">details →</a>`);
grp.addLayer(m);
}
layers[cat]=grp; map.addLayer(grp);
const n = all.filter(x=>x.category===cat).length;
F.insertAdjacentHTML('beforeend',
`<label class="chip"><input type="checkbox" checked data-cat="${cat}">
<span class="dot" style="background:${COLORS[cat]||'#888'}"></span>${cat} (${n.toLocaleString()})</label>`);
}
F.querySelectorAll('input').forEach(cb=>cb.onchange=()=>{
const g=layers[cb.dataset.cat]; if(cb.checked) map.addLayer(g); else map.removeLayer(g);
});
}
load();
</script>
</body></html>