183 lines
5.7 KiB
Markdown
183 lines
5.7 KiB
Markdown
# Milestone 3 Spec — Statistics Page
|
||
|
||
**Goal:** A `/stats` page showing key trip numbers: days on the road, entries posted, countries visited, and approximate distance traveled.
|
||
|
||
---
|
||
|
||
## User Stories
|
||
|
||
- As a reader, I want to see a quick summary of how far Mischa has traveled and how many countries they've visited, without having to read every entry.
|
||
- As a traveler (Mischa), I want to see my own trip stats at a glance — a satisfying progress indicator while traveling.
|
||
- As a reader, I want stats that update automatically as new entries are posted — no manual maintenance.
|
||
|
||
---
|
||
|
||
## Feature Details
|
||
|
||
### 3.1 — Stats Page
|
||
|
||
**Route:** `/stats`
|
||
|
||
**Template:** `stats.html.twig` — extends `partials/base.html.twig`
|
||
|
||
**Page file:** `user/pages/04.stats/stats.md`
|
||
|
||
**Computed in Twig** (server-side, from published entries under `/tracker`):
|
||
|
||
---
|
||
|
||
### 3.2 — Stat: Days on the Road
|
||
|
||
**Definition:** Number of calendar days from the date of the first published entry to today.
|
||
|
||
**Formula (Twig):**
|
||
```twig
|
||
{% set first_entry = entries|first %}
|
||
{% set days = (now.timestamp - first_entry.date|date('U'))|round / 86400 %}
|
||
{% set days_on_road = [days|round(0, 'floor'), 0]|max %}
|
||
```
|
||
|
||
**Display:** `42 days on the road`
|
||
|
||
**Edge cases:**
|
||
- No entries: show `0 days on the road` or `Trip not started yet`
|
||
- Only one entry (today): show `1 day on the road`
|
||
|
||
---
|
||
|
||
### 3.3 — Stat: Entries Posted
|
||
|
||
**Definition:** Count of all published entries under `/tracker`.
|
||
|
||
**Display:** `17 entries posted`
|
||
|
||
**Edge cases:**
|
||
- 0 entries: `0 entries posted`
|
||
- 1 entry: `1 entry posted` (singular)
|
||
|
||
---
|
||
|
||
### 3.4 — Stat: Countries Visited
|
||
|
||
**Definition:** Unique values of `location_country` across all published entries, non-empty.
|
||
|
||
**Display:** Count + list
|
||
|
||
```
|
||
6 countries visited
|
||
Japan · South Korea · Mongolia · Russia · Finland · Estonia
|
||
```
|
||
|
||
**Edge cases:**
|
||
- No entries have `location_country`: show `Countries: —`
|
||
- Some entries missing `location_country`: count only those that have it; note "(based on X of Y entries)"
|
||
- Duplicate country names are de-duplicated (case-insensitive)
|
||
|
||
---
|
||
|
||
### 3.5 — Stat: Approximate Distance Traveled
|
||
|
||
**Definition:** Sum of great-circle (haversine) distances between consecutive entries that have valid lat/lng, in ascending date order.
|
||
|
||
**Implementation:** Computed in Twig using a haversine formula macro.
|
||
|
||
**Haversine in Twig:**
|
||
```twig
|
||
{% macro haversine(lat1, lng1, lat2, lng2) %}
|
||
{% set R = 6371 %}
|
||
{% set dLat = ((lat2 - lat1) * 3.14159265 / 180) %}
|
||
{% set dLng = ((lng2 - lng1) * 3.14159265 / 180) %}
|
||
{% set a = (dLat/2)|sin * (dLat/2)|sin + (lat1 * 3.14159265 / 180)|cos * (lat2 * 3.14159265 / 180)|cos * (dLng/2)|sin * (dLng/2)|sin %}
|
||
{% set c = 2 * a|sqrt|asin %}
|
||
{{ (R * c)|round }}
|
||
{% endmacro %}
|
||
```
|
||
|
||
Note: Twig does not have `sin`/`cos`/`asin`/`sqrt` built-in. Use a JavaScript-side calculation instead:
|
||
|
||
**Implementation:** Embed the entry GPS data as JSON in the template (same pattern as Milestone 2), compute distance in vanilla JS, and write the result into the DOM on page load.
|
||
|
||
```js
|
||
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)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2;
|
||
return R * 2 * Math.asin(Math.sqrt(a));
|
||
}
|
||
var total = 0;
|
||
for (var i = 1; i < GPS_POINTS.length; i++) {
|
||
total += haversine(GPS_POINTS[i-1][0], GPS_POINTS[i-1][1], GPS_POINTS[i][0], GPS_POINTS[i][1]);
|
||
}
|
||
document.getElementById('stat-distance').textContent = Math.round(total).toLocaleString() + ' km';
|
||
```
|
||
|
||
**Display:** `~3,400 km traveled`
|
||
|
||
**Edge cases:**
|
||
- 0 or 1 GPS points: `Distance: —`
|
||
- Very large numbers (trans-continental trip): use thousands separator: `12,400 km`
|
||
- Disclaimer note: "approximate — based on straight lines between entry locations"
|
||
|
||
---
|
||
|
||
### 3.6 — Visual Layout
|
||
|
||
**Layout:** 4 large stat blocks in a 2×2 grid on desktop, stacked on mobile.
|
||
|
||
Each block:
|
||
```
|
||
┌─────────────────┐
|
||
│ 42 │
|
||
│ days on road │
|
||
└─────────────────┘
|
||
```
|
||
|
||
- Number: large (3rem), bold, brand blue
|
||
- Label: small (0.85rem), muted grey
|
||
- Background: white, 1px border, 8px radius, subtle shadow
|
||
- Mobile: 2-col grid (2 stats per row)
|
||
|
||
Below the grid: list of countries visited (plain text, centered, muted).
|
||
|
||
---
|
||
|
||
### 3.7 — Navigation Link
|
||
|
||
Add "Stats" to the site navigation in `partials/base.html.twig`.
|
||
|
||
---
|
||
|
||
## Out of Scope (Milestone 3)
|
||
|
||
- Charts or graphs (bar charts, line graphs, etc.)
|
||
- World map with highlighted countries (that's a visual enhancement, deferred)
|
||
- Per-country breakdown (km in each country, days in each country)
|
||
- Speed statistics (km/day average)
|
||
- Elevation statistics
|
||
- Historical comparison (vs. last trip)
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. `/stats` page exists and returns HTTP 200
|
||
2. "Days on the road" shows correct count from first entry date to today
|
||
3. "Entries posted" shows count of published entries
|
||
4. "Countries visited" shows correct count + list of unique non-empty `location_country` values
|
||
5. "Distance traveled" shows km sum of haversine distances between consecutive GPS entries
|
||
6. All four stats display in a 2×2 grid on desktop
|
||
7. On mobile (375px), stats stack into a 2-column responsive grid
|
||
8. Stats auto-update when new entries are published (no manual maintenance)
|
||
9. If no entries: all stats show 0 or `—`, no JS errors
|
||
10. "Stats" link in navigation routes to `/stats`
|
||
|
||
---
|
||
|
||
## Design Notes
|
||
|
||
- Stats should feel like a dashboard, not a table — big numbers, small labels
|
||
- Do not use any external charting library for v1
|
||
- Countries list below the grid: inline, separated by `·`, muted grey
|
||
- The "approximate" disclaimer for distance should be in small print below the distance stat
|