chore: move docs/plans/specs to main repo
This commit is contained in:
@@ -1,182 +0,0 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user