Files
intotheeast-com-content/themes/intotheeast/templates/dailies.html.twig
T
m038 770a96b099 feat(dailies): 4:3 photo strip, lightbox, and dot sync
- Aspect ratio 3:2 → 4:3 (less aggressive crop; closer to phone native)
- Slides become <button> elements with data-full pointing to original image
- Tap/click any photo in the feed opens a full-screen lightbox showing the
  uncropped original; prev/next browses all feed photos; Esc/arrows/backdrop
  click close
- Dot indicator now syncs with scroll via IntersectionObserver

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 19:06:54 +02:00

237 lines
10 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'default.html.twig' %}
{% block content %}
{% set journal_entries = page.collection() %}
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
{% set all_items = [] %}
{% for e in journal_entries %}
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.date}]) %}
{% endfor %}
{% for s in story_entries %}
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
{% endfor %}
{# No sort needed: page.collection() returns journal entries date-descending per dailies.md config. Dailies has no stories, so no re-merge sort is needed. #}
{# Collect GPS entries for mini-map #}
{% set map_entries = [] %}
{% for item in all_items %}
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
{% set map_entries = map_entries|merge([{
'lat': item.page.header.lat,
'lng': item.page.header.lng,
'title': item.page.title,
'slug': item.page.slug,
'url': item.page.url,
'force_connect': item.page.header.force_connect ? true : false,
'transport_mode': item.page.header.transport_mode ? item.page.header.transport_mode : null
}]) %}
{% endif %}
{% endfor %}
{% set trip_page = page.parent() %}
{% if map_entries|length > 0 %}
<div class="feed-map-wrap">
<div class="feed-map" id="feed-map"></div>
<a class="feed-map-link" href="{{ page.parent().url }}/map">View full map →</a>
</div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var FEED_ENTRIES = {{ map_entries|json_encode|raw }};
{% set _ac = trip_page.header.autoconnect ?? 'on' %}
var AUTOCONNECT = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
var feedMap = new maplibregl.Map({
container: 'feed-map',
style: MapUtils.MAP_STYLE,
center: [20, 20],
zoom: 2
});
feedMap.on('load', function () {
var bounds = new maplibregl.LngLatBounds();
FEED_ENTRIES.forEach(function (entry, i) {
var isLatest = (i === FEED_ENTRIES.length - 1);
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
bounds.extend(lngLat);
var el = MapUtils.createDotMarker(isLatest);
el.dataset.url = entry.url;
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
.setLngLat(lngLat)
.setHTML('<span class="map-tip">' + entry.title + '</span>');
el.addEventListener('mouseenter', function () { popup.addTo(feedMap); });
el.addEventListener('mouseleave', function () { popup.remove(); });
el.addEventListener('click', function () { window.location.href = entry.url; });
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(feedMap);
});
if (FEED_ENTRIES.length === 1) {
feedMap.jumpTo({ center: [parseFloat(FEED_ENTRIES[0].lng), parseFloat(FEED_ENTRIES[0].lat)], zoom: 10 });
} else {
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
}
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { connectMode: AUTOCONNECT });
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
});
</script>
{% endif %}
<div class="feed">
{% 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-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 %}
<button class="journal-photo-slide" data-full="{{ img.url }}" data-alt="{{ entry.title }}" aria-label="View photo">
<img src="{{ img.cropResize(900, 675).url }}" alt="{{ entry.title }}" loading="lazy">
</button>
{% 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>
{% 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 }}" data-type="story" 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>
{% endif %}
{% endfor %}
{% else %}
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
{% endif %}
</div>
<div class="lightbox" id="feed-lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
<button class="lightbox-close" id="feed-lb-close" aria-label="Close">✕</button>
<button class="lightbox-prev" id="feed-lb-prev" aria-label="Previous"></button>
<img class="lightbox-img" id="feed-lb-img" src="" alt="">
<button class="lightbox-next" id="feed-lb-next" aria-label="Next"></button>
</div>
<script>
/* ── Journal photo strip: dot sync + lightbox ───────────────── */
(function () {
/* Dot sync */
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
var slides = strip.querySelectorAll('.journal-photo-slide');
var strip_id = strip.closest('article');
if (!strip_id) return;
var dots = strip_id.querySelectorAll('.journal-photo-dot');
if (!dots.length) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
var idx = Array.from(slides).indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[idx]) dots[idx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
});
/* Lightbox */
var allSlides = Array.from(document.querySelectorAll('.journal-photo-slide[data-full]'));
if (!allSlides.length) return;
var lightbox = document.getElementById('feed-lightbox');
var lbImg = document.getElementById('feed-lb-img');
var current = 0;
function open(index) {
current = index;
lbImg.src = allSlides[index].dataset.full;
lbImg.alt = allSlides[index].dataset.alt;
lightbox.hidden = false;
document.body.style.overflow = 'hidden';
document.getElementById('feed-lb-close').focus();
}
function close() {
lightbox.hidden = true;
document.body.style.overflow = '';
allSlides[current].focus();
}
function prev() { open((current - 1 + allSlides.length) % allSlides.length); }
function next() { open((current + 1) % allSlides.length); }
allSlides.forEach(function (slide, i) {
slide.addEventListener('click', function () { open(i); });
});
document.getElementById('feed-lb-close').addEventListener('click', close);
document.getElementById('feed-lb-prev').addEventListener('click', prev);
document.getElementById('feed-lb-next').addEventListener('click', next);
lightbox.addEventListener('click', function (e) { if (e.target === lightbox) close(); });
document.addEventListener('keydown', function (e) {
if (lightbox.hidden) return;
if (e.key === 'Escape') close();
if (e.key === 'ArrowLeft') prev();
if (e.key === 'ArrowRight') next();
});
})();
</script>
{% endblock %}