Phase 4 M3: Statistics page with days, entries, countries, distance

This commit is contained in:
2026-06-18 01:13:13 +02:00
parent 82efc6450f
commit df18b9cd5a
2 changed files with 111 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
---
title: 'Trip Stats'
template: stats
---
@@ -0,0 +1,107 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% set tracker_page = grav.pages.find('/tracker') %}
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
{# Basic counts #}
{% set entry_count = all_entries|length %}
{# Days on the road — find earliest entry timestamp by iterating #}
{% set days_on_road = 0 %}
{% set first_ts = null %}
{% for entry in all_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 now_ts = "now"|date('U') %}
{% set diff_seconds = now_ts - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{# Countries — unique, case-insensitive dedup, preserve original casing #}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in all_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 %}
{# GPS points for distance — collect as JSON for JS computation #}
{% set gps_points = [] %}
{% for entry in all_entries %}
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
{% set gps_points = gps_points|merge([[entry.header.lat, entry.header.lng]]) %}
{% endif %}
{% endfor %}
<div class="stats-page">
<h1 style="font-size:1.5rem;margin-bottom:1.5rem;">Trip Statistics</h1>
<div class="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">{{ entry_count }}</span>
<span class="stat-label">{{ entry_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" id="stat-distance">—</span>
<span class="stat-label">km traveled</span>
</div>
</div>
{% if country_display|length > 0 %}
<div class="stats-countries">
<span class="stats-countries-label">Countries visited</span>
{{ country_display|join(' · ') }}
</div>
{% endif %}
<p class="stats-note">Distance is approximate — straight lines between entry locations.</p>
</div>
<script>
var GPS_POINTS = {{ gps_points|json_encode|raw }};
function haversine(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng/2) * Math.sin(dLng/2);
return R * 2 * Math.asin(Math.sqrt(a));
}
var total = 0;
for (var i = 1; i < GPS_POINTS.length; i++) {
total += haversine(
parseFloat(GPS_POINTS[i-1][0]), parseFloat(GPS_POINTS[i-1][1]),
parseFloat(GPS_POINTS[i][0]), parseFloat(GPS_POINTS[i][1])
);
}
var el = document.getElementById('stat-distance');
if (GPS_POINTS.length < 2) {
el.textContent = '—';
} else {
el.textContent = '~' + Math.round(total).toLocaleString();
}
</script>
{% endblock %}