refactor: extract stats and cycling panels to Twig macros

Move stats computation and both panel HTML divs out of trip.html.twig
into dedicated macros/stats.html.twig and macros/cycling.html.twig.
trip.html.twig imports both at the top of {% block content %} and
calls them via stats_m.stats_panel() and cycling_m.cycling_panel().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
2026-06-23 23:57:54 +02:00
parent 1458c24aa3
commit 04bcab2d55
3 changed files with 138 additions and 134 deletions
@@ -0,0 +1,41 @@
{% macro cycling_panel() %}
<div id="trip-cycling-block" class="trip-cycling-block">
<div class="trip-panel-inner">
<div class="trip-cycling-header">
<span class="trip-cycling-icon">🚴</span>
<span class="trip-cycling-title">Cycling Stats</span>
</div>
<div class="trip-cycling-grid">
<div class="stat-block">
<span class="stat-value" id="cyc-distance">—</span>
<span class="stat-label">km distance</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-gain">—</span>
<span class="stat-label">m ↑ gain</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-loss">—</span>
<span class="stat-label">m ↓ loss</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-highest">—</span>
<span class="stat-label">m highest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-lowest">—</span>
<span class="stat-label">m lowest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-moving-time">—</span>
<span class="stat-label">moving time</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-avg-speed">—</span>
<span class="stat-label">km/h avg speed</span>
</div>
</div>
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
</div>
</div>
{% endmacro %}
@@ -0,0 +1,93 @@
{% macro stats_panel(journal_entries, page, journal_count, has_gpx) %}
{% set days_on_road = 0 %}
{% if page.header.date_end is not empty %}
{% set start_ts = page.header.date_start|date('U') %}
{% set end_ts = page.header.date_end|date('U') %}
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
{% else %}
{% set first_ts = null %}
{% for entry in journal_entries %}
{% set ts = entry.date|date('U') %}
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
{% endfor %}
{% if first_ts is not null %}
{% set diff_seconds = "now"|date('U') - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% endif %}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_country is not empty %}
{% set lower = entry.header.location_country|trim|lower %}
{% if lower not in seen_lower %}
{% set seen_lower = seen_lower|merge([lower]) %}
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set seen_city_lower = [] %}
{% set city_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_city is not empty %}
{% set lower = entry.header.location_city|trim|lower %}
{% if lower not in seen_city_lower %}
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set temp_min = null %}
{% set temp_max = null %}
{% for entry in journal_entries %}
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
{% set t = entry.header.weather_temp_c %}
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
{% endif %}
{% endfor %}
<div id="trip-stats-block" class="trip-stats-block">
<div class="trip-panel-inner">
<div class="trip-stats-grid">
<div class="stat-block">
<span class="stat-value">{{ days_on_road }}</span>
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ journal_count }}</span>
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ country_display|length }}</span>
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ city_display|length }}</span>
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value" id="stat-distance">—</span>
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
</div>
<div class="stat-block">
{% if temp_min is not null %}
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
{% else %}
<span class="stat-value">—</span>
{% endif %}
<span class="stat-label">°C range</span>
</div>
</div>
{% if country_display|length > 0 %}
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
{% endif %}
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
<button class="trip-panel-close" data-toggle="trip-stats-toggle">↑ Close stats</button>
</div>
</div>
{% endmacro %}
+4 -134
View File
@@ -1,6 +1,8 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% import 'macros/stats.html.twig' as stats_m %}
{% import 'macros/cycling.html.twig' as cycling_m %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
@@ -22,59 +24,6 @@
{% set journal_count = journal_entries|length %}
{% set story_count = story_entries|length %}
{# Stats computation #}
{% set days_on_road = 0 %}
{% if page.header.date_end is not empty %}
{% set start_ts = page.header.date_start|date('U') %}
{% set end_ts = page.header.date_end|date('U') %}
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
{% else %}
{% set first_ts = null %}
{% for entry in journal_entries %}
{% set ts = entry.date|date('U') %}
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
{% endfor %}
{% if first_ts is not null %}
{% set diff_seconds = "now"|date('U') - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% endif %}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_country is not empty %}
{% set lower = entry.header.location_country|trim|lower %}
{% if lower not in seen_lower %}
{% set seen_lower = seen_lower|merge([lower]) %}
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set seen_city_lower = [] %}
{% set city_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_city is not empty %}
{% set lower = entry.header.location_city|trim|lower %}
{% if lower not in seen_city_lower %}
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set temp_min = null %}
{% set temp_max = null %}
{% for entry in journal_entries %}
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
{% set t = entry.header.weather_temp_c %}
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
{% endif %}
{% endfor %}
{% set gps_points = [] %}
{% for entry in journal_entries %}
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
@@ -147,86 +96,10 @@
</div>
</div>
<div id="trip-stats-block" class="trip-stats-block">
<div class="trip-panel-inner">
<div class="trip-stats-grid">
<div class="stat-block">
<span class="stat-value">{{ days_on_road }}</span>
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ journal_count }}</span>
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ country_display|length }}</span>
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ city_display|length }}</span>
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value" id="stat-distance">—</span>
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
</div>
<div class="stat-block">
{% if temp_min is not null %}
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
{% else %}
<span class="stat-value">—</span>
{% endif %}
<span class="stat-label">°C range</span>
</div>
</div>
{% if country_display|length > 0 %}
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
{% endif %}
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
<button class="trip-panel-close" data-toggle="trip-stats-toggle">↑ Close stats</button>
</div>
</div>
{{ stats_m.stats_panel(journal_entries, page, journal_count, has_gpx) }}
{% if has_gpx %}
<div id="trip-cycling-block" class="trip-cycling-block">
<div class="trip-panel-inner">
<div class="trip-cycling-header">
<span class="trip-cycling-icon">🚴</span>
<span class="trip-cycling-title">Cycling Stats</span>
</div>
<div class="trip-cycling-grid">
<div class="stat-block">
<span class="stat-value" id="cyc-distance">—</span>
<span class="stat-label">km distance</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-gain">—</span>
<span class="stat-label">m ↑ gain</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-loss">—</span>
<span class="stat-label">m ↓ loss</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-highest">—</span>
<span class="stat-label">m highest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-lowest">—</span>
<span class="stat-label">m lowest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-moving-time">—</span>
<span class="stat-label">moving time</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-avg-speed">—</span>
<span class="stat-label">km/h avg speed</span>
</div>
</div>
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
</div>
</div>
{{ cycling_m.cycling_panel() }}
{% endif %}
<div class="feed">
@@ -345,11 +218,9 @@ var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
if (HAS_GPX) {
MapUtils.parseGpxFiles(GPX_URLS, function(result) {
// Mode A: update distance stat
if (distEl) {
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
}
// Populate cycling panel
function setText(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = val;
@@ -363,7 +234,6 @@ var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
});
} else {
// Mode B: haversine between entry points
var total = 0;
for (var i = 1; i < STATS_GPS.length; i++) {
total += MapUtils.haversineKm(