Files
intotheeast-com/docs/working/specs/2026-06-27-home-trip-view-convergence-design.md

147 lines
11 KiB
Markdown

# Home / Trip View Convergence Design
**Date:** 2026-06-27
**Status:** Approved for implementation
## Problem
The home page in active-trip mode (`home.html.twig`, `config.site.travelling` branch) and the trip page (`trip.html.twig`) are meant to present the same experience — the same content, behaving near-identically. Today their feeds already match (both render journal + story entries via the shared `entry-journal`/`entry-story` partials), but the **feed-col chrome diverges**:
| Feature | Trip page | Home-active | Converge? |
|---|---|---|---|
| Feed lists journal + stories | ✅ | ✅ | already matches |
| Date-range header | ✅ | ❌ | **yes** |
| Filter bar (All / Journal / Stories) | ✅ | ❌ | **yes** |
| Stats panel | ✅ | ❌ | **yes** |
| Cycling panel | ✅ (if GPX) | ❌ | **yes** |
| Sort toggle button | ✅ | ❌ | **no — intended difference** |
| Default feed order | oldest→newest (sort flag 4) | its own (sort flag 3) | **no — intended difference** |
The chrome markup is the divergence. The supporting **behavior is already global**: `js/main.js` (loaded for every page via `base.html.twig:10`) runs `initFilterBar()`, `initPanelToggles()`, and `initSortButton('trip-sort-toggle', …)`, each a silent no-op when its markup is absent. The entry partials already emit `data-type`, which the filter relies on. So rendering the same markup on home is enough for the filter bar, panel toggles, and (where present) the sort button to work with **zero new JS**.
The single exception is the **stats/cycling computation glue** (writing distance/elevation values into `#stat-distance`, `#cyc-*`). That code is currently *inline* in `trip.html.twig` and not global, so the stats/cycling panels cannot function on home until it is shared.
## Goals
- Home-active gains the date-range header, filter bar, and stats/cycling panels — matching the trip page.
- The shared feed-col chrome lives in **one** place (a partial), so future header/chrome changes apply to both pages.
- Home-active keeps its own default feed order and has **no** sort button (the two intended differences).
- Stats/cycling computation works on both pages from a single shared JS function.
- No change to the trip page's rendered output (structural refactor only on that side).
## Non-goals
- **Map convergence is out of scope.** The home-active map omitting story markers, lacking a fullscreen button, and its hash-only click behavior are all deferred to a later map-init spec (see `project-map-init-refactor` memory). Both inline map scripts and both map-col markup blocks stay exactly as they are.
- Extracting the `all_items` / `map_entries` build loops to a macro — Twig macros output HTML, not arrays (established constraint; see `2026-06-23-template-refactor-design.md`). Each page keeps its own data-build loops.
- Any visual restyling of the chrome — home reuses the trip's existing CSS classes unchanged.
- Adding a sort button to home, or changing home's default order.
## Architecture
### 1. New shared partial: `templates/partials/trip-feed-col.html.twig`
Holds the entire `.home-feed-col` content currently inline in `trip.html.twig:70-120`:
- header (`.home-trip-header`): title, date range (when `trip_page.header.date_start` set), counts
- filter bar (`.trip-filter-bar`): All / Journal / Stories buttons
- the sort button (`#trip-sort-toggle`) — **rendered only when `show_sort` is true**
- panel toggles (`.trip-panel-toggles`): Stats, and Cycling (when `has_gpx`)
- `stats_panel(...)` and (when `has_gpx`) `cycling_panel(...)` macro calls
- the feed loop over `all_items` with the `#feed-filter-empty` sentinel
**Interface** (called 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` |
| `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.
Both pages already build `all_items`, the counts, and `has_gpx` for their existing map data, so these are passed in rather than rebuilt — no new duplication is introduced.
### 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` — 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. 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`).
### 3. Home-active data additions
Home-active currently builds `map_entries` (journal-only) and `home_gpx_urls`. For the stats panel's no-GPX fallback it must also build `gps_points` (journal entries with lat/lng), mirroring `trip.html.twig:27-32`.
### 4. Template wiring
**`trip.html.twig`**: replace the inline `.home-feed-col` block (`:70-120`) with the partial include; remove the inline stats script (`:213-249`) in favor of `initTripStats(...)`. Map script, fullscreen wiring, and map data-build stay untouched.
**`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
```
base.html.twig ──loads──> js/main.js (global)
├─ initFilterBar() ← works on both via .trip-filter-btn + [data-type]
├─ initPanelToggles() ← works on both via .trip-panel-toggle
├─ initSortButton('trip-sort-toggle', …) ← trip only (home omits button → no-op)
└─ initTripStats(cfg) ← works on both via #stat-distance guard
trip.html.twig ─include─> partials/trip-feed-col.html.twig (show_sort: true)
home.html.twig ─include─> partials/trip-feed-col.html.twig (show_sort: false)
└─ stats_panel(), cycling_panel(), feed loop
```
## Testing
No JS test harness exists in this project; verification is manual browser smoke testing at `http://localhost:8081`, consistent with prior template work.
**Trip page (regression — must be unchanged):**
1. Load the trip page. Confirm header, date range, filter bar, sort button, stats/cycling panels, and feed render identically to before.
2. Filter bar All/Journal/Stories filters the feed; sort button flips order; Stats/Cycling panels toggle open/closed.
3. Stats panel distance and cycling figures populate (GPX present) or show the `~` estimate (no GPX).
**Home page, active-trip mode (new behavior):**
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).
## Files touched
- **New:** `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`
- **Edit:** `user/themes/intotheeast/templates/trip.html.twig` (feed-col → include; remove inline stats script)
- **Edit:** `user/themes/intotheeast/templates/home.html.twig` (active branch feed-col → include; add `gps_points`; wire stats)
- **Edit:** `user/themes/intotheeast/js/src/main.js` (+`initTripStats`); rebuild `js/main.js`