docs: add home/trip convergence plan (complete) and update design spec
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
# Home / Trip View Convergence Implementation Plan
|
||||
|
||||
**Status:** ✅ Complete (2026-06-27)
|
||||
|
||||
> **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:** Make the home page's active-trip view present the same feed-col chrome (date range, filter bar, stats/cycling panels) as the trip page, by extracting the chrome into one shared Twig partial and the stats computation into one shared JS function.
|
||||
|
||||
**Architecture:** A new partial `templates/partials/trip-feed-col.html.twig` holds the entire `.home-feed-col` markup (header, filter bar, panel toggles, stats/cycling macro calls, feed loop) and is included by both `trip.html.twig` and `home.html.twig` (active branch). The inline stats/cycling computation currently in `trip.html.twig` becomes a window-exposed `initTripStats(config)` in `js/src/main.js`; the partial emits a small `DOMContentLoaded` inline script that calls it with page-specific data. The two intended differences (home has no sort button and keeps its own feed order) are driven by partial params, not separate markup.
|
||||
|
||||
**Tech Stack:** Grav 2.0 / Twig templates, esbuild-bundled vanilla JS (`js/src/main.js` → `js/main.js`), MapLibre via `map.js` (`window.MapUtils`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Only ever write changes inside `travel-blog-intotheeast/` or subfolders.** The `user/` tree is a standalone git repo synced via `make content-push`; commit there as instructed by the execution skill.
|
||||
- **Dev mode stays dev** — `twig.cache: false` is already set. Do NOT toggle any dev/prod config flag to work around caching; theme edits take effect on reload.
|
||||
- **No map convergence.** Both inline map `<script>` blocks and both `.home-map-col` markup blocks stay exactly as they are. Do not touch map markers, fullscreen wiring, or map data-build loops.
|
||||
- **No visual restyling.** Home reuses the trip's existing CSS classes unchanged. No new CSS class names except the pre-departure divider (`home-predeparture-divider`) and reuse of existing `home-highlights-cta` / `home-highlights-cta-wrap` for the pre-departure button.
|
||||
- **No new JS for filter/sort/panels** — `initFilterBar()`, `initPanelToggles()`, `initSortButton()` are already global and selector-guarded. Only `initTripStats` is new.
|
||||
- **Trip page rendered output must be visually and functionally identical** to before for the populated and empty cases — exact bytes may differ (the partial re-indents the feed-col markup, and the stats logic moves into a relocated inline `<script>`). Structural refactor only on that side; verify by behavioral smoke test, not a literal diff.
|
||||
- **Built JS is generated** — never hand-edit `js/main.js`; edit `js/src/main.js` and rebuild with `make build-assets`.
|
||||
- Dev server: `http://localhost:8081`. All verification is manual browser smoke testing (no JS test harness exists).
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `user/themes/intotheeast/js/src/main.js` (edit) | Add `initTripStats(config)`; expose on `window`. Rebuild → `js/main.js`. |
|
||||
| `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig` (new) | The entire shared `.home-feed-col`: header, filter bar (sort button gated), panel toggles, stats/cycling macro calls, feed loop, pre-departure block, and the inline `initTripStats` call. |
|
||||
| `user/themes/intotheeast/templates/trip.html.twig` (edit) | Replace inline `.home-feed-col` (`:70-120`) with the partial include; remove inline stats script (`:213-249`). Map untouched. |
|
||||
| `user/themes/intotheeast/templates/home.html.twig` (edit) | Active branch: add `gps_points` build; replace bespoke feed-col (`:60-83`) with the partial include (`show_sort: false`, `pre_departure` gated). Map untouched. Between-trips branch untouched. |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Shared stats glue `initTripStats(config)` in main.js
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/js/src/main.js` (add function near the other init functions, ~after `initPanelToggles` at `:239`; expose on `window`)
|
||||
- Rebuild artifact: `user/themes/intotheeast/js/main.js` (via `make build-assets`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `window.MapUtils.parseGpxFiles(urls, cb)`, `window.MapUtils.haversineKm(lat1, lng1, lat2, lng2)` (from `map.js`, loaded in the `bottom` asset group).
|
||||
- Produces: `window.initTripStats(config)` where `config = { gpxUrls: string[], gpsPoints: [number,number][], hasGpx: boolean }`. Selector-guarded: no-op when `#stat-distance` is absent. No-GPX fallback writes `'—'` (not `~0`) and returns when `gpsPoints.length < 2`. This is the exact contract the partial's inline script (Task 2) and both templates (Tasks 3–4) rely on.
|
||||
|
||||
- [ ] **Step 1: Add the `initTripStats` function**
|
||||
|
||||
In `user/themes/intotheeast/js/src/main.js`, immediately after the `initPanelToggles` function (after line 239, before the `/* ── Boot ── */` comment), add:
|
||||
|
||||
```js
|
||||
/* ── Trip stats / cycling computation (trip + home-active) ───
|
||||
config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool }
|
||||
No-op if #stat-distance is absent (page rendered no stats panel).
|
||||
No-GPX fallback: if gpsPoints.length < 2, write '—' and return (no '~0'). */
|
||||
function initTripStats(config) {
|
||||
var distEl = document.getElementById('stat-distance');
|
||||
if (!distEl) return;
|
||||
|
||||
var gpxUrls = config.gpxUrls || [];
|
||||
var gpsPoints = config.gpsPoints || [];
|
||||
|
||||
if (config.hasGpx) {
|
||||
MapUtils.parseGpxFiles(gpxUrls, function (result) {
|
||||
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
|
||||
function setText(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = val;
|
||||
}
|
||||
setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—');
|
||||
setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—');
|
||||
setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—');
|
||||
setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—');
|
||||
setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—');
|
||||
setText('cyc-moving-time', result.movingTime || '—');
|
||||
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
|
||||
});
|
||||
} else {
|
||||
if (gpsPoints.length < 2) {
|
||||
distEl.textContent = '—';
|
||||
return;
|
||||
}
|
||||
var total = 0;
|
||||
for (var i = 1; i < gpsPoints.length; i++) {
|
||||
total += MapUtils.haversineKm(
|
||||
parseFloat(gpsPoints[i-1][0]), parseFloat(gpsPoints[i-1][1]),
|
||||
parseFloat(gpsPoints[i][0]), parseFloat(gpsPoints[i][1])
|
||||
);
|
||||
}
|
||||
distEl.textContent = '~' + Math.round(total).toLocaleString();
|
||||
}
|
||||
}
|
||||
window.initTripStats = initTripStats;
|
||||
```
|
||||
|
||||
Note: the function is **not** added to the `DOMContentLoaded` boot block — it is called per-page from the partial's inline script (Task 2) with page-specific config. `window.initTripStats =` is required because `main.js` is bundled as an IIFE, so the function is otherwise not reachable from inline template scripts.
|
||||
|
||||
- [ ] **Step 2: Rebuild the JS bundle**
|
||||
|
||||
Run: `make build-assets`
|
||||
Expected: completes without esbuild errors; `user/themes/intotheeast/js/main.js` is regenerated.
|
||||
|
||||
- [ ] **Step 3: Verify the function is exposed in the built bundle**
|
||||
|
||||
Run: `grep -c "initTripStats" /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast/js/main.js`
|
||||
Expected: a non-zero count (the minified bundle contains the symbol).
|
||||
|
||||
- [ ] **Step 4: Smoke-test that existing pages still work (no regression from the additive change)**
|
||||
|
||||
Load `http://localhost:8081/trips/japan-korea-2026` (or the active trip) in a browser. The trip page still uses its own inline stats script at this point, so stats should populate exactly as before. Open the console and confirm **no errors** and that `typeof window.initTripStats === 'function'`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast
|
||||
git add js/src/main.js js/main.js
|
||||
git commit -m "feat(theme): add shared initTripStats() for trip+home stats panels"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Shared partial `trip-feed-col.html.twig`
|
||||
|
||||
**Files:**
|
||||
- Create: `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes (params, passed via `{% include 'partials/trip-feed-col.html.twig' with {…} only %}`):
|
||||
|
||||
| Param | Type | Trip passes | Home-active passes |
|
||||
|---|---|---|---|
|
||||
| `trip_page` | Page | `page` | `trip` |
|
||||
| `all_items` | array | sorted by date, flag 4 | sorted by date, flag 3 |
|
||||
| `journal_entries` | array | dailies children | dailies children |
|
||||
| `journal_count` | int | count | count |
|
||||
| `story_count` | int | count | count |
|
||||
| `has_gpx` | bool | `gpx_urls\|length > 0` | `home_gpx_urls\|length > 0` |
|
||||
| `gpx_urls` | array | `gpx_urls` | `home_gpx_urls` |
|
||||
| `gps_points` | array | `gps_points` | `gps_points` (new on home, Task 4) |
|
||||
| `show_sort` | bool | `true` | `false` |
|
||||
| `pre_departure` | bool | `false` | `all_items\|length == 0` |
|
||||
|
||||
`gpx_urls` and `gps_points` are added to the spec's interface table as the agreed implementation choice: the partial emits the `initTripStats` inline call itself (single place), so it needs the page-specific data.
|
||||
- Consumes globally: `window.initTripStats` (Task 1), `window.MapUtils` (map.js), CSS classes from the existing theme.
|
||||
- Produces: the `.home-feed-col` DOM that `initFilterBar` / `initPanelToggles` / `initSortButton('trip-sort-toggle', …)` already key off (`.trip-filter-btn`, `[data-type]`, `.trip-panel-toggle`, `#feed-filter-empty`, `#trip-sort-toggle`).
|
||||
|
||||
- [ ] **Step 1: Create the partial file**
|
||||
|
||||
Create `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig` with exactly:
|
||||
|
||||
```twig
|
||||
{% import 'macros/stats.html.twig' as stats_m %}
|
||||
{% import 'macros/cycling.html.twig' as cycling_m %}
|
||||
<div class="home-feed-col">
|
||||
{% if pre_departure %}
|
||||
{# ── Pre-departure landing state (home-active only) ──────────── #}
|
||||
<div class="home-trip-header">
|
||||
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
|
||||
{% if trip_page.header.date_start %}
|
||||
<p class="trip-dates">Departing {{ trip_page.header.date_start|date('d M Y') }}</p>
|
||||
{% endif %}
|
||||
<span class="home-trip-counts">Coming soon</span>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<hr class="home-predeparture-divider">
|
||||
<p class="feed-empty">The journey hasn't begun yet — check back once we're on the road.</p>
|
||||
<div class="home-highlights-cta-wrap">
|
||||
<a class="home-highlights-cta" href="/trips">In the meantime, explore my other trips →</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="home-trip-header">
|
||||
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
|
||||
{% if trip_page.header.date_start %}
|
||||
<p class="trip-dates">
|
||||
{{ trip_page.header.date_start|date('d M Y') }}
|
||||
{% if trip_page.header.date_end %} — {{ trip_page.header.date_end|date('d M Y') }}{% else %} — Ongoing{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<span class="home-trip-counts">
|
||||
{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }}
|
||||
{% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
|
||||
</span>
|
||||
<div class="trip-filter-bar">
|
||||
<div class="trip-filter-group">
|
||||
<button class="trip-filter-btn is-active" data-filter="all" aria-pressed="true">All content</button>
|
||||
<button class="trip-filter-btn" data-filter="journal" aria-pressed="false">Journal</button>
|
||||
<button class="trip-filter-btn" data-filter="story" aria-pressed="false">Stories</button>
|
||||
</div>
|
||||
{% if show_sort %}
|
||||
<button class="trip-stats-btn" id="trip-sort-toggle" aria-label="Sort: oldest first">↑</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="trip-panel-toggles">
|
||||
<button class="trip-panel-toggle" id="trip-stats-toggle" aria-expanded="false" aria-controls="trip-stats-block">Stats <span class="trip-panel-caret" aria-hidden="true">▾</span></button>
|
||||
{% if has_gpx %}
|
||||
<button class="trip-panel-toggle" id="trip-cycling-toggle" aria-expanded="false" aria-controls="trip-cycling-block">Cycling <span class="trip-panel-caret" aria-hidden="true">▾</span></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ stats_m.stats_panel(journal_entries, trip_page, journal_count, has_gpx) }}
|
||||
|
||||
{% if has_gpx %}
|
||||
{{ cycling_m.cycling_panel() }}
|
||||
{% endif %}
|
||||
|
||||
<div class="feed">
|
||||
{% if all_items|length > 0 %}
|
||||
{% for item in all_items %}
|
||||
{% set entry = item.page %}
|
||||
{% if item.type == 'journal' %}
|
||||
{% include 'partials/entry-journal.html.twig' %}
|
||||
{% else %}
|
||||
{% include 'partials/entry-story.html.twig' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
||||
{% endif %}
|
||||
<p id="feed-filter-empty" class="feed-empty" style="display:none;"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initTripStats({
|
||||
gpxUrls: {{ gpx_urls|json_encode|raw }},
|
||||
gpsPoints: {{ gps_points|json_encode|raw }},
|
||||
hasGpx: {{ has_gpx ? 'true' : 'false' }}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
```
|
||||
|
||||
Notes baked into this markup:
|
||||
- The non-pre-departure feed keeps the `{% else %}` "No entries yet" fallback so the trip page's empty-case output is unchanged (trip always passes `pre_departure: false`). Home never reaches this fallback because home-empty sets `pre_departure: true`.
|
||||
- The `initTripStats` call is wrapped in `DOMContentLoaded` so `window.initTripStats` and `window.MapUtils` (both in the `bottom` asset group rendered at the end of `<body>`) are defined when it runs.
|
||||
- The call is **not** nested inside any map-entries condition, so a trip with GPX but zero geocoded journal entries still populates the panels.
|
||||
- The partial is included with `only`, so it imports the `stats`/`cycling` macros itself.
|
||||
|
||||
- [ ] **Step 2: Verify Twig syntax compiles (no include yet, so render via a temporary check)**
|
||||
|
||||
The partial isn't referenced anywhere yet, so it can't render on its own. Verify there are no obvious Twig errors by confirming the file is well-formed:
|
||||
|
||||
Run: `grep -c "endif\|endfor\|endmacro" /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`
|
||||
Expected: non-zero (sanity check the file saved). Real verification happens in Task 3 when the trip page includes it.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast
|
||||
git add templates/partials/trip-feed-col.html.twig
|
||||
git commit -m "feat(theme): add shared trip-feed-col partial"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Refactor `trip.html.twig` to use the partial
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/trip.html.twig` (replace `:70-120`; remove `:213-249`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the partial from Task 2, `window.initTripStats` from Task 1.
|
||||
- Produces: visually and functionally identical trip-page output (regression-critical) — exact bytes may differ (re-indented markup, relocated stats `<script>`); confirm via the behavioral checks in Step 3, not a literal diff. The trip page already builds `all_items` (flag 4), `journal_entries`, `journal_count`, `story_count`, `gps_points`, `gpx_urls`, `has_gpx` — all passed straight through.
|
||||
|
||||
- [ ] **Step 1: Replace the inline `.home-feed-col` block with the include**
|
||||
|
||||
In `user/themes/intotheeast/templates/trip.html.twig`, replace the entire block from line 70 (` <div class="home-feed-col">`) through line 120 (` </div>`, the closing of `.home-feed-col`) with:
|
||||
|
||||
```twig
|
||||
{% include 'partials/trip-feed-col.html.twig' with {
|
||||
trip_page: page,
|
||||
all_items: all_items,
|
||||
journal_entries: journal_entries,
|
||||
journal_count: journal_count,
|
||||
story_count: story_count,
|
||||
has_gpx: has_gpx,
|
||||
gpx_urls: gpx_urls,
|
||||
gps_points: gps_points,
|
||||
show_sort: true,
|
||||
pre_departure: false
|
||||
} only %}
|
||||
```
|
||||
|
||||
Leave the surrounding `<div class="home-layout">` and `.home-map-col` block (lines 58–68) and the closing `</div>` of `.home-layout` (line 121) intact.
|
||||
|
||||
- [ ] **Step 2: Remove the inline stats script**
|
||||
|
||||
In the same file, delete the inline stats block — from line 213 (`var STATS_GPS = …`) through line 249 (the closing `})();` of the stats IIFE), inclusive. Specifically remove:
|
||||
|
||||
```twig
|
||||
var STATS_GPS = {{ gps_points|json_encode|raw }};
|
||||
var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
|
||||
|
||||
(function() {
|
||||
var distEl = document.getElementById('stat-distance');
|
||||
|
||||
if (HAS_GPX) {
|
||||
MapUtils.parseGpxFiles(GPX_URLS, function(result) {
|
||||
...
|
||||
});
|
||||
} else {
|
||||
var total = 0;
|
||||
...
|
||||
}
|
||||
|
||||
})();
|
||||
```
|
||||
|
||||
The map `<script>`'s `document.addEventListener('DOMContentLoaded', function() { … });` wrapper and its closing `}); // DOMContentLoaded` (line 251) stay — only the stats portion inside it is removed. The map setup, marker loop, fitBounds, `renderGpxJourney`, and the fullscreen IIFE (`:201-211`) remain untouched.
|
||||
|
||||
- [ ] **Step 3: Reload and regression-test the trip page**
|
||||
|
||||
Load `http://localhost:8081/trips/japan-korea-2026` (active trip with content). Confirm:
|
||||
- Header, date range, counts render as before.
|
||||
- Filter bar **with** the sort button (`↑`) is present.
|
||||
- Stats panel toggles open; distance populates (GPX → exact number; no GPX → `~`-prefixed estimate).
|
||||
- If the trip has GPX: Cycling toggle present and its panel populates.
|
||||
- Feed lists journal + stories, default order oldest→newest (flag 4, unchanged).
|
||||
- Filter All/Journal/Stories works; sort button flips order.
|
||||
- Console shows no errors.
|
||||
|
||||
- [ ] **Step 4: Verify the map is unaffected**
|
||||
|
||||
On the same page, confirm the map renders with markers, fits bounds, draws the GPX/journey route, and the mobile fullscreen button still works (resize on toggle). Marker click still scrolls to and flashes the card.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast
|
||||
git add templates/trip.html.twig
|
||||
git commit -m "refactor(theme): trip.html.twig uses shared trip-feed-col partial + initTripStats"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire `home.html.twig` active branch to the partial
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/home.html.twig` (active branch: add `gps_points` build at `:30-31`; replace `:60-83`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the partial from Task 2, `window.initTripStats` from Task 1.
|
||||
- Produces: home-active now renders date range, filter bar (no sort button), and stats/cycling panels, plus the pre-departure block when no entries exist. The between-trips branch (`{% else %}`) is untouched and does not use the partial.
|
||||
|
||||
- [ ] **Step 1: Add the `gps_points` build (no-GPX stats fallback)**
|
||||
|
||||
In `user/themes/intotheeast/templates/home.html.twig`, in the active-trip branch, after the counts at line 30 (`{% set story_count = story_entries|length %}`) and before the `map_entries` build (line 32), insert:
|
||||
|
||||
```twig
|
||||
|
||||
{% set gps_points = [] %}
|
||||
{% for entry in journal_entries %}
|
||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
||||
{% set gps_points = gps_points|merge([[entry.header.lat, entry.header.lng]]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
This mirrors `trip.html.twig:27-32`.
|
||||
|
||||
- [ ] **Step 2: Replace the bespoke feed-col with the include**
|
||||
|
||||
In the same file, replace the entire `<div class="home-feed-col">` block from line 60 through its closing `</div>` at line 83 with:
|
||||
|
||||
```twig
|
||||
{% include 'partials/trip-feed-col.html.twig' with {
|
||||
trip_page: trip,
|
||||
all_items: all_items,
|
||||
journal_entries: journal_entries,
|
||||
journal_count: journal_count,
|
||||
story_count: story_count,
|
||||
has_gpx: home_gpx_urls|length > 0,
|
||||
gpx_urls: home_gpx_urls,
|
||||
gps_points: gps_points,
|
||||
show_sort: false,
|
||||
pre_departure: all_items|length == 0
|
||||
} only %}
|
||||
```
|
||||
|
||||
Leave `<div class="home-layout">` and the `.home-map-col` block (lines 55–58) and the closing `</div>` of `.home-layout` (line 84) intact. The map `<script>` block (lines 86–139, gated by `map_entries|length > 0`) stays untouched.
|
||||
|
||||
- [ ] **Step 3: Reload and test home-active (with content)**
|
||||
|
||||
Ensure `config.site.travelling: true` and the active trip has posts. Load `http://localhost:8081/`. Confirm:
|
||||
- Date range, counts, and filter bar appear — **no** sort button.
|
||||
- Stats panel toggles open and distance populates (`~` estimate from `gps_points` when no GPX; exact when GPX present); Cycling panel appears and populates only if the trip has GPX.
|
||||
- Filter All/Journal/Stories works; panel toggles work.
|
||||
- Feed default order is home's own (flag 3, unchanged from today).
|
||||
- Console shows no errors; map still renders.
|
||||
|
||||
- [ ] **Step 4: Test the pre-departure empty state**
|
||||
|
||||
With `travelling: true` and **no posts** in the active trip's `dailies`/`stories` (temporarily, or on a fresh trip), load `/`. Confirm:
|
||||
- The pre-departure block shows the trip title, "Departing <date>", "Coming soon", a divider, and the "In the meantime, explore my other trips →" button linking to `/trips`.
|
||||
- The filter bar, panel toggles, and the generic "No entries yet" fallback do **not** appear.
|
||||
- After posting one entry (or restoring content), the pre-departure block disappears and the normal filter bar + feed render.
|
||||
|
||||
- [ ] **Step 5: Regression-test between-trips mode**
|
||||
|
||||
Set `config.site.travelling: false`, load `/`. Confirm the highlights grid layout is unchanged (this branch does not use the partial). Restore `travelling: true` afterward if that is the intended dev state.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast
|
||||
git add templates/home.html.twig
|
||||
git commit -m "feat(theme): home-active reuses trip-feed-col partial with stats + pre-departure state"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- Date-range header, filter bar, stats/cycling panels on home-active → Tasks 2 + 4. ✅
|
||||
- Chrome in one place (partial) → Task 2; both pages include it → Tasks 3, 4. ✅
|
||||
- Home keeps own order, no sort button → `show_sort: false`, `all_items` flag 3 unchanged (Task 4). ✅
|
||||
- Stats/cycling from single shared JS → Task 1 (`initTripStats`), called via partial. ✅
|
||||
- No visual/functional change to trip output (exact bytes may differ: re-indented markup, relocated stats `<script>`) → Task 3 passes through existing vars; partial preserves the empty-case `{% else %}` fallback. ✅
|
||||
- Stats glue runs in `DOMContentLoaded`, not nested in map block, `<2`-points guard writes `—` → Task 1 + partial script. ✅
|
||||
- Home `gps_points` build added → Task 4 Step 1. ✅
|
||||
- Pre-departure block (title + start date + "Coming soon" + divider + button; suppresses filter bar/fallback; panels hidden) → Task 2 markup + Task 4 gating. ✅
|
||||
- Map convergence out of scope; both map blocks untouched → Tasks 3, 4 leave map markup/scripts intact. ✅
|
||||
- Between-trips branch untouched → Task 4 only edits the active branch. ✅
|
||||
|
||||
**Placeholder scan:** No TBD/TODO/"handle edge cases" — every step has concrete code or an exact command. ✅
|
||||
|
||||
**Type consistency:** `initTripStats` config keys (`gpxUrls`, `gpsPoints`, `hasGpx`) match between Task 1 (definition), the partial's inline call (Task 2), and the data both pages pass (Tasks 3, 4). Partial param names match the include calls in both templates. `gpx_urls`/`gps_points`/`has_gpx` consistent throughout. ✅
|
||||
|
||||
---
|
||||
|
||||
**Plan complete and saved to `docs/working/plans/2026-06-27-home-trip-view-convergence.md`. Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.
|
||||
|
||||
**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints.
|
||||
|
||||
Which approach?
|
||||
@@ -60,6 +60,7 @@ Holds the entire `.home-feed-col` content currently inline in `trip.html.twig:70
|
||||
| `story_count` | int | count | count |
|
||||
| `has_gpx` | bool | `gpx_urls\|length > 0` | `home_gpx_urls\|length > 0` |
|
||||
| `show_sort` | bool | `true` | `false` |
|
||||
| `pre_departure` | bool | `false` | `all_items\|length == 0` |
|
||||
|
||||
Because the partial is called with `only`, it must `{% import 'macros/stats.html.twig' %}` and `{% import 'macros/cycling.html.twig' %}` itself.
|
||||
|
||||
@@ -67,16 +68,17 @@ Both pages already build `all_items`, the counts, and `has_gpx` for their existi
|
||||
|
||||
### 2. Shared stats glue: `initTripStats(config)` in `js/src/main.js`
|
||||
|
||||
Extract the inline stats/cycling computation from `trip.html.twig:213-249` into a config-driven function. Current inline logic: if GPX present, `MapUtils.parseGpxFiles(urls, …)` fills `#stat-distance` and all `#cyc-*` fields; otherwise sum `haversineKm` over `gps_points` and write the `~`-prefixed estimate to `#stat-distance`.
|
||||
Extract the inline stats/cycling computation from `trip.html.twig:213-249` into a config-driven function. Current inline logic: if GPX present, `MapUtils.parseGpxFiles(urls, …)` fills `#stat-distance` and all `#cyc-*` fields; otherwise sum `haversineKm` over `gps_points` and write the `~`-prefixed estimate to `#stat-distance` — but when `gps_points` has fewer than 2 points, write `—` (not `~0`) and return, preserving the existing guard at `trip.html.twig:245`. This matters on home-active in the pre-departure / zero-entry state, where dropping the guard would render "~0 km roamed" instead of the macro's `—` placeholder.
|
||||
|
||||
```js
|
||||
function initTripStats(config) {
|
||||
// config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool }
|
||||
// No-op if #stat-distance is absent (page has no stats panel).
|
||||
// No-GPX fallback: if gpsPoints.length < 2, write '—' and return (no '~0').
|
||||
}
|
||||
```
|
||||
|
||||
Called from the boot block alongside the other inits. Each page provides the config via a small inline `<script>` that defines the data (the `*_GPX_URLS` / `gps_points` arrays are page-specific Twig output), then calls `initTripStats(...)` — or the boot reads globals the page sets. Implementation detail for the plan; the contract is: the function is selector-guarded and runs on any page that rendered a stats panel.
|
||||
Called from the boot block alongside the other inits. Each page provides the config via a small inline `<script>` that defines the data (the `*_GPX_URLS` / `gps_points` arrays are page-specific Twig output), then calls `initTripStats(...)` — or the boot reads globals the page sets. Implementation detail for the plan; the contract is: the function is selector-guarded and runs on any page that rendered a stats panel. Two placement invariants the current working code relies on must carry forward: the inline call **must run inside a `DOMContentLoaded` handler** (mirroring the current `trip.html.twig` stats IIFE) so that `initTripStats` and `MapUtils` — loaded via the `bottom` asset group rendered at the end of `<body>`, after content-block inline scripts — are defined when it executes; and it **must not be nested inside the `{% if map_entries|length > 0 %}` map block**, or a trip with GPX but zero geocoded journal entries would render the panels yet never populate them.
|
||||
|
||||
`js/main.js` is the built artifact; the asset pipeline rebuilds it from `js/src/main.js` (see `2026-06-22-asset-pipeline-design.md`).
|
||||
|
||||
@@ -90,6 +92,20 @@ Home-active currently builds `map_entries` (journal-only) and `home_gpx_urls`. F
|
||||
|
||||
**`home.html.twig`** (active branch): replace the bespoke feed-col (`:60-83`) with the same partial include (`show_sort: false`); add `gps_points` build; wire `initTripStats(...)`. Map script and map-col stay untouched.
|
||||
|
||||
### 5. Home-active pre-departure empty state
|
||||
|
||||
Resolves the review finding that home-active (the landing page) would otherwise show two competing empty states before the first post — the static `{% else %}` "No entries yet" feed fallback *and* the JS `#feed-filter-empty` sentinel — once a filter tab is clicked.
|
||||
|
||||
When `all_items` is empty (gated by the new `pre_departure` param), the partial renders a single **pre-departure block** instead of the filter bar, panel toggles, and the generic feed fallback:
|
||||
|
||||
- the active trip's title and `trip_page.header.date_start` (e.g. "Departing 17 Jun 2026"), with a "Coming soon" note
|
||||
- a clear divider
|
||||
- a short line + button — "In the meantime, explore my other trips →" — linking to the Past Trips page
|
||||
|
||||
This block renders **only while `all_items|length == 0`** and disappears entirely once the first entry is posted, at which point the normal filter bar + feed render. It is home-active-only: the trip page passes `pre_departure: false` and is unaffected (it is not reachable before content exists). The block is new home-only markup but reuses existing typography/button classes — no new visual language.
|
||||
|
||||
Open sub-decision for implementation: whether the Stats/Cycling panels are also hidden in this state (they would otherwise show "0 days / 0 entries"). Defaulting to hidden, for consistency with the suppressed filter bar.
|
||||
|
||||
## Data / behavior flow after change
|
||||
|
||||
```
|
||||
@@ -117,6 +133,7 @@ No JS test harness exists in this project; verification is manual browser smoke
|
||||
4. With `config.site.travelling: true`, load `/`. Confirm date range, counts, filter bar (no sort button), and Stats panel (+ Cycling if the trip has GPX) now appear.
|
||||
5. Filter bar filters the feed; panel toggles work; stats figures populate.
|
||||
6. Confirm the feed default order is home's own order (unchanged from today) and that no sort button is present.
|
||||
6b. **Pre-departure state:** with `travelling: true` and no posts yet, confirm home shows the trip title + start date + "Coming soon" and the "explore my other trips" divider/button — and that the filter bar and the "No entries yet" fallback do *not* both appear. Post one entry and confirm the pre-departure block disappears and the normal filter bar + feed render.
|
||||
|
||||
**Home page, between-trips mode (regression):**
|
||||
7. With `config.site.travelling: false`, load `/`. Confirm the highlights layout is unaffected (this branch does not use the partial).
|
||||
|
||||
Reference in New Issue
Block a user