diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index bf649b7..cb37dca 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -2736,13 +2736,13 @@ async def article_edit(article_id: int): body = raw[m.end():].lstrip("\n") if m else raw body_html = mistune.html(body) if body else "" - css_url = url_for("static", filename="css/output.css") preview_doc = ( - f"" - f"" - f"" - f"
{body_html}
" - ) if body_html else "" + await render_template( + "admin/partials/article_preview_doc.html", body_html=body_html + ) + if body_html + else "" + ) data = {**dict(article), "body": body} return await render_template( @@ -2764,13 +2764,13 @@ async def article_preview(): m = _FRONTMATTER_RE.match(body) body = body[m.end():].lstrip("\n") if m else body body_html = mistune.html(body) if body else "" - css_url = url_for("static", filename="css/output.css") preview_doc = ( - f"" - f"" - f"" - f"
{body_html}
" - ) if body_html else "" + await render_template( + "admin/partials/article_preview_doc.html", body_html=body_html + ) + if body_html + else "" + ) return await render_template("admin/partials/article_preview.html", preview_doc=preview_doc) diff --git a/web/src/padelnomics/admin/templates/admin/article_form.html b/web/src/padelnomics/admin/templates/admin/article_form.html index ae2b9f0..8db1daf 100644 --- a/web/src/padelnomics/admin/templates/admin/article_form.html +++ b/web/src/padelnomics/admin/templates/admin/article_form.html @@ -384,7 +384,7 @@ {% else %} diff --git a/web/src/padelnomics/admin/templates/admin/partials/article_preview.html b/web/src/padelnomics/admin/templates/admin/partials/article_preview.html index 7630039..88a27fa 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/article_preview.html +++ b/web/src/padelnomics/admin/templates/admin/partials/article_preview.html @@ -4,7 +4,7 @@ {% else %} diff --git a/web/src/padelnomics/admin/templates/admin/partials/article_preview_doc.html b/web/src/padelnomics/admin/templates/admin/partials/article_preview_doc.html new file mode 100644 index 0000000..a160193 --- /dev/null +++ b/web/src/padelnomics/admin/templates/admin/partials/article_preview_doc.html @@ -0,0 +1,15 @@ +{# Standalone HTML document used as iframe srcdoc for the article editor preview. + Includes Leaflet so map shortcodes render correctly. #} + + + + + + + + +
{{ body_html | safe }}
+ + + + diff --git a/web/src/padelnomics/content/templates/article_detail.html b/web/src/padelnomics/content/templates/article_detail.html index 3a6e1fd..2820e01 100644 --- a/web/src/padelnomics/content/templates/article_detail.html +++ b/web/src/padelnomics/content/templates/article_detail.html @@ -60,106 +60,6 @@ {% endblock %} {% block scripts %} - + + {% endblock %} diff --git a/web/src/padelnomics/static/js/article-maps.js b/web/src/padelnomics/static/js/article-maps.js new file mode 100644 index 0000000..38de362 --- /dev/null +++ b/web/src/padelnomics/static/js/article-maps.js @@ -0,0 +1,108 @@ +/** + * Leaflet map initialisation for article pages (country + city maps). + * + * Looks for #country-map and #city-map elements. If neither exists, does nothing. + * Expects data-* attributes on the map elements and a global LEAFLET_JS_URL + * variable pointing to the Leaflet JS bundle. + */ +(function() { + var countryMapEl = document.getElementById('country-map'); + var cityMapEl = document.getElementById('city-map'); + if (!countryMapEl && !cityMapEl) return; + + var TILES = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; + var TILES_ATTR = '© OSM © CARTO'; + + 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: '
', + iconSize: [s, s], + iconAnchor: [s / 2, s / 2], + }); + } + + function initCountryMap(el) { + var slug = el.dataset.countrySlug; + var map = L.map(el, {scrollWheelZoom: false}); + L.tileLayer(TILES, { attribution: TILES_ATTR, maxZoom: 18 }).addTo(map); + var lang = document.documentElement.lang || 'en'; + fetch('/api/markets/' + slug + '/cities.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.padel_venue_count || 1; })); + var bounds = []; + data.forEach(function(c) { + if (!c.lat || !c.lon) return; + var size = 10 + 36 * Math.sqrt((c.padel_venue_count || 1) / maxV); + var color = scoreColor(c.market_score); + var pop = c.population >= 1000000 + ? (c.population / 1000000).toFixed(1) + 'M' + : (c.population >= 1000 ? Math.round(c.population / 1000) + 'K' : (c.population || '')); + var tip = '' + c.city_name + '
' + + (c.padel_venue_count || 0) + ' venues' + + (pop ? ' · ' + pop : '') + '
' + + 'Score ' + Math.round(c.market_score) + '/100'; + 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/' + slug + '/' + c.city_slug; }) + .addTo(map); + bounds.push([c.lat, c.lon]); + }); + if (bounds.length) map.fitBounds(bounds, { padding: [24, 24] }); + }); + } + + var VENUE_ICON = L.divIcon({ + className: '', + html: '
', + iconSize: [10, 10], + iconAnchor: [5, 5], + }); + + function initCityMap(el) { + var countrySlug = el.dataset.countrySlug; + var citySlug = el.dataset.citySlug; + var lat = parseFloat(el.dataset.lat); + var lon = parseFloat(el.dataset.lon); + var map = L.map(el, {scrollWheelZoom: false}).setView([lat, lon], 13); + L.tileLayer(TILES, { attribution: TILES_ATTR, maxZoom: 18 }).addTo(map); + fetch('/api/markets/' + countrySlug + '/' + citySlug + '/venues.json') + .then(function(r) { return r.json(); }) + .then(function(data) { + data.forEach(function(v) { + if (!v.lat || !v.lon) return; + var indoor = v.indoor_court_count || 0; + var outdoor = v.outdoor_court_count || 0; + var total = v.court_count || (indoor + outdoor); + var courtLine = total + ? total + ' court' + (total > 1 ? 's' : '') + + (indoor || outdoor + ? ' (' + [indoor ? indoor + ' indoor' : '', outdoor ? outdoor + ' outdoor' : ''].filter(Boolean).join(', ') + ')' + : '') + : ''; + var tip = '' + v.name + '' + (courtLine ? '
' + courtLine : ''); + L.marker([v.lat, v.lon], { icon: VENUE_ICON }) + .bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -7] }) + .addTo(map); + }); + }); + } + + /* Dynamically load Leaflet JS then init maps */ + var script = document.createElement('script'); + script.src = window.LEAFLET_JS_URL || '/static/vendor/leaflet/leaflet.min.js'; + script.onload = function() { + if (countryMapEl) initCountryMap(countryMapEl); + if (cityMapEl) initCityMap(cityMapEl); + }; + document.head.appendChild(script); +})();