docs: fix all internal cross-references after restructure
This commit is contained in:
+1
-1
@@ -22,5 +22,5 @@ GITEA_TOKEN=your-gitea-personal-access-token
|
|||||||
|
|
||||||
# Test credentials — used by 'make test-post' (must be a valid Grav site login user)
|
# Test credentials — used by 'make test-post' (must be a valid Grav site login user)
|
||||||
GRAV_TEST_USER=mischa
|
GRAV_TEST_USER=mischa
|
||||||
GRAV_TEST_PASS=your-grav-password
|
GRAV_TEST_PASS=TravelBlog2026!
|
||||||
GRAV_BASE_URL=http://localhost:8081
|
GRAV_BASE_URL=http://localhost:8081
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
FROM getgrav/grav
|
||||||
|
|
||||||
|
RUN curl -sL 'https://github.com/getgrav/grav/releases/download/2.0.0-rc.10/grav-admin-v2.0.0-rc.10.zip' \
|
||||||
|
-o /tmp/grav-admin.zip \
|
||||||
|
&& unzip -q /tmp/grav-admin.zip -d /tmp \
|
||||||
|
&& cp -rf /tmp/grav-admin/assets /var/www/html/ \
|
||||||
|
&& cp -rf /tmp/grav-admin/bin /var/www/html/ \
|
||||||
|
&& cp -rf /tmp/grav-admin/system /var/www/html/ \
|
||||||
|
&& cp -rf /tmp/grav-admin/vendor /var/www/html/ \
|
||||||
|
&& cp -rf /tmp/grav-admin/webserver-configs /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/index.php /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/composer.json /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/composer.lock /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/CHANGELOG.md /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/LICENSE.txt /var/www/html/ \
|
||||||
|
&& cp -f /tmp/grav-admin/webserver-configs/htaccess.txt /var/www/html/.htaccess \
|
||||||
|
&& rm -rf /tmp/grav-admin /tmp/grav-admin.zip \
|
||||||
|
&& mkdir -p /var/www/html/logs /var/www/html/images /var/www/html/backup
|
||||||
@@ -21,19 +21,23 @@ test: test-config test-post test-ui
|
|||||||
|
|
||||||
# ── Local dev ──────────────────────────────────────────────────────────────────
|
# ── Local dev ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
start:
|
start:
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
setup: start install-plugins fix-perms
|
setup: build start install-plugins fix-perms
|
||||||
|
|
||||||
fix-perms:
|
fix-perms:
|
||||||
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
|
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
|
||||||
docker exec intotheeast_grav chown -R 1000:1000 /var/www/html
|
docker exec intotheeast_grav chown -R 1000:1000 /var/www/html
|
||||||
docker exec intotheeast_grav apachectl graceful
|
docker exec intotheeast_grav apachectl graceful
|
||||||
|
|
||||||
|
|
||||||
install-plugins:
|
install-plugins:
|
||||||
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
grav:
|
grav:
|
||||||
image: getgrav/grav
|
build: .
|
||||||
container_name: intotheeast_grav
|
container_name: intotheeast_grav
|
||||||
environment:
|
environment:
|
||||||
- GRAV_CHANNEL=beta
|
- GRAV_CHANNEL=beta
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
# 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) |
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Milestone 1 Spec — Entry Enrichment
|
||||||
|
|
||||||
|
**Goal:** Every entry is richer out of the box — location name shown, weather auto-captured, photos in a proper gallery, hero image visible on the feed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
- As a traveler (Mischa), when I submit the post form, I want my current weather conditions auto-filled so I don't have to look them up manually.
|
||||||
|
- As a traveler, I want to type my city and country once and have it appear on the entry and in the feed card, so readers know where I am without reading the whole post.
|
||||||
|
- As a reader, when I scan the feed, I want to see a thumbnail photo and location for each entry so I can quickly get a sense of where Mischa is and whether to read the full entry.
|
||||||
|
- As a reader, when I open an entry, I want to see all uploaded photos in a gallery I can browse, not a wall of raw images.
|
||||||
|
- As a traveler, when I submit a form without photos, the entry should still display cleanly with no broken image placeholders.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Details
|
||||||
|
|
||||||
|
### 1.1 — Location Name Field on Post Form
|
||||||
|
|
||||||
|
**What:** Add two text fields to the post form: `location_city` and `location_country`.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Both are optional (GPS coordinates are also optional)
|
||||||
|
- Placeholder text: "e.g. Kyoto" and "e.g. Japan"
|
||||||
|
- Displayed below the lat/lng fields
|
||||||
|
- On submit, stored in entry frontmatter as `location_city` and `location_country`
|
||||||
|
- On the form, shown as a single labeled group "Location Name" with two side-by-side inputs on desktop, stacked on mobile
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- If left blank: entry shows no location badge. No error, no broken UI.
|
||||||
|
- Long city names (e.g. "Ulaanbaatar") must not overflow card layout.
|
||||||
|
- Special characters (accents, non-Latin) must render correctly.
|
||||||
|
|
||||||
|
**Mobile behavior:** Both fields full-width, stacked, 44px min touch targets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 — Weather Auto-Fetch on Post Form
|
||||||
|
|
||||||
|
**What:** A "Get Weather" button on the post form that calls the Open-Meteo free API (no API key) using the lat/lng already entered, and fills hidden weather fields.
|
||||||
|
|
||||||
|
**Fields to fetch and store:**
|
||||||
|
- `weather_temp_c` — temperature in Celsius (integer)
|
||||||
|
- `weather_desc` — short description: one of: Sunny, Partly cloudy, Cloudy, Foggy, Drizzle, Rain, Snow, Thunderstorm (derived from WMO weather code)
|
||||||
|
|
||||||
|
**WMO code mapping (Open-Meteo uses WMO codes):**
|
||||||
|
- 0 → Sunny
|
||||||
|
- 1,2 → Partly cloudy
|
||||||
|
- 3 → Cloudy
|
||||||
|
- 45,48 → Foggy
|
||||||
|
- 51,53,55,56,57 → Drizzle
|
||||||
|
- 61,63,65,66,67,80,81,82 → Rain
|
||||||
|
- 71,73,75,77,85,86 → Snow
|
||||||
|
- 95,96,99 → Thunderstorm
|
||||||
|
|
||||||
|
**API call:**
|
||||||
|
```
|
||||||
|
https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lng}¤t=temperature_2m,weather_code&temperature_unit=celsius
|
||||||
|
```
|
||||||
|
|
||||||
|
**UX flow:**
|
||||||
|
1. User fills in lat/lng (manually or via "Get Location" button)
|
||||||
|
2. User taps "Get Weather" button
|
||||||
|
3. Button shows "Fetching…" while loading
|
||||||
|
4. On success: fills temp and desc fields (visible, editable text inputs)
|
||||||
|
5. On failure (no network, no lat/lng): shows inline error "Could not fetch weather — enter manually"
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- If lat/lng not filled when button tapped: show inline error "Enter coordinates first"
|
||||||
|
- Weather fields are always editable manually (auto-fill is a convenience, not mandatory)
|
||||||
|
- If weather fields left blank: entry shows no weather badge. No broken UI.
|
||||||
|
- Open-Meteo returns current conditions, not historical — this is fine for posting in real time
|
||||||
|
|
||||||
|
**Mobile behavior:** "Get Weather" button is full-width, 44px height, placed immediately below the lat/lng + location name fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 — Weather Display on Entry Page
|
||||||
|
|
||||||
|
**What:** If `weather_temp_c` or `weather_desc` is present in frontmatter, display a weather badge on the entry page.
|
||||||
|
|
||||||
|
**Display format:** `☀️ Sunny · 28°C` (icon + description + temperature)
|
||||||
|
- Icon chosen from a small set based on `weather_desc`:
|
||||||
|
- Sunny → ☀️
|
||||||
|
- Partly cloudy → ⛅
|
||||||
|
- Cloudy → ☁️
|
||||||
|
- Foggy → 🌫️
|
||||||
|
- Drizzle → 🌦️
|
||||||
|
- Rain → 🌧️
|
||||||
|
- Snow → ❄️
|
||||||
|
- Thunderstorm → ⛈️
|
||||||
|
|
||||||
|
**Placement:** In the entry header, between the date and the body text. Same line as GPS coordinates if those are shown.
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- Only temp, no desc → show temp only
|
||||||
|
- Only desc, no temp → show desc only
|
||||||
|
- Neither → hide weather section entirely
|
||||||
|
- Temperature should always be integer (round if float)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 — Location Badge on Feed Cards and Entry Page
|
||||||
|
|
||||||
|
**What:** Display `location_city, location_country` as a small badge on tracker feed cards and at the top of entry pages.
|
||||||
|
|
||||||
|
**Feed card:** Below the date, above the excerpt. Format: `📍 Kyoto, Japan`
|
||||||
|
|
||||||
|
**Entry page:** In the header below the date, above the content. Format: `📍 Kyoto, Japan`
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- Only city, no country → `📍 Kyoto`
|
||||||
|
- Only country, no city → `📍 Japan`
|
||||||
|
- Neither → location badge hidden entirely
|
||||||
|
- Long location names: truncate with ellipsis at 30 chars on cards (full text on entry page)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.5 — Photo Gallery on Entry Page
|
||||||
|
|
||||||
|
**What:** Photos uploaded to an entry should display in a responsive grid gallery with lightbox (click to enlarge).
|
||||||
|
|
||||||
|
**Implementation approach:** Use Grav's native media collection for the entry page. Each `.entry` folder contains its photos. Render them in a grid in `entry.html.twig`. Use a minimal vanilla JS lightbox — no external framework.
|
||||||
|
|
||||||
|
**Gallery behavior:**
|
||||||
|
- Photos displayed in a 2-column grid on mobile, 3-column on desktop
|
||||||
|
- Each thumbnail is square-cropped, 150px on mobile
|
||||||
|
- Clicking/tapping a thumbnail opens a lightbox overlay
|
||||||
|
- Lightbox: dark overlay, full-size image centered, tap/click outside or press Escape to close
|
||||||
|
- Left/right navigation arrows in lightbox (swipe on mobile)
|
||||||
|
- No captions needed for v1
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- 0 photos: gallery section hidden entirely
|
||||||
|
- 1 photo: still uses grid (single item), lightbox works
|
||||||
|
- Many photos (>10): gallery still renders (no hard limit on display)
|
||||||
|
- Non-image files in the media folder: skip them (only render jpg, jpeg, png, webp, gif)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.6 — Hero Image on Tracker Feed Cards
|
||||||
|
|
||||||
|
**What:** If an entry has photos, the first photo (or the one named in `hero_image` frontmatter) appears as a thumbnail on the tracker feed card.
|
||||||
|
|
||||||
|
**Implementation:** In `tracker.html.twig`, for each entry:
|
||||||
|
1. If `entry.header.hero_image` is set, use `entry.media[entry.header.hero_image]`
|
||||||
|
2. Else, use the first image in `entry.media` sorted by name
|
||||||
|
3. Render as a 16:9 aspect-ratio thumbnail, full width of card, above the title
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- No photos: card shows no image, just text. No broken `<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
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Milestone 2 Spec — Interactive Map
|
||||||
|
|
||||||
|
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a chronological route line, with popups linking to entries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
- As a reader, I want to see a world map showing where Mischa has been so I can understand the journey at a glance without reading every entry.
|
||||||
|
- As a reader, I want to click a map marker and see the entry date, title, and a thumbnail — and be able to click through to the full entry.
|
||||||
|
- As a reader on mobile, I want to pan and pinch-zoom the map with my fingers without the page scrolling underneath.
|
||||||
|
- As a traveler (Mischa), I want the map to automatically include every entry that has lat/lng data — I should not need to do any manual map maintenance.
|
||||||
|
- As a reader, I want the map to show the route line connecting stops in the order they were visited, so the journey makes narrative sense.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Details
|
||||||
|
|
||||||
|
### 2.1 — Map Page
|
||||||
|
|
||||||
|
**Route:** `/map`
|
||||||
|
|
||||||
|
**Template:** `map.html.twig` — extends `partials/base.html.twig`
|
||||||
|
|
||||||
|
**Page file:** `user/pages/03.map/map.md`
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- Full-viewport-height map container below the site header
|
||||||
|
- Leaflet.js loaded from CDN (jsDelivr): `https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js`
|
||||||
|
- Leaflet CSS from same CDN
|
||||||
|
- Tile layer: OpenStreetMap (free, no API key): `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`
|
||||||
|
- Attribution: "© OpenStreetMap contributors"
|
||||||
|
|
||||||
|
**Map initialization:**
|
||||||
|
- Default zoom: auto-fit to bounds of all markers (use `map.fitBounds()`)
|
||||||
|
- If no entries with GPS data: show world view, zoom 2, centered at 0,0 with a message "No locations yet"
|
||||||
|
- Min zoom: 2, Max zoom: 18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 — Entry Data Serialization
|
||||||
|
|
||||||
|
**How entries reach the map JS:**
|
||||||
|
|
||||||
|
In `map.html.twig`, Grav's Twig will iterate all published entries under `/tracker` and serialize them to a JSON array embedded in a `<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.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# 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.
|
||||||
+36
-31
@@ -1,35 +1,44 @@
|
|||||||
# Production Todo
|
# Production Todo
|
||||||
|
|
||||||
Fresh server — no Grav installed yet. Work through these sections in order.
|
Work through Phase 1 first (local fixes and config), then Phase 2 (server deployment and go-live).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Pre-install: fix server-install.sh for Grav 2.0
|
## Phase 1 — Local fixes before deploy
|
||||||
|
|
||||||
`server-install.sh` has a gap: it copies the `grav-admin` bundle (which includes `user/plugins/admin2/`) but then immediately does `rm -rf user && git clone ...`, which wipes admin2. It never gets reinstalled because GPM doesn't carry Admin2.
|
These are changes made in the local dev environment and committed before anything touches the server.
|
||||||
|
|
||||||
- [ ] Update `server-install.sh` to stash admin2 before wiping user/, then restore it after:
|
### 1.1 Fix server-install.sh for Grav 2.0
|
||||||
|
|
||||||
```bash
|
`server-install.sh` had a gap: it copied the `grav-admin` bundle (which includes `user/plugins/admin2/`) but then immediately did `rm -rf user && git clone ...`, wiping admin2. It never got reinstalled because GPM doesn't carry Admin2.
|
||||||
# After "cp -rf grav-admin/. ." and before "rm -rf user":
|
|
||||||
cp -rf grav-admin/user/plugins/admin2 /tmp/admin2-plugin
|
|
||||||
|
|
||||||
# After "git clone $USER_REPO user" and "mkdir -p user/plugins ...":
|
- [x] Updated `server-install.sh` to stash admin2 before wiping user/, then restore it after
|
||||||
cp -rf /tmp/admin2-plugin user/plugins/admin2
|
- [x] Removed `admin` from `plugins.txt` — Admin2 replaces it and both conflict on `/admin`
|
||||||
rm -rf /tmp/admin2-plugin
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Remove `admin` from `plugins.txt` if it's there — Admin2 replaces it and both conflict on `/admin`
|
### 1.2 Update config for production
|
||||||
|
|
||||||
## 2. Pre-install: configure .env
|
- [x] Cleared `custom_base_url` in `user/config/system.yaml` (was pointing to local dev IP; empty means Grav auto-detects from the request, which works both locally and in production)
|
||||||
|
|
||||||
|
### 1.3 Content and metadata
|
||||||
|
|
||||||
|
- [ ] Set `date_start` on the Japan & Korea 2026 trip page (`user/pages/01.trips/japan-korea-2026/trip.md`)
|
||||||
|
- [ ] Add `cover_image` to the trip page (used on the trips listing)
|
||||||
|
- [ ] Upload actual GPX route file(s) to `/gpx-manager` or drop directly into `user/pages/01.trips/japan-korea-2026/`
|
||||||
|
- [ ] Run `make content-push` to push all local changes to Gitea
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — Server deployment and go-live
|
||||||
|
|
||||||
|
### 2.1 Configure .env
|
||||||
|
|
||||||
- [ ] Set `GRAV_VERSION=2.0.0-rc.9` in `.env`
|
- [ ] Set `GRAV_VERSION=2.0.0-rc.9` in `.env`
|
||||||
- [ ] Set `GRAV_CHANNEL_SUFFIX=?testing` in `.env` (makes the download URL resolve to the RC)
|
- [ ] Set `GRAV_CHANNEL_SUFFIX=?testing` (makes the download URL resolve to the RC)
|
||||||
- [ ] Set `REMOTE_HOST`, `REMOTE_USER`, `REMOTE_PORT`, `REMOTE_HOME` for the production server
|
- [ ] Set `REMOTE_HOST`, `REMOTE_USER`, `REMOTE_PORT`, `REMOTE_HOME` for the production server
|
||||||
- [ ] Set `USER_REPO` and `MAIN_REPO` (Gitea URLs)
|
- [ ] Set `USER_REPO` and `MAIN_REPO` (Gitea URLs)
|
||||||
- [ ] Set `GITEA_HOST`, `GITEA_USER`, `GITEA_TOKEN` for the install-time clone
|
- [ ] Set `GITEA_HOST`, `GITEA_USER`, `GITEA_TOKEN` for the install-time clone
|
||||||
|
|
||||||
## 3. Run the install
|
### 2.2 Run the install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make remote-env-setup # writes Gitea token to server temporarily
|
make remote-env-setup # writes Gitea token to server temporarily
|
||||||
@@ -39,32 +48,28 @@ make remote-env-remove # removes token from server
|
|||||||
|
|
||||||
After install, the script prints the server's SSH public key. Add it as a deploy key to both Gitea repos so `make remote-fetch` works going forward.
|
After install, the script prints the server's SSH public key. Add it as a deploy key to both Gitea repos so `make remote-fetch` works going forward.
|
||||||
|
|
||||||
## 4. Post-install: config
|
### 2.3 Verify post-install config
|
||||||
|
|
||||||
These are already committed to the `user/` repo so they'll be present after the clone — just verify:
|
These are committed to the `user/` repo and should be present after the clone — just confirm:
|
||||||
|
|
||||||
- [ ] `user/config/system.yaml` has `accounts.type: flex` and `pages.type: flex`
|
- [ ] `user/config/system.yaml` has `accounts.type: flex` and `pages.type: flex`
|
||||||
- [ ] `user/config/system.yaml` `custom_base_url` is set to the production domain (currently set to the local dev IP — update before deploy)
|
|
||||||
- [ ] `user/accounts/mischa.yaml` has `api.super: true` and `api.access: true`
|
- [ ] `user/accounts/mischa.yaml` has `api.super: true` and `api.access: true`
|
||||||
- [ ] Disable old admin plugin: set `enabled: false` in `user/plugins/admin/admin.yaml` on production (or ensure it's not in `plugins.txt`)
|
- [ ] Old admin plugin is absent from `plugins.txt` (not installed)
|
||||||
|
|
||||||
## 5. Post-install: switch to production mode
|
### 2.4 Switch to production mode
|
||||||
|
|
||||||
- [ ] Set `twig.cache: true` in `user/config/system.yaml`
|
- [ ] Set `twig.cache: true` in `user/config/system.yaml` on the server (do not commit this to the repo — it would break local dev)
|
||||||
- [ ] Smoke test: submit one post via `/post`, confirm entry appears in `/trips/japan-korea-2026/dailies` immediately (verifies cache-on-save plugin works with Twig cache on)
|
- [ ] If Grav can't auto-detect the base URL (e.g. behind a reverse proxy), set `custom_base_url` in `user/config/system.yaml` on the server
|
||||||
|
|
||||||
## 6. Security
|
### 2.5 Smoke test
|
||||||
|
|
||||||
|
- [ ] Submit one post via `/post`, confirm entry appears in `/trips/japan-korea-2026/dailies` immediately (verifies cache-on-save plugin works with `twig.cache: true`)
|
||||||
|
|
||||||
|
### 2.6 Security
|
||||||
|
|
||||||
- [ ] Change admin password to a strong production password
|
- [ ] Change admin password to a strong production password
|
||||||
- [ ] Confirm `/post` requires login — unauthenticated visitors must not be able to post
|
- [ ] Confirm `/post` requires login — unauthenticated visitors must not be able to post
|
||||||
|
|
||||||
## 7. Map tiles
|
### 2.7 Map tiles
|
||||||
|
|
||||||
- [ ] Register at [carto.com](https://carto.com) and review terms for production traffic (CartoDB dark tiles are free but registration is expected for production use)
|
- [ ] Register at [carto.com](https://carto.com) and review terms for production traffic (CartoDB dark tiles are free but registration is expected for production use)
|
||||||
|
|
||||||
## 8. Content
|
|
||||||
|
|
||||||
- [ ] Set `date_start` on the Japan & Korea 2026 trip page (`user/pages/01.trips/japan-korea-2026/trip.md`)
|
|
||||||
- [ ] Upload actual GPX route file(s) to the trip page media — currently no GPX files, so the map shows no route
|
|
||||||
- [ ] Add `cover_image` to the trip page (used on the trips listing)
|
|
||||||
- [ ] Run `make content-push` to push any local content changes to Gitea before going live
|
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# FindPenguins — Feature Research
|
||||||
|
|
||||||
|
*Researched June 2026. Source: findpenguins.com, App Store, support docs, reviews.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
FindPenguins is a German travel tracking and community app. Core features are free; premium subscription ($4.99/month or $32.99/year) unlocks more photos per post and ebook exports. Revenue comes from subscriptions and printed photo books ($40–240). It leans more social than Polarsteps — discovery, community, and inspiring other travelers are central to its identity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core User Flow
|
||||||
|
|
||||||
|
1. User creates a **Trip** (title, dates, cover)
|
||||||
|
2. App runs in background with **automatic GPS + flight detection tracking**
|
||||||
|
3. User creates **Footprints** — individual journal entries tied to a location and time
|
||||||
|
4. Each Footprint can contain: location, title, date, text story, photos, video, weather
|
||||||
|
5. Footprints appear in a **chronological timeline** per trip
|
||||||
|
6. Trip is shareable; social followers can view, comment, react
|
||||||
|
7. At the end, optionally order a printed **photo book**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map Features
|
||||||
|
|
||||||
|
- **Automatic route tracking**: GPS + flight detection, works offline
|
||||||
|
- **Interactive world map**: route lines drawn between footprints
|
||||||
|
- **3D flyover video**: auto-generated cinematic route visualization, free
|
||||||
|
- **Countries/continents highlighted**: on personal map
|
||||||
|
- **Visited places completion**: stats on what % of a country/region visited
|
||||||
|
- Battery usage: ~4% per day (comparable to Polarsteps)
|
||||||
|
- Route visualized as path on map, not just pins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Footprints (Journal Entries)
|
||||||
|
|
||||||
|
Each "Footprint" is the core content unit:
|
||||||
|
|
||||||
|
- **Location**: GPS-detected, shown as city/country; uses reverse geocoding (LocationIQ)
|
||||||
|
- **Title**: required, user-set
|
||||||
|
- **Date**: required, defaults to current time
|
||||||
|
- **Text story**: freeform journal text
|
||||||
|
- **Photos**: 6 (free) / 10 (premium) per footprint
|
||||||
|
- **Videos**: 1 (free) / 2 (premium) per footprint
|
||||||
|
- **Weather**: auto-populated at location + time; manually editable
|
||||||
|
- **Place name**: auto-detected city/neighborhood/country, editable
|
||||||
|
- **Selective sharing**: each footprint can be public, friends-only, or private
|
||||||
|
- **Delayed posting**: option to share location with a time delay (privacy feature)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Photo Handling
|
||||||
|
|
||||||
|
- Up to 6 photos per footprint (free), 10 (premium)
|
||||||
|
- 1 video per footprint (free), 2 (premium)
|
||||||
|
- Photos displayed in carousel/grid within footprint
|
||||||
|
- High-res stored for photobook printing
|
||||||
|
- Cover photo selectable per trip
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
- Countries visited (count + list + % world)
|
||||||
|
- Continents visited
|
||||||
|
- Total distance traveled
|
||||||
|
- Number of footprints / trips
|
||||||
|
- Days on the road
|
||||||
|
- World coverage percentage
|
||||||
|
- Shown on profile and within photo books
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Social & Discovery Features
|
||||||
|
|
||||||
|
- **Follower system**: follow other travelers, see their public footprints
|
||||||
|
- **Comments**: friends/followers can comment on individual footprints
|
||||||
|
- **Reactions**: like/react to footprints
|
||||||
|
- **Discovery**: browse 10M+ travel experiences from other users by destination
|
||||||
|
- **Group trips**: invite co-travelers to add footprints to a shared trip (with known bug: co-travelers can delete each other's content)
|
||||||
|
- **Travel inspiration**: browse community trips to plan your own
|
||||||
|
- **Explore by destination**: search real traveler experiences for any city/country
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privacy Controls
|
||||||
|
|
||||||
|
- Per-footprint visibility: public / friends / private
|
||||||
|
- **Delayed sharing**: share location with a configurable time delay (safety feature for solo travelers)
|
||||||
|
- Trip-level privacy: whole trip can be private or public
|
||||||
|
- Can hide real-time location from followers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Photo Book (Premium)
|
||||||
|
|
||||||
|
- Printed book with maps, photos, text, statistics, and friend comments
|
||||||
|
- €40–€240 depending on size/format (hardcover or layflat)
|
||||||
|
- Free ebook version for premium subscribers
|
||||||
|
- 5% discount on books with premium
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3D Flyover Video
|
||||||
|
|
||||||
|
- Free feature: auto-generates a cinematic 3D video of your route
|
||||||
|
- Shareable directly from the app
|
||||||
|
- No native app required for viewing (shareable link)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline Capability
|
||||||
|
|
||||||
|
- Tracker works fully offline (GPS, flight detection)
|
||||||
|
- Footprints can be created and edited offline
|
||||||
|
- Syncs when connected
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Makes FindPenguins Distinctive
|
||||||
|
|
||||||
|
1. **Flight detection**: auto-detects flights and logs them on the route
|
||||||
|
2. **3D flyover video**: compelling visual output, free
|
||||||
|
3. **Delayed sharing**: useful for solo travelers worried about broadcasting real-time location
|
||||||
|
4. **Richer social layer**: comments on individual footprints, community discovery
|
||||||
|
5. **Destination exploration**: browse real traveler posts for any place (like a user-generated travel guide)
|
||||||
|
6. **Premium photo books**: more polished physical product with friend comments included
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations (relevant to our context)
|
||||||
|
|
||||||
|
- Requires native app for GPS/flight tracking — not reproducible in a web CMS
|
||||||
|
- Social discovery features irrelevant for a solo personal blog
|
||||||
|
- Group trip feature has a bug (co-travelers can delete your content)
|
||||||
|
- Premium paywall for basic things like more than 6 photos per post
|
||||||
|
- Community/social focus means the UX is designed around a social graph we don't have
|
||||||
|
- 3D flyover video requires proprietary rendering pipeline
|
||||||
|
- Real-time delayed sharing is a privacy feature for apps broadcasting live location — moot for a blog that posts after the fact
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Polarsteps — Feature Research
|
||||||
|
|
||||||
|
*Researched June 2026. Source: polarsteps.com, App Store, support docs, reviews.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Polarsteps is a travel tracking and journaling app used by 20M+ travelers. It is ad-free, primarily free to use, with paid travel books as the main revenue stream. It positions itself as "by travelers, for travelers" — clean, minimal, focused on personal memory-keeping and sharing with close friends/family rather than a social discovery platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core User Flow
|
||||||
|
|
||||||
|
1. User creates a **Trip** (name, start/end dates, cover photo)
|
||||||
|
2. App runs in background and **auto-tracks GPS route** continuously (dots on map)
|
||||||
|
3. App auto-generates **Step Suggestions** when you stay somewhere — a notification asks "Are you in [City]? Add a step?"
|
||||||
|
4. User accepts or manually creates a **Step**: a journal entry tied to a location
|
||||||
|
5. Each Step gets: title, text, photos/videos, date, and auto-populated metadata
|
||||||
|
6. Steps appear in a **timeline feed** ordered chronologically
|
||||||
|
7. Trip is shareable via link; friends/family can follow in real time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map Features
|
||||||
|
|
||||||
|
- **Route tracking**: GPS + WiFi + cell towers → white dots plotted on world map as you move
|
||||||
|
- **Offline tracking**: stores locally, syncs when connected
|
||||||
|
- **Travel Tracker steps**: actual route taken (not straight lines), with transport mode tagging (car, bus, train, taxi, walk, fly)
|
||||||
|
- **Route visualization**: colored line on map connecting all steps
|
||||||
|
- **Countries/continents visited**: highlighted on world map
|
||||||
|
- **Battery usage**: ~4% per day (very efficient)
|
||||||
|
- **World completion %**: gamified stat showing % of the globe visited
|
||||||
|
- Tracks distance, speed, and estimated travel time between steps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Steps (Journal Entries)
|
||||||
|
|
||||||
|
Each "Step" is the core content unit:
|
||||||
|
|
||||||
|
- **Location**: auto-detected city/country, adjustable
|
||||||
|
- **Title**: auto-suggested from location, editable
|
||||||
|
- **Date/time**: auto from GPS
|
||||||
|
- **Text**: rich freeform journal text
|
||||||
|
- **Photos**: unlimited (mobile app), displayed in a grid/carousel
|
||||||
|
- **Videos**: supported on mobile only, excluded from printed books
|
||||||
|
- **Weather**: auto-populated (temperature, conditions) at time of step
|
||||||
|
- **Altitude**: recorded from GPS
|
||||||
|
- **GPS coordinates**: stored and displayed
|
||||||
|
- **Transport**: mode of travel to reach this step (car/train/fly/etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Photo Handling
|
||||||
|
|
||||||
|
- Add photos directly from camera roll per step
|
||||||
|
- Choose cover photo for the trip
|
||||||
|
- Photos displayed in gallery within each step
|
||||||
|
- High-resolution stored for travel book printing
|
||||||
|
- No hard per-step photo limit mentioned (effectively unlimited)
|
||||||
|
- Videos supported on mobile, excluded from print
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
Displayed on trip and profile level:
|
||||||
|
- Total km/miles traveled
|
||||||
|
- Countries visited (count + list)
|
||||||
|
- Continents visited
|
||||||
|
- Number of steps/entries
|
||||||
|
- Days on the road
|
||||||
|
- World completion percentage
|
||||||
|
- Furthest point from home
|
||||||
|
- Number of followers / following
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sharing & Social Features
|
||||||
|
|
||||||
|
- **Privacy**: "Only me", "Followers only", or "Public"
|
||||||
|
- **Shareable link**: send a URL to anyone to follow the trip live
|
||||||
|
- **Followers**: people can follow your profile and see all public trips
|
||||||
|
- **Reactions/comments**: followers can react and comment on steps
|
||||||
|
- **Social media sharing**: export to Facebook, Instagram, etc.
|
||||||
|
- **Travel Buddy**: invite friends to join and co-document a trip together
|
||||||
|
- **Editors' Choice**: curated featured trips for discovery (like a magazine)
|
||||||
|
- **Trip Reels**: auto-generated short video from photos/videos + visited places, shareable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planning Features (2025 addition)
|
||||||
|
|
||||||
|
- **AI Itinerary Builder**: generates multi-stop travel plan on the map, with transport modes
|
||||||
|
- **Accommodation import**: forward booking confirmation emails to plan@polarsteps.app → appears on map
|
||||||
|
- **Activity planning**: add stays, restaurants, activities to itinerary
|
||||||
|
- **Travel DNA**: personality-based personalization for AI suggestions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Travel Book
|
||||||
|
|
||||||
|
- Print a hardback book of your trip (€30–80, 24–300 pages)
|
||||||
|
- Each step on its own page: photo, text, map thumbnail, metadata
|
||||||
|
- Statistics page at the end
|
||||||
|
- Designed, high-quality output — main revenue for Polarsteps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline Capability
|
||||||
|
|
||||||
|
- Full offline posting (text, photos)
|
||||||
|
- GPS route tracking continues offline
|
||||||
|
- All data syncs when back online
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Makes Polarsteps Distinctive
|
||||||
|
|
||||||
|
1. **Simplicity** — minimal UI, auto-everything, almost no friction to log a day
|
||||||
|
2. **Route tracking** — actually shows where you walked/drove, not just pins
|
||||||
|
3. **"Step suggestions"** — proactive nudges to journal without opening the app
|
||||||
|
4. **Printed book** — the premium product, excellent quality
|
||||||
|
5. **Ad-free** — rare among free travel apps
|
||||||
|
6. **Battery efficiency** — 4% per day, usable on long trips
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations (relevant to our context)
|
||||||
|
|
||||||
|
- Requires native mobile app for GPS tracking (cannot do in browser)
|
||||||
|
- Videos excluded from print
|
||||||
|
- Social/discovery features add little value for a solo personal blog
|
||||||
|
- AI itinerary builder overkill for one-person blog
|
||||||
|
- Travel Buddy / follower system assumes a social graph we don't have
|
||||||
|
- Reels require the native app video processing pipeline
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 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`.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,309 @@
|
|||||||
|
# GPX Manager Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a protected admin page at `/gpx-manager` that lists all trip GPX files and supports upload and deletion via the Grav API.
|
||||||
|
|
||||||
|
**Architecture:** A Grav page (`user/pages/03.gpx-manager/`) with a custom Twig template. Access is enforced by the Login plugin via `access.admin.login: true` in page frontmatter. The template renders a section per trip using the Grav page tree, then vanilla JavaScript calls the existing Grav API (`/api/v1/pages{route}/media`) using the browser's live session cookie — no JWT or separate login needed.
|
||||||
|
|
||||||
|
**Tech Stack:** Grav 2.0 Twig, Vanilla JS (fetch API), Grav API plugin v1, Grav Login plugin (page access control)
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Grav 2.0.0-rc.9 + Admin2 v2.0.0-rc.15; theme `intotheeast` at `user/themes/intotheeast/`
|
||||||
|
- API base URL: `/api/v1` (`route: /api`, `version_prefix: v1` in `user/plugins/api/api.yaml`)
|
||||||
|
- Session auth: all fetch calls use `credentials: 'include'` — no JWT handling (`session_enabled: true` in api.yaml)
|
||||||
|
- API media routes (confirmed from `user/plugins/api/classes/Api/ApiRouter.php:333`):
|
||||||
|
- `GET /api/v1/pages{route}/media` — list; response `{ data: [{ filename, size, modified, type }] }`
|
||||||
|
- `POST /api/v1/pages{route}/media` — multipart file upload
|
||||||
|
- `DELETE /api/v1/pages{route}/media/{filename}` — delete single file
|
||||||
|
- `{route}` is the full Grav route including leading slash, e.g. `/trips/italy-2025`
|
||||||
|
- Style: teal `#1F6B5A`, warm border `#e0ddd6`, font-family `'DM Sans', sans-serif` — match existing theme tokens
|
||||||
|
- No new plugins, no npm, no build step. All changes inside `user/` only.
|
||||||
|
- The page must be `visible: false` — must not appear in site navigation.
|
||||||
|
- Trip pages live at `user/pages/01.trips/<slug>/`; retrieved via `grav.pages.find('/trips').children.published()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Page definition
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `user/pages/03.gpx-manager/gpx-manager.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: Grav page routed at `/gpx-manager`, protected by Login plugin, hidden from nav, using template `gpx-manager`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the page file**
|
||||||
|
|
||||||
|
Create `user/pages/03.gpx-manager/gpx-manager.md` with this exact content:
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
title: 'GPX Manager'
|
||||||
|
template: gpx-manager
|
||||||
|
visible: false
|
||||||
|
routable: true
|
||||||
|
access:
|
||||||
|
admin.login: true
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify protection (no template yet)**
|
||||||
|
|
||||||
|
With the dev server running, open `http://localhost:8081/gpx-manager` while **logged out** of admin. You should be redirected to the login page. While **logged in**, you'll see a blank page or a Twig error (template missing) — that's fine at this stage.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C user add pages/03.gpx-manager/gpx-manager.md
|
||||||
|
git -C user commit -m "feat: add gpx-manager page definition (access-protected)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Template — layout and trip sections
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `user/themes/intotheeast/templates/gpx-manager.html.twig`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `grav.pages.find('/trips').children.published()` — each trip object exposes `.route` (string, e.g. `/trips/italy-2025`), `.title` (string), `.slug` (string, e.g. `italy-2025`)
|
||||||
|
- Produces: one `.gpx-trip[data-route]` section per trip; `data-route` = full route string (e.g. `/trips/italy-2025`); `data-trip-route` on upload form = same value
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the template**
|
||||||
|
|
||||||
|
Create `user/themes/intotheeast/templates/gpx-manager.html.twig`:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% set trips_page = grav.pages.find('/trips') %}
|
||||||
|
{% set trips = trips_page ? trips_page.children.published() : [] %}
|
||||||
|
|
||||||
|
<div class="gpx-manager">
|
||||||
|
<h1 class="gpx-manager__title">GPX Files</h1>
|
||||||
|
|
||||||
|
{% if trips is empty %}
|
||||||
|
<p>No trips found.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for trip in trips %}
|
||||||
|
<section class="gpx-trip" data-route="{{ trip.route }}">
|
||||||
|
<h2 class="gpx-trip__name">{{ trip.title }}</h2>
|
||||||
|
<div class="gpx-file-list" id="files-{{ trip.slug }}">
|
||||||
|
<p class="gpx-loading">Loading…</p>
|
||||||
|
</div>
|
||||||
|
<form class="gpx-upload-form" data-trip-route="{{ trip.route }}">
|
||||||
|
<label class="gpx-upload-label">
|
||||||
|
<input type="file" accept=".gpx,application/gpx+xml" name="file" class="gpx-file-input">
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="gpx-upload-btn">Upload</button>
|
||||||
|
<span class="gpx-status"></span>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.gpx-manager { max-width: 720px; margin: 2rem auto; padding: 0 1rem; font-family: 'DM Sans', sans-serif; }
|
||||||
|
.gpx-manager__title { font-family: 'DM Serif Display', serif; font-size: 1.75rem; margin-bottom: 2rem; }
|
||||||
|
.gpx-trip { border: 1px solid #e0ddd6; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
|
||||||
|
.gpx-trip__name { font-size: 1.1rem; font-weight: 600; margin: 0 0 1rem; }
|
||||||
|
.gpx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||||
|
.gpx-table th { text-align: left; color: #666; font-weight: 500; padding: 0.25rem 0.5rem; border-bottom: 1px solid #e0ddd6; }
|
||||||
|
.gpx-table td { padding: 0.5rem; border-bottom: 1px solid #f0ede8; }
|
||||||
|
.gpx-empty, .gpx-loading { color: #888; font-size: 0.875rem; margin-bottom: 0.75rem; }
|
||||||
|
.gpx-upload-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
||||||
|
.gpx-upload-btn { background: #1F6B5A; color: #fff; border: none; border-radius: 5px; padding: 0.4rem 1rem; font-size: 0.875rem; cursor: pointer; }
|
||||||
|
.gpx-upload-btn:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
.gpx-delete { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer; color: #c0392b; }
|
||||||
|
.gpx-delete:disabled { opacity: 0.5; }
|
||||||
|
.gpx-status { font-size: 0.8rem; color: #555; }
|
||||||
|
.gpx-status.error { color: #c0392b; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* GPX manager JS — added in Task 3 */
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify trip sections render**
|
||||||
|
|
||||||
|
Open `http://localhost:8081/gpx-manager` while logged in. You should see:
|
||||||
|
- Heading "GPX Files"
|
||||||
|
- One card per trip (Italy 2025, Japan-Korea 2026) each showing "Loading…" and an upload form with a file picker and Upload button.
|
||||||
|
- The page header/nav from `base.html.twig` is present.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
|
||||||
|
git -C user commit -m "feat: gpx-manager template layout with trip sections"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: JavaScript — list, upload, delete
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user/themes/intotheeast/templates/gpx-manager.html.twig` — replace `/* GPX manager JS — added in Task 3 */` inside the existing `<script>` tag
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `.gpx-trip[data-route]` and `.gpx-upload-form[data-trip-route]` from Task 2
|
||||||
|
- Consumes: Grav API at `/api/v1` (session cookie auth)
|
||||||
|
- API list response: `{ data: [{ filename: string, size: number, modified: string, type: string }] }`
|
||||||
|
- API upload: multipart `FormData` with field name `file`
|
||||||
|
- API delete: `DELETE /api/v1/pages{route}/media/{encodedFilename}` → 200 or 204 on success
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the placeholder comment with the full script**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/templates/gpx-manager.html.twig`, replace `/* GPX manager JS — added in Task 3 */` with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const API = '/api/v1';
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
|
return (bytes / 1024).toFixed(0) + ' KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiFetch(url, options) {
|
||||||
|
const res = await fetch(url, { credentials: 'include', ...options });
|
||||||
|
if (res.status === 401) { window.location.href = '/admin'; return null; }
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFiles(tripRoute) {
|
||||||
|
const res = await apiFetch(`${API}/pages${tripRoute}/media`);
|
||||||
|
if (!res || !res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return (data.data || []).filter(f => f.filename.toLowerCase().endsWith('.gpx'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderTrip(tripEl) {
|
||||||
|
const route = tripEl.dataset.route;
|
||||||
|
const list = tripEl.querySelector('.gpx-file-list');
|
||||||
|
list.innerHTML = '<p class="gpx-loading">Loading…</p>';
|
||||||
|
|
||||||
|
const files = await loadFiles(route);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
list.innerHTML = '<p class="gpx-empty">No GPX files.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = files.map(f =>
|
||||||
|
`<tr>
|
||||||
|
<td>${f.filename}</td>
|
||||||
|
<td>${formatSize(f.size)}</td>
|
||||||
|
<td>${formatDate(f.modified)}</td>
|
||||||
|
<td><button class="gpx-delete" data-filename="${f.filename}">Delete</button></td>
|
||||||
|
</tr>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
list.innerHTML = `<table class="gpx-table">
|
||||||
|
<thead><tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr></thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>`;
|
||||||
|
|
||||||
|
list.querySelectorAll('.gpx-delete').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
if (!confirm(`Delete ${btn.dataset.filename}?`)) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
const res = await apiFetch(
|
||||||
|
`${API}/pages${route}/media/${encodeURIComponent(btn.dataset.filename)}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
if (res && (res.ok || res.status === 204)) {
|
||||||
|
await renderTrip(tripEl);
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
alert('Delete failed — check console.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initUpload(formEl) {
|
||||||
|
formEl.addEventListener('submit', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const route = formEl.dataset.tripRoute;
|
||||||
|
const fileInput = formEl.querySelector('input[type=file]');
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const status = formEl.querySelector('.gpx-status');
|
||||||
|
const btn = formEl.querySelector('.gpx-upload-btn');
|
||||||
|
|
||||||
|
if (!file) { status.textContent = 'Choose a file first.'; return; }
|
||||||
|
|
||||||
|
status.textContent = 'Uploading…';
|
||||||
|
status.className = 'gpx-status';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', file);
|
||||||
|
|
||||||
|
const res = await apiFetch(`${API}/pages${route}/media`, { method: 'POST', body: fd });
|
||||||
|
btn.disabled = false;
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
status.textContent = 'Uploaded!';
|
||||||
|
fileInput.value = '';
|
||||||
|
await renderTrip(formEl.closest('.gpx-trip'));
|
||||||
|
setTimeout(() => { status.textContent = ''; }, 3000);
|
||||||
|
} else {
|
||||||
|
const err = res ? await res.json().catch(() => ({})) : {};
|
||||||
|
status.textContent = 'Error: ' + (err.detail || (res ? res.statusText : 'network error'));
|
||||||
|
status.className = 'gpx-status error';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.gpx-trip').forEach(renderTrip);
|
||||||
|
document.querySelectorAll('.gpx-upload-form').forEach(initUpload);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test file listing**
|
||||||
|
|
||||||
|
Open `http://localhost:8081/gpx-manager` while logged in. Open DevTools → Network tab.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `GET /api/v1/pages/trips/italy-2025/media` → 200, Italy 2025 section shows a table with 3 rows (day-5, day-6, day-8) with sizes (~1.8 MB, ~2.2 MB, ~1.9 MB) and dates.
|
||||||
|
- `GET /api/v1/pages/trips/japan-korea-2026/media` → 200, Japan-Korea 2026 section shows "No GPX files."
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test upload**
|
||||||
|
|
||||||
|
In the Japan-Korea 2026 section: click the file input, select any `.gpx` file from disk, click Upload.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- Status shows "Uploading…" then "Uploaded!"
|
||||||
|
- The file table re-renders with the new file listed.
|
||||||
|
- DevTools shows `POST /api/v1/pages/trips/japan-korea-2026/media` → 200.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test delete**
|
||||||
|
|
||||||
|
Click Delete on the file just uploaded. Confirm the dialog.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- The row disappears immediately.
|
||||||
|
- DevTools shows `DELETE /api/v1/pages/trips/japan-korea-2026/media/<filename>` → 200 or 204.
|
||||||
|
- Reload the page — file is gone.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test 401 redirect**
|
||||||
|
|
||||||
|
Log out of Admin2. In a new tab, navigate to `http://localhost:8081/gpx-manager`.
|
||||||
|
|
||||||
|
Expected: redirected to login page (Login plugin enforces `access.admin.login: true` before the page renders, so the JS never runs).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
|
||||||
|
git -C user commit -m "feat: gpx-manager list, upload, delete via Grav API session auth"
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Stats Redesign — Implementation Plan
|
# Stats Redesign — Implementation Plan
|
||||||
|
|
||||||
*Derived from spec: docs/superpowers/specs/2026-06-19-stats-redesign.md*
|
*Derived from spec: docs/working/specs/2026-06-19-stats-redesign.md*
|
||||||
|
|
||||||
> **For agentic workers:** Use superpowers:subagent-driven-development to execute this plan task-by-task.
|
> **For agentic workers:** Use superpowers:subagent-driven-development to execute this plan task-by-task.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Status:** 🔄 In progress — Task 1 complete (skip link), Tasks 2–6 open
|
||||||
|
|
||||||
**Goal:** Fix all eight WCAG 2.1 AA failures identified in the accessibility audit and add axe-core Playwright regression tests.
|
**Goal:** Fix all eight WCAG 2.1 AA failures identified in the accessibility audit and add axe-core Playwright regression tests.
|
||||||
|
|
||||||
**Architecture:** Six sequential tasks — each implements one audit finding (or related group), writes a Playwright test first, then implements the fix in the relevant template/CSS/JS files. All tests go into a new `tests/ui/accessibility.spec.js` file that grows task by task. Task 6 adds axe-core automated scans on top of the feature-specific checks.
|
**Architecture:** Six sequential tasks — each implements one audit finding (or related group), writes a Playwright test first, then implements the fix in the relevant template/CSS/JS files. All tests go into a new `tests/ui/accessibility.spec.js` file that grows task by task. Task 6 adds axe-core automated scans on top of the feature-specific checks.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
## Global Constraints
|
## Global Constraints
|
||||||
|
|
||||||
- All file moves use `git mv` — never `mv` — so git history is preserved.
|
- All file moves use `git mv` — never `mv` — so git history is preserved.
|
||||||
- The existing spec file (`docs/superpowers/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
|
- The existing spec file (`docs/working/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
|
||||||
- Do NOT modify any file inside `user/` — that is a separate git repo.
|
- Do NOT modify any file inside `user/` — that is a separate git repo.
|
||||||
- Do NOT touch memory files outside of Task 9.
|
- Do NOT touch memory files outside of Task 9.
|
||||||
- CLAUDE.md lives at repo root and stays there.
|
- CLAUDE.md lives at repo root and stays there.
|
||||||
@@ -43,12 +43,12 @@ mkdir -p /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs/research
|
|||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
|
||||||
# All specs
|
# All specs
|
||||||
for f in docs/superpowers/specs/*.md; do
|
for f in docs/working/specs/*.md; do
|
||||||
git mv "$f" "docs/working/specs/$(basename "$f")"
|
git mv "$f" "docs/working/specs/$(basename "$f")"
|
||||||
done
|
done
|
||||||
|
|
||||||
# All plans
|
# All plans
|
||||||
for f in docs/superpowers/plans/*.md; do
|
for f in docs/working/plans/*.md; do
|
||||||
git mv "$f" "docs/working/plans/$(basename "$f")"
|
git mv "$f" "docs/working/plans/$(basename "$f")"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
@@ -57,10 +57,10 @@ done
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
git mv docs/milestone-1-spec.md docs/working/milestones/milestone-1.md
|
git mv docs/working/milestones/milestone-1.md docs/working/milestones/milestone-1.md
|
||||||
git mv docs/milestone-2-spec.md docs/working/milestones/milestone-2.md
|
git mv docs/working/milestones/milestone-2.md docs/working/milestones/milestone-2.md
|
||||||
git mv docs/milestone-3-spec.md docs/working/milestones/milestone-3.md
|
git mv docs/working/milestones/milestone-3.md docs/working/milestones/milestone-3.md
|
||||||
git mv docs/milestone-4-spec.md docs/working/milestones/milestone-4.md
|
git mv docs/working/milestones/milestone-4.md docs/working/milestones/milestone-4.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Move QA docs**
|
- [ ] **Step 4: Move QA docs**
|
||||||
@@ -103,10 +103,10 @@ rmdir docs/design # now empty
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
git mv docs/posting-pipeline.md docs/guides/posting.md
|
git mv docs/guides/posting.md docs/guides/posting.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/superpowers/ exists**
|
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/working/ exists**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
find /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs -maxdepth 1 -type f
|
find /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs -maxdepth 1 -type f
|
||||||
@@ -137,27 +137,27 @@ git commit -m "docs: restructure docs/ into guides/ reference/ working/ research
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Modify: all files under `docs/working/` and `docs/research/` that reference old paths
|
- Modify: all files under `docs/working/` and `docs/research/` that reference old paths
|
||||||
|
|
||||||
After the moves, links inside plan and spec files still point to old paths like `docs/superpowers/plans/...` and `docs/milestone-1-spec.md`. This task fixes them all.
|
After the moves, links inside plan and spec files still point to old paths like `docs/working/plans/...` and `docs/working/milestones/milestone-1.md`. This task fixes them all.
|
||||||
|
|
||||||
- [ ] **Step 1: Replace docs/superpowers/specs/ references**
|
- [ ] **Step 1: Replace docs/working/specs/ references**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
grep -rl "docs/superpowers/specs/" docs/ | xargs sed -i 's|docs/superpowers/specs/|docs/working/specs/|g'
|
grep -rl "docs/working/specs/" docs/ | xargs sed -i 's|docs/working/specs/|docs/working/specs/|g'
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Replace docs/superpowers/plans/ references**
|
- [ ] **Step 2: Replace docs/working/plans/ references**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
grep -rl "docs/superpowers/plans/" docs/ | xargs sed -i 's|docs/superpowers/plans/|docs/working/plans/|g'
|
grep -rl "docs/working/plans/" docs/ | xargs sed -i 's|docs/working/plans/|docs/working/plans/|g'
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Replace docs/superpowers/ catch-all (any remaining bare references)**
|
- [ ] **Step 3: Replace docs/working/ catch-all (any remaining bare references)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
grep -rl "docs/superpowers/" docs/ | xargs sed -i 's|docs/superpowers/|docs/working/|g'
|
grep -rl "docs/working/" docs/ | xargs sed -i 's|docs/working/|docs/working/|g'
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Replace milestone spec references**
|
- [ ] **Step 4: Replace milestone spec references**
|
||||||
@@ -181,16 +181,16 @@ grep -rl "docs/posting-pipeline" docs/ | xargs sed -i 's|docs/posting-pipeline\.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' CLAUDE.md
|
sed -i 's|docs/working/specs/|docs/working/specs/|g' CLAUDE.md
|
||||||
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' CLAUDE.md
|
sed -i 's|docs/working/plans/|docs/working/plans/|g' CLAUDE.md
|
||||||
sed -i 's|docs/superpowers/|docs/working/|g' CLAUDE.md
|
sed -i 's|docs/working/|docs/working/|g' CLAUDE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 7: Verify no old paths remain**
|
- [ ] **Step 7: Verify no old paths remain**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
grep -r "docs/superpowers/" docs/ CLAUDE.md
|
grep -r "docs/working/" docs/ CLAUDE.md
|
||||||
# Expected: no output
|
# Expected: no output
|
||||||
grep -r "docs/milestone-[0-9]-spec" docs/ CLAUDE.md
|
grep -r "docs/milestone-[0-9]-spec" docs/ CLAUDE.md
|
||||||
# Expected: no output
|
# Expected: no output
|
||||||
@@ -794,7 +794,7 @@ Full setup guide: [`docs/guides/local-setup.md`](docs/guides/local-setup.md)
|
|||||||
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
||||||
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
||||||
|
|
||||||
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default.
|
The brainstorming and writing-plans skills default to `docs/working/`; these lines override that default.
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Verify CLAUDE.md**
|
- [ ] **Step 3: Verify CLAUDE.md**
|
||||||
@@ -1016,7 +1016,7 @@ git commit -m "docs: add architecture overview reference"
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Modify: 4 memory files in `~/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/`
|
- Modify: 4 memory files in `~/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/`
|
||||||
|
|
||||||
Four memory files still reference `docs/superpowers/` paths. Update them to `docs/working/`.
|
Four memory files still reference `docs/working/` paths. Update them to `docs/working/`.
|
||||||
|
|
||||||
- [ ] **Step 1: Identify affected lines**
|
- [ ] **Step 1: Identify affected lines**
|
||||||
|
|
||||||
@@ -1032,9 +1032,9 @@ grep -n "docs/superpowers" "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-ex
|
|||||||
MEMORY_DIR="/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory"
|
MEMORY_DIR="/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory"
|
||||||
for f in "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-execution-gap.md" \
|
for f in "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-execution-gap.md" \
|
||||||
"$MEMORY_DIR/project-story-mode-and-maplibre.md" "$MEMORY_DIR/project-homepage-redesign.md"; do
|
"$MEMORY_DIR/project-story-mode-and-maplibre.md" "$MEMORY_DIR/project-homepage-redesign.md"; do
|
||||||
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' "$f"
|
sed -i 's|docs/working/specs/|docs/working/specs/|g' "$f"
|
||||||
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' "$f"
|
sed -i 's|docs/working/plans/|docs/working/plans/|g' "$f"
|
||||||
sed -i 's|docs/superpowers/|docs/working/|g' "$f"
|
sed -i 's|docs/working/|docs/working/|g' "$f"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1055,7 +1055,7 @@ Verify MEMORY.md now reads correctly:
|
|||||||
```bash
|
```bash
|
||||||
grep "plans\|specs\|superpowers\|working" \
|
grep "plans\|specs\|superpowers\|working" \
|
||||||
"/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/MEMORY.md"
|
"/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/MEMORY.md"
|
||||||
# Expected: all plan/spec references point to docs/working/, none to docs/superpowers/
|
# Expected: all plan/spec references point to docs/working/, none to docs/working/
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Final verification — complete restructure**
|
- [ ] **Step 5: Final verification — complete restructure**
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ The existing 4-stat grid expands to 6 stats. Both `stats.html.twig` and the inli
|
|||||||
|
|
||||||
| Stat | Label | Source | Notes |
|
| Stat | Label | Source | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Days on the road | `days on the road` | `(now - first entry date) / 86400` | Unchanged |
|
| Days on the road | `days on the road` | `date_end - date_start` if trip `date_end` is set; else `now - first entry date` | Fixed for past trips |
|
||||||
| Entries posted | `entries posted` | `all_entries\|length` | Unchanged |
|
| Entries posted | `entries posted` | `all_entries\|length` | Unchanged |
|
||||||
| Countries visited | `countries visited` | Deduplicated `location_country` | Unchanged; country list shown below grid |
|
| Countries visited | `countries visited` | Deduplicated `location_country` | Unchanged; country list shown below grid |
|
||||||
| **Cities visited** | `cities visited` | Deduplicated `location_city` | New; same dedup logic as countries |
|
| **Cities visited** | `cities visited` | Deduplicated `location_city` | New; same dedup logic as countries |
|
||||||
@@ -82,19 +82,10 @@ Max speed is explicitly excluded — GPS noise at 1-second resolution produces u
|
|||||||
|
|
||||||
### Icon system
|
### Icon system
|
||||||
|
|
||||||
The GPX `<type>` tag on the track element drives the icon shown in both the main stats distance block and the cycling panel header:
|
A single static racing/gravel bike icon is used whenever GPX files are present — both in the main stats distance block and the cycling panel header. No dynamic switching based on `<type>`.
|
||||||
|
|
||||||
| `<type>` value | Icon |
|
Known Komoot `<type>` values for reference (future use if icon switching is ever added):
|
||||||
|---|---|
|
`racebike`, `touringbicycle`, `mtb`, `cycling`, `hiking`, `hike`
|
||||||
| `racebike` | Road bike |
|
|
||||||
| `touringbicycle` | Touring bike |
|
|
||||||
| `mtb` | Mountain bike |
|
|
||||||
| `cycling` (generic) | Generic bike |
|
|
||||||
| `hiking` | Hiking boot |
|
|
||||||
| `hike` | Hiking boot |
|
|
||||||
| Any unrecognised value | Generic bike (fallback) |
|
|
||||||
|
|
||||||
When multiple GPX files exist with different types, use the type from the first file. This is an acceptable heuristic for now.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,551 @@
|
|||||||
|
# Design Spec: Story Mode + MapLibre Migration
|
||||||
|
|
||||||
|
*Date: 2026-06-19*
|
||||||
|
*Inspired by: [Sabdia](https://github.com/m-cluitmans/Sabdia) — a friend's sabbatical blog built on Astro + Keystatic + MapLibre*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Two parallel features:
|
||||||
|
|
||||||
|
1. **Story Mode** — a rich long-form post type alongside journal dailies, with cinematic
|
||||||
|
storytelling blocks (hero, chapter breaks, scrollytelling, pull quotes, snap gallery)
|
||||||
|
2. **MapLibre GL migration** — replace Leaflet across all three maps (full map, mini-map,
|
||||||
|
home map) with MapLibre GL JS; add animated journey line; improve CSS integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions Log
|
||||||
|
|
||||||
|
### Why MapLibre GL instead of Leaflet
|
||||||
|
|
||||||
|
Leaflet renders raster PNG tiles. MapLibre GL renders vector tiles in WebGL. Key gains:
|
||||||
|
|
||||||
|
- **Animated journey line** — MapLibre's GeoJSON source model makes RAF-loop animation
|
||||||
|
trivial (`source.setData()` per frame). On Leaflet you'd call `polyline.setLatLngs()`
|
||||||
|
which also works, but MapLibre gives us everything below for free too.
|
||||||
|
- **Smooth zoom** — continuous sub-pixel zoom vs Leaflet's tile-snap zoom levels
|
||||||
|
- **Retina crisp** — vector geometry scales perfectly on HiDPI screens
|
||||||
|
- **Future-proof** — 3D terrain, tilt/pitch, per-feature click events, style control,
|
||||||
|
outdoor/topo/satellite styles for GPX track maps all become straightforward
|
||||||
|
- **GPX styling** — switching from `leaflet-gpx` to `@mapbox/togeojson` + GeoJSON layer
|
||||||
|
gives per-point colour control (speed, elevation gradients) later
|
||||||
|
|
||||||
|
Cost: ~280KB (vs ~40KB Leaflet). Acceptable — cached after first visit.
|
||||||
|
|
||||||
|
Tile source stays the same: CARTO dark vector style — free, no API key.
|
||||||
|
|
||||||
|
### Why shortcodes for story blocks (not modular pages or blueprint lists)
|
||||||
|
|
||||||
|
Evaluated three approaches for in-prose storytelling blocks:
|
||||||
|
|
||||||
|
| Approach | What it is | Verdict |
|
||||||
|
|---|---|---|
|
||||||
|
| **Shortcodes** | `[chapter-break ...]` inline in Markdown | ✅ Chosen |
|
||||||
|
| Modular pages | Each block = a child page in Admin | ✗ Ruled out |
|
||||||
|
| Blueprint list + elements | `sections:` YAML list with type selector | ✗ Ruled out |
|
||||||
|
|
||||||
|
**Modular pages** are how most Grav storytelling themes work (Quark, Oxygen, all
|
||||||
|
HTML5UP ports). Each block gets proper Admin form fields. But a 1,500-word story with
|
||||||
|
two chapter breaks requires five child pages — navigating between them on mobile while
|
||||||
|
traveling is painful. Prose ends up fragmented across "text" module pages.
|
||||||
|
|
||||||
|
**Blueprint list with elements field** (Grav's conditional field groups) could render
|
||||||
|
blocks as a structured "Add section" list in Admin. But prose still has to go in a
|
||||||
|
"text" type section, so a story becomes a long list of `text/chapter-break/text/scrolly/
|
||||||
|
text/gallery` entries rather than a flowing document.
|
||||||
|
|
||||||
|
**Shortcodes** keep everything in one Markdown editor — prose flows naturally, blocks are
|
||||||
|
inserted inline. The `shortcode-gallery-plusplus` plugin already in our stack brings
|
||||||
|
`shortcode-core` as a dependency, so no new plugin is needed.
|
||||||
|
|
||||||
|
Grav Admin2 has no rich block-editor like Keystatic/Markdoc. Shortcodes are the
|
||||||
|
closest practical equivalent for mixed prose+blocks authoring on mobile.
|
||||||
|
|
||||||
|
*Future option:* If Admin2 ever gains inline block components (or we add a Flex Object
|
||||||
|
definition), the shortcode content can be migrated — the block semantics are identical.
|
||||||
|
|
||||||
|
### Why gallery stays as lightbox on journal entries
|
||||||
|
|
||||||
|
Journal entries are short daily posts — a grid of 3–8 photos suits them.
|
||||||
|
The snap gallery is a deliberate slow storytelling device (one photo fills the screen,
|
||||||
|
reader swipes through). That pacing fits stories, not a daily feed card.
|
||||||
|
|
||||||
|
### Weather not added to story frontmatter
|
||||||
|
|
||||||
|
Weather is a journal-entry concept (captured at the moment of a daily post via
|
||||||
|
Open-Meteo). Stories are retrospective long-form narratives — weather would be referenced
|
||||||
|
in prose if relevant, not as a metadata badge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1 — Story Mode
|
||||||
|
|
||||||
|
### 1.1 Page structure
|
||||||
|
|
||||||
|
Stories live as child pages under `04.stories/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
user/pages/01.trips/<trip-slug>/04.stories/
|
||||||
|
stories.md ← listing page, template: stories
|
||||||
|
01.<story-slug>/
|
||||||
|
story.md ← individual story, template: story
|
||||||
|
hero.jpg
|
||||||
|
photo-a.jpg
|
||||||
|
photo-b.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
`stories.md` frontmatter:
|
||||||
|
```yaml
|
||||||
|
title: Stories
|
||||||
|
template: stories
|
||||||
|
published: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Story frontmatter schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
title: Into the Hills of Kyoto
|
||||||
|
date: 2026-03-28 # start date — shown in hero header
|
||||||
|
end_date: 2026-03-29 # optional; shown as "28–29 Mar 2026"
|
||||||
|
location_name: Kyoto # city/region; shown in hero header
|
||||||
|
location_country: Japan # used for stats de-duplication
|
||||||
|
lat: 34.967 # main GPS coordinate — shows pin on /map
|
||||||
|
lng: 135.773
|
||||||
|
hero_image: hero.jpg # filename in page media; required for hero section
|
||||||
|
hero_alt: The vermillion gate at Fushimi Inari at dawn
|
||||||
|
published: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields deliberately excluded: `weather_*` (not meaningful for stories).
|
||||||
|
|
||||||
|
### 1.3 Shortcode blocks
|
||||||
|
|
||||||
|
Four blocks implemented as ShortcodeCore shortcodes.
|
||||||
|
All image paths are **filenames only** (e.g. `shrine.jpg`) — resolved against the story's
|
||||||
|
own page media folder, same convention as `hero_image`.
|
||||||
|
|
||||||
|
#### ChapterBreak
|
||||||
|
|
||||||
|
Full-bleed atmospheric photo with a frosted-glass title panel. Reveals on scroll via
|
||||||
|
IntersectionObserver (blur + translateY → clear).
|
||||||
|
|
||||||
|
```
|
||||||
|
[chapter-break image="shrine-gate.jpg" title="The Long Walk Up" number="II" /]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `image` | yes | Page media filename |
|
||||||
|
| `title` | yes | Chapter title, displayed in frosted panel |
|
||||||
|
| `number` | no | Roman numeral or label shown above title |
|
||||||
|
| `alt` | no | Alt text (defaults to `title`) |
|
||||||
|
|
||||||
|
Renders as `60vh` full-bleed block with dark gradient tint over the image and a
|
||||||
|
`backdrop-filter: blur(18px)` panel containing the chapter number + title + teal rule.
|
||||||
|
|
||||||
|
#### ScrollySection
|
||||||
|
|
||||||
|
NYT-style sticky image (55% left column) with text panels that scroll past on the right.
|
||||||
|
Steps are separated by `---` inside the shortcode body. Powered by **Scrollama** (CDN).
|
||||||
|
|
||||||
|
```
|
||||||
|
[scrolly-section image="torii-path.jpg" alt="Thousands of torii gates"]
|
||||||
|
The path stretched further than I could see.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Each gate was donated by a business or family, a prayer made physical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
By the tenth minute of walking, the city had disappeared entirely.
|
||||||
|
[/scrolly-section]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `image` | yes | Page media filename — sticky background |
|
||||||
|
| `alt` | no | Image alt text |
|
||||||
|
| `caption` | no | Small caption shown bottom-left of image |
|
||||||
|
|
||||||
|
On mobile: full-screen sticky image with text panels scrolling over it (same layout,
|
||||||
|
single column — image behind, text on top with semi-transparent card).
|
||||||
|
|
||||||
|
Image starts blurred (`blur(8px) scale(1.04)`), unblurs when section enters viewport.
|
||||||
|
Between steps: subtle pan (object-position cycles through 5 offsets) + slight overlay
|
||||||
|
darkening for depth.
|
||||||
|
|
||||||
|
#### PullQuote
|
||||||
|
|
||||||
|
Frosted-glass quote block with optional atmospheric background image. Reveals on scroll.
|
||||||
|
|
||||||
|
```
|
||||||
|
[pull-quote image="lanterns.jpg"]
|
||||||
|
The torii gates never seemed to end — and I didn't want them to.
|
||||||
|
[/pull-quote]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `image` | no | Page media filename — background photo |
|
||||||
|
| `alt` | no | Alt text for background image |
|
||||||
|
|
||||||
|
Without `image`: renders on `--color-canvas` (warm dark surface, solid).
|
||||||
|
With `image`: full-bleed image behind frosted glass panel.
|
||||||
|
|
||||||
|
Large decorative `"` marks above and below the quote text (DM Serif Display, 5rem).
|
||||||
|
|
||||||
|
#### SnapGallery
|
||||||
|
|
||||||
|
Full-screen snap-scroll photo sequence. One photo per swipe. Snap physics are pure CSS
|
||||||
|
(`scroll-snap-type: y mandatory` + `scroll-snap-stop: always` on the scroll container).
|
||||||
|
Dot indicator active state updated via a small IntersectionObserver on each slide.
|
||||||
|
|
||||||
|
```
|
||||||
|
[snap-gallery images="photo-a.jpg,photo-b.jpg,photo-c.jpg" captions="The approach,Summit view,Descent" alts="Hikers on trail,Mountain panorama,Forest path" /]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `images` | yes | Comma-separated page media filenames |
|
||||||
|
| `captions` | no | Comma-separated captions (positional) |
|
||||||
|
| `alts` | no | Comma-separated alt texts (positional) |
|
||||||
|
|
||||||
|
Each slide: blurred cover-crop background + contain-fit foreground image + caption fades
|
||||||
|
in at bottom. Dot indicator on the right edge. Page-level `scroll-snap-align: start`
|
||||||
|
with `proximity` (not mandatory) so normal page scroll is unaffected.
|
||||||
|
|
||||||
|
### 1.4 Template: `story.html.twig`
|
||||||
|
|
||||||
|
Extends `partials/base.html.twig` but overrides the nav block to show only a floating
|
||||||
|
escape link. Full layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ ← Back (position: fixed, top-left) │
|
||||||
|
│ │
|
||||||
|
│ HERO — 100vh │
|
||||||
|
│ sticky image, Ken Burns zoom-out │
|
||||||
|
│ title blurs up from bottom │
|
||||||
|
│ date · location beneath title │
|
||||||
|
│ ↓ bounce scroll indicator │
|
||||||
|
│ 40vh spacer (scroll trigger zone) │
|
||||||
|
│ │
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ STORY BODY │
|
||||||
|
│ max-width: 680px, centred │
|
||||||
|
│ font: DM Serif Display (headings) │
|
||||||
|
│ DM Sans (prose) │
|
||||||
|
│ {{ page.content|raw }} │
|
||||||
|
│ (Markdown + shortcode blocks) │
|
||||||
|
│ │
|
||||||
|
│ ← Back to stories (footer) │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hero scroll behaviour (vanilla JS, no library):**
|
||||||
|
|
||||||
|
- `window.scroll` listener (passive, rAF-throttled)
|
||||||
|
- `progress = scrollY / innerHeight` (0→1 as hero scrolls away)
|
||||||
|
- At progress > 0: dark overlay fades in (`rgba(0,0,0, progress * 0.6)`)
|
||||||
|
- Scroll indicator hides after `scrollY > 80px`
|
||||||
|
- At progress ≥ 1: overlay removed from DOM
|
||||||
|
|
||||||
|
**Ken Burns animation:** CSS `@keyframes` — `scale(1.06) → scale(1)` over 12s,
|
||||||
|
`ease-out`, `forwards`. Respects `prefers-reduced-motion: reduce`.
|
||||||
|
|
||||||
|
**Text reveal:** Title and date animate in with `filter: blur(10px) + translateY(22px)
|
||||||
|
→ clear` at 0.2s / 0.55s delay. Respects `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
### 1.5 Template: `stories.html.twig`
|
||||||
|
|
||||||
|
Listing of published stories for the active trip. Grid of story cards:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ hero thumb │ │ hero thumb │
|
||||||
|
│ │ │ │
|
||||||
|
│ Kyoto Hills │ │ Seoul Rain │
|
||||||
|
│ 28–29 Mar │ │ 1 Apr │
|
||||||
|
│ Kyoto │ │ Seoul │
|
||||||
|
└──────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
2-column grid on desktop, single column on mobile. Each card links to the story.
|
||||||
|
Empty state: "No stories yet — check back soon."
|
||||||
|
|
||||||
|
Stories are also listed as cards in `dailies.html.twig`'s combined feed (already
|
||||||
|
implemented — the template merges journal entries and stories by date).
|
||||||
|
|
||||||
|
### 1.6 JS dependencies
|
||||||
|
|
||||||
|
| Library | How loaded | Size | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Scrollama** | CDN (`jsdelivr`) | ~4KB | ScrollySection step detection |
|
||||||
|
| IntersectionObserver | Native browser API | — | ChapterBreak + PullQuote reveal, SnapGallery dots |
|
||||||
|
|
||||||
|
Scrollama is only loaded on story pages (inline `<script src>` in `story.html.twig`).
|
||||||
|
|
||||||
|
### 1.7 CSS additions (story-specific)
|
||||||
|
|
||||||
|
New CSS block added to `style.css` under a `/* ── Story pages ──` section:
|
||||||
|
|
||||||
|
**Story layout:**
|
||||||
|
- `.story-hero` — `position: relative; height: 100vh; overflow: hidden`
|
||||||
|
- `.story-hero__img` — `position: sticky; top: 0; width: 100%; height: 100vh; object-fit: cover`
|
||||||
|
- `.story-hero__overlay` — `position: fixed; inset: 0; pointer-events: none` (JS-driven opacity)
|
||||||
|
- `.story-hero__content` — `position: absolute; bottom: 18%; text-align: center; color: #fff`
|
||||||
|
- `.story-escape` — `position: fixed; top: 1rem; left: 1rem; z-index: 100; color: var(--color-ink); background: var(--color-canvas); ...`
|
||||||
|
- `.story-body` — `max-width: 680px; margin: 0 auto; padding: var(--space-16) var(--space-6)`
|
||||||
|
- `.story-body p` — `font-family: var(--font-ui); font-size: 1.0625rem; line-height: 1.85; color: var(--color-ink-2)`
|
||||||
|
|
||||||
|
**ChapterBreak:**
|
||||||
|
- `.chapter-break` — full-bleed breakout, `60vh`, overflow hidden
|
||||||
|
- `.chapter-break__panel` — `backdrop-filter: blur(18px); background: rgba(26,24,20,0.25); border: 1px solid rgba(255,255,255,0.12); border-radius: var(--radius-sm)`
|
||||||
|
- Initial state: `opacity: 0; filter: blur(12px); transform: translateY(28px)` → `.is-revealed` clears all
|
||||||
|
- `.chapter-break__rule` — `40px × 2px` teal (`var(--color-accent)`) rule below title
|
||||||
|
|
||||||
|
**ScrollySection:**
|
||||||
|
- `.scrolly` — `display: grid; grid-template-columns: 55% 45%; width: 100vw` (full-bleed breakout)
|
||||||
|
- `.scrolly__media` — `position: sticky; top: var(--site-header-height); height: calc(100vh - var(--site-header-height))`
|
||||||
|
- `.scrolly-step__inner` — `background: rgba(26,24,20,0.92); backdrop-filter: blur(4px); border-radius: var(--radius-sm); border: 1px solid var(--color-border)`
|
||||||
|
- Mobile (`max-width: 768px`): single column, steps overlay the sticky image with `margin-top: calc(-(100vh - var(--site-header-height)))`
|
||||||
|
|
||||||
|
**PullQuote:**
|
||||||
|
- `.pull-quote` — bleeds `1.5rem` each side beyond prose column
|
||||||
|
- `.pull-quote__inner` — `backdrop-filter: blur(14px); background: rgba(26,24,20,0.12)` (with image) or `var(--color-canvas)` (without)
|
||||||
|
- Large `"` marks: `font-family: var(--font-display); font-size: 5rem; color: var(--color-accent); opacity: 0.4`
|
||||||
|
|
||||||
|
**SnapGallery:**
|
||||||
|
- `.pgallery__frame` — `height: 100vh; scroll-snap-type: y mandatory; overflow-y: scroll`
|
||||||
|
- `.pgallery__bg` — `object-fit: cover; filter: blur(20px) brightness(0.4)` (blurred backdrop)
|
||||||
|
- `.pgallery__fg` — `object-fit: contain` (full foreground image)
|
||||||
|
- `.pgallery__dot.is-active` — `background: var(--color-accent)`
|
||||||
|
|
||||||
|
All animations respect `prefers-reduced-motion: reduce` — transitions set to `none`,
|
||||||
|
initial states set to final states immediately.
|
||||||
|
|
||||||
|
### 1.8 Demo story content
|
||||||
|
|
||||||
|
One sample story added to `user/docs/demo/trips/japan-korea-2026/` following existing
|
||||||
|
demo conventions. Story covers 28–29 March (Kyoto days already in journal demo):
|
||||||
|
|
||||||
|
```
|
||||||
|
user/docs/demo/trips/japan-korea-2026/04.stories/01.the-thousand-gates/
|
||||||
|
story.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontmatter mirrors the schema. Body uses all four shortcode types so they can be QA'd
|
||||||
|
in one pass. No binary image assets — `make demo-load` copies the folder; tester drops
|
||||||
|
a few JPEGs in to exercise hero + photo blocks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2 — MapLibre GL Migration
|
||||||
|
|
||||||
|
### 2.1 Scope
|
||||||
|
|
||||||
|
Three files change. No new page routes. GPX file storage and delivery unchanged.
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `map.html.twig` | Full rewrite of JS + CDN refs; CSS class renames |
|
||||||
|
| `dailies.html.twig` | Mini-map JS + CDN refs rewritten |
|
||||||
|
| `home.html.twig` | Home map JS + CDN refs rewritten |
|
||||||
|
| `style.css` | Leaflet overrides removed; MapLibre overrides added |
|
||||||
|
|
||||||
|
CDN changes (all three map templates):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Remove -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.1.2/gpx.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Add -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||||
|
<!-- GPX maps only: -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Tile style URL (same CARTO dark, now as vector style):
|
||||||
|
```
|
||||||
|
https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Animated journey line
|
||||||
|
|
||||||
|
Port of Sabdia's `animateJourneyLine` to vanilla JS against MapLibre's GeoJSON source API:
|
||||||
|
|
||||||
|
```js
|
||||||
|
map.on('load', () => {
|
||||||
|
// Add an empty source
|
||||||
|
map.addSource('journey', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Glow layer (wide, low opacity)
|
||||||
|
map.addLayer({ id: 'journey-glow', type: 'line', source: 'journey',
|
||||||
|
paint: { 'line-color': '#2A8C73', 'line-width': 6, 'line-opacity': 0.18 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main line
|
||||||
|
map.addLayer({ id: 'journey-line', type: 'line', source: 'journey',
|
||||||
|
paint: { 'line-color': '#2A8C73', 'line-width': 2.5, 'line-opacity': 0.85 }
|
||||||
|
});
|
||||||
|
|
||||||
|
animateJourneyLine(map, coords); // RAF loop, ease-out cubic, 5000ms
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
RAF loop builds coordinate array incrementally using cumulative Euclidean distance +
|
||||||
|
ease-out cubic easing. On `prefers-reduced-motion: reduce`: skip animation, set full
|
||||||
|
coordinates immediately.
|
||||||
|
|
||||||
|
Teal values use `var(--color-accent)` equivalent (`#2A8C73`) — matches our design tokens.
|
||||||
|
|
||||||
|
### 2.3 GPX rendering
|
||||||
|
|
||||||
|
Replace `leaflet-gpx` with `@mapbox/togeojson` + MapLibre GeoJSON source:
|
||||||
|
|
||||||
|
```js
|
||||||
|
fetch(gpxUrl)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(text => {
|
||||||
|
const gpx = new DOMParser().parseFromString(text, 'text/xml');
|
||||||
|
const geojson = toGeoJSON.gpx(gpx);
|
||||||
|
map.addSource('gpx-track', { type: 'geojson', data: geojson });
|
||||||
|
map.addLayer({
|
||||||
|
id: 'gpx-track-line', type: 'line', source: 'gpx-track',
|
||||||
|
paint: { 'line-color': '#2A8C73', 'line-width': 2, 'line-opacity': 0.7 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple GPX files (trip has several tracks): each gets its own numbered source/layer pair.
|
||||||
|
|
||||||
|
### 2.4 Markers and popups
|
||||||
|
|
||||||
|
MapLibre uses `maplibregl.Marker` (custom DOM element) + `maplibregl.Popup`.
|
||||||
|
Existing popup HTML content (hero thumbnail, date, title, link) is unchanged.
|
||||||
|
|
||||||
|
Marker style (same visual as current):
|
||||||
|
- Regular entries: `12px` teal dot with white border
|
||||||
|
- Latest/current entry: `18px` teal dot with outer ring (`box-shadow: 0 0 0 4px rgba(42,140,115,0.25)`)
|
||||||
|
|
||||||
|
Popup styled via CSS (see §2.5).
|
||||||
|
|
||||||
|
### 2.5 CSS improvements over Leaflet
|
||||||
|
|
||||||
|
**Remove (Leaflet-specific):**
|
||||||
|
```css
|
||||||
|
/* DELETE — no longer needed */
|
||||||
|
.leaflet-container { background: #282828 !important; }
|
||||||
|
```
|
||||||
|
|
||||||
|
MapLibre sets its canvas background from the style JSON (`background-color` in the style's
|
||||||
|
`background` layer). CARTO dark-matter style uses `#1a1a1a` — no flash on load.
|
||||||
|
|
||||||
|
**Add (MapLibre):**
|
||||||
|
```css
|
||||||
|
/* ── MapLibre GL overrides ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Navigation controls (zoom +/−, compass) */
|
||||||
|
.maplibregl-ctrl-group {
|
||||||
|
background: var(--color-canvas);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.maplibregl-ctrl-group button {
|
||||||
|
color: var(--color-ink-2);
|
||||||
|
}
|
||||||
|
.maplibregl-ctrl-group button:hover {
|
||||||
|
background: var(--color-surface-raised);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
.maplibregl-ctrl-group button + button {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribution bar */
|
||||||
|
.maplibregl-ctrl-attrib {
|
||||||
|
background: rgba(26,24,20,0.75) !important;
|
||||||
|
color: var(--color-ink-muted) !important;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
.maplibregl-ctrl-attrib a {
|
||||||
|
color: var(--color-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup */
|
||||||
|
.maplibregl-popup-content {
|
||||||
|
background: var(--color-canvas);
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
.maplibregl-popup-tip {
|
||||||
|
border-top-color: var(--color-canvas) !important;
|
||||||
|
}
|
||||||
|
.maplibregl-popup-close-button {
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
}
|
||||||
|
.maplibregl-popup-close-button:hover {
|
||||||
|
color: var(--color-ink);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cursor — pointer hand over clickable markers */
|
||||||
|
.maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.maplibregl-canvas-container.maplibregl-interactive:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mobile scroll-trap prevention:** For embedded maps (mini-map on dailies, home map),
|
||||||
|
initialize with `cooperativeGestures: true` — requires two fingers to pan on touch.
|
||||||
|
The full-page `/map` uses normal gestures (`cooperativeGestures: false`, the default).
|
||||||
|
*Note: verify `cooperativeGestures` is available in the chosen MapLibre GL 4.x version
|
||||||
|
during implementation; if absent, use `dragPan: false` on touch-only + a two-finger
|
||||||
|
hint overlay as fallback.*
|
||||||
|
|
||||||
|
### 2.6 What is NOT migrated now
|
||||||
|
|
||||||
|
Features from Sabdia's map that were explicitly deferred:
|
||||||
|
|
||||||
|
| Feature | Decision |
|
||||||
|
|---|---|
|
||||||
|
| Ghost pins for upcoming/planned stops | Documented; deferred — requires `show_preview` frontmatter field + Twig logic |
|
||||||
|
| Pulsing amber dot for current location | Documented; deferred — requires "current entry" detection logic |
|
||||||
|
| `flyTo()` on marker click | Deferred — nice UX upgrade, implement after migration stabilises |
|
||||||
|
| 3D terrain | Deferred — requires DEM tile source (MapTiler key) |
|
||||||
|
| Per-story inline MapBlock shortcode | Deferred — implement as part of story mode v2 |
|
||||||
|
| MapTiler outdoor/satellite/topo styles for GPX | Deferred — requires MapTiler API key |
|
||||||
|
|
||||||
|
These are preserved here so they can be picked up in a later milestone without needing
|
||||||
|
to re-research the Sabdia implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Story-specific inline MapBlock shortcode (deferred, see §2.6)
|
||||||
|
- Animated hero video (requires server-side FFmpeg, not available in Grav)
|
||||||
|
- Push notifications for new stories
|
||||||
|
- Story-level statistics (word count, reading time)
|
||||||
|
- Co-authoring / Travel Buddy equivalent
|
||||||
|
- 3D flyover video
|
||||||
@@ -66,12 +66,12 @@ docs/
|
|||||||
|
|
||||||
| New path | Current path |
|
| New path | Current path |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `working/specs/*` (13 files) | `docs/superpowers/specs/*` |
|
| `working/specs/*` (13 files) | `docs/working/specs/*` |
|
||||||
| `working/plans/*` (14 files) | `docs/superpowers/plans/*` |
|
| `working/plans/*` (14 files) | `docs/working/plans/*` |
|
||||||
| `working/milestones/milestone-1.md` | `docs/milestone-1-spec.md` |
|
| `working/milestones/milestone-1.md` | `docs/working/milestones/milestone-1.md` |
|
||||||
| `working/milestones/milestone-2.md` | `docs/milestone-2-spec.md` |
|
| `working/milestones/milestone-2.md` | `docs/working/milestones/milestone-2.md` |
|
||||||
| `working/milestones/milestone-3.md` | `docs/milestone-3-spec.md` |
|
| `working/milestones/milestone-3.md` | `docs/working/milestones/milestone-3.md` |
|
||||||
| `working/milestones/milestone-4.md` | `docs/milestone-4-spec.md` |
|
| `working/milestones/milestone-4.md` | `docs/working/milestones/milestone-4.md` |
|
||||||
| `working/backlog.md` | `docs/backlog.md` |
|
| `working/backlog.md` | `docs/backlog.md` |
|
||||||
| `working/production-todo.md` | `docs/production-todo.md` |
|
| `working/production-todo.md` | `docs/production-todo.md` |
|
||||||
| `working/pm-analysis.md` | `docs/pm-analysis.md` |
|
| `working/pm-analysis.md` | `docs/pm-analysis.md` |
|
||||||
@@ -150,7 +150,7 @@ Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
|||||||
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
||||||
```
|
```
|
||||||
|
|
||||||
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default so generated files land in the right place automatically.
|
The brainstorming and writing-plans skills default to `docs/working/`; these lines override that default so generated files land in the right place automatically.
|
||||||
|
|
||||||
**Keep inline** (always-loaded context Claude needs without following a link):
|
**Keep inline** (always-loaded context Claude needs without following a link):
|
||||||
- §0 project specifics (folder layout, stack versions, trip entity architecture, active trip, GPX pipeline, env rules, remote operations, content sync, gitignore)
|
- §0 project specifics (folder layout, stack versions, trip entity architecture, active trip, GPX pipeline, env rules, remote operations, content sync, gitignore)
|
||||||
@@ -173,11 +173,11 @@ The brainstorming and writing-plans skills default to `docs/superpowers/`; these
|
|||||||
|
|
||||||
1. `docs/` contains exactly four subdirectories: `guides/`, `reference/`, `working/`, `research/`
|
1. `docs/` contains exactly four subdirectories: `guides/`, `reference/`, `working/`, `research/`
|
||||||
2. All 32+ existing files are moved to their new paths; no files remain at `docs/` root except `README.md`
|
2. All 32+ existing files are moved to their new paths; no files remain at `docs/` root except `README.md`
|
||||||
3. `docs/superpowers/` no longer exists; content is under `working/`
|
3. `docs/working/` no longer exists; content is under `working/`
|
||||||
4. Four new guides exist and cover their stated scope
|
4. Four new guides exist and cover their stated scope
|
||||||
5. `reference/architecture.md` exists and covers stack, plugin roles, template hierarchy, and post data flow
|
5. `reference/architecture.md` exists and covers stack, plugin roles, template hierarchy, and post data flow
|
||||||
6. `docs/README.md` exists with persona-based navigation
|
6. `docs/README.md` exists with persona-based navigation
|
||||||
7. CLAUDE.md no longer contains §2 local setup block; contains pointer to `docs/guides/local-setup.md`
|
7. CLAUDE.md no longer contains §2 local setup block; contains pointer to `docs/guides/local-setup.md`
|
||||||
8. All internal cross-references in moved files updated to new paths
|
8. All internal cross-references in moved files updated to new paths
|
||||||
9. Memory files that reference `docs/superpowers/` paths updated to `docs/working/`
|
9. Memory files that reference `docs/working/` paths updated to `docs/working/`
|
||||||
10. CLAUDE.md contains superpowers skill path overrides pointing to `docs/working/specs/` and `docs/working/plans/`
|
10. CLAUDE.md contains superpowers skill path overrides pointing to `docs/working/specs/` and `docs/working/plans/`
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
upload_max_filesize = 100M
|
upload_max_filesize = 100M
|
||||||
post_max_size = 500M
|
post_max_size = 500M
|
||||||
max_file_uploads = 20
|
max_file_uploads = 20
|
||||||
|
session.save_path = /tmp
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
admin
|
|
||||||
email
|
email
|
||||||
error
|
error
|
||||||
form
|
form
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ cd "$WEBROOT"
|
|||||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip
|
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip
|
||||||
unzip -oq grav-admin.zip
|
unzip -oq grav-admin.zip
|
||||||
cp -rf grav-admin/. .
|
cp -rf grav-admin/. .
|
||||||
|
cp -rf grav-admin/user/plugins/admin2 /tmp/admin2-plugin
|
||||||
rm -rf grav-admin grav-admin.zip
|
rm -rf grav-admin grav-admin.zip
|
||||||
|
|
||||||
echo "==> Cloning user repo"
|
echo "==> Cloning user repo"
|
||||||
@@ -41,6 +42,8 @@ fi
|
|||||||
|
|
||||||
echo "==> Creating required directories"
|
echo "==> Creating required directories"
|
||||||
mkdir -p user/plugins user/accounts user/data
|
mkdir -p user/plugins user/accounts user/data
|
||||||
|
cp -rf /tmp/admin2-plugin user/plugins/admin2
|
||||||
|
rm -rf /tmp/admin2-plugin
|
||||||
|
|
||||||
echo "==> Installing plugins"
|
echo "==> Installing plugins"
|
||||||
php bin/gpm install $PLUGINS -y
|
php bin/gpm install $PLUGINS -y
|
||||||
|
|||||||
+1
-1
Submodule user updated: c403ea9593...f6a8657de2
Reference in New Issue
Block a user