docs: add trip page filter bar implementation plan

This commit is contained in:
2026-06-19 21:24:19 +02:00
parent c9ce336b18
commit 5e864b0c03
@@ -0,0 +1,472 @@
# Trip Page Filter Bar — 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:** Replace the three unstyled nav links on the trip page with an in-page filter bar (All content / Journal / Stories) and an inline Stats toggle — no page navigation needed.
**Architecture:** Pure client-side. `data-type` attributes on article cards let vanilla JS show/hide by content type. Stats computation is inlined into `trip.html.twig` from `stats.html.twig`. No new files, no Grav config changes, no page navigation.
**Tech Stack:** Twig (Grav 2.0), vanilla JS (ES5), CSS custom properties
## Global Constraints
- CSS variables only — no raw hex values; use tokens from `tokens.css`
- ES5 JS — no arrow functions, no `const`/`let`, no template literals (inline script in Twig)
- Touch the minimum: only `trip.html.twig` and `style.css`
- Do not modify `stats.html.twig`, `dailies.html.twig`, or any sub-page template
---
### Task 1: Story card border + data-type attributes
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig:77,119`
- Modify: `user/themes/intotheeast/css/style.css:816-819`
**Interfaces:**
- Produces: `data-type="journal"` and `data-type="story"` attributes on all article cards — consumed by Tasks 3 and 4
- [ ] **Step 1: Add data-type to journal article (trip.html.twig line 77)**
Find this line:
```twig
<article class="entry-card" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
```
Replace with:
```twig
<article class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
```
- [ ] **Step 2: Add data-type to story article (trip.html.twig line 119)**
Find this line:
```twig
<article class="entry-card entry-card--story" id="entry-{{ entry.slug }}">
```
Replace with:
```twig
<article class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story">
```
- [ ] **Step 3: Replace story card border in style.css**
Find the existing `.entry-card--story` rule (around line 816):
```css
.entry-card--story {
border-left: 3px solid var(--color-accent);
padding-left: var(--space-5);
}
```
Replace with:
```css
.entry-card--story {
border: 2px solid var(--color-accent);
border-radius: var(--radius-md);
padding: var(--space-6);
background: var(--color-canvas);
}
```
- [ ] **Step 4: Verify visually**
Open the trip page in the browser. In DevTools:
- Select a journal article → confirm it has `data-type="journal"`
- Select a story article → confirm it has `data-type="story"`
- Story cards should now appear as a boxed card with a full teal border and rounded corners instead of a left-only bar
- [ ] **Step 5: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig user/themes/intotheeast/css/style.css
git commit -m "feat: add data-type attributes to feed cards; restyle story card with full border"
```
---
### Task 2: Filter bar markup + CSS (static, no JS yet)
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig:58-62`
- Modify: `user/themes/intotheeast/css/style.css` (append after `.home-trip-counts` block, around line 694)
**Interfaces:**
- Consumes: nothing from prior tasks (static HTML)
- Produces: `.trip-filter-bar`, `.trip-filter-btn`, `.trip-stats-btn` CSS classes consumed by Task 3
- [ ] **Step 1: Replace nav with filter bar in trip.html.twig**
Find the existing nav block (lines 5862):
```twig
<nav class="trip-nav">
<a href="{{ page.route }}/dailies">Journal</a>
<a href="{{ page.route }}/stats">Stats</a>
<a href="{{ page.route }}/stories">Stories</a>
</nav>
```
Replace with:
```twig
<div class="trip-filter-bar">
<div class="trip-filter-group">
<button class="trip-filter-btn is-active" data-filter="all">All content</button>
<button class="trip-filter-btn" data-filter="journal">Journal</button>
<button class="trip-filter-btn" data-filter="story">Stories</button>
</div>
<button class="trip-stats-btn" id="trip-stats-toggle">Stats</button>
</div>
```
- [ ] **Step 2: Add filter bar CSS to style.css**
After the `.home-trip-counts` rule (around line 694), append:
```css
/* ── Trip page filter bar ────────────────────────────────────────────────────── */
.trip-filter-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
margin-top: var(--space-4);
flex-wrap: wrap;
}
.trip-filter-group {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.trip-filter-btn,
.trip-stats-btn {
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink-muted);
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
padding: var(--space-1) var(--space-4);
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.trip-filter-btn:hover,
.trip-stats-btn:hover {
color: var(--color-ink);
border-color: var(--color-ink-muted);
}
.trip-filter-btn.is-active,
.trip-stats-btn.is-active {
color: var(--color-accent);
border-color: var(--color-accent);
background: var(--color-accent-light);
}
```
- [ ] **Step 3: Verify visually**
Open the trip page. Confirm:
- Three filter pills (All content / Journal / Stories) and a Stats button appear below the trip title
- "All content" pill has teal active styling
- Other pills are muted/bordered
- Clicking the buttons does nothing yet (no JS)
- [ ] **Step 4: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig user/themes/intotheeast/css/style.css
git commit -m "feat: add filter bar markup and pill button styles to trip page"
```
---
### Task 3: Filter JS (show/hide cards by type)
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig` (append to the existing `<script>` block at the bottom, before `</script>`)
**Interfaces:**
- Consumes: `data-type` on articles (Task 1); `.trip-filter-btn`, `data-filter` (Task 2)
- Produces: working filter interaction
- [ ] **Step 1: Add an empty-state element to the feed**
In `trip.html.twig`, find the closing `</div>` of the `.feed` block (after the `{% else %}` empty message). Add a hidden filter-empty message right before `</div>`:
```twig
<p id="feed-filter-empty" class="feed-empty" style="display:none;"></p>
</div>
```
The full `.feed` block close should look like:
```twig
{% 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>
```
- [ ] **Step 2: Append filter JS to the existing script block**
In `trip.html.twig`, find the closing `</script>` tag at the bottom. Insert before it:
```javascript
(function() {
var filterBtns = document.querySelectorAll('.trip-filter-btn');
var cards = document.querySelectorAll('[data-type]');
var filterEmpty = document.getElementById('feed-filter-empty');
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
filterBtns.forEach(function(b) { b.classList.remove('is-active'); });
btn.classList.add('is-active');
var filter = btn.getAttribute('data-filter');
var visible = 0;
cards.forEach(function(card) {
var show = filter === 'all' || card.getAttribute('data-type') === filter;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
if (filterEmpty) {
if (visible === 0) {
filterEmpty.textContent = filter === 'story'
? 'No stories yet for this trip.'
: 'No entries yet.';
filterEmpty.style.display = '';
} else {
filterEmpty.style.display = 'none';
}
}
});
});
})();
```
- [ ] **Step 3: Verify filter behavior**
Open the trip page. With demo entries loaded (run `make demo-load` if needed):
- Click **Journal** → only journal cards visible, story cards hidden
- Click **Stories** → only story cards visible, journal cards hidden
- Click **All content** → all cards visible again
- If no stories exist, clicking Stories shows "No stories yet for this trip."
- "All content" pill always has active state after clicking it
- [ ] **Step 4: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig
git commit -m "feat: wire up feed filter — All content / Journal / Stories"
```
---
### Task 4: Inline stats block (Twig computation + HTML + toggle JS)
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig`
- Modify: `user/themes/intotheeast/css/style.css` (append after filter bar CSS from Task 2)
**Interfaces:**
- Consumes: `.trip-stats-btn#trip-stats-toggle` (Task 2); `journal_entries` variable already set at top of template
- Produces: expandable stats block; `STATS_GPS` JS variable for haversine distance
- [ ] **Step 1: Add stats Twig computation at the top of the template**
In `trip.html.twig`, after line 19 (`{% set story_count = story_entries|length %}`), add:
```twig
{# Stats computation #}
{% set days_on_road = 0 %}
{% set first_ts = null %}
{% for entry in journal_entries %}
{% set ts = entry.date|date('U') %}
{% if first_ts is null or ts < first_ts %}
{% set first_ts = ts %}
{% endif %}
{% endfor %}
{% if first_ts is not null %}
{% set now_ts = "now"|date('U') %}
{% set diff_seconds = now_ts - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_country is not empty %}
{% set lower = entry.header.location_country|trim|lower %}
{% if lower not in seen_lower %}
{% set seen_lower = seen_lower|merge([lower]) %}
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% 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 %}
```
- [ ] **Step 2: Add stats block HTML between filter bar and feed**
In `trip.html.twig`, find the `<div class="feed">` line and insert the stats block immediately before it:
```twig
<div id="trip-stats-block" class="trip-stats-block" style="display:none">
<div class="trip-stats-grid">
<div class="stat-block">
<span class="stat-value">{{ days_on_road }}</span>
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ journal_count }}</span>
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ country_display|length }}</span>
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value" id="stat-distance">—</span>
<span class="stat-label">km traveled</span>
</div>
</div>
{% if country_display|length > 0 %}
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
{% endif %}
<p class="trip-stats-note">Distance is approximate — straight lines between entry locations.</p>
</div>
```
- [ ] **Step 3: Add stats block CSS to style.css**
Append after the filter bar CSS added in Task 2:
```css
/* ── Trip page inline stats block ───────────────────────────────────────────── */
.trip-stats-block {
background: var(--color-canvas);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6);
margin-bottom: var(--space-6);
}
.trip-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
margin-bottom: var(--space-4);
}
@media (max-width: 600px) {
.trip-stats-grid { grid-template-columns: repeat(2, 1fr); }
}
.trip-stats-countries {
font-size: var(--text-sm);
color: var(--color-ink-2);
margin-bottom: var(--space-2);
}
.trip-stats-note {
font-size: var(--text-xs);
color: var(--color-ink-muted);
}
```
Note: `.stat-block`, `.stat-value`, `.stat-label` are reused from `stats.html.twig` and already have CSS defined. Do not add duplicate rules.
- [ ] **Step 4: Verify those existing CSS classes exist**
Run:
```bash
grep -n "\.stat-block\|\.stat-value\|\.stat-label" user/themes/intotheeast/css/style.css
```
Expected: at least 3 matches. If not found, copy from `stats.html.twig`'s inline `<style>` block if one exists.
- [ ] **Step 5: Add stats toggle JS + haversine distance**
In `trip.html.twig`, append to the existing `<script>` block (before `</script>`):
```javascript
var STATS_GPS = {{ gps_points|json_encode|raw }};
(function() {
function haversine(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
return R * 2 * Math.asin(Math.sqrt(a));
}
var totalKm = 0;
for (var i = 1; i < STATS_GPS.length; i++) {
totalKm += haversine(
parseFloat(STATS_GPS[i - 1][0]), parseFloat(STATS_GPS[i - 1][1]),
parseFloat(STATS_GPS[i][0]), parseFloat(STATS_GPS[i][1])
);
}
var distEl = document.getElementById('stat-distance');
if (distEl) {
distEl.textContent = STATS_GPS.length < 2 ? '—' : '~' + Math.round(totalKm).toLocaleString();
}
var statsToggle = document.getElementById('trip-stats-toggle');
var statsBlock = document.getElementById('trip-stats-block');
if (statsToggle && statsBlock) {
statsToggle.addEventListener('click', function() {
var isOpen = statsBlock.style.display !== 'none';
statsBlock.style.display = isOpen ? 'none' : '';
statsToggle.classList.toggle('is-active', !isOpen);
});
}
})();
```
- [ ] **Step 6: Verify stats block**
Open the trip page with demo entries loaded:
- Click **Stats** → inline block expands between filter bar and feed; Stats button turns teal
- Block shows: days on road, entries count, countries count, km distance (or `—` if < 2 GPS points)
- Countries list shows below the grid if any entries have `location_country`
- Click **Stats** again → block collapses, button returns to default style
- [ ] **Step 7: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig user/themes/intotheeast/css/style.css
git commit -m "feat: add inline stats block with toggle to trip page"
```
---
## Self-Review
**Spec coverage:**
- ✅ Filter bar with All / Journal / Stories (Task 2, 3)
- ✅ Mutually exclusive, one active at a time (Task 3 JS)
- ✅ JS show/hide via data-type (Task 1, 3)
- ✅ Empty state for Stories filter (Task 3)
- ✅ Stats as inline expansion (Task 4)
- ✅ Stats toggle with active state (Task 4)
- ✅ Story card full border (Task 1)
- ✅ Sub-pages untouched — no changes to stats.html.twig or dailies.html.twig
**Placeholder scan:** None — all steps contain exact code.
**Type consistency:**
- `data-filter="story"` on button matches `data-type="story"` on article — comparison in Task 3 JS: `card.getAttribute('data-type') === filter`
- `id="trip-stats-toggle"` set in Task 2 HTML, read in Task 4 JS ✅
- `id="trip-stats-block"` set in Task 4 HTML, read in Task 4 JS ✅
- `id="feed-filter-empty"` set in Task 3 HTML, read in Task 3 JS ✅
- `id="stat-distance"` set in Task 4 HTML, read in Task 4 JS ✅
- `STATS_GPS` set in Task 4 JS, consumed in Task 4 haversine loop ✅
- `.stat-block` / `.stat-value` / `.stat-label` reused from existing CSS — Task 4 Step 4 verifies they exist ✅