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

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-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.

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