refactor(templates): extract entry markup into shared partials

Creates partials/entry-journal.html.twig and partials/entry-story.html.twig
so trip, dailies, and home all use the same up-to-date markup. Home page
gains PhotoSwipe, blurred fill, adaptive aspect ratio, and hash-based
marker scroll. Future changes only need to happen in one place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
2026-06-21 21:07:12 +02:00
parent fdaed1033a
commit 933652fd57
5 changed files with 120 additions and 220 deletions
+40 -66
View File
@@ -1,6 +1,7 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
{% set trip_route = config.site.active_trip %}
{% set trip = grav.pages.find(trip_route) %}
@@ -65,73 +66,10 @@
{% if all_items|length > 0 %}
{% for item in all_items %}
{% set entry = item.page %}
{% 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>
{% include 'partials/entry-journal.html.twig' %}
{% else %}
{% set hero = null %}
{% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %}
{% set hero = entry.media[entry.header.hero_image] %}
{% elseif entry.media.images|length > 0 %}
{% set hero = entry.media.images|first %}
{% endif %}
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" href="{{ entry.url }}">
{% if hero %}
<div class="entry-card-photo entry-card-photo--story">
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
</div>
{% endif %}
<div class="entry-card-body">
<span class="story-badge">✦ Story</span>
<h2 class="entry-title">{{ entry.title }}</h2>
</div>
</a>
{% include 'partials/entry-story.html.twig' %}
{% endif %}
{% endfor %}
{% else %}
@@ -180,7 +118,8 @@ homeMap.on('load', function () {
el.addEventListener('mouseleave', function () { popup.remove(); });
el.addEventListener('click', function () {
var card = document.getElementById('entry-' + entry.slug);
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (!card) return;
window.location.hash = 'entry-' + entry.slug;
});
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
@@ -199,6 +138,41 @@ homeMap.on('load', function () {
</script>
{% endif %}
<script type="module">
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
});
lightbox.init();
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
});
}
});
</script>
{% else %}
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}