Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
11 KiB
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-refactormemory). Both inline map scripts and both map-col markup blocks stay exactly as they are. - Extracting the
all_items/map_entriesbuild loops to a macro — Twig macros output HTML, not arrays (established constraint; see2026-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 (whentrip_page.header.date_startset), counts - filter bar (
.trip-filter-bar): All / Journal / Stories buttons - the sort button (
#trip-sort-toggle) — rendered only whenshow_sortis true - panel toggles (
.trip-panel-toggles): Stats, and Cycling (whenhas_gpx) stats_panel(...)and (whenhas_gpx)cycling_panel(...)macro calls- the feed loop over
all_itemswith the#feed-filter-emptysentinel
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.
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):
- Load the trip page. Confirm header, date range, filter bar, sort button, stats/cycling panels, and feed render identically to before.
- Filter bar All/Journal/Stories filters the feed; sort button flips order; Stats/Cycling panels toggle open/closed.
- 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; addgps_points; wire stats) - Edit:
user/themes/intotheeast/js/src/main.js(+initTripStats); rebuildjs/main.js