docs: add inline journal feed implementation plan
This commit is contained in:
@@ -0,0 +1,855 @@
|
|||||||
|
# Inline Journal Feed 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 click-through journal entry cards with fully inline posts (photo strip + full text) across the trip page, dailies page, and home page.
|
||||||
|
|
||||||
|
**Architecture:** Each journal entry becomes an `<article class="journal-post">` block that renders all its images in a CSS scroll-snap strip with dot indicators, followed by the full body text. The `id`, `data-type`, `data-lat`, `data-lng` attributes stay on the root so map targeting, filter JS, and flash animation continue to work. Story cards in all three feeds are unchanged.
|
||||||
|
|
||||||
|
**Tech Stack:** Grav 2.0 Twig templates, CSS scroll-snap (no library), vanilla JS IntersectionObserver-free dot sync via scroll event, Playwright tests
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- All CSS values must use design tokens (`var(--...)`) — no hard-coded colours, sizes, or radii
|
||||||
|
- `id="entry-{{ entry.slug }}"` must remain on the journal post root (map scroll targeting)
|
||||||
|
- `data-type="journal"` must remain on the journal post root (filter bar JS)
|
||||||
|
- `data-lat` and `data-lng` must remain on the journal post root (map marker rendering)
|
||||||
|
- Story cards (`<a class="entry-card entry-card--story">`) are not touched by any task
|
||||||
|
- Two git repos: user content at `/home/mischa/Projects/travel-blog-intotheeast/user/` (separate git repo); outer repo at `/home/mischa/Nextcloud/Projects/travel-blog-intotheeast/`. Templates and CSS commit to the user subrepo; tests commit to the outer repo. Always update the outer repo's `user` submodule pointer in the same commit as the test changes.
|
||||||
|
- Dev server: http://localhost:8081
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `user/themes/intotheeast/css/style.css` | Add `.journal-post` component; remove journal-card-only rules; update `.is-highlighted` selector |
|
||||||
|
| `user/themes/intotheeast/templates/partials/base.html.twig` | Add photo-strip dot-sync JS before `</body>` |
|
||||||
|
| `user/themes/intotheeast/templates/dailies.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` |
|
||||||
|
| `user/themes/intotheeast/templates/trip.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` |
|
||||||
|
| `user/themes/intotheeast/templates/home.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` |
|
||||||
|
| `tests/ui/dailies.spec.js` | Update T1 selector; update T2 selectors |
|
||||||
|
| `tests/ui/maps.spec.js` | Update M7 selector |
|
||||||
|
| `tests/ui/home.spec.js` | New file — H1 test |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: CSS foundation + dot-sync JS
|
||||||
|
|
||||||
|
Add all new `.journal-post` CSS and the photo-strip dot-sync JS. Remove CSS classes that are only used by the old journal entry card (not by story cards). This task has no template changes — existing tests must still pass at the end.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user/themes/intotheeast/css/style.css`
|
||||||
|
- Modify: `user/themes/intotheeast/templates/partials/base.html.twig`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `.journal-post`, `.journal-post-header`, `.journal-post-title`, `.journal-post-meta`, `.journal-post-permalink`, `.journal-post-location`, `.journal-post-weather`, `.journal-photo-strip`, `.journal-photo-slide`, `.journal-photo-dots`, `.journal-photo-dot.is-active`, `.journal-post-body`, `.journal-post.is-highlighted` — all usable by Tasks 2–4
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `.journal-post` CSS block to `style.css`**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/css/style.css`, find the line:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ── Single entry ────────────────────────────────────────────────────────────── */
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert the following block **before** that comment:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ── Journal post (inline feed) ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.journal-post {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: var(--space-12);
|
||||||
|
margin-bottom: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-header {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
color: var(--color-ink);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-meta {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-permalink {
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-permalink:hover { color: var(--color-accent); }
|
||||||
|
|
||||||
|
.journal-post-location,
|
||||||
|
.journal-post-weather {
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-strip {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scrollbar-width: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-strip::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.journal-photo-slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-slide img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-border);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-dot.is-active {
|
||||||
|
background: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-body {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
color: var(--color-ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-post-body p { margin-bottom: var(--space-4); }
|
||||||
|
.journal-post-body p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.journal-post.is-highlighted {
|
||||||
|
animation: card-highlight 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove journal-card-only CSS rules from `style.css`**
|
||||||
|
|
||||||
|
These rules are only used by the old journal entry card. Story cards do not use them. Remove each block exactly as shown.
|
||||||
|
|
||||||
|
**Remove `.entry-card-photo-overlay` and its children:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.entry-card-photo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0 0;
|
||||||
|
padding: var(--space-5) var(--space-4) var(--space-3);
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.58) 0%, transparent 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-date-overlay {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgba(255,255,255,0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-location-overlay {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with nothing (delete the block entirely).
|
||||||
|
|
||||||
|
**Remove the text-only meta block and its comment:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Card: text-only variant */
|
||||||
|
|
||||||
|
.entry-card-textmeta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-date-plain {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-location-plain {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with nothing.
|
||||||
|
|
||||||
|
**Remove `.entry-excerpt` and `.entry-read-more`:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.entry-excerpt {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
color: var(--color-ink-2);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-read-more {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with nothing.
|
||||||
|
|
||||||
|
**Replace `.entry-card.is-highlighted` with `.journal-post.is-highlighted`:**
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```css
|
||||||
|
.entry-card.is-highlighted {
|
||||||
|
animation: card-highlight 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```css
|
||||||
|
.journal-post.is-highlighted {
|
||||||
|
animation: card-highlight 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add dot-sync JS to `base.html.twig`**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/templates/partials/base.html.twig`, find:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ assets.js('bottom')|raw }}
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ assets.js('bottom')|raw }}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
|
||||||
|
var dots = strip.nextElementSibling;
|
||||||
|
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
|
||||||
|
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
|
||||||
|
strip.addEventListener('scroll', function () {
|
||||||
|
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
|
||||||
|
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
|
||||||
|
}, { passive: true });
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run existing tests to confirm nothing broke**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all existing tests pass (M7 still passes because `trip.html.twig` has not changed yet — the JS still adds `is-highlighted` to `.entry-card` elements, and the old M7 selector `.entry-card.is-highlighted` finds the element).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit user subrepo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Projects/travel-blog-intotheeast/user
|
||||||
|
git add themes/intotheeast/css/style.css themes/intotheeast/templates/partials/base.html.twig
|
||||||
|
git commit -m "feat: add journal-post CSS component and dot-sync JS; remove stale journal-card-only rules"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: dailies.html.twig + T1/T2 test updates
|
||||||
|
|
||||||
|
Replace the journal entry card in `dailies.html.twig` with the new `.journal-post` inline block. Update T1 and T2 tests to match the new structure.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user/themes/intotheeast/templates/dailies.html.twig`
|
||||||
|
- Modify: `tests/ui/dailies.spec.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `.journal-post` CSS from Task 1
|
||||||
|
- Produces: `/trips/japan-korea-2026/dailies` renders `.journal-post` blocks; T1 and T2 pass with new selectors
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update T1 and T2 tests to their new selectors**
|
||||||
|
|
||||||
|
In `tests/ui/dailies.spec.js`, make the following changes:
|
||||||
|
|
||||||
|
**T1** — change `.entry-card` to `.journal-post`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// OLD
|
||||||
|
await expect(page.locator('.entry-card').first()).toBeVisible();
|
||||||
|
// NEW
|
||||||
|
await expect(page.locator('.journal-post').first()).toBeVisible();
|
||||||
|
```
|
||||||
|
|
||||||
|
**T2** — replace the entire card locator + index block with id-based selectors:
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```js
|
||||||
|
// Both fixture entries must be visible on the page
|
||||||
|
const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`);
|
||||||
|
const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`);
|
||||||
|
|
||||||
|
await expect(newerCard).toBeVisible();
|
||||||
|
await expect(olderCard).toBeVisible();
|
||||||
|
|
||||||
|
// The newer entry should appear higher in the DOM (lower index)
|
||||||
|
const newerIdx = await newerCard.evaluate(el => {
|
||||||
|
return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el);
|
||||||
|
});
|
||||||
|
const olderIdx = await olderCard.evaluate(el => {
|
||||||
|
return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```js
|
||||||
|
// Both fixture entries must be visible on the page
|
||||||
|
const newerCard = page.locator(`#entry-${NEWER_SLUG}`);
|
||||||
|
const olderCard = page.locator(`#entry-${OLDER_SLUG}`);
|
||||||
|
|
||||||
|
await expect(newerCard).toBeVisible();
|
||||||
|
await expect(olderCard).toBeVisible();
|
||||||
|
|
||||||
|
// The newer entry should appear higher in the DOM (lower index)
|
||||||
|
const newerIdx = await newerCard.evaluate(el => {
|
||||||
|
return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id);
|
||||||
|
});
|
||||||
|
const olderIdx = await olderCard.evaluate(el => {
|
||||||
|
return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run T1 and T2 to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL — `.journal-post` selector finds no elements (the page still renders `.entry-card`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `weather_icons` map and replace journal card in `dailies.html.twig`**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/templates/dailies.html.twig`, find the line:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
<a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
|
||||||
|
```
|
||||||
|
|
||||||
|
This `{% if item.type == 'journal' %}` block ends at `</a>` before `{% else %}`. Replace the entire journal card block (from `{% if item.type == 'journal' %}` through the closing `</a>` of the journal branch, leaving the `{% else %}` story branch intact) with:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
{% set weather_icons = {
|
||||||
|
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
||||||
|
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
||||||
|
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
||||||
|
} %}
|
||||||
|
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||||
|
<header class="journal-post-header">
|
||||||
|
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||||
|
<p class="journal-post-meta">
|
||||||
|
<a class="journal-post-permalink" href="{{ entry.url }}">
|
||||||
|
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
|
||||||
|
</a>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="journal-post-location">
|
||||||
|
· 📍
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
{{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.header.weather_desc %}
|
||||||
|
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% set images = entry.media.images %}
|
||||||
|
{% if images|length > 0 %}
|
||||||
|
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
||||||
|
{% for img in images %}
|
||||||
|
<div class="journal-photo-slide">
|
||||||
|
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if images|length > 1 %}
|
||||||
|
<div class="journal-photo-dots" aria-hidden="true">
|
||||||
|
{% for img in images %}
|
||||||
|
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact text to find and replace is the old journal branch. The old branch starts with:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
<a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
|
||||||
|
{% if hero %}
|
||||||
|
<div class="entry-card-photo">
|
||||||
|
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
<div class="entry-card-photo-overlay">
|
||||||
|
<time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-overlay">
|
||||||
|
📍
|
||||||
|
{% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %}
|
||||||
|
{% if entry.header.location_city and entry.header.location_country %}, {% endif %}
|
||||||
|
{% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="entry-card-textmeta">
|
||||||
|
<time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-plain">
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
📍 {{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="entry-card-body">
|
||||||
|
<h2 class="entry-title">{{ entry.title }}</h2>
|
||||||
|
<p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
|
||||||
|
<span class="entry-read-more">Read entry →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run T1 and T2 to verify they pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the full suite to check no regressions**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Projects/travel-blog-intotheeast/user
|
||||||
|
git add themes/intotheeast/templates/dailies.html.twig
|
||||||
|
git commit -m "feat: replace journal entry card with inline journal-post in dailies feed"
|
||||||
|
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
git add tests/ui/dailies.spec.js user
|
||||||
|
git commit -m "test: update T1/T2 selectors for inline journal-post structure"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: trip.html.twig + M7 test update
|
||||||
|
|
||||||
|
Replace the journal entry card in `trip.html.twig` with the `.journal-post` block. Update M7 which currently tests `.entry-card.is-highlighted` on the trip page.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user/themes/intotheeast/templates/trip.html.twig`
|
||||||
|
- Modify: `tests/ui/maps.spec.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `.journal-post` CSS and `.journal-post.is-highlighted` from Task 1; journal-post HTML pattern from Task 2
|
||||||
|
- Produces: `/trips/japan-korea-2026` renders `.journal-post` blocks; M7 passes with `.journal-post.is-highlighted`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update M7 to the new selector**
|
||||||
|
|
||||||
|
In `tests/ui/maps.spec.js`, find:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Within 500ms of click + delay, one entry-card should have is-highlighted
|
||||||
|
await expect(page.locator('.entry-card.is-highlighted')).toBeVisible({ timeout: 1500 });
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Within 500ms of click + delay, one journal-post should have is-highlighted
|
||||||
|
await expect(page.locator('.journal-post.is-highlighted')).toBeVisible({ timeout: 1500 });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run M7 to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL — `.journal-post.is-highlighted` not found (trip.html.twig still renders `<a class="entry-card">`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace journal card in `trip.html.twig`**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/templates/trip.html.twig`, find and replace the journal branch of the `{% if item.type == 'journal' %}` block. The old branch to replace is:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
<a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
|
||||||
|
{% if hero %}
|
||||||
|
<div class="entry-card-photo">
|
||||||
|
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
<div class="entry-card-photo-overlay">
|
||||||
|
<time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-overlay">
|
||||||
|
📍
|
||||||
|
{% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %}
|
||||||
|
{% if entry.header.location_city and entry.header.location_country %}, {% endif %}
|
||||||
|
{% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="entry-card-textmeta">
|
||||||
|
<time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-plain">
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
📍 {{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="entry-card-body">
|
||||||
|
<h2 class="entry-title">{{ entry.title }}</h2>
|
||||||
|
<p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
|
||||||
|
<span class="entry-read-more">Read entry →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
{% set weather_icons = {
|
||||||
|
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
||||||
|
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
||||||
|
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
||||||
|
} %}
|
||||||
|
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||||
|
<header class="journal-post-header">
|
||||||
|
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||||
|
<p class="journal-post-meta">
|
||||||
|
<a class="journal-post-permalink" href="{{ entry.url }}">
|
||||||
|
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
|
||||||
|
</a>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="journal-post-location">
|
||||||
|
· 📍
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
{{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.header.weather_desc %}
|
||||||
|
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% set images = entry.media.images %}
|
||||||
|
{% if images|length > 0 %}
|
||||||
|
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
||||||
|
{% for img in images %}
|
||||||
|
<div class="journal-photo-slide">
|
||||||
|
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if images|length > 1 %}
|
||||||
|
<div class="journal-photo-dots" aria-hidden="true">
|
||||||
|
{% for img in images %}
|
||||||
|
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run M7 to verify it passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run full suite**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Projects/travel-blog-intotheeast/user
|
||||||
|
git add themes/intotheeast/templates/trip.html.twig
|
||||||
|
git commit -m "feat: replace journal entry card with inline journal-post in trip feed"
|
||||||
|
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
git add tests/ui/maps.spec.js user
|
||||||
|
git commit -m "test: update M7 selector for journal-post.is-highlighted"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: home.html.twig + H1 test
|
||||||
|
|
||||||
|
Replace the journal entry card in `home.html.twig` and add a minimal home page test.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user/themes/intotheeast/templates/home.html.twig`
|
||||||
|
- Create: `tests/ui/home.spec.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `.journal-post` CSS from Task 1; journal-post HTML pattern from Task 2
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing H1 test**
|
||||||
|
|
||||||
|
Create `tests/ui/home.spec.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// @ts-check
|
||||||
|
// Tests: H1 — home page journal feed
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
// ── H1: Home page renders inline journal posts ─────────────────────────────────
|
||||||
|
test('H1: home page shows at least one inline journal-post block', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('.journal-post').first()).toBeVisible();
|
||||||
|
await expect(page.locator('.site-header')).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run H1 to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/home.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL — `.journal-post` not found (home page still renders `<a class="entry-card">`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace journal card in `home.html.twig`**
|
||||||
|
|
||||||
|
In `user/themes/intotheeast/templates/home.html.twig`, find the journal branch:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
<a class="entry-card" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
|
||||||
|
{% if hero %}
|
||||||
|
<div class="entry-card-photo">
|
||||||
|
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
<div class="entry-card-photo-overlay">
|
||||||
|
<time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-overlay">
|
||||||
|
📍
|
||||||
|
{% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %}
|
||||||
|
{% if entry.header.location_city and entry.header.location_country %}, {% endif %}
|
||||||
|
{% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="entry-card-textmeta">
|
||||||
|
<time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
|
||||||
|
{{ entry.date|date('d M Y')|upper }}
|
||||||
|
</time>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="entry-location-plain">
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
📍 {{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="entry-card-body">
|
||||||
|
<h2 class="entry-title">{{ entry.title }}</h2>
|
||||||
|
<p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
|
||||||
|
<span class="entry-read-more">Read entry →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
{% set weather_icons = {
|
||||||
|
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
||||||
|
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
||||||
|
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
||||||
|
} %}
|
||||||
|
<article class="journal-post" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||||
|
<header class="journal-post-header">
|
||||||
|
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||||
|
<p class="journal-post-meta">
|
||||||
|
<a class="journal-post-permalink" href="{{ entry.url }}">
|
||||||
|
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
|
||||||
|
</a>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="journal-post-location">
|
||||||
|
· 📍
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
{{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.header.weather_desc %}
|
||||||
|
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% set images = entry.media.images %}
|
||||||
|
{% if images|length > 0 %}
|
||||||
|
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
||||||
|
{% for img in images %}
|
||||||
|
<div class="journal-photo-slide">
|
||||||
|
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if images|length > 1 %}
|
||||||
|
<div class="journal-photo-dots" aria-hidden="true">
|
||||||
|
{% for img in images %}
|
||||||
|
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `home.html.twig` journal posts do **not** include `data-type` (the home page has no filter bar) — this matches the existing `<a class="entry-card">` on home which also had no `data-type`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run H1 to verify it passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
npx playwright test --project=chromium tests/ui/home.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the full suite**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js tests/ui/home.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mischa/Projects/travel-blog-intotheeast/user
|
||||||
|
git add themes/intotheeast/templates/home.html.twig
|
||||||
|
git commit -m "feat: replace journal entry card with inline journal-post on home page"
|
||||||
|
|
||||||
|
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||||
|
git add tests/ui/home.spec.js user
|
||||||
|
git commit -m "test: add H1 home page journal-post test"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user