merge: fix map bubble styling + improve hover UX

This commit is contained in:
Deeman
2026-03-06 10:11:26 +01:00
3 changed files with 23 additions and 7 deletions

View File

@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added ### Added
- **Custom 404/500 error pages** — styled error pages extending `base.html` with i18n support (EN/DE). The 404 page is context-aware: when the URL matches `/markets/{country}/{city}`, it shows a city-specific message with a link back to the country overview instead of a generic "page not found". - **Custom 404/500 error pages** — styled error pages extending `base.html` with i18n support (EN/DE). The 404 page is context-aware: when the URL matches `/markets/{country}/{city}`, it shows a city-specific message with a link back to the country overview instead of a generic "page not found".
- **Map: city article indicators** — country overview map bubbles now differentiate cities with/without published articles. Cities without articles appear in muted gray and are not clickable, preventing dead-end navigations. The `/api/markets/<country>/cities.json` endpoint includes a `has_article` boolean per city. - **Map: city article indicators** — country overview map bubbles now differentiate cities with/without published articles. All cities retain score-based colors (green/amber/red); non-article cities are visually receded with lower opacity, dashed borders, desaturated color, and default cursor (no click). Tooltips show scores for all cities — article cities get "Click to explore →", non-article cities get "Coming soon". The `/api/markets/<country>/cities.json` endpoint includes a `has_article` boolean per city.
### Fixed ### Fixed
- **Admin template preview maps** — Leaflet maps rendered blank because `article-maps.js` called `L.divIcon()` at the IIFE top level before Leaflet was dynamically loaded, crashing the script. Moved `VENUE_ICON` creation into the `script.onload` callback so it runs after Leaflet is available. Previous commit's `.card` `overflow: visible` fix remains (clips tile layers otherwise). - **Admin template preview maps** — Leaflet maps rendered blank because `article-maps.js` called `L.divIcon()` at the IIFE top level before Leaflet was dynamically loaded, crashing the script. Moved `VENUE_ICON` creation into the `script.onload` callback so it runs after Leaflet is available. Previous commit's `.card` `overflow: visible` fix remains (clips tile layers otherwise).

View File

@@ -892,6 +892,18 @@
transform: scale(1.1); transform: scale(1.1);
} }
/* Non-article city markers: faded + dashed border, no click affordance */
.pn-marker--muted {
opacity: 0.45;
border: 2px dashed rgba(255,255,255,0.6);
cursor: default;
filter: saturate(0.7);
}
.pn-marker--muted:hover {
transform: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
}
/* Small fixed venue dot */ /* Small fixed venue dot */
.pn-venue { .pn-venue {
width: 10px; width: 10px;

View File

@@ -19,11 +19,12 @@
return '#DC2626'; return '#DC2626';
} }
function makeIcon(size, color) { function makeIcon(size, color, muted) {
var s = Math.round(size); var s = Math.round(size);
var cls = 'pn-marker' + (muted ? ' pn-marker--muted' : '');
return L.divIcon({ return L.divIcon({
className: '', className: '',
html: '<div class="pn-marker" style="width:' + s + 'px;height:' + s + 'px;background:' + color + ';opacity:0.82;"></div>', html: '<div class="' + cls + '" style="width:' + s + 'px;height:' + s + 'px;background:' + color + ';"></div>',
iconSize: [s, s], iconSize: [s, s],
iconAnchor: [s / 2, s / 2], iconAnchor: [s / 2, s / 2],
}); });
@@ -44,17 +45,20 @@
if (!c.lat || !c.lon) return; if (!c.lat || !c.lon) return;
var size = 10 + 36 * Math.sqrt((c.padel_venue_count || 1) / maxV); var size = 10 + 36 * Math.sqrt((c.padel_venue_count || 1) / maxV);
var hasArticle = c.has_article !== false; var hasArticle = c.has_article !== false;
var color = hasArticle ? scoreColor(c.market_score) : '#9CA3AF'; var color = scoreColor(c.market_score);
var pop = c.population >= 1000000 var pop = c.population >= 1000000
? (c.population / 1000000).toFixed(1) + 'M' ? (c.population / 1000000).toFixed(1) + 'M'
: (c.population >= 1000 ? Math.round(c.population / 1000) + 'K' : (c.population || '')); : (c.population >= 1000 ? Math.round(c.population / 1000) + 'K' : (c.population || ''));
var tip = '<strong>' + c.city_name + '</strong><br>' var tip = '<strong>' + c.city_name + '</strong><br>'
+ (c.padel_venue_count || 0) + ' venues' + (c.padel_venue_count || 0) + ' venues'
+ (pop ? ' · ' + pop : ''); + (pop ? ' · ' + pop : '')
+ '<br><span style="color:' + color + ';font-weight:600;">Score ' + Math.round(c.market_score) + '/100</span>';
if (hasArticle) { if (hasArticle) {
tip += '<br><span style="color:' + color + ';font-weight:600;">Score ' + Math.round(c.market_score) + '/100</span>'; tip += '<br><span style="color:#94A3B8;font-size:0.75rem;">Click to explore →</span>';
} else {
tip += '<br><span style="color:#94A3B8;font-size:0.75rem;">Coming soon</span>';
} }
var marker = L.marker([c.lat, c.lon], { icon: makeIcon(size, color) }) var marker = L.marker([c.lat, c.lon], { icon: makeIcon(size, color, !hasArticle) })
.bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -Math.round(size / 2)] }) .bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -Math.round(size / 2)] })
.addTo(map); .addTo(map);
if (hasArticle) { if (hasArticle) {