feat: differentiate cities with/without articles on country map
Cities without published articles appear in muted gray and are not clickable. The cities.json API endpoint now queries SQLite for published articles and adds a has_article boolean to each city row. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ daily when the pipeline runs).
|
|||||||
from quart import Blueprint, abort, jsonify
|
from quart import Blueprint, abort, jsonify
|
||||||
|
|
||||||
from .analytics import fetch_analytics
|
from .analytics import fetch_analytics
|
||||||
from .core import is_flag_enabled
|
from .core import fetch_all, is_flag_enabled
|
||||||
|
|
||||||
bp = Blueprint("api", __name__)
|
bp = Blueprint("api", __name__)
|
||||||
|
|
||||||
@@ -59,6 +59,20 @@ async def country_cities(country_slug: str):
|
|||||||
""",
|
""",
|
||||||
[country_slug],
|
[country_slug],
|
||||||
)
|
)
|
||||||
|
# Check which cities have published articles (any language).
|
||||||
|
article_rows = await fetch_all(
|
||||||
|
"""SELECT url_path FROM articles
|
||||||
|
WHERE url_path LIKE ? AND status = 'published'
|
||||||
|
AND published_at <= datetime('now')""",
|
||||||
|
(f"/markets/{country_slug}/%",),
|
||||||
|
)
|
||||||
|
article_slugs = set()
|
||||||
|
for a in article_rows:
|
||||||
|
parts = a["url_path"].rstrip("/").split("/")
|
||||||
|
if len(parts) >= 4:
|
||||||
|
article_slugs.add(parts[3])
|
||||||
|
for row in rows:
|
||||||
|
row["has_article"] = row["city_slug"] in article_slugs
|
||||||
return jsonify(rows), 200, _CACHE_HEADERS
|
return jsonify(rows), 200, _CACHE_HEADERS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,18 +43,23 @@
|
|||||||
data.forEach(function(c) {
|
data.forEach(function(c) {
|
||||||
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 color = scoreColor(c.market_score);
|
var hasArticle = c.has_article !== false;
|
||||||
|
var color = hasArticle ? scoreColor(c.market_score) : '#9CA3AF';
|
||||||
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 : '') + '<br>'
|
+ (pop ? ' · ' + pop : '');
|
||||||
+ '<span style="color:' + color + ';font-weight:600;">Score ' + Math.round(c.market_score) + '/100</span>';
|
if (hasArticle) {
|
||||||
L.marker([c.lat, c.lon], { icon: makeIcon(size, color) })
|
tip += '<br><span style="color:' + color + ';font-weight:600;">Score ' + Math.round(c.market_score) + '/100</span>';
|
||||||
|
}
|
||||||
|
var marker = L.marker([c.lat, c.lon], { icon: makeIcon(size, color) })
|
||||||
.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)] })
|
||||||
.on('click', function() { window.location = '/' + lang + '/markets/' + slug + '/' + c.city_slug; })
|
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
if (hasArticle) {
|
||||||
|
marker.on('click', function() { window.location = '/' + lang + '/markets/' + slug + '/' + c.city_slug; });
|
||||||
|
}
|
||||||
bounds.push([c.lat, c.lon]);
|
bounds.push([c.lat, c.lon]);
|
||||||
});
|
});
|
||||||
if (bounds.length) map.fitBounds(bounds, { padding: [24, 24] });
|
if (bounds.length) map.fitBounds(bounds, { padding: [24, 24] });
|
||||||
|
|||||||
Reference in New Issue
Block a user