diff --git a/.env.example b/.env.example index be6fb7b..b221b68 100644 --- a/.env.example +++ b/.env.example @@ -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) GRAV_TEST_USER=mischa -GRAV_TEST_PASS=your-grav-password +GRAV_TEST_PASS=TravelBlog2026! GRAV_BASE_URL=http://localhost:8081 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ceb046 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/Makefile b/Makefile index 565e45d..c2960f1 100644 --- a/Makefile +++ b/Makefile @@ -21,19 +21,23 @@ test: test-config test-post test-ui # ── Local dev ────────────────────────────────────────────────────────────────── +build: + docker compose build + start: docker compose up -d stop: docker compose down -setup: start install-plugins fix-perms +setup: build start install-plugins fix-perms fix-perms: 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 apachectl graceful + install-plugins: docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y diff --git a/docker-compose.yml b/docker-compose.yml index ab2789c..8311883 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: grav: - image: getgrav/grav + build: . container_name: intotheeast_grav environment: - GRAV_CHANNEL=beta diff --git a/docs/backlog.md b/docs/backlog.md new file mode 100644 index 0000000..393b1e0 --- /dev/null +++ b/docs/backlog.md @@ -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). diff --git a/docs/design/design-spec.md b/docs/design/design-spec.md new file mode 100644 index 0000000..b6519d9 --- /dev/null +++ b/docs/design/design-spec.md @@ -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) | diff --git a/docs/milestone-1-spec.md b/docs/milestone-1-spec.md new file mode 100644 index 0000000..6413fff --- /dev/null +++ b/docs/milestone-1-spec.md @@ -0,0 +1,193 @@ +# Milestone 1 Spec — Entry Enrichment + +**Goal:** Every entry is richer out of the box — location name shown, weather auto-captured, photos in a proper gallery, hero image visible on the feed. + +--- + +## User Stories + +- As a traveler (Mischa), when I submit the post form, I want my current weather conditions auto-filled so I don't have to look them up manually. +- As a traveler, I want to type my city and country once and have it appear on the entry and in the feed card, so readers know where I am without reading the whole post. +- As a reader, when I scan the feed, I want to see a thumbnail photo and location for each entry so I can quickly get a sense of where Mischa is and whether to read the full entry. +- As a reader, when I open an entry, I want to see all uploaded photos in a gallery I can browse, not a wall of raw images. +- As a traveler, when I submit a form without photos, the entry should still display cleanly with no broken image placeholders. + +--- + +## Feature Details + +### 1.1 — Location Name Field on Post Form + +**What:** Add two text fields to the post form: `location_city` and `location_country`. + +**Behavior:** +- Both are optional (GPS coordinates are also optional) +- Placeholder text: "e.g. Kyoto" and "e.g. Japan" +- Displayed below the lat/lng fields +- On submit, stored in entry frontmatter as `location_city` and `location_country` +- On the form, shown as a single labeled group "Location Name" with two side-by-side inputs on desktop, stacked on mobile + +**Edge cases:** +- If left blank: entry shows no location badge. No error, no broken UI. +- Long city names (e.g. "Ulaanbaatar") must not overflow card layout. +- Special characters (accents, non-Latin) must render correctly. + +**Mobile behavior:** Both fields full-width, stacked, 44px min touch targets. + +--- + +### 1.2 — Weather Auto-Fetch on Post Form + +**What:** A "Get Weather" button on the post form that calls the Open-Meteo free API (no API key) using the lat/lng already entered, and fills hidden weather fields. + +**Fields to fetch and store:** +- `weather_temp_c` — temperature in Celsius (integer) +- `weather_desc` — short description: one of: Sunny, Partly cloudy, Cloudy, Foggy, Drizzle, Rain, Snow, Thunderstorm (derived from WMO weather code) + +**WMO code mapping (Open-Meteo uses WMO codes):** +- 0 → Sunny +- 1,2 → Partly cloudy +- 3 → Cloudy +- 45,48 → Foggy +- 51,53,55,56,57 → Drizzle +- 61,63,65,66,67,80,81,82 → Rain +- 71,73,75,77,85,86 → Snow +- 95,96,99 → Thunderstorm + +**API call:** +``` +https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lng}¤t=temperature_2m,weather_code&temperature_unit=celsius +``` + +**UX flow:** +1. User fills in lat/lng (manually or via "Get Location" button) +2. User taps "Get Weather" button +3. Button shows "Fetching…" while loading +4. On success: fills temp and desc fields (visible, editable text inputs) +5. On failure (no network, no lat/lng): shows inline error "Could not fetch weather — enter manually" + +**Edge cases:** +- If lat/lng not filled when button tapped: show inline error "Enter coordinates first" +- Weather fields are always editable manually (auto-fill is a convenience, not mandatory) +- If weather fields left blank: entry shows no weather badge. No broken UI. +- Open-Meteo returns current conditions, not historical — this is fine for posting in real time + +**Mobile behavior:** "Get Weather" button is full-width, 44px height, placed immediately below the lat/lng + location name fields. + +--- + +### 1.3 — Weather Display on Entry Page + +**What:** If `weather_temp_c` or `weather_desc` is present in frontmatter, display a weather badge on the entry page. + +**Display format:** `☀️ Sunny · 28°C` (icon + description + temperature) +- Icon chosen from a small set based on `weather_desc`: + - Sunny → ☀️ + - Partly cloudy → ⛅ + - Cloudy → ☁️ + - Foggy → 🌫️ + - Drizzle → 🌦️ + - Rain → 🌧️ + - Snow → ❄️ + - Thunderstorm → ⛈️ + +**Placement:** In the entry header, between the date and the body text. Same line as GPS coordinates if those are shown. + +**Edge cases:** +- Only temp, no desc → show temp only +- Only desc, no temp → show desc only +- Neither → hide weather section entirely +- Temperature should always be integer (round if float) + +--- + +### 1.4 — Location Badge on Feed Cards and Entry Page + +**What:** Display `location_city, location_country` as a small badge on tracker feed cards and at the top of entry pages. + +**Feed card:** Below the date, above the excerpt. Format: `📍 Kyoto, Japan` + +**Entry page:** In the header below the date, above the content. Format: `📍 Kyoto, Japan` + +**Edge cases:** +- Only city, no country → `📍 Kyoto` +- Only country, no city → `📍 Japan` +- Neither → location badge hidden entirely +- Long location names: truncate with ellipsis at 30 chars on cards (full text on entry page) + +--- + +### 1.5 — Photo Gallery on Entry Page + +**What:** Photos uploaded to an entry should display in a responsive grid gallery with lightbox (click to enlarge). + +**Implementation approach:** Use Grav's native media collection for the entry page. Each `.entry` folder contains its photos. Render them in a grid in `entry.html.twig`. Use a minimal vanilla JS lightbox — no external framework. + +**Gallery behavior:** +- Photos displayed in a 2-column grid on mobile, 3-column on desktop +- Each thumbnail is square-cropped, 150px on mobile +- Clicking/tapping a thumbnail opens a lightbox overlay +- Lightbox: dark overlay, full-size image centered, tap/click outside or press Escape to close +- Left/right navigation arrows in lightbox (swipe on mobile) +- No captions needed for v1 + +**Edge cases:** +- 0 photos: gallery section hidden entirely +- 1 photo: still uses grid (single item), lightbox works +- Many photos (>10): gallery still renders (no hard limit on display) +- Non-image files in the media folder: skip them (only render jpg, jpeg, png, webp, gif) + +--- + +### 1.6 — Hero Image on Tracker Feed Cards + +**What:** If an entry has photos, the first photo (or the one named in `hero_image` frontmatter) appears as a thumbnail on the tracker feed card. + +**Implementation:** In `tracker.html.twig`, for each entry: +1. If `entry.header.hero_image` is set, use `entry.media[entry.header.hero_image]` +2. Else, use the first image in `entry.media` sorted by name +3. Render as a 16:9 aspect-ratio thumbnail, full width of card, above the title + +**Edge cases:** +- No photos: card shows no image, just text. No broken `` tag. +- `hero_image` set but file missing: fall back to first media file, or no image +- Very tall/wide images: CSS `object-fit: cover` maintains card aspect ratio + +--- + +## Out of Scope (Milestone 1) + +- Map features (Milestone 2) +- Statistics page (Milestone 3) +- Video support +- Comments or reactions +- Automated reverse geocoding (city name comes from form input, not auto-detected) +- Altitude display (data may not be present) +- Historical weather (Open-Meteo current endpoint only) + +--- + +## Acceptance Criteria + +1. Post form has `location_city` and `location_country` fields that save to entry frontmatter +2. Post form has "Get Weather" button that fills `weather_temp_c` and `weather_desc` via Open-Meteo when lat/lng are provided +3. Entry page shows weather badge when weather fields are present; hidden when absent +4. Entry page shows location badge `📍 City, Country` when location fields are present; hidden when absent +5. Tracker feed card shows location badge when present +6. Tracker feed card shows a hero image when photos exist for an entry +7. Entry page shows a 2-col (mobile) / 3-col (desktop) photo grid +8. Clicking any photo opens a full-screen lightbox with prev/next navigation +9. Pressing Escape or clicking outside lightbox closes it +10. All fields are optional — empty values produce no broken UI elements +11. All interactive elements meet 44px minimum touch target on mobile +12. Form submits correctly with all new fields populated or all blank + +--- + +## Design Notes + +- Weather and location badges should be subtle — small text, muted color, not the visual focus +- Use emoji icons for weather — universal, no icon font dependency +- Gallery grid: `gap: 4px` between thumbs, no borders, square crops +- Lightbox: `background: rgba(0,0,0,0.92)`, image centered with `max-height: 90vh` +- Feed card image: `aspect-ratio: 16/9`, `object-fit: cover`, rounded top corners matching card diff --git a/docs/milestone-2-spec.md b/docs/milestone-2-spec.md new file mode 100644 index 0000000..2e7337b --- /dev/null +++ b/docs/milestone-2-spec.md @@ -0,0 +1,166 @@ +# Milestone 2 Spec — Interactive Map + +**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a chronological route line, with popups linking to entries. + +--- + +## User Stories + +- As a reader, I want to see a world map showing where Mischa has been so I can understand the journey at a glance without reading every entry. +- As a reader, I want to click a map marker and see the entry date, title, and a thumbnail — and be able to click through to the full entry. +- As a reader on mobile, I want to pan and pinch-zoom the map with my fingers without the page scrolling underneath. +- As a traveler (Mischa), I want the map to automatically include every entry that has lat/lng data — I should not need to do any manual map maintenance. +- As a reader, I want the map to show the route line connecting stops in the order they were visited, so the journey makes narrative sense. + +--- + +## Feature Details + +### 2.1 — Map Page + +**Route:** `/map` + +**Template:** `map.html.twig` — extends `partials/base.html.twig` + +**Page file:** `user/pages/03.map/map.md` + +**Content:** +- Full-viewport-height map container below the site header +- Leaflet.js loaded from CDN (jsDelivr): `https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js` +- Leaflet CSS from same CDN +- Tile layer: OpenStreetMap (free, no API key): `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png` +- Attribution: "© OpenStreetMap contributors" + +**Map initialization:** +- Default zoom: auto-fit to bounds of all markers (use `map.fitBounds()`) +- If no entries with GPS data: show world view, zoom 2, centered at 0,0 with a message "No locations yet" +- Min zoom: 2, Max zoom: 18 + +--- + +### 2.2 — Entry Data Serialization + +**How entries reach the map JS:** + +In `map.html.twig`, Grav's Twig will iterate all published entries under `/tracker` and serialize them to a JSON array embedded in a ` +{% endblock %} +``` + +- [ ] **Step 3: Replace the Post form CSS section** + +Find `/* ── Post form ──` in style.css. Replace it entirely with: + +```css +/* ── Post form ──────────────────────────────────────────────────────────────── */ + +.post-form-wrap h1 { + font-family: var(--font-display); + font-size: var(--text-xl); + font-weight: 400; + margin-bottom: var(--space-6); + color: var(--color-ink); +} + +/* Hide GPS coordinate fields — filled by JS, not user-facing */ +.gps-hidden-field { display: none !important; } + +/* Grav form field wrappers */ +.form-field { margin-bottom: var(--space-5); } +.form-label label { + display: block; + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-ink); + margin-bottom: var(--space-2); +} + +.form-field input[type="text"], +.form-field input[type="email"], +.form-field input[type="datetime-local"], +.form-field textarea { + width: 100%; + font-family: var(--font-ui); + font-size: var(--text-base); + padding: 0.875rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-canvas); + color: var(--color-ink); + min-height: 44px; + transition: border-color 0.15s; + -webkit-appearance: none; +} + +.form-field input:focus, +.form-field textarea:focus { + outline: 2px solid var(--color-accent); + outline-offset: 1px; + border-color: var(--color-accent); +} + +.form-field textarea { resize: vertical; min-height: 160px; line-height: var(--leading-normal); } + +/* Submit button — Grav renders it as .btn or input[type=submit] */ +.form-actions input[type="submit"], +.form-actions .btn, +.form-actions button[type="submit"] { + display: block; + width: 100%; + padding: 1rem; + min-height: 52px; + background: var(--color-accent); + color: var(--color-accent-on); + border: none; + border-radius: var(--radius-md); + font-family: var(--font-ui); + font-size: var(--text-base); + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + margin-top: var(--space-6); +} + +.form-actions input[type="submit"]:hover, +.form-actions button[type="submit"]:hover { background: var(--color-accent-hover); } + +/* Action buttons row (Get Location, Get Weather) */ +.form-action-row { + display: flex; + gap: var(--space-3); + margin-top: var(--space-5); +} + +.btn-action { + flex: 1; + padding: 0.75rem var(--space-3); + min-height: 44px; + background: var(--color-canvas); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-family: var(--font-ui); + font-size: var(--text-sm); + font-weight: 500; + cursor: pointer; + color: var(--color-ink); + transition: background 0.15s, border-color 0.15s; +} + +.btn-action:hover { background: var(--color-paper); border-color: var(--color-accent); } + +/* Status feedback lines */ +.form-status { + font-size: var(--text-sm); + color: var(--color-ink-muted); + margin-top: var(--space-2); + min-height: 1.4em; +} + +.form-status--ok { color: var(--color-accent); } +.form-status--err { color: #B44A2A; } +``` + +- [ ] **Step 4: Verify post form** + +Open `http://100.96.115.96:8081/post` (logged in). Verify: +- Lat/lng inputs not visible (`.gps-hidden-field` hidden via CSS) +- Inputs have rounded corners, proper padding, focus ring in teal +- "Get Location" and "Get Weather" buttons side by side, same width +- "Post Entry" (or whatever the submit label is) in teal, full-width +- Tap "Get Location" → status line shows "✓ Location captured · lat, lng" in teal +- Mobile at 375px: all inputs and buttons are thumb-friendly + +- [ ] **Step 5: Commit** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user +git add pages/02.post/post-form.md themes/intotheeast/templates/post-form.html.twig themes/intotheeast/css/style.css +git commit -m "feat: redesign post form — hide GPS fields, teal CTA, better mobile UX" +``` + +--- + +### Task 7: Stats + map + mini-map styling + +**Files:** +- Modify: `user/themes/intotheeast/templates/stats.html.twig` +- Modify: `user/themes/intotheeast/css/style.css` (Map, Stats, Mini-map sections) + +**Interfaces:** +- Produces: styled stats page and map page using design tokens + +- [ ] **Step 1: Update stats page heading** + +In `user/themes/intotheeast/templates/stats.html.twig`, replace the `

` tag with: + +```twig +
+

Trip Statistics

+``` + +(Remove the inline `style` attribute from the existing h1.) + +- [ ] **Step 2: Replace Stats + Map + Mini-map CSS sections** + +Find `/* ── Map page ──` through end of `/* ── Mini-map on tracker feed ──`. Replace all three sections with: + +```css +/* ── Map page ───────────────────────────────────────────────────────────────── */ + +.map-page .site-main { max-width: none; padding: 0; } + +.map-container { + height: calc(100vh - var(--site-header-height)); + width: 100%; +} + +.map-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-ink-muted); + font-style: italic; +} + +/* ── Stats page ─────────────────────────────────────────────────────────────── */ + +.stats-heading { + font-family: var(--font-display); + font-size: var(--text-2xl); + font-weight: 400; + margin-bottom: var(--space-8); + color: var(--color-ink); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-4); + margin-bottom: var(--space-8); +} + +.stat-block { + background: var(--color-canvas); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-6) var(--space-5); + text-align: center; + box-shadow: var(--shadow-sm); +} + +.stat-value { + display: block; + font-family: var(--font-display); + font-size: var(--text-3xl); + font-weight: 400; + color: var(--color-accent); + line-height: 1.1; + margin-bottom: var(--space-2); +} + +.stat-label { + display: block; + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-ink-muted); + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.stats-countries { + font-size: var(--text-sm); + color: var(--color-ink-2); + text-align: center; + line-height: 1.9; +} + +.stats-countries-label { + font-weight: 600; + display: block; + margin-bottom: var(--space-2); + color: var(--color-ink); + text-transform: uppercase; + font-size: var(--text-xs); + letter-spacing: 0.07em; +} + +.stats-note { + font-size: var(--text-xs); + color: var(--color-ink-muted); + text-align: center; + margin-top: var(--space-6); +} + +/* ── Mini-map on tracker feed ────────────────────────────────────────────────── */ + +.feed-map-wrap { + margin-bottom: var(--space-10); + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); +} + +.feed-map { + height: 240px; + width: 100%; +} + +@media (min-width: 520px) { + .feed-map { height: 300px; } +} + +.feed-map-link { + display: block; + text-align: right; + font-size: var(--text-xs); + font-weight: 500; + color: var(--color-accent); + text-decoration: none; + padding: var(--space-2) var(--space-4); + background: var(--color-paper); + border-top: 1px solid var(--color-border); +} + +.feed-map-link:hover { color: var(--color-accent-hover); } +``` + +- [ ] **Step 3: Update Leaflet marker colors** + +In `tracker.html.twig`, find the JS that sets marker colors. Update from `#0066cc` to `#1F6B5A` and from `#0044aa` to `#155244`: + +```js +var color = isLatest ? '#155244' : '#1F6B5A'; +``` + +Also update the polyline color: +```js +L.polyline(latLngs, { color: '#1F6B5A', weight: 3, opacity: 0.7 }).addTo(map); +``` + +Do the same in `map.html.twig` — update any hardcoded `#0066cc` colors to `#1F6B5A`. + +- [ ] **Step 4: Verify stats and map pages** + +Open `http://100.96.115.96:8081/stats`. Verify: +- Heading in DM Serif Display +- Numbers in DM Serif Display, teal color +- Stat cards on warm white background with subtle shadow +- Labels uppercase, muted gray + +Open `http://100.96.115.96:8081/map`. Verify: +- Map fills viewport below header +- Markers are teal circles +- Route line is teal +- No horizontal scroll or layout issues + +- [ ] **Step 5: Commit** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/templates/stats.html.twig themes/intotheeast/templates/tracker.html.twig themes/intotheeast/templates/map.html.twig themes/intotheeast/css/style.css +git commit -m "feat: apply design tokens to stats, map, and mini-map" +``` + +--- + +### Task 8: Mobile polish + reduced motion + final QA + +**Files:** +- Modify: `user/themes/intotheeast/css/style.css` (add responsive + motion CSS) + +**Interfaces:** +- Produces: fully responsive, accessible design at 320px–1440px viewport widths + +- [ ] **Step 1: Add reduced-motion support** + +At the bottom of `style.css`, append: + +```css +/* ── Accessibility ───────────────────────────────────────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Keyboard focus ring: visible on all interactive elements */ +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} +``` + +- [ ] **Step 2: Verify header on 320px (smallest phone)** + +Set browser devtools to 320px viewport. Verify: +- Site title and nav links both visible and not overlapping +- If title overflows, add this to style.css: + ```css + @media (max-width: 380px) { + .site-title { font-size: var(--text-md); } + .site-nav a { padding: var(--space-2); font-size: 0.8rem; } + } + ``` + +- [ ] **Step 3: Verify post form on mobile** + +On a real phone (or devtools 375px), open `/post` and verify: +- Inputs do not trigger zoom on focus (font-size is 1rem ≥ 16px — already set) +- "Post Entry" button is thumb-reachable (full-width, 52px min height) +- Get Location / Get Weather buttons are side-by-side, each at least 44px tall +- Status feedback visible after tapping location/weather + +- [ ] **Step 4: Cross-page smoke test checklist** + +Check each of these manually in the browser: + +| Page | Check | +|---|---| +| `/tracker` | Feed loads, entry cards show hero photos with overlays | +| `/tracker` | Text-only cards (no photo) show date+location meta above title | +| `/tracker` | Mini-map renders, teal markers and route | +| `/map` | Full-height map, teal markers, route polyline | +| `/map` | Tap marker → popup with date, title, "Read entry →" link | +| `/stats` | 2×2 grid of stat blocks, teal numbers, correct counts | +| `/tracker/` | Hero image full-width at top (if photos exist) | +| `/tracker/` | Title in DM Serif Display, large | +| `/tracker/` | Photo gallery (2/3-col grid), lightbox opens on tap | +| `/post` | Lat/lng fields NOT visible | +| `/post` | Tap Post Entry → success message → redirects to /tracker | +| Mobile 375px | All pages usable without horizontal scroll | +| Mobile 375px | No font size < 14px for readable text | + +- [ ] **Step 5: Final commit** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/css/style.css +git commit -m "feat: add reduced-motion support, keyboard focus, mobile polish" +``` + +--- + +### Task 9: Update documentation + +**Files:** +- Modify: `user/docs/qa-test-plan.md` (add visual QA section) + +- [ ] **Step 1: Add visual design QA section to qa-test-plan.md** + +Append a new section to `user/docs/qa-test-plan.md`: + +```markdown +## Visual Design QA — Redesign Checklist + +**Design spec:** `user/docs/design/design-spec.md` + +### Typography +- [ ] DM Serif Display loads on entry titles, page headings, stats numbers, site title +- [ ] DM Sans loads on all body text, nav, labels, form fields +- [ ] No fallback font (Georgia/system-sans) visible in place of custom fonts + +### 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 +- [ ] Map markers and route polyline are teal + +### Entry cards +- [ ] Cards with photos show full-bleed 16:9 image +- [ ] Date + location overlay visible on photo gradient +- [ ] Entry title below photo in DM Serif Display +- [ ] Cards without photos show date/location meta row above title +- [ ] Photo zoom on hover (desktop only) + +### Header +- [ ] 3px teal bar at top of header +- [ ] "into the east" title in DM Serif Display +- [ ] Sticky on scroll + +### Post form +- [ ] Lat/lng inputs not visible +- [ ] "✓ Location captured" feedback in teal on success +- [ ] Submit button full-width, teal, 52px+ height + +### Mobile +- [ ] All interactive elements ≥ 44px touch target +- [ ] No horizontal scroll at 375px +- [ ] iOS: no font-size zoom on input focus +``` + +- [ ] **Step 2: Commit documentation** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user +git add docs/qa-test-plan.md docs/design/design-spec.md docs/working/plans/2026-06-18-ui-redesign.md +git commit -m "docs: add UI redesign spec, plan, and visual QA checklist" +``` diff --git a/docs/superpowers/plans/2026-06-19-gpx-manager.md b/docs/superpowers/plans/2026-06-19-gpx-manager.md new file mode 100644 index 0000000..486b21c --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-gpx-manager.md @@ -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//`; 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() : [] %} + +
+

GPX Files

+ + {% if trips is empty %} +

No trips found.

+ {% else %} + {% for trip in trips %} +
+

{{ trip.title }}

+
+

Loading…

+
+
+ + + +
+
+ {% endfor %} + {% endif %} +
+ + + + + +{% 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 ` + + + + + + + +``` + +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 diff --git a/docs/superpowers/specs/2026-06-21-documentation-restructure-design.md b/docs/superpowers/specs/2026-06-21-documentation-restructure-design.md index e67d6cc..251e620 100644 --- a/docs/superpowers/specs/2026-06-21-documentation-restructure-design.md +++ b/docs/superpowers/specs/2026-06-21-documentation-restructure-design.md @@ -66,12 +66,12 @@ docs/ | New path | Current path | |---|---| -| `working/specs/*` (13 files) | `docs/superpowers/specs/*` | -| `working/plans/*` (14 files) | `docs/superpowers/plans/*` | -| `working/milestones/milestone-1.md` | `docs/milestone-1-spec.md` | -| `working/milestones/milestone-2.md` | `docs/milestone-2-spec.md` | -| `working/milestones/milestone-3.md` | `docs/milestone-3-spec.md` | -| `working/milestones/milestone-4.md` | `docs/milestone-4-spec.md` | +| `working/specs/*` (13 files) | `docs/working/specs/*` | +| `working/plans/*` (14 files) | `docs/working/plans/*` | +| `working/milestones/milestone-1.md` | `docs/working/milestones/milestone-1.md` | +| `working/milestones/milestone-2.md` | `docs/working/milestones/milestone-2.md` | +| `working/milestones/milestone-3.md` | `docs/working/milestones/milestone-3.md` | +| `working/milestones/milestone-4.md` | `docs/working/milestones/milestone-4.md` | | `working/backlog.md` | `docs/backlog.md` | | `working/production-todo.md` | `docs/production-todo.md` | | `working/pm-analysis.md` | `docs/pm-analysis.md` | @@ -150,7 +150,7 @@ Specs: `docs/working/specs/YYYY-MM-DD--design.md` Plans: `docs/working/plans/YYYY-MM-DD-.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): - §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/` 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 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 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 -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/` diff --git a/php/php-local.ini b/php/php-local.ini index 23e6e85..be51c14 100644 --- a/php/php-local.ini +++ b/php/php-local.ini @@ -2,3 +2,4 @@ upload_max_filesize = 100M post_max_size = 500M max_file_uploads = 20 +session.save_path = /tmp diff --git a/plugins.txt b/plugins.txt index d1b9fb5..127924c 100644 --- a/plugins.txt +++ b/plugins.txt @@ -1,4 +1,3 @@ -admin email error form diff --git a/scripts/server-install.sh b/scripts/server-install.sh index 967d0ef..084b2f6 100755 --- a/scripts/server-install.sh +++ b/scripts/server-install.sh @@ -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 unzip -oq grav-admin.zip cp -rf grav-admin/. . +cp -rf grav-admin/user/plugins/admin2 /tmp/admin2-plugin rm -rf grav-admin grav-admin.zip echo "==> Cloning user repo" @@ -41,6 +42,8 @@ fi echo "==> Creating required directories" mkdir -p user/plugins user/accounts user/data +cp -rf /tmp/admin2-plugin user/plugins/admin2 +rm -rf /tmp/admin2-plugin echo "==> Installing plugins" php bin/gpm install $PLUGINS -y diff --git a/user b/user index c403ea9..f6a8657 160000 --- a/user +++ b/user @@ -1 +1 @@ -Subproject commit c403ea9593939c660d2046a920575a67d94e3531 +Subproject commit f6a8657de2e963038e05d1d71af89ff9e7f80880