docs: remove old-path duplicate files left on main before restructure
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
# Backlog
|
||||
|
||||
Ideas and improvements not yet planned or scheduled.
|
||||
|
||||
---
|
||||
|
||||
## GPX Manager (`/gpx-manager`)
|
||||
|
||||
- [ ] **Polish the UI** — the current design is functional but bare; align with the Field Notes aesthetic, add better empty states, drag-and-drop upload area
|
||||
- [ ] **Link from Admin2** — Admin2 is a compiled SPA so we can't inject a sidebar link; options: (1) add a link to the site's nav when logged in, (2) a bookmarklet, or (3) wait for Admin2 to support plugin-contributed sidebar entries
|
||||
- [ ] **Komoot integration** — explore how to pull GPX routes directly from Komoot without a manual export step. Komoot has an API (`api.komoot.de`) that returns GPX for a tour given its ID. Could be: a field on the GPX manager where you paste a Komoot tour URL/ID and it fetches + saves server-side, or a script run via `make`. Worth researching auth requirements (public tours may not need auth).
|
||||
@@ -1,368 +0,0 @@
|
||||
# Into the East — Design Spec
|
||||
|
||||
**Date:** 2026-06-18
|
||||
**Status:** Approved for implementation
|
||||
|
||||
---
|
||||
|
||||
## 1. Direction
|
||||
|
||||
**The brief:** A personal travel journal, sole author, trip to East Asia. Three weeks to implement before departure. Audience is both friends/family and the occasional curious stranger.
|
||||
|
||||
**The position:** Neither Polarsteps nor FindPenguins. Both optimize for social sharing of travel data. This site optimizes for **the story** — and should feel like reading a well-edited travel journal, not using an app.
|
||||
|
||||
**What we steal from each:**
|
||||
- Polarsteps: photography-first hierarchy, airy whitespace, map as the emotional spine of the trip
|
||||
- FindPenguins: typography as brand identity, stats as trophy case, hierarchical trip → entry structure
|
||||
|
||||
**What we do better than both:**
|
||||
- Web-native: fast, linkable, no install, works on any browser
|
||||
- Single author = pure editorial voice, no social noise
|
||||
- Full CSS control = real typographic identity, not generic app chrome
|
||||
- Editorial feel: more travel magazine, less productivity dashboard
|
||||
|
||||
**Aesthetic direction:** Field notes. The kind of journal a thoughtful traveler would carry — clean, direct, lets the photography speak. Sophisticated without effort.
|
||||
|
||||
**The one aesthetic risk:** Full-bleed hero photography with a translucent date+location overlay at the bottom of each card. The photo IS the entry card — not a thumbnail beside text. This is the single element that distinguishes this design from both reference apps and from typical blog layouts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Color System
|
||||
|
||||
### Palette
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|---|---|---|
|
||||
| `--color-ink` | `#17171A` | Primary text (near-black with cool undertone, like ink) |
|
||||
| `--color-ink-2` | `#4A4850` | Secondary text, body paragraphs |
|
||||
| `--color-ink-muted` | `#9896A0` | Labels, timestamps, captions, placeholder text |
|
||||
| `--color-paper` | `#F7F5F2` | Page background (warm paper white, not blue-white) |
|
||||
| `--color-canvas` | `#FFFFFF` | Card backgrounds, modals, form surfaces |
|
||||
| `--color-border` | `#E8E6E3` | Standard dividers, card borders |
|
||||
| `--color-border-soft` | `#F0EDEA` | Subtle section dividers |
|
||||
| `--color-accent` | `#1F6B5A` | Deep teal — brand color, links, CTAs, active states |
|
||||
| `--color-accent-hover` | `#185647` | Darkened accent for hover/pressed states |
|
||||
| `--color-accent-light` | `#EBF5F2` | Pale teal for highlight backgrounds |
|
||||
| `--color-accent-on` | `#FFFFFF` | Text on accent-colored surfaces |
|
||||
|
||||
### Rationale for accent color
|
||||
|
||||
Deep teal `#1F6B5A` was chosen over:
|
||||
- Blue (#0066cc current): too generic, too tech
|
||||
- Orange/saffron: clichéd for "Asia" travel design
|
||||
- Terracotta/cream: the most common default for lifestyle/travel blogs
|
||||
|
||||
Teal evokes bamboo, celadon porcelain, ancient jade, the color of temple gardens — all without being literal or kitsch. It works cleanly against both the warm paper background and white card surfaces.
|
||||
|
||||
---
|
||||
|
||||
## 3. Typography
|
||||
|
||||
### Fonts
|
||||
|
||||
| Role | Family | Fallback | Source |
|
||||
|---|---|---|---|
|
||||
| Display / Headings | DM Serif Display | Georgia, serif | Google Fonts |
|
||||
| UI / Body / Labels | DM Sans | -apple-system, BlinkMacSystemFont, sans-serif | Google Fonts |
|
||||
|
||||
**Google Fonts URL:**
|
||||
```
|
||||
https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap
|
||||
```
|
||||
|
||||
**Why this pairing:**
|
||||
DM Serif Display has a calligraphic quality — slightly editorial, authoritative but not stiff. Paired with DM Sans (its designed companion) the system is cohesive. DM Sans is neutral and highly legible at all sizes. Both are under-used relative to Inter/Lato/Playfair, so the combination has a distinctive voice without being trendy.
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Token | Size | Line Height | Usage |
|
||||
|---|---|---|---|
|
||||
| `--text-xs` | 0.75rem (12px) | 1.5 | Badges, captions |
|
||||
| `--text-sm` | 0.875rem (14px) | 1.5 | Meta, timestamps, labels |
|
||||
| `--text-base` | 1rem (16px) | 1.65 | Body paragraphs |
|
||||
| `--text-md` | 1.125rem (18px) | 1.55 | Lead text, intro paragraphs |
|
||||
| `--text-lg` | 1.375rem (22px) | 1.35 | Subheadings, card titles (mobile) |
|
||||
| `--text-xl` | 1.75rem (28px) | 1.25 | Entry card titles |
|
||||
| `--text-2xl` | 2.25rem (36px) | 1.2 | Page headings, entry titles (desktop) |
|
||||
| `--text-3xl` | 3rem (48px) | 1.1 | Hero entry title |
|
||||
|
||||
### Usage rules
|
||||
|
||||
- Entry titles: `--font-display`, `--text-xl` (mobile) / `--text-2xl` (desktop)
|
||||
- Site title in header: `--font-display`, `--text-lg`
|
||||
- All other UI text: `--font-ui`
|
||||
- Body paragraphs: `--font-ui`, `--text-base`, `--leading-normal`
|
||||
- Timestamps/badges: `--font-ui`, `--text-xs`, uppercase, `letter-spacing: 0.07em`
|
||||
|
||||
---
|
||||
|
||||
## 4. Spacing & Layout
|
||||
|
||||
### Spacing scale (4px base unit)
|
||||
|
||||
| Token | Value |
|
||||
|---|---|
|
||||
| `--space-1` | 0.25rem (4px) |
|
||||
| `--space-2` | 0.5rem (8px) |
|
||||
| `--space-3` | 0.75rem (12px) |
|
||||
| `--space-4` | 1rem (16px) |
|
||||
| `--space-5` | 1.25rem (20px) |
|
||||
| `--space-6` | 1.5rem (24px) |
|
||||
| `--space-8` | 2rem (32px) |
|
||||
| `--space-10` | 2.5rem (40px) |
|
||||
| `--space-12` | 3rem (48px) |
|
||||
| `--space-16` | 4rem (64px) |
|
||||
|
||||
### Layout
|
||||
|
||||
- Content max-width: `720px` (comfortable reading at any font size)
|
||||
- Page horizontal padding: `1.25rem` (mobile), `1.5rem` (desktop ≥520px)
|
||||
- Header height: `60px` (fixed, for JS offset calculations)
|
||||
- Map page: full viewport, no content max-width constraint
|
||||
|
||||
### Border radius
|
||||
|
||||
| Token | Value | Usage |
|
||||
|---|---|---|
|
||||
| `--radius-sm` | 4px | Photo corners, small chips |
|
||||
| `--radius-md` | 8px | Cards, buttons, inputs |
|
||||
| `--radius-lg` | 12px | Large cards, modals |
|
||||
| `--radius-full` | 9999px | Pills, badges |
|
||||
|
||||
### Shadows
|
||||
|
||||
| Token | Value | Usage |
|
||||
|---|---|---|
|
||||
| `--shadow-sm` | `0 1px 3px rgba(0,0,0,0.08)` | Stat blocks, subtle elevation |
|
||||
| `--shadow-md` | `0 4px 12px rgba(0,0,0,0.10)` | Cards on hover, dropdowns |
|
||||
| `--shadow-lg` | `0 8px 24px rgba(0,0,0,0.14)` | Lightbox, modals |
|
||||
|
||||
---
|
||||
|
||||
## 5. Component Inventory
|
||||
|
||||
### 5.1 Site Header
|
||||
|
||||
```
|
||||
[ into the east ] [ Journal Map Stats ]
|
||||
← accent bar across top (3px) ───────────────────────────────
|
||||
```
|
||||
|
||||
- Top border: `3px solid var(--color-accent)` — thin accent bar signals the brand color without decorating
|
||||
- Site title: DM Serif Display, `--text-lg`, no decoration
|
||||
- Nav links: DM Sans, `--text-sm`, weight 500, `--color-ink-2`
|
||||
- Active nav link: `--color-accent`, weight 600
|
||||
- Mobile: same layout, title slightly smaller, nav links compact
|
||||
- Background: `--color-canvas` (white), bottom border `1px solid var(--color-border)`
|
||||
|
||||
### 5.2 Entry Feed Card — With Photo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ [photo] │ ← full-width, 16:9, rounded corners
|
||||
│ │
|
||||
│ 18 JUN · 📍 Kyoto, Japan │ ← overlaid at bottom, gradient mask
|
||||
└─────────────────────────────────────┘
|
||||
Arrived in Tokyo ← DM Serif Display, --text-xl
|
||||
After 14 hours of flying I finally ← body excerpt, --color-ink-2
|
||||
set foot on Japanese soil...
|
||||
Read entry → ← --color-accent, --text-sm
|
||||
```
|
||||
|
||||
- Photo: `aspect-ratio: 16/9`, `object-fit: cover`, `border-radius: var(--radius-md)`
|
||||
- Photo has a `linear-gradient(to top, rgba(0,0,0,0.55), transparent)` overlay at the bottom 40%
|
||||
- Date + location sit on top of gradient in white text (`rgba(255,255,255,0.92)`)
|
||||
- On hover: photo scales to 1.03 (subtle zoom, 0.4s ease)
|
||||
- Title below photo: DM Serif Display, hover turns `--color-accent`
|
||||
- Card separation: `padding-bottom: var(--space-12)` + `border-bottom: 1px solid var(--color-border)`
|
||||
|
||||
### 5.3 Entry Feed Card — No Photo
|
||||
|
||||
When no photo is available, fall back to a text-only layout:
|
||||
|
||||
```
|
||||
18 JUN 2026 · 📍 Kyoto, Japan ← meta row, --text-sm, --color-ink-muted
|
||||
|
||||
Arrived in Tokyo ← DM Serif Display, --text-xl
|
||||
After 14 hours of flying...
|
||||
Read entry →
|
||||
```
|
||||
|
||||
- No photo container
|
||||
- Meta (date + location) on one line above title, small + muted
|
||||
|
||||
### 5.4 Single Entry Page
|
||||
|
||||
```
|
||||
Wednesday, 18 June 2026 ← --text-sm, --color-ink-muted, uppercase
|
||||
📍 Kyoto, Japan · ⛅ Partly cloudy · 22°C
|
||||
|
||||
Arrived in Tokyo ← DM Serif Display, --text-2xl / --text-3xl
|
||||
─────────────────────────────────────
|
||||
Body text content... ← --font-ui, --text-base/md
|
||||
|
||||
[Photo gallery — 2 or 3 col grid]
|
||||
|
||||
← Back to journal
|
||||
```
|
||||
|
||||
- The entry title uses `--font-display` at largest scale
|
||||
- A thin `--color-border` rule separates the header from the body
|
||||
- Body text is `--text-md` (18px) for comfortable long-form reading
|
||||
- Full-bleed hero option: if a `hero_image` is set, it spans the full content width with a bottom margin
|
||||
|
||||
### 5.5 Post Form (Author View)
|
||||
|
||||
```
|
||||
New Entry
|
||||
|
||||
Title * [________________________]
|
||||
Date & Time [2026-06-18 14:30 ]
|
||||
What happened [ ]
|
||||
today? [ ]
|
||||
[ ]
|
||||
|
||||
Photos [ + Add photos (max 4) ]
|
||||
|
||||
City [________________________]
|
||||
Country [________________________]
|
||||
|
||||
[ 📍 Get Location ] [ 🌤 Get Weather ]
|
||||
✓ Location captured: Kyoto, Japan ← status line
|
||||
|
||||
[ Post Entry ]
|
||||
```
|
||||
|
||||
UX changes from current:
|
||||
- Lat/lng inputs **hidden from the UI** (remain in the form as `display:none` for data capture, filled by JS)
|
||||
- Location status shows captured city/country + coordinates in a single line (not separate status paragraphs)
|
||||
- Photo upload area: larger touch target, visual indication of count
|
||||
- "Post Entry" button: `--color-accent` background, full-width on mobile, `min-height: 52px`
|
||||
- Form fields: `--radius-md` corners, `--color-border` border, focus ring in `--color-accent`
|
||||
- Section spacing: generous vertical rhythm on mobile
|
||||
|
||||
### 5.6 Stats Page
|
||||
|
||||
```
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ 42 │ │ 18 │
|
||||
│ days on │ │ entries │
|
||||
│ the road │ │ posted │
|
||||
└────────────┘ └────────────┘
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ 6 │ │ ~14,200 │
|
||||
│ countries │ │ km │
|
||||
│ visited │ │ traveled │
|
||||
└────────────┘ └────────────┘
|
||||
|
||||
Countries visited
|
||||
Japan · South Korea · Mongolia · Russia · Finland · Estonia
|
||||
```
|
||||
|
||||
- Numbers: `--font-display`, `--text-3xl`, `--color-accent`
|
||||
- Labels: `--font-ui`, `--text-xs`, uppercase, `--color-ink-muted`
|
||||
- Cards: white, `--shadow-sm`, `--radius-md`, centered
|
||||
|
||||
### 5.7 Map Page
|
||||
|
||||
Minimal changes — the map itself is good. Style improvements:
|
||||
- Leaflet popups: match the new design (DM Sans, `--radius-md`, `--shadow-md`)
|
||||
- Markers: keep current circle style, update color to `--color-accent`
|
||||
- Feed mini-map wrapper: match `--radius-md`, `--border`
|
||||
|
||||
---
|
||||
|
||||
## 6. UX Flows
|
||||
|
||||
### 6.1 Reader — First Visit
|
||||
|
||||
1. Land on `/tracker` (journal feed)
|
||||
2. See mini-map above fold (if entries exist) — route tells the geographic story at a glance
|
||||
3. First entry card: full-bleed hero photo with date/location overlay — immediate emotional pull
|
||||
4. Scroll through chronological entries
|
||||
5. Tap/click entry → entry detail page
|
||||
6. Navigate back via "← Back to journal"
|
||||
|
||||
**Key principle:** The reader should understand the journey spatially (mini-map) and emotionally (hero photo) before reading a single word.
|
||||
|
||||
### 6.2 Reader — Navigation
|
||||
|
||||
- Journal: primary destination, the feed
|
||||
- Map: geographic exploration mode
|
||||
- Stats: quick numbers, satisfying progress indicator
|
||||
- No account required, no social friction, no login prompt for readers
|
||||
|
||||
### 6.3 Author — Posting from Mobile
|
||||
|
||||
1. Navigate to `/post` (bookmark on home screen)
|
||||
2. Already logged in (Grav session persists) — form loads directly
|
||||
3. **Title**: tap → type (autofocused)
|
||||
4. **Date & Time**: auto-filled to now, adjust if needed
|
||||
5. **Content**: write what happened
|
||||
6. **Photos**: tap "Add photos" → camera or gallery → select up to 4
|
||||
7. **Location**: tap "📍 Get Location" → GPS fires → status shows "Kyoto, Japan · 34.985, 135.758" in one line
|
||||
8. **Weather**: tap "🌤 Get Weather" (works only if location was captured) → status shows "Partly cloudy · 22°C"
|
||||
9. **City/Country**: auto-populated from GPS is a nice-to-have for v2; in v1 type manually if needed
|
||||
10. Tap "Post Entry" → success message → 2-second pause → redirect to /tracker (new entry visible at top)
|
||||
|
||||
**Key principles:**
|
||||
- One-thumb operation for all critical actions on mobile
|
||||
- Location/weather are conveniences, not blockers — can skip both
|
||||
- Visual feedback is immediate (status line updates on GPS response)
|
||||
- After submit: don't leave author on a success message page; redirect to see their new post
|
||||
|
||||
---
|
||||
|
||||
## 7. Mobile Specifics
|
||||
|
||||
### Touch targets
|
||||
- All interactive elements: `min-height: 44px`, `min-width: 44px` (Apple HIG standard)
|
||||
- Form buttons: `min-height: 52px` on the post form (primary CTA)
|
||||
- Nav links: `padding: 0.5rem 0.75rem`
|
||||
|
||||
### Viewport concerns
|
||||
- Map page: `height: calc(100vh - 60px)`, `touch-action: none` on map container — prevents scroll trap
|
||||
- Photo lightbox: full viewport overlay, swipe-friendly (keyboard + click already implemented)
|
||||
- Form on mobile: single-column, generous input padding `0.875rem 1rem`, `font-size: 1rem` (prevents iOS zoom on focus)
|
||||
|
||||
### Performance
|
||||
- Google Fonts: loaded with `preconnect` hints
|
||||
- Images: `loading="lazy"` on all non-above-fold images (already in place)
|
||||
- Leaflet: loaded from CDN, only on pages that need it
|
||||
- No new JS frameworks — vanilla JS throughout
|
||||
|
||||
---
|
||||
|
||||
## 8. Tech Stack Decision
|
||||
|
||||
**Keep Grav CMS.** With a 3-week timeline, replacing it would consume all available time on migration rather than design improvements.
|
||||
|
||||
| Layer | Decision | Rationale |
|
||||
|---|---|---|
|
||||
| Backend | Grav CMS (PHP, Twig) — unchanged | Works, flat-file, no DB |
|
||||
| CSS | Vanilla CSS + custom properties (design tokens) | No build step, full control, ships as one file |
|
||||
| JS | Vanilla JS — unchanged | Current JS is well-structured, scope doesn't justify a framework |
|
||||
| Icons | Unicode + emoji (current) | No dependency, works everywhere |
|
||||
| Fonts | Google Fonts via CDN | Two fonts, display-swap, negligible impact |
|
||||
| Maps | Leaflet.js (current) | Already in use, no reason to change |
|
||||
| Build | None — no build pipeline | Grav's asset pipeline handles minification if needed |
|
||||
|
||||
**No Alpine.js, no TypeScript, no Tailwind.** The site has clean vanilla JS and CSS today; a redesign is about visual quality, not framework migration. Introducing a build pipeline on a 3-week timeline is a distraction.
|
||||
|
||||
---
|
||||
|
||||
## 9. What Changes From Current Design
|
||||
|
||||
| Area | Current | New |
|
||||
|---|---|---|
|
||||
| Typography | System sans-serif only | DM Serif Display for headings + DM Sans for UI |
|
||||
| Accent color | `#0066cc` (generic blue) | `#1F6B5A` (deep teal) |
|
||||
| Background | `#ffffff` (pure white) | `#F7F5F2` (warm paper) |
|
||||
| Entry cards | Thumbnail + text below | Full-bleed 16:9 photo with overlay |
|
||||
| Header | No visual identity | Accent top-border, typographic title |
|
||||
| Design tokens | Hardcoded values throughout | CSS custom properties throughout |
|
||||
| Post form | Lat/lng visible inputs | Lat/lng hidden, single status line |
|
||||
| Font loading | None | Google Fonts DM pairing |
|
||||
| Hover states | Minimal | Photo zoom, title color change |
|
||||
| Stat numbers | `#0066cc` | `--color-accent` (#1F6B5A) |
|
||||
@@ -1,193 +0,0 @@
|
||||
# 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 `<img>` 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
|
||||
@@ -1,166 +0,0 @@
|
||||
# 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 `<script>` tag:
|
||||
|
||||
```js
|
||||
var ENTRIES = [
|
||||
{
|
||||
"lat": 48.8566,
|
||||
"lng": 2.3522,
|
||||
"title": "Paris morning",
|
||||
"date": "2026-06-18",
|
||||
"url": "/tracker/2026-06-18",
|
||||
"hero": "/path/to/thumb.jpg" // null if no photo
|
||||
},
|
||||
...
|
||||
];
|
||||
```
|
||||
|
||||
**Only entries with valid lat AND lng are included** (skip entries where either is empty/null).
|
||||
|
||||
Entries sorted ascending by date (oldest first) so the route line is drawn in travel order.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 — Route Polyline
|
||||
|
||||
**What:** A colored line drawn between entry markers in chronological order.
|
||||
|
||||
**Style:**
|
||||
- Color: `#0066cc` (brand blue, matches existing CSS)
|
||||
- Weight: 3px
|
||||
- Opacity: 0.7
|
||||
- No arrow heads for v1
|
||||
|
||||
**Behavior:**
|
||||
- Line drawn between consecutive entries (by date) that have valid GPS
|
||||
- If only 1 entry: no line (just a single marker)
|
||||
- If two consecutive entries are very far apart (>5000km): line still drawn — it's a flight, expected
|
||||
|
||||
---
|
||||
|
||||
### 2.4 — Entry Markers
|
||||
|
||||
**What:** One circular marker per entry with GPS coordinates.
|
||||
|
||||
**Marker design:**
|
||||
- Custom circular marker (not default Leaflet teardrop)
|
||||
- Color: `#0066cc` fill, white border, 2px border
|
||||
- Size: 12px diameter on mobile, 14px on desktop
|
||||
- Most recent entry: larger (18px) and brighter color to indicate "current location"
|
||||
|
||||
**Popup on click/tap:**
|
||||
```
|
||||
[thumbnail if available — 120px wide, 80px tall, cover cropped]
|
||||
📅 18 June 2026
|
||||
Paris morning
|
||||
[Read entry →]
|
||||
```
|
||||
- Popup width: 180px max
|
||||
- "Read entry →" links to the entry page
|
||||
- Tapping outside popup closes it
|
||||
|
||||
**Edge cases:**
|
||||
- Two entries at the same lat/lng: Leaflet clusters or offsets them slightly (use small offset to prevent exact overlap — just add 0.0001° offset per duplicate)
|
||||
- Entry with GPS but no photo: popup shows no image, just date + title + link
|
||||
|
||||
---
|
||||
|
||||
### 2.5 — Mobile Map UX
|
||||
|
||||
**Problem:** On mobile, a map inside a scrollable page creates a scroll-trap (finger intended for page scroll gets captured by map pan).
|
||||
|
||||
**Solution:**
|
||||
- Map container is `height: calc(100vh - 60px)` (full viewport minus header)
|
||||
- Map is the primary content of the page — no scroll needed
|
||||
- `touch-action: none` on the map container prevents page scroll interference
|
||||
- Leaflet handles touch pan/zoom natively
|
||||
|
||||
---
|
||||
|
||||
### 2.6 — Navigation Link
|
||||
|
||||
**What:** "Map" link added to the site header navigation.
|
||||
|
||||
**Where:** `partials/base.html.twig` nav section — add `<a href="{{ base_url_absolute }}/map">Map</a>`
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Milestone 2)
|
||||
|
||||
- Filtering markers by date range
|
||||
- Clustering markers at low zoom levels
|
||||
- Heatmap or density visualization
|
||||
- Showing the route on the tracker feed page (Milestone 4)
|
||||
- Showing elevation profile
|
||||
- Country highlight/fill on the map
|
||||
- Offline map tiles
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `/map` page exists and returns HTTP 200
|
||||
2. Page renders a full-height interactive map
|
||||
3. All published entries with valid lat/lng appear as markers
|
||||
4. Markers are connected by a route line in date order
|
||||
5. Clicking/tapping a marker shows a popup with date, title, and link
|
||||
6. Popup link navigates to the correct entry page
|
||||
7. Most recent entry marker is visually distinct (larger/brighter)
|
||||
8. If no entries have GPS: map renders at world zoom with "No locations yet" message
|
||||
9. Map is pannable and zoomable by touch on mobile
|
||||
10. "Map" link appears in site navigation and routes to `/map`
|
||||
11. Map auto-fits to show all markers on page load
|
||||
12. Entries without lat/lng are silently excluded (no JS errors)
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Map tile layer: OpenStreetMap default tiles. Clean, recognizable, free.
|
||||
- Keep the Grav site header visible above the map — don't go full-screen (users need the nav)
|
||||
- Popup design: minimal. White background, slight box-shadow, 8px border-radius
|
||||
- Do not use any Leaflet plugins beyond the core library — keep the dependency footprint tiny
|
||||
- The map page should load fast: Leaflet is ~42KB gzipped. Tile images load progressively. No blocking.
|
||||
@@ -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
|
||||
@@ -1,91 +0,0 @@
|
||||
# Milestone 4 Spec — Mini-Map on Tracker Feed
|
||||
|
||||
**Goal:** Embed a compact interactive map above the entry feed on the tracker page, showing recent entry positions and the current location, giving readers immediate spatial context.
|
||||
|
||||
---
|
||||
|
||||
## User Stories
|
||||
|
||||
- As a reader landing on the tracker feed, I want to immediately see where Mischa currently is without having to navigate to the full map page.
|
||||
- As a reader, I want to click a marker on the mini-map and jump to that entry.
|
||||
- As a traveler (Mischa), I want the feed page to feel like a live travel dashboard, not just a blog list.
|
||||
|
||||
---
|
||||
|
||||
## Feature Details
|
||||
|
||||
### 4.1 — Mini-Map Placement
|
||||
|
||||
**Where:** At the top of `tracker.html.twig`, before the entry card list.
|
||||
|
||||
**Height:** 240px on mobile, 320px on desktop.
|
||||
|
||||
**Width:** Full width of content column (max 680px).
|
||||
|
||||
**Tile layer:** Same OpenStreetMap tiles as Milestone 2.
|
||||
|
||||
**No duplicate Leaflet load:** Leaflet is already loaded on the map page; on the tracker page, load it only if needed. Check with `if (typeof L === 'undefined')` before initializing. (In practice, the CSS and JS are loaded unconditionally from the same CDN — caching handles it.)
|
||||
|
||||
---
|
||||
|
||||
### 4.2 — What's Shown
|
||||
|
||||
- **All entries with GPS** shown as small markers (not just recent 10 — the map auto-fits to bounds)
|
||||
- **Route line** connecting them in chronological order (same style as Milestone 2)
|
||||
- **Most recent marker** highlighted (larger, brighter)
|
||||
- **No popups by default** — tapping a marker links directly to the entry (no popup intermediary for the mini-map, keeps it fast)
|
||||
- Map auto-fits bounds to all markers; if only 1 marker, zoom to 10
|
||||
|
||||
---
|
||||
|
||||
### 4.3 — Interaction
|
||||
|
||||
- Tap/click marker → navigate to entry URL directly
|
||||
- Map is pannable and zoomable (same touch handling as M2)
|
||||
- "View full map →" link below the mini-map → navigates to `/map`
|
||||
|
||||
---
|
||||
|
||||
### 4.4 — Entry Data
|
||||
|
||||
Same JSON serialization as Milestone 2 (embed `TRACKER_ENTRIES` in the Twig template). This can reuse the same data variable name if both map and tracker pages use the same template pattern.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 — Empty State
|
||||
|
||||
If no entries have GPS coordinates:
|
||||
- Mini-map hidden entirely (don't show an empty world map on the feed page)
|
||||
- Entry list still shows normally
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Milestone 4)
|
||||
|
||||
- Clustering markers at low zoom
|
||||
- Filtering by date
|
||||
- Satellite/terrain tile layers
|
||||
- Search on the mini-map
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Mini-map appears above entry cards on the tracker feed page
|
||||
2. All entries with valid lat/lng appear as markers on the mini-map
|
||||
3. Route line connects markers in date order
|
||||
4. Most recent marker is visually distinct
|
||||
5. Clicking/tapping a marker navigates directly to that entry
|
||||
6. "View full map →" link appears below the mini-map and routes to `/map`
|
||||
7. If no entries have GPS, mini-map is hidden and entry list shows normally
|
||||
8. Mini-map is pannable and zoomable by touch on mobile
|
||||
9. Mini-map does not block page scrolling on mobile (map is fixed height, not full-screen)
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Mini-map border-radius should match the card design (8px)
|
||||
- Light 1px border or subtle shadow to separate from content
|
||||
- "View full map →" in small muted text, right-aligned
|
||||
- Keep the mini-map lightweight: same Leaflet instance, no additional plugins
|
||||
@@ -1,161 +0,0 @@
|
||||
# PM Analysis — What to Build (and What to Skip)
|
||||
|
||||
*Role: Senior Product Manager. Audience: one solo traveler (Mischa), platform: Grav CMS flat-file PHP, no native app.*
|
||||
|
||||
---
|
||||
|
||||
## Starting position
|
||||
|
||||
Polarsteps and FindPenguins are native mobile apps built around:
|
||||
1. Background GPS tracking (requires OS-level access)
|
||||
2. Social networks (followers, discovery, comments)
|
||||
3. App-side video/reel processing
|
||||
|
||||
**None of these three pillars are reproducible in a web CMS.** Any plan that tries to replicate them wholesale is delusional. What we can do is cherry-pick the *outputs* — the things those apps display to readers — and build them into the blog in ways that add real value to both Mischa (the poster) and readers (friends/family following along).
|
||||
|
||||
---
|
||||
|
||||
## Feature-by-Feature Audit
|
||||
|
||||
| Feature | Makes sense solo? | Buildable in Grav+JS? | Value to readers? | Worth the cost? | Decision |
|
||||
|---|---|---|---|---|---|
|
||||
| Auto background GPS tracking | No — posting manually anyway | No — requires native app | — | — | **SKIP** |
|
||||
| Interactive map of visited locations | Yes | Yes — Leaflet.js + frontmatter lat/lng | High | High | **BUILD** |
|
||||
| Route line on map between entries | Yes | Yes — connect entry coords in order | High | Medium | **BUILD** |
|
||||
| Entry location name (city, country) | Yes | Yes — manual input on form | High | Low | **BUILD** |
|
||||
| Weather metadata per entry | Yes | Yes — Open-Meteo free API, no key needed | Medium | Medium | **BUILD** |
|
||||
| Photo gallery per entry | Yes | Yes — shortcode-gallery-plusplus installed | High | Low | **BUILD** (already partial) |
|
||||
| Hero image on feed cards | Yes | Yes — already in frontmatter | High | Low | **BUILD** |
|
||||
| Trip statistics page | Yes | Yes — compute from frontmatter | Medium | Low | **BUILD** |
|
||||
| Countries visited world map | Yes | Yes — highlight SVG or Leaflet layers | Medium | Medium | **BUILD** |
|
||||
| Follower system | No — solo blog | Would need auth + DB | None | — | **SKIP** |
|
||||
| Comments on entries | No — spam risk, no community | Would need plugin + moderation | Minimal | — | **SKIP** |
|
||||
| Social discovery / explore | No — not a platform | Would need indexing infrastructure | None | — | **SKIP** |
|
||||
| Group trip / travel buddies | No — solo trip | — | — | — | **SKIP** |
|
||||
| Reactions / likes | No | — | — | — | **SKIP** |
|
||||
| 3D flyover video | No — proprietary pipeline | No | Nice | — | **SKIP** |
|
||||
| Trip reels / short video | No — app-side processing | No | Nice | — | **SKIP** |
|
||||
| Travel book / print | No — out of scope | No | — | — | **SKIP** |
|
||||
| AI itinerary builder | No — trip already started | No | — | — | **SKIP** |
|
||||
| Flight detection | No — requires native app sensors | No | — | — | **SKIP** |
|
||||
| Delayed sharing / live location | No — blog posts after the fact | Irrelevant | — | — | **SKIP** |
|
||||
| Offline posting | Already works | Already works (Grav form offline) | — | — | **ALREADY EXISTS** |
|
||||
| Scheduled / draft posts | Already exists | Already exists (publish_date) | — | — | **ALREADY EXISTS** |
|
||||
| Step suggestions / nudges | No — push notifications not possible | No | — | — | **SKIP** |
|
||||
| Eebook / export | No — out of scope | Possible but niche | — | — | **SKIP** |
|
||||
|
||||
---
|
||||
|
||||
## What to Build — Summary
|
||||
|
||||
### Keep (already exists, just needs to work reliably)
|
||||
- Login-gated mobile posting form ✓
|
||||
- Draft and scheduled publishing ✓
|
||||
|
||||
### Build
|
||||
|
||||
**1. Entry enrichment** — make each entry richer with zero extra effort from Mischa:
|
||||
- Location name (city, country) captured at post time
|
||||
- Weather auto-fetched via Open-Meteo at post time using lat/lng
|
||||
- Photos displayed in a proper gallery (lightbox)
|
||||
- Hero image shown on feed card
|
||||
|
||||
**2. Interactive map** — the single most "Polarsteps-like" thing that's genuinely achievable:
|
||||
- `/map` page with Leaflet.js
|
||||
- Marker per entry (lat/lng from frontmatter)
|
||||
- Route line connecting entries in date order
|
||||
- Popup with title, date, thumbnail, link to entry
|
||||
- Mobile-friendly (touch pan/zoom)
|
||||
|
||||
**3. Trip statistics** — a simple stats page:
|
||||
- Days on the road (count of entries with distinct dates)
|
||||
- Entries posted
|
||||
- Countries/regions visited (derived from location name field)
|
||||
- Approx distance traveled (sum of haversine distances between GPS points)
|
||||
|
||||
---
|
||||
|
||||
## What to Skip — with reasons
|
||||
|
||||
| Feature | Reason skipped |
|
||||
|---|---|
|
||||
| Background GPS tracking | Requires native app. Grav runs on a server. |
|
||||
| Social features (followers, comments, likes) | Adds spam risk, moderation burden, zero value for a solo travel blog with a personal audience. A "share link" is enough. |
|
||||
| Video reels | App-side video processing pipeline, not available in a web CMS. |
|
||||
| 3D flyover | Proprietary rendering. Not worth building from scratch. |
|
||||
| Travel book printing | Out of scope. Mischa can use Polarsteps or FindPenguins for this if desired. |
|
||||
| AI itinerary builder | Trip is already in progress. Out of scope. |
|
||||
| Discovery / explore | Not a platform. No community. |
|
||||
| Group trips | Solo traveler. |
|
||||
| Flight detection | Requires native OS sensor access. |
|
||||
| Delayed sharing | Moot — we don't broadcast real-time location at all. |
|
||||
|
||||
---
|
||||
|
||||
## Milestone Plan
|
||||
|
||||
### Milestone 1 — Entry Enrichment (2–3 days)
|
||||
**Goal:** Every entry is richer out of the box — photo gallery works, location name shown, weather captured, hero image on feed.
|
||||
|
||||
Features:
|
||||
- Location name field (city + country) added to post form and displayed on entries/cards
|
||||
- Weather auto-fetch on post form (JS call to Open-Meteo using entered lat/lng, fills hidden fields)
|
||||
- Weather displayed on entry page
|
||||
- Photo gallery working (shortcode-gallery-plusplus or native media display)
|
||||
- Hero image shown on tracker feed cards
|
||||
|
||||
**Value:** Immediate. Makes each entry feel like a real travel log entry, not just a text post.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 2 — Interactive Map (2–3 days)
|
||||
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a route line, with popups.
|
||||
|
||||
Features:
|
||||
- New `map` page and template
|
||||
- Leaflet.js loaded from CDN (no build step)
|
||||
- Entries serialized to JSON in the template (lat/lng, title, date, url, hero_image)
|
||||
- Route polyline in chronological order
|
||||
- Marker popup: date, title, thumbnail, "Read entry →" link
|
||||
- Map added to site navigation
|
||||
|
||||
**Value:** High for readers — gives a bird's-eye view of the trip. The single most compelling "where is Mischa?" feature.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 3 — Statistics Page (1–2 days)
|
||||
**Goal:** A `/stats` page with key trip numbers.
|
||||
|
||||
Features:
|
||||
- Days on the road (first entry date to today)
|
||||
- Total entries posted
|
||||
- Unique countries visited (derived from location names)
|
||||
- Approximate distance traveled (haversine between consecutive entry GPS points)
|
||||
- Simple, scannable layout — no charts needed for v1
|
||||
|
||||
**Value:** Medium — nice context for readers, satisfying for Mischa to see progress.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 4 — Map on Tracker Feed (1 day)
|
||||
**Goal:** A mini-map showing recent positions above or alongside the feed, so the first thing readers see is "where is Mischa now?"
|
||||
|
||||
Features:
|
||||
- Small embedded Leaflet map on the tracker/feed page
|
||||
- Shows last 10 entries as markers, with the most recent highlighted
|
||||
- Route line between them
|
||||
- Tapping a marker opens the entry
|
||||
|
||||
**Value:** Medium — gives context to the feed without navigating away. Nice "current location" feel.
|
||||
|
||||
---
|
||||
|
||||
## Milestone Priority Order
|
||||
|
||||
**M1 first** — entry quality affects every post Mischa makes from day 1 of the trip. Get this right immediately.
|
||||
|
||||
**M2 second** — the map is the headline feature that makes this feel like a Polarsteps-style blog. Technically independent from M1 (uses lat/lng already in frontmatter).
|
||||
|
||||
**M3 third** — stats are a nice-to-have. Easy to add once M1 and M2 are stable.
|
||||
|
||||
**M4 fourth** — the mini-map on the feed is polish. Only worth doing once the full map (M2) is solid.
|
||||
@@ -1,217 +0,0 @@
|
||||
# QA Test Results
|
||||
|
||||
*Executed: 2026-06-18. Environment: Docker local (http://localhost:8081). Branch: experimental-polar-steps.*
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Result | Count |
|
||||
|---|---|
|
||||
| ✅ PASS (automated) | 22 |
|
||||
| ⚠️ REQUIRES MANUAL VERIFICATION | 10 |
|
||||
| ❌ FAIL | 0 |
|
||||
|
||||
All automatable tests pass. No failures found. Manual tests require a physical mobile device and/or browser session.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1 — Entry Enrichment Results
|
||||
|
||||
### TC-1.1: Location badge on entry page ✅ PASS
|
||||
```
|
||||
curl http://localhost:8081/tracker/2026-06-17.entry
|
||||
→ <p class="entry-location"> ... Amsterdam ... Netherlands ... </p>
|
||||
```
|
||||
|
||||
### TC-1.2: Weather badge on entry page ✅ PASS
|
||||
```
|
||||
curl http://localhost:8081/tracker/2026-06-17.entry
|
||||
→ <p class="entry-weather"> ⛅ Partly cloudy · 19°C </p>
|
||||
```
|
||||
|
||||
### TC-1.3: Location badge hidden when fields empty ✅ PASS (by inspection)
|
||||
Twig template uses `{% if page.header.location_city or page.header.location_country %}` — conditional confirmed. No empty `<p>` tag rendered when values absent.
|
||||
|
||||
### TC-1.4: Weather badge hidden when fields empty ✅ PASS (by inspection)
|
||||
Twig uses `{% if page.header.weather_desc or page.header.weather_temp_c %}` — same conditional pattern confirmed.
|
||||
|
||||
### TC-1.5: Hero image on tracker feed card ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
The example entry has no photos. Fallback logic is implemented (`media.images|first`) but cannot be automated without uploading a real photo.
|
||||
- **Steps:** Log into Admin → open 2026-06-17.entry → Media tab → upload a photo → reload /tracker → verify 16:9 thumbnail appears
|
||||
|
||||
### TC-1.6: Location badge on tracker feed card ✅ PASS
|
||||
```
|
||||
curl http://localhost:8081/tracker
|
||||
→ <span class="entry-location entry-location--card"> 📍 Amsterdam , Netherlands </span>
|
||||
```
|
||||
|
||||
### TC-1.7: Photo gallery and lightbox ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
No photos in example entry. Template code verified correct (iterates `page.media.images`, renders `.gallery-thumb` buttons, lightbox JS implemented). Test requires uploading photos.
|
||||
- **Steps:** Upload 2–3 photos to example entry → open /tracker/2026-06-17.entry → verify grid, click thumbnail → verify lightbox opens → press Escape → verify closes → click outside → verify closes → use arrow buttons → verify navigation
|
||||
|
||||
### TC-1.8: Post form has City/Country fields ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
Post form requires authenticated session. Fields are defined in post-form.md frontmatter: `location_city` (text), `location_country` (text), `weather_temp_c` (hidden), `weather_desc` (hidden). Template includes `forms/form.html.twig`.
|
||||
- **Steps:** Log in → open /post → verify City and Country inputs present → verify two buttons ("Get Current Location", "Get Weather") appear below form
|
||||
|
||||
### TC-1.9: Get Weather button fills fields ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
- **Steps:** Open /post on mobile → fill lat/lng (use Get Location button) → tap Get Weather → verify status shows temp and condition → submit form → verify entry has weather in Admin
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2 — Interactive Map Results
|
||||
|
||||
### TC-2.1: Map page loads with Leaflet ✅ PASS
|
||||
```
|
||||
HTTP 200 /map
|
||||
→ <div id="trip-map"></div>
|
||||
→ leaflet@1.9.4 CSS and JS from CDN present
|
||||
```
|
||||
|
||||
### TC-2.2: Entry GPS data serialized to ENTRIES JSON ✅ PASS
|
||||
```
|
||||
var ENTRIES = [{"lat":"52.367600","lng":"4.904100","title":"The Journey Begins","date":"17 Jun 2026","url":"\/tracker\/2026-06-17.entry","hero":null}];
|
||||
```
|
||||
Amsterdam entry correctly included. hero is null (no photos — expected).
|
||||
|
||||
### TC-2.3: Map renders marker and popup in browser ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
- **Steps:** Open /map in browser → verify Amsterdam marker visible → click marker → verify popup shows "The Journey Begins", date, "Read entry →" link → click link → verify navigates to entry
|
||||
|
||||
### TC-2.4: Map link in header navigation ✅ PASS
|
||||
```
|
||||
grep /tracker HTML → href="http://100.96.115.96:8081/map" ✓
|
||||
grep /map HTML → href="http://100.96.115.96:8081/map" ✓
|
||||
grep /stats HTML → href="http://100.96.115.96:8081/map" ✓
|
||||
```
|
||||
|
||||
### TC-2.5: Empty state ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
Requires temporarily removing lat/lng from test entry. Template code verified: `if (ENTRIES.length === 0)` block renders "No locations yet" message.
|
||||
|
||||
### TC-2.6: Map full-height on mobile ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
CSS: `.map-container { height: calc(100vh - 61px); }` and `.map-page .site-main { max-width: none; padding: 0; }` confirmed in stylesheet.
|
||||
- **Steps:** Open /map on phone → verify map fills screen → pinch zoom → verify map zooms, page does not scroll
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3 — Statistics Page Results
|
||||
|
||||
### TC-3.1: Stats page loads with 4 stat blocks ✅ PASS
|
||||
```
|
||||
HTTP 200 /stats
|
||||
→ grep "stat-block" count: 4 ✓
|
||||
```
|
||||
|
||||
### TC-3.2: Days on road count ✅ PASS
|
||||
```
|
||||
<span class="stat-value">1</span>
|
||||
<span class="stat-label">day on the road</span>
|
||||
```
|
||||
Entry date: 2026-06-17. Today: 2026-06-18. Difference: 1 day. ✓
|
||||
|
||||
### TC-3.3: Entries count ✅ PASS
|
||||
```
|
||||
<span class="stat-value">1</span>
|
||||
<span class="stat-label">entry posted</span>
|
||||
```
|
||||
|
||||
### TC-3.4: Countries visited ✅ PASS
|
||||
```
|
||||
<span class="stat-value">1</span>
|
||||
<span class="stat-label">country visited</span>
|
||||
Netherlands (listed below)
|
||||
```
|
||||
|
||||
### TC-3.5: Distance shows "—" for single GPS point ✅ PASS (by inspection)
|
||||
```
|
||||
GPS_POINTS = [["52.3676","4.9041"]] — 1 point only
|
||||
JS: if (GPS_POINTS.length < 2) { el.textContent = '—'; }
|
||||
stat-distance element initialized as "—" in HTML
|
||||
```
|
||||
JS behavior confirmed by code inspection. Browser render requires manual check.
|
||||
|
||||
### TC-3.6: Stats navigation link ✅ PASS
|
||||
```
|
||||
grep /tracker HTML → href=".../stats" ✓
|
||||
grep /map HTML → href=".../stats" ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4 — Mini-map on Tracker Feed Results
|
||||
|
||||
### TC-4.1: Mini-map present on tracker feed ✅ PASS
|
||||
```
|
||||
curl http://localhost:8081/tracker
|
||||
→ <div class="feed-map-wrap"> ✓
|
||||
→ <div class="feed-map" id="feed-map"> ✓
|
||||
→ <a class="feed-map-link" href=".../map">View full map →</a> ✓
|
||||
→ var FEED_ENTRIES = [{"lat":"52.3676","lng":"4.9041",...}] ✓
|
||||
→ Leaflet JS initialized ✓
|
||||
```
|
||||
|
||||
### TC-4.2: Mini-map hidden when no GPS ✅ PASS (by inspection)
|
||||
Template wraps entire mini-map in `{% if map_entries|length > 0 %}`. Confirmed no feed-map div rendered when list empty.
|
||||
|
||||
### TC-4.3: Marker click navigates to entry ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
JS: `.on('click', function() { window.location = entry.url; })` confirmed. Browser interaction required.
|
||||
- **Steps:** Open /tracker on phone → tap Amsterdam marker → verify navigates to entry page
|
||||
|
||||
### TC-4.4: Entry list visible below mini-map ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
- **Steps:** Open /tracker → verify mini-map renders → scroll down → verify entry cards below map
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting Results
|
||||
|
||||
### TC-X.1: Nav links on all pages ✅ PASS
|
||||
| Page | Journal | Map | Stats |
|
||||
|---|---|---|---|
|
||||
| /tracker | ✅ | ✅ | ✅ |
|
||||
| /map | ✅ | ✅ | ✅ |
|
||||
| /stats | ✅ | ✅ | ✅ |
|
||||
| /tracker/2026-06-17.entry | ✅ (inherited from base template) | ✅ | ✅ |
|
||||
|
||||
### TC-X.2: All pages return 200 ✅ PASS
|
||||
| Page | HTTP Status |
|
||||
|---|---|
|
||||
| /tracker | 200 ✅ |
|
||||
| /tracker/2026-06-17.entry | 200 ✅ |
|
||||
| /map | 200 ✅ |
|
||||
| /stats | 200 ✅ |
|
||||
|
||||
### TC-X.3: Mobile touch targets ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
CSS verified:
|
||||
- Nav links: `min-height: 44px; display: inline-flex; align-items: center` ✅
|
||||
- Lightbox buttons: `width: 44px; height: 44px` ✅
|
||||
- `.btn-extra`: `min-height: 44px` ✅
|
||||
- Gallery thumbs: CSS `aspect-ratio: 1` — size depends on grid width; at 2 columns on 375px, each is ~(375-16-4)/2 = ~177px ✅
|
||||
- Visual confirmation requires physical device
|
||||
|
||||
### TC-X.4: No JS errors in browser console ⚠️ REQUIRES MANUAL VERIFICATION
|
||||
Code reviewed: no obvious syntax errors, proper null checks before DOM access, Leaflet initialized after DOM ready. Console check requires browser DevTools.
|
||||
|
||||
---
|
||||
|
||||
## Issues Found
|
||||
|
||||
**None.** All automated tests pass. No broken HTML, no server errors, no template errors, no missing routes.
|
||||
|
||||
**Note on whitespace in Twig output:** Location and weather badges render with extra whitespace around values due to Twig `{% if %}` block indentation. This is cosmetic only — display is correct in browser rendering and does not affect functionality.
|
||||
|
||||
---
|
||||
|
||||
## Manual Verification Checklist for Mischa
|
||||
|
||||
When you review this branch in the morning, these items need a human eye (phone + browser):
|
||||
|
||||
- [ ] Upload 1–4 photos to a test entry, verify hero image shows on feed card
|
||||
- [ ] Upload 3 photos, open entry, verify gallery grid, tap thumbnail → lightbox opens
|
||||
- [ ] Test lightbox: Escape closes, tap outside closes, arrow buttons navigate
|
||||
- [ ] Open /post on phone (logged in), verify City/Country fields and two buttons visible
|
||||
- [ ] Tap "Get Current Location" → coordinates fill → tap "Get Weather" → weather fills
|
||||
- [ ] Submit a full form entry → verify it appears on /tracker with location badge
|
||||
- [ ] Open /map in browser → verify Amsterdam marker, click it → popup → click link
|
||||
- [ ] Open /map on phone → pinch zoom (map zooms, page doesn't scroll)
|
||||
- [ ] Open /tracker on phone → tap map marker → navigates to entry
|
||||
- [ ] Check /stats in browser → verify distance stat updates from "—" to a number once 2+ GPS entries exist
|
||||
- [ ] Check browser console on all pages → no JS errors
|
||||
@@ -1,628 +0,0 @@
|
||||
# QA Test Plan
|
||||
|
||||
*Branch: experimental-polar-steps. Tester role: Senior Staff QA Engineer.*
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
All features implemented in Phase 4 (Milestones 1–4):
|
||||
- M1: Entry enrichment (location badge, weather badge, photo gallery, hero image)
|
||||
- M2: Interactive map page
|
||||
- M3: Statistics page
|
||||
- M4: Mini-map on tracker feed
|
||||
|
||||
Test URLs:
|
||||
- Desktop: http://localhost:8081
|
||||
- Mobile: http://100.96.115.96:8081 (Tailscale — requires physical phone)
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1 — Entry Enrichment
|
||||
|
||||
### TC-1.1: Location badge on entry page
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open http://localhost:8081/tracker/2026-06-17.entry | Entry page loads (200) |
|
||||
| 2 | Look at entry header | `📍 Amsterdam, Netherlands` visible below date |
|
||||
| 3 | Inspect HTML | `<p class="entry-location">` present with city and country |
|
||||
|
||||
**Automation:** grep for `.entry-location` and "Amsterdam" in curl output
|
||||
|
||||
---
|
||||
|
||||
### TC-1.2: Weather badge on entry page
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open http://localhost:8081/tracker/2026-06-17.entry | Entry page loads |
|
||||
| 2 | Look at entry header | `⛅ Partly cloudy · 19°C` visible |
|
||||
| 3 | Inspect HTML | `<p class="entry-weather">` present |
|
||||
|
||||
**Automation:** grep for `.entry-weather` and "Partly cloudy" and "19°C"
|
||||
|
||||
---
|
||||
|
||||
### TC-1.3: Location badge hidden when fields empty
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Create test entry with no location_city/location_country | — |
|
||||
| 2 | Open that entry | No `📍` badge shown, no empty `<p>` rendered |
|
||||
|
||||
**Automation:** Check example entry before fields were added (not needed — fields are now set); create a second test entry without location
|
||||
|
||||
---
|
||||
|
||||
### TC-1.4: Weather badge hidden when fields empty
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Entry with no weather fields | No weather section in HTML |
|
||||
|
||||
**Automation:** grep for `entry-weather` in HTML — should only appear if value present
|
||||
|
||||
---
|
||||
|
||||
### TC-1.5: Hero image on tracker feed card
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open http://localhost:8081/tracker | Feed loads |
|
||||
| 2 | Entry card for 2026-06-17 | No image shown (example entry has no photos) |
|
||||
| 3 | Upload a photo to the entry via Admin media manager | — |
|
||||
| 4 | Reload tracker | Hero image shows as 16:9 thumbnail |
|
||||
|
||||
**Manual verification required:** Photo upload requires browser Admin interaction
|
||||
|
||||
---
|
||||
|
||||
### TC-1.6: Location badge on tracker feed card
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open http://localhost:8081/tracker | Feed loads |
|
||||
| 2 | Entry card | `📍 Amsterdam, Netherlands` visible |
|
||||
|
||||
**Automation:** grep feed HTML for `entry-location--card` and "Amsterdam"
|
||||
|
||||
---
|
||||
|
||||
### TC-1.7: Photo gallery renders on entry page (with photos)
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Upload 3 photos to the example entry via Admin | — |
|
||||
| 2 | Open entry page | Gallery grid appears below entry body |
|
||||
| 3 | Count thumbnails | 3 thumbnails in 2-col (mobile) / 3-col (desktop) grid |
|
||||
| 4 | Click a thumbnail | Lightbox overlay opens with full-size image |
|
||||
| 5 | Press Escape | Lightbox closes |
|
||||
| 6 | Click left/right arrow buttons | Navigates between images |
|
||||
| 7 | Click outside lightbox | Lightbox closes |
|
||||
|
||||
**Manual verification required:** Photo upload and interactive lightbox require browser
|
||||
|
||||
---
|
||||
|
||||
### TC-1.8: Post form has location and weather fields
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open http://localhost:8081/post (logged in) | Post form renders |
|
||||
| 2 | Inspect form | `City` and `Country` text inputs present |
|
||||
| 3 | Inspect form | `📍 Get Current Location` and `🌤 Get Weather` buttons present |
|
||||
|
||||
**Automation:** grep /post HTML for `location_city`, `location_country`, `get-location`, `get-weather`
|
||||
|
||||
---
|
||||
|
||||
### TC-1.9: Get Weather button fills fields
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open /post on phone | Post form loads |
|
||||
| 2 | Tap "Get Current Location" | Lat/lng fields fill with coordinates |
|
||||
| 3 | Tap "Get Weather" | Status shows "🌤 Weather set: [desc] · [temp]°C" |
|
||||
| 4 | Submit form | New entry created with weather in frontmatter |
|
||||
| 5 | Open entry in Admin | weather_temp_c and weather_desc fields populated |
|
||||
|
||||
**Manual verification required:** Geolocation and form submission require mobile browser
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2 — Interactive Map
|
||||
|
||||
### TC-2.1: Map page loads
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://localhost:8081/map | HTTP 200 |
|
||||
| 2 | Inspect HTML | `<div id="trip-map">` present |
|
||||
| 3 | Inspect HTML | Leaflet CSS and JS from CDN present |
|
||||
|
||||
**Automation:** curl + HTTP status check; grep for "trip-map" and "leaflet"
|
||||
|
||||
---
|
||||
|
||||
### TC-2.2: Entry with GPS appears in ENTRIES JSON
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | curl http://localhost:8081/map | Map page HTML |
|
||||
| 2 | grep for `var ENTRIES` | Array contains Amsterdam entry with lat 52.3676 |
|
||||
| 3 | Check entry has title, date, url | All fields present |
|
||||
|
||||
**Automation:** grep output for ENTRIES and lat value
|
||||
|
||||
---
|
||||
|
||||
### TC-2.3: Map renders marker and route in browser
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open /map in browser | Map tiles load, marker visible |
|
||||
| 2 | Click marker | Popup opens with "The Journey Begins" title and "Read entry →" link |
|
||||
| 3 | Click "Read entry →" | Navigates to entry page |
|
||||
|
||||
**Manual verification required:** Leaflet rendering requires browser
|
||||
|
||||
---
|
||||
|
||||
### TC-2.4: Map navigation link in header
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open any page | Header shows Journal, Map, Stats nav links |
|
||||
| 2 | Click Map | Navigates to /map |
|
||||
|
||||
**Automation:** grep base template output for "/map" nav link
|
||||
|
||||
---
|
||||
|
||||
### TC-2.5: Empty state (no GPS entries)
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Remove lat/lng from test entry temporarily | — |
|
||||
| 2 | Visit /map | Map at world zoom, "No locations yet" message shown |
|
||||
| 3 | Restore lat/lng | — |
|
||||
|
||||
**Manual verification required:** Requires temporarily editing entry
|
||||
|
||||
---
|
||||
|
||||
### TC-2.6: Map page is full-height on mobile
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open /map on mobile browser | Map fills screen below header |
|
||||
| 2 | Pinch to zoom | Map zooms without page scrolling |
|
||||
| 3 | Pan with finger | Map pans without page scrolling |
|
||||
|
||||
**Manual verification required:** Touch interaction requires physical device
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3 — Statistics Page
|
||||
|
||||
### TC-3.1: Stats page loads
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://localhost:8081/stats | HTTP 200 |
|
||||
| 2 | Inspect HTML | Four stat blocks present |
|
||||
|
||||
**Automation:** curl + HTTP status + grep for "stat-block"
|
||||
|
||||
---
|
||||
|
||||
### TC-3.2: Days on the road count
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | curl /stats | Page HTML |
|
||||
| 2 | grep for "days" | Shows "1 day on the road" (entry date: 2026-06-17, today: 2026-06-18) |
|
||||
|
||||
**Automation:** grep stat-value output and compare to expected day count
|
||||
|
||||
---
|
||||
|
||||
### TC-3.3: Entries count
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | curl /stats | grep for "entry posted" | Shows "1 entry posted" |
|
||||
|
||||
**Automation:** grep for "entry posted"
|
||||
|
||||
---
|
||||
|
||||
### TC-3.4: Countries visited
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | curl /stats | grep for "Netherlands" | "Netherlands" appears in countries list |
|
||||
| 2 | grep for "country visited" | Shows "1 country visited" |
|
||||
|
||||
**Automation:** grep output
|
||||
|
||||
---
|
||||
|
||||
### TC-3.5: Distance shows "—" for single GPS point
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | curl /stats | grep for GPS_POINTS | One point in array |
|
||||
| 2 | In browser, check stat-distance | Shows "—" (JS computes, needs browser) |
|
||||
|
||||
**Automation:** grep GPS_POINTS array length from page source
|
||||
|
||||
---
|
||||
|
||||
### TC-3.6: Stats navigation link
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open any page header | "Stats" link present in nav |
|
||||
| 2 | Click Stats | Navigates to /stats |
|
||||
|
||||
**Automation:** grep any page HTML for "/stats" in nav
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4 — Mini-map on Tracker Feed
|
||||
|
||||
### TC-4.1: Mini-map appears on tracker feed
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://localhost:8081/tracker | HTTP 200 |
|
||||
| 2 | grep for "feed-map" | Mini-map div present |
|
||||
| 3 | grep for "FEED_ENTRIES" | JSON array with Amsterdam entry |
|
||||
| 4 | grep for "View full map →" | Link to /map present |
|
||||
|
||||
**Automation:** curl + grep
|
||||
|
||||
---
|
||||
|
||||
### TC-4.2: Mini-map hidden when no GPS entries
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Remove lat/lng from example entry | — |
|
||||
| 2 | curl /tracker | No "feed-map" div in output |
|
||||
| 3 | Restore lat/lng | — |
|
||||
|
||||
**Manual verification:** Requires temporarily editing entry
|
||||
|
||||
---
|
||||
|
||||
### TC-4.3: Marker click navigates to entry (mobile)
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open /tracker on phone | Mini-map renders above entry list |
|
||||
| 2 | Tap Amsterdam marker | Navigates to /tracker/2026-06-17.entry |
|
||||
|
||||
**Manual verification required:** Touch interaction requires browser
|
||||
|
||||
---
|
||||
|
||||
### TC-4.4: Entry list still visible below mini-map
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open /tracker | Mini-map shows, scroll down | Entry cards visible below map |
|
||||
|
||||
**Manual verification required:** Visual layout check
|
||||
|
||||
---
|
||||
|
||||
## Post Submission Flow
|
||||
|
||||
These scenarios cover the full round-trip: filling the form → saving → verifying values in the UI and on disk. Use the exact test values specified so that each assertion can be precise.
|
||||
|
||||
**Test data (use verbatim):**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Title | `QA Test Entry` |
|
||||
| Date & Time | `2026-06-18 10:00` |
|
||||
| Content | `This is the QA test body. Second sentence for length.` |
|
||||
| City | `Tokyo` |
|
||||
| Country | `Japan` |
|
||||
| Latitude | `35.689487` |
|
||||
| Longitude | `139.691711` |
|
||||
| Photos | none (keep simple for first run) |
|
||||
|
||||
**Expected slug:** `2026-06-18-1000-qa-test-entry`
|
||||
**Expected folder:** `2026-06-18-1000-qa-test-entry.entry/`
|
||||
**Expected URL:** `/tracker/2026-06-18-1000-qa-test-entry.entry`
|
||||
|
||||
The slug is built from `date(Y-m-d-Hi)` + title lowercased with `[^a-z0-9]+` replaced by hyphens.
|
||||
|
||||
---
|
||||
|
||||
### TC-P.1: Post form requires authentication
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Open private/incognito tab (no session) | — |
|
||||
| 2 | GET http://100.96.115.96:8081/post | Page loads at /post URL (no redirect) but renders the login form inline |
|
||||
| 3 | Inspect page content | Login form fields (username, password) visible; post form fields absent |
|
||||
|
||||
**Automation:** curl /post without auth; assert `login-form-nonce` present AND `data[title]` absent
|
||||
|
||||
---
|
||||
|
||||
### TC-P.2: Post form renders all fields
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Log in at /login | Redirected to /tracker |
|
||||
| 2 | Navigate to /post | Post form page loads (200) |
|
||||
| 3 | Check form fields present | Title, Date & Time, description textarea, Photos upload |
|
||||
| 4 | Check location fields | Latitude, Longitude, City, Country inputs visible |
|
||||
| 5 | Check action buttons | `📍 Get Current Location` and `🌤 Get Weather` buttons visible |
|
||||
| 6 | Check submit button | `Post Entry` button visible |
|
||||
| 7 | Check date field default | Pre-filled with today's date and time (not blank) |
|
||||
|
||||
**Automation:** curl /post with auth; grep for `data[title]`, `data[lat]`, `data[location_city]`, `get-location`, `get-weather`
|
||||
|
||||
---
|
||||
|
||||
### TC-P.3: Required field validation
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Log in and open /post | Form loads |
|
||||
| 2 | Leave Title blank, fill in only the description | — |
|
||||
| 3 | Submit form | Page reloads with validation error on Title |
|
||||
| 4 | Error message | Indicates title is required |
|
||||
| 5 | Fill in Title, clear Description/Content, submit | Validation error on Content field |
|
||||
| 6 | Confirm | No new entry file created in pages/01.tracker/ during failed submissions |
|
||||
|
||||
**Manual verification required:** Validation feedback requires browser
|
||||
|
||||
---
|
||||
|
||||
### TC-P.4: Successful post submission — all fields
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Log in and open /post | Form loads |
|
||||
| 2 | Enter Title: `QA Test Entry` | — |
|
||||
| 3 | Set Date to `2026-06-18 10:00` | — |
|
||||
| 4 | Enter Content: `This is the QA test body. Second sentence for length.` | — |
|
||||
| 5 | Enter City: `Tokyo`, Country: `Japan` | — |
|
||||
| 6 | Enter Latitude: `35.689487`, Longitude: `139.691711` | — |
|
||||
| 7 | Leave Photos empty | — |
|
||||
| 8 | Click `Post Entry` | Form submits (POST to /post) |
|
||||
| 9 | Observe result | Success message `Entry posted successfully!` shown on page |
|
||||
| 10 | Form state | Form is reset / fields cleared |
|
||||
|
||||
**Manual verification required:** Form submission and success message require browser
|
||||
|
||||
---
|
||||
|
||||
### TC-P.5: Entry file created on disk with correct values
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | After TC-P.4 completes | — |
|
||||
| 2 | Check directory `user/pages/01.tracker/` | Folder `2026-06-18-1000-qa-test-entry.entry/` exists (add-page-by-form appends template name per `physical_template_name: true`) |
|
||||
| 3 | Read `user/pages/01.tracker/2026-06-18-1000-qa-test-entry.entry/entry.md` | File exists |
|
||||
| 4 | Verify frontmatter `title` | Equals `QA Test Entry` |
|
||||
| 5 | Verify frontmatter `date` | Equals `2026-06-18 10:00` |
|
||||
| 6 | Verify frontmatter `location_city` | Equals `Tokyo` |
|
||||
| 7 | Verify frontmatter `location_country` | Equals `Japan` |
|
||||
| 8 | Verify frontmatter `lat` | Equals `35.689487` |
|
||||
| 9 | Verify frontmatter `lng` | Equals `139.691711` |
|
||||
| 10 | Verify frontmatter `template` | Equals `entry` |
|
||||
| 11 | Verify frontmatter `published` | Equals `true` |
|
||||
| 12 | Verify page body | Contains `This is the QA test body. Second sentence for length.` |
|
||||
|
||||
**Automation:** Read file from filesystem; parse YAML frontmatter; assert each field value exactly
|
||||
|
||||
---
|
||||
|
||||
### TC-P.6: Entry appears in tracker feed
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | BUG-001 fixed — no manual cache clear needed | — |
|
||||
| 2 | GET http://100.96.115.96:8081/tracker | Page loads (200) |
|
||||
| 3 | Entry card present | Card with title `QA Test Entry` visible |
|
||||
| 4 | Date shown on card | `18 Jun 2026` |
|
||||
| 5 | Location badge on card | `📍 Tokyo, Japan` visible |
|
||||
| 6 | Entry card link | `href` points to `/tracker/2026-06-18-1000-qa-test-entry.entry` |
|
||||
| 7 | Excerpt shown | Partial text of the body content visible |
|
||||
|
||||
**Automation:** curl /tracker; grep for "QA Test Entry", "18 Jun 2026", "Tokyo", "Japan", "/tracker/2026-06-18-1000-qa-test-entry.entry"
|
||||
|
||||
---
|
||||
|
||||
### TC-P.7: Entry detail page shows correct values
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://100.96.115.96:8081/tracker/2026-06-18-1000-qa-test-entry.entry | Page loads (200) |
|
||||
| 2 | Page title | `QA Test Entry` in `<h1>` |
|
||||
| 3 | Date header | `Thursday, 18 June 2026` (or locale equivalent) |
|
||||
| 4 | Location badge | `📍 Tokyo, Japan` |
|
||||
| 5 | Body content | Full text `This is the QA test body. Second sentence for length.` rendered |
|
||||
| 6 | No gallery | Photo gallery section absent (no photos were uploaded) |
|
||||
| 7 | Back link | `← Back to journal` link present, points to /tracker |
|
||||
|
||||
**Automation:** curl /tracker/2026-06-18-1000-qa-test-entry.entry; grep for "QA Test Entry", "Tokyo", "Japan", "This is the QA test body", "Back to journal"
|
||||
|
||||
---
|
||||
|
||||
### TC-P.8: Entry appears on map and mini-map
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://100.96.115.96:8081/tracker | Mini-map section visible |
|
||||
| 2 | Inspect FEED_ENTRIES JSON | Contains entry with `lat: "35.689487"`, `lng: "139.691711"`, `title: "QA Test Entry"` |
|
||||
| 3 | GET http://100.96.115.96:8081/map | Map page loads |
|
||||
| 4 | Inspect ENTRIES JSON | Contains same entry |
|
||||
|
||||
**Automation:** curl /tracker and /map; grep FEED_ENTRIES and ENTRIES JSON for lat/lng values
|
||||
|
||||
---
|
||||
|
||||
### TC-P.9: Entry appears in stats
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | GET http://100.96.115.96:8081/stats | Page loads (200) |
|
||||
| 2 | Entries count | Shows `2` entries (existing test entry + new QA entry) |
|
||||
| 3 | Countries list | `Japan` and `Netherlands` both listed |
|
||||
|
||||
**Automation:** curl /stats; grep entry count and country names
|
||||
|
||||
---
|
||||
|
||||
### TC-P.10: Two posts on the same day
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|---|---|---|
|
||||
| 1 | Submit a first post: date `2026-06-18 10:00`, title `Morning Update` | Success message shown |
|
||||
| 2 | Submit a second post: date `2026-06-18 14:30`, title `Afternoon Update` | Success message shown |
|
||||
| 3 | Check filesystem | Two separate folders exist: `2026-06-18-1000-morning-update.entry/` and `2026-06-18-1430-afternoon-update.entry/` |
|
||||
| 4 | Visit /tracker | Both entries visible as separate cards |
|
||||
|
||||
**Note:** The slug encodes date + time + title, so same-day posts are fully supported as long as they have different times or titles. A true collision (same date, same time, same title) would silently fail — treat this as acceptable given solo use.
|
||||
|
||||
**Manual verification required:** Requires two browser submissions
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting Tests
|
||||
|
||||
### TC-X.1: Nav links present on all pages
|
||||
|
||||
| Page | Expected nav links |
|
||||
|---|---|
|
||||
| /tracker | Journal, Map, Stats |
|
||||
| /map | Journal, Map, Stats |
|
||||
| /stats | Journal, Map, Stats |
|
||||
| /tracker/2026-06-17.entry | Journal, Map, Stats |
|
||||
|
||||
**Automation:** curl each page, grep for all three links
|
||||
|
||||
---
|
||||
|
||||
### TC-X.2: All pages return 200
|
||||
|
||||
| Page | Expected HTTP status |
|
||||
|---|---|
|
||||
| / (redirects to /tracker) | 200 or 302→200 |
|
||||
| /tracker | 200 |
|
||||
| /tracker/2026-06-17.entry | 200 |
|
||||
| /map | 200 |
|
||||
| /stats | 200 |
|
||||
| /post | 200 (after login) or 302 (login redirect) |
|
||||
|
||||
**Automation:** curl HTTP status checks
|
||||
|
||||
---
|
||||
|
||||
### TC-X.3: Mobile touch targets ≥44px
|
||||
|
||||
| Element | Expected min height/width |
|
||||
|---|---|
|
||||
| Nav links | 44px height |
|
||||
| Gallery thumbnails | 44px on shortest side |
|
||||
| Lightbox close/prev/next buttons | 44px |
|
||||
| Post form buttons | 44px height |
|
||||
| "Get Location" button | 44px height |
|
||||
| "Get Weather" button | 44px height |
|
||||
|
||||
**Manual verification required:** Inspect computed CSS or measure visually on device
|
||||
|
||||
---
|
||||
|
||||
### TC-X.4: No JS errors in browser console
|
||||
|
||||
| Page | Expected |
|
||||
|---|---|
|
||||
| /tracker | No console errors |
|
||||
| /map | No console errors (may have tile 404s for tiles not in viewport — acceptable) |
|
||||
| /stats | No console errors |
|
||||
| /tracker/2026-06-17.entry | No console errors |
|
||||
|
||||
**Manual verification required:** Open browser DevTools
|
||||
|
||||
---
|
||||
|
||||
## Visual Design QA — Redesign Checklist
|
||||
|
||||
**Design spec:** `user/docs/design/design-spec.md`
|
||||
**Implementation plan:** `user/docs/working/plans/2026-06-18-ui-redesign.md`
|
||||
|
||||
### Typography
|
||||
- [ ] DM Serif Display loads for: entry titles, page headings (`h1`), stat numbers, site title
|
||||
- [ ] DM Sans loads for: body text, nav links, labels, form fields, timestamps
|
||||
- [ ] No fallback font (Georgia / system-sans) visible in place of custom fonts
|
||||
- [ ] Body text font-size ≥ 16px (no iOS zoom on form focus)
|
||||
|
||||
### Colors
|
||||
- [ ] Page background is warm paper (#F7F5F2), not pure white
|
||||
- [ ] All links and CTAs use teal (#1F6B5A), not blue (#0066cc)
|
||||
- [ ] Active nav link is teal and bold
|
||||
- [ ] Map markers and route polylines are teal
|
||||
|
||||
### Header
|
||||
- [ ] 3px teal border-top visible at top of header
|
||||
- [ ] Site title renders in DM Serif Display ("into the east")
|
||||
- [ ] Header sticks to top on scroll
|
||||
- [ ] On 320px viewport: title and nav both visible without overlap
|
||||
|
||||
### Entry feed cards
|
||||
- [ ] Cards with photos show full-bleed 16:9 image with rounded corners
|
||||
- [ ] Date + location text overlay visible on gradient at bottom of photo
|
||||
- [ ] Entry title below photo in DM Serif Display
|
||||
- [ ] Subtle photo scale animation on hover (desktop)
|
||||
- [ ] Cards without photos show date/location meta row above title
|
||||
- [ ] "Read entry →" link is teal
|
||||
|
||||
### Single entry page
|
||||
- [ ] If entry has photos: hero image spans full content width, max 480px tall
|
||||
- [ ] Entry title in DM Serif Display at large size (~48px desktop)
|
||||
- [ ] Thin border rule separates header from body text
|
||||
- [ ] Body text at 18px (--text-md)
|
||||
- [ ] "← Back to journal" footer link in teal
|
||||
|
||||
### Post form
|
||||
- [ ] Lat/lng inputs NOT visible (hidden by CSS :has() selector)
|
||||
- [ ] Inputs have rounded corners and correct border
|
||||
- [ ] Focus ring on inputs is teal, not default browser blue
|
||||
- [ ] "Post Entry" submit button is teal, full-width, ≥52px height
|
||||
- [ ] After tapping "Get Location": status line shows "✓ Location captured · lat, lng" in teal
|
||||
- [ ] After tapping "Get Weather": status line shows "✓ Weather set · desc · temp°C" in teal
|
||||
- [ ] On error: status line shows in brick red, not teal
|
||||
|
||||
### Stats page
|
||||
- [ ] Page heading "Trip Statistics" in DM Serif Display
|
||||
- [ ] Stat numbers in DM Serif Display, teal color
|
||||
- [ ] Stat cards on white background (not paper), with subtle shadow
|
||||
- [ ] Labels uppercase, muted gray, small
|
||||
|
||||
### Map page
|
||||
- [ ] Map fills viewport below header with no gap
|
||||
- [ ] Map container height uses CSS variable (not hardcoded 61px)
|
||||
- [ ] Markers are teal circles (not blue)
|
||||
- [ ] Route polyline is teal
|
||||
|
||||
### Mobile (375px viewport)
|
||||
- [ ] All pages scroll without horizontal overflow
|
||||
- [ ] Header title and nav fit in one row
|
||||
- [ ] Entry card photo fills full width
|
||||
- [ ] Post form buttons are thumb-reachable (44px+ targets)
|
||||
- [ ] Map page: map pans without page scrolling underneath (touch-action)
|
||||
|
||||
### Accessibility
|
||||
- [ ] Focus ring visible on all interactive elements (keyboard navigation)
|
||||
- [ ] With prefers-reduced-motion: no animations/transitions fire
|
||||
@@ -1,89 +0,0 @@
|
||||
# Experimental Branch Summary
|
||||
|
||||
*Branch: `experimental-polar-steps`. Ready for morning review.*
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
This branch researched Polarsteps and FindPenguins, distilled their best ideas for a solo travel blog on Grav CMS, planned four milestones, and implemented all four.
|
||||
|
||||
---
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Milestone 1 — Entry Enrichment
|
||||
- **Location badge** (`📍 City, Country`) on entry page and tracker feed cards
|
||||
- **Weather badge** (`⛅ Partly cloudy · 19°C`) on entry page header
|
||||
- **"Get Weather" button** on post form — auto-fetches via Open-Meteo (free, no key)
|
||||
- **Photo gallery** on entry pages — 2-col/3-col grid with full lightbox
|
||||
- **Hero image** on feed cards — falls back to first photo if no hero_image set
|
||||
- New post form fields: City, Country, weather auto-fill
|
||||
|
||||
### Milestone 2 — Interactive Map (`/map`)
|
||||
- Leaflet.js with OpenStreetMap tiles
|
||||
- Marker per entry with GPS, route polyline in date order
|
||||
- Most recent entry highlighted
|
||||
- Click marker → popup with date, title, link to entry
|
||||
- Full-height map, mobile touch-friendly
|
||||
|
||||
### Milestone 3 — Statistics Page (`/stats`)
|
||||
- Days on the road, entries posted, countries visited, distance traveled
|
||||
- Auto-updates as new entries are posted
|
||||
|
||||
### Milestone 4 — Mini-map on Tracker Feed
|
||||
- Compact map above the entry list on /tracker
|
||||
- Tap marker → navigates to that entry
|
||||
- Hidden when no entries have GPS
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
Three links in site header: **Journal · Map · Stats**
|
||||
|
||||
---
|
||||
|
||||
## Manual Verification Required on Mobile
|
||||
|
||||
1. Upload photos → verify gallery grid + lightbox works
|
||||
2. Upload photo → verify hero image on feed card
|
||||
3. Open /post logged in → Get Location + Get Weather buttons work end-to-end
|
||||
4. Submit full entry → verify all badges appear
|
||||
5. Open /map on phone → pinch zoom (no page scroll behind map)
|
||||
6. Open /tracker → tap mini-map marker → navigates to entry
|
||||
7. Check browser console → no JS errors
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## UI Redesign (2026-06-18)
|
||||
|
||||
Design direction: **Field Notes** — editorial travel journal aesthetic, not social app.
|
||||
|
||||
- **Typography:** DM Serif Display (headings) + DM Sans (UI/body) — loaded via Google Fonts
|
||||
- **Accent color:** Deep teal `#1F6B5A` (replaces generic blue)
|
||||
- **Background:** Warm paper `#F7F5F2`
|
||||
- **Signature element:** Full-bleed 16:9 hero photos on feed cards with translucent date/location overlay
|
||||
- **Design tokens:** `user/themes/intotheeast/css/tokens.css` — single source of truth for all values
|
||||
- **Post form:** GPS lat/lng fields hidden from UI (filled by JS), cleaner status feedback
|
||||
- **Design spec:** `user/docs/design/design-spec.md`
|
||||
- **Implementation plan:** `user/docs/working/plans/2026-06-18-ui-redesign.md`
|
||||
|
||||
---
|
||||
|
||||
## Demo Content
|
||||
|
||||
Seven sample entries for design/QA showcasing: feed, map route, stats, weather variety (including snow).
|
||||
|
||||
```bash
|
||||
make demo-load # copy entries into tracker, clear cache
|
||||
make demo-reset # remove demo entries, clear cache
|
||||
```
|
||||
|
||||
Full instructions: `user/docs/demo/README.md`
|
||||
|
||||
---
|
||||
|
||||
## What Was Skipped
|
||||
Background GPS tracking, social features, video reels, 3D flyover, printed books, AI itinerary builder — all require native apps or don't suit a solo personal blog. Full reasoning in `docs/pm-analysis.md`.
|
||||
Reference in New Issue
Block a user