feat(maps): Phase 2 — markets hub country bubble map

Add Leaflet map to /markets with country-level bubbles sized by
total_venues and colored by avg_market_score. Click navigates to
country overview page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-03-04 13:04:40 +01:00
parent db0d7cfee9
commit 8e53fda283

View File

@@ -6,6 +6,7 @@
<meta name="description" content="{{ t.markets_page_description }}">
<meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
<meta property="og:description" content="{{ t.markets_page_og_description }}">
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/leaflet/leaflet.min.css') }}">
{% endblock %}
{% block content %}
@@ -15,6 +16,8 @@
<p class="text-slate">{{ t.mkt_subheading }}</p>
</header>
<div id="markets-map" style="height:420px; border-radius:12px;" class="mb-6"></div>
<!-- Filters -->
<div class="card mb-8">
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
@@ -62,3 +65,43 @@
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/leaflet/leaflet.min.js') }}"></script>
<script>
(function() {
var map = L.map('markets-map', {scrollWheelZoom: false}).setView([48.5, 10], 4);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
maxZoom: 18
}).addTo(map);
function scoreColor(score) {
if (score >= 60) return '#16A34A';
if (score >= 30) return '#D97706';
return '#DC2626';
}
fetch('/api/markets/countries.json')
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.length) return;
var maxV = Math.max.apply(null, data.map(function(d) { return d.total_venues; }));
var lang = document.documentElement.lang || 'en';
data.forEach(function(c) {
if (!c.lat || !c.lon) return;
var radius = 6 + 22 * Math.sqrt(c.total_venues / maxV);
var color = scoreColor(c.avg_market_score);
L.circleMarker([c.lat, c.lon], {
radius: radius,
fillColor: color, color: color,
fillOpacity: 0.6, opacity: 0.9, weight: 1
})
.bindTooltip(c.country_name_en + '<br>' + c.total_venues + ' venues in ' + c.city_count + ' cities', {className: 'map-tooltip'})
.on('click', function() { window.location = '/' + lang + '/markets/' + c.country_slug; })
.addTo(map);
});
});
})();
</script>
{% endblock %}