diff --git a/docs/milestone-1-spec.md b/docs/milestone-1-spec.md new file mode 100644 index 0000000..6413fff --- /dev/null +++ b/docs/milestone-1-spec.md @@ -0,0 +1,193 @@ +# Milestone 1 Spec — Entry Enrichment + +**Goal:** Every entry is richer out of the box — location name shown, weather auto-captured, photos in a proper gallery, hero image visible on the feed. + +--- + +## User Stories + +- As a traveler (Mischa), when I submit the post form, I want my current weather conditions auto-filled so I don't have to look them up manually. +- As a traveler, I want to type my city and country once and have it appear on the entry and in the feed card, so readers know where I am without reading the whole post. +- As a reader, when I scan the feed, I want to see a thumbnail photo and location for each entry so I can quickly get a sense of where Mischa is and whether to read the full entry. +- As a reader, when I open an entry, I want to see all uploaded photos in a gallery I can browse, not a wall of raw images. +- As a traveler, when I submit a form without photos, the entry should still display cleanly with no broken image placeholders. + +--- + +## Feature Details + +### 1.1 — Location Name Field on Post Form + +**What:** Add two text fields to the post form: `location_city` and `location_country`. + +**Behavior:** +- Both are optional (GPS coordinates are also optional) +- Placeholder text: "e.g. Kyoto" and "e.g. Japan" +- Displayed below the lat/lng fields +- On submit, stored in entry frontmatter as `location_city` and `location_country` +- On the form, shown as a single labeled group "Location Name" with two side-by-side inputs on desktop, stacked on mobile + +**Edge cases:** +- If left blank: entry shows no location badge. No error, no broken UI. +- Long city names (e.g. "Ulaanbaatar") must not overflow card layout. +- Special characters (accents, non-Latin) must render correctly. + +**Mobile behavior:** Both fields full-width, stacked, 44px min touch targets. + +--- + +### 1.2 — Weather Auto-Fetch on Post Form + +**What:** A "Get Weather" button on the post form that calls the Open-Meteo free API (no API key) using the lat/lng already entered, and fills hidden weather fields. + +**Fields to fetch and store:** +- `weather_temp_c` — temperature in Celsius (integer) +- `weather_desc` — short description: one of: Sunny, Partly cloudy, Cloudy, Foggy, Drizzle, Rain, Snow, Thunderstorm (derived from WMO weather code) + +**WMO code mapping (Open-Meteo uses WMO codes):** +- 0 → Sunny +- 1,2 → Partly cloudy +- 3 → Cloudy +- 45,48 → Foggy +- 51,53,55,56,57 → Drizzle +- 61,63,65,66,67,80,81,82 → Rain +- 71,73,75,77,85,86 → Snow +- 95,96,99 → Thunderstorm + +**API call:** +``` +https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lng}¤t=temperature_2m,weather_code&temperature_unit=celsius +``` + +**UX flow:** +1. User fills in lat/lng (manually or via "Get Location" button) +2. User taps "Get Weather" button +3. Button shows "Fetching…" while loading +4. On success: fills temp and desc fields (visible, editable text inputs) +5. On failure (no network, no lat/lng): shows inline error "Could not fetch weather — enter manually" + +**Edge cases:** +- If lat/lng not filled when button tapped: show inline error "Enter coordinates first" +- Weather fields are always editable manually (auto-fill is a convenience, not mandatory) +- If weather fields left blank: entry shows no weather badge. No broken UI. +- Open-Meteo returns current conditions, not historical — this is fine for posting in real time + +**Mobile behavior:** "Get Weather" button is full-width, 44px height, placed immediately below the lat/lng + location name fields. + +--- + +### 1.3 — Weather Display on Entry Page + +**What:** If `weather_temp_c` or `weather_desc` is present in frontmatter, display a weather badge on the entry page. + +**Display format:** `☀️ Sunny · 28°C` (icon + description + temperature) +- Icon chosen from a small set based on `weather_desc`: + - Sunny → ☀️ + - Partly cloudy → ⛅ + - Cloudy → ☁️ + - Foggy → 🌫️ + - Drizzle → 🌦️ + - Rain → 🌧️ + - Snow → ❄️ + - Thunderstorm → ⛈️ + +**Placement:** In the entry header, between the date and the body text. Same line as GPS coordinates if those are shown. + +**Edge cases:** +- Only temp, no desc → show temp only +- Only desc, no temp → show desc only +- Neither → hide weather section entirely +- Temperature should always be integer (round if float) + +--- + +### 1.4 — Location Badge on Feed Cards and Entry Page + +**What:** Display `location_city, location_country` as a small badge on tracker feed cards and at the top of entry pages. + +**Feed card:** Below the date, above the excerpt. Format: `📍 Kyoto, Japan` + +**Entry page:** In the header below the date, above the content. Format: `📍 Kyoto, Japan` + +**Edge cases:** +- Only city, no country → `📍 Kyoto` +- Only country, no city → `📍 Japan` +- Neither → location badge hidden entirely +- Long location names: truncate with ellipsis at 30 chars on cards (full text on entry page) + +--- + +### 1.5 — Photo Gallery on Entry Page + +**What:** Photos uploaded to an entry should display in a responsive grid gallery with lightbox (click to enlarge). + +**Implementation approach:** Use Grav's native media collection for the entry page. Each `.entry` folder contains its photos. Render them in a grid in `entry.html.twig`. Use a minimal vanilla JS lightbox — no external framework. + +**Gallery behavior:** +- Photos displayed in a 2-column grid on mobile, 3-column on desktop +- Each thumbnail is square-cropped, 150px on mobile +- Clicking/tapping a thumbnail opens a lightbox overlay +- Lightbox: dark overlay, full-size image centered, tap/click outside or press Escape to close +- Left/right navigation arrows in lightbox (swipe on mobile) +- No captions needed for v1 + +**Edge cases:** +- 0 photos: gallery section hidden entirely +- 1 photo: still uses grid (single item), lightbox works +- Many photos (>10): gallery still renders (no hard limit on display) +- Non-image files in the media folder: skip them (only render jpg, jpeg, png, webp, gif) + +--- + +### 1.6 — Hero Image on Tracker Feed Cards + +**What:** If an entry has photos, the first photo (or the one named in `hero_image` frontmatter) appears as a thumbnail on the tracker feed card. + +**Implementation:** In `tracker.html.twig`, for each entry: +1. If `entry.header.hero_image` is set, use `entry.media[entry.header.hero_image]` +2. Else, use the first image in `entry.media` sorted by name +3. Render as a 16:9 aspect-ratio thumbnail, full width of card, above the title + +**Edge cases:** +- No photos: card shows no image, just text. No broken `` tag. +- `hero_image` set but file missing: fall back to first media file, or no image +- Very tall/wide images: CSS `object-fit: cover` maintains card aspect ratio + +--- + +## Out of Scope (Milestone 1) + +- Map features (Milestone 2) +- Statistics page (Milestone 3) +- Video support +- Comments or reactions +- Automated reverse geocoding (city name comes from form input, not auto-detected) +- Altitude display (data may not be present) +- Historical weather (Open-Meteo current endpoint only) + +--- + +## Acceptance Criteria + +1. Post form has `location_city` and `location_country` fields that save to entry frontmatter +2. Post form has "Get Weather" button that fills `weather_temp_c` and `weather_desc` via Open-Meteo when lat/lng are provided +3. Entry page shows weather badge when weather fields are present; hidden when absent +4. Entry page shows location badge `📍 City, Country` when location fields are present; hidden when absent +5. Tracker feed card shows location badge when present +6. Tracker feed card shows a hero image when photos exist for an entry +7. Entry page shows a 2-col (mobile) / 3-col (desktop) photo grid +8. Clicking any photo opens a full-screen lightbox with prev/next navigation +9. Pressing Escape or clicking outside lightbox closes it +10. All fields are optional — empty values produce no broken UI elements +11. All interactive elements meet 44px minimum touch target on mobile +12. Form submits correctly with all new fields populated or all blank + +--- + +## Design Notes + +- Weather and location badges should be subtle — small text, muted color, not the visual focus +- Use emoji icons for weather — universal, no icon font dependency +- Gallery grid: `gap: 4px` between thumbs, no borders, square crops +- Lightbox: `background: rgba(0,0,0,0.92)`, image centered with `max-height: 90vh` +- Feed card image: `aspect-ratio: 16/9`, `object-fit: cover`, rounded top corners matching card diff --git a/docs/milestone-2-spec.md b/docs/milestone-2-spec.md new file mode 100644 index 0000000..2e7337b --- /dev/null +++ b/docs/milestone-2-spec.md @@ -0,0 +1,166 @@ +# Milestone 2 Spec — Interactive Map + +**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a chronological route line, with popups linking to entries. + +--- + +## User Stories + +- As a reader, I want to see a world map showing where Mischa has been so I can understand the journey at a glance without reading every entry. +- As a reader, I want to click a map marker and see the entry date, title, and a thumbnail — and be able to click through to the full entry. +- As a reader on mobile, I want to pan and pinch-zoom the map with my fingers without the page scrolling underneath. +- As a traveler (Mischa), I want the map to automatically include every entry that has lat/lng data — I should not need to do any manual map maintenance. +- As a reader, I want the map to show the route line connecting stops in the order they were visited, so the journey makes narrative sense. + +--- + +## Feature Details + +### 2.1 — Map Page + +**Route:** `/map` + +**Template:** `map.html.twig` — extends `partials/base.html.twig` + +**Page file:** `user/pages/03.map/map.md` + +**Content:** +- Full-viewport-height map container below the site header +- Leaflet.js loaded from CDN (jsDelivr): `https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js` +- Leaflet CSS from same CDN +- Tile layer: OpenStreetMap (free, no API key): `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png` +- Attribution: "© OpenStreetMap contributors" + +**Map initialization:** +- Default zoom: auto-fit to bounds of all markers (use `map.fitBounds()`) +- If no entries with GPS data: show world view, zoom 2, centered at 0,0 with a message "No locations yet" +- Min zoom: 2, Max zoom: 18 + +--- + +### 2.2 — Entry Data Serialization + +**How entries reach the map JS:** + +In `map.html.twig`, Grav's Twig will iterate all published entries under `/tracker` and serialize them to a JSON array embedded in a `