Files
padelnomics/web/src/padelnomics/content/templates/markets.html
Deeman 77772b7ea4 feat(maps): beanflows-style divIcon bubbles + feature flag gate
Replace L.circleMarker with L.divIcon + .pn-marker CSS class (white
border, box-shadow, hover scale) matching the beanflows growing
conditions map pattern. Dark .map-tooltip CSS override (no arrow,
dark navy background). Small venue dots use .pn-venue class.

Add _require_maps_flag() to all 4 API endpoints (default=True so
dev works without seeding the flag row). Gate /opportunity-map route
the same way.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 20:51:00 +01:00

117 lines
4.6 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ t.markets_page_title }} - {{ config.APP_NAME }}{% endblock %}
{% block head %}
<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 %}
<main class="container-page py-12">
<header class="mb-8">
<h1 class="text-3xl mb-2">{{ t.mkt_heading }}</h1>
<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;">
<div>
<label class="form-label" for="market-q">{{ t.markets_search_label }}</label>
<input type="text" id="market-q" name="q" value="{{ current_q }}" placeholder="{{ t.mkt_search_placeholder }}"
class="form-input"
hx-get="{{ url_for('content.market_results') }}"
hx-target="#market-results"
hx-trigger="input changed delay:300ms"
hx-include="#market-country, #market-region">
</div>
<div>
<label class="form-label" for="market-country">{{ t.markets_country_label }}</label>
<select id="market-country" name="country" class="form-input"
hx-get="{{ url_for('content.market_results') }}"
hx-target="#market-results"
hx-trigger="change"
hx-include="#market-q, #market-region">
<option value="">{{ t.mkt_all_countries }}</option>
{% for c in countries %}
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="form-label" for="market-region">Region</label>
<select id="market-region" name="region" class="form-input"
hx-get="{{ url_for('content.market_results') }}"
hx-target="#market-results"
hx-trigger="change"
hx-include="#market-q, #market-country">
<option value="">{{ t.mkt_all_regions }}</option>
{% for r in regions %}
<option value="{{ r }}" {% if r == current_region %}selected{% endif %}>{{ r }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<!-- Results -->
<div id="market-results">
{% include "partials/market_results.html" %}
</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';
}
function makeIcon(size, color) {
var s = Math.round(size);
return L.divIcon({
className: '',
html: '<div class="pn-marker" style="width:' + s + 'px;height:' + s + 'px;background:' + color + ';opacity:0.82;"></div>',
iconSize: [s, s],
iconAnchor: [s / 2, s / 2],
});
}
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 size = 12 + 44 * Math.sqrt(c.total_venues / maxV);
var color = scoreColor(c.avg_market_score);
var tip = '<strong>' + c.country_name_en + '</strong><br>'
+ c.total_venues + ' venues · ' + c.city_count + ' cities<br>'
+ '<span style="color:' + color + ';font-weight:600;">Score ' + c.avg_market_score + '/100</span>';
L.marker([c.lat, c.lon], { icon: makeIcon(size, color) })
.bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -Math.round(size / 2)] })
.on('click', function() { window.location = '/' + lang + '/markets/' + c.country_slug; })
.addTo(map);
});
});
})();
</script>
{% endblock %}