Phase 4 M1: Entry enrichment — location, weather, gallery, hero image

This commit is contained in:
2026-06-18 01:10:41 +02:00
parent f1181a07b4
commit d3fcde9b0b
9 changed files with 531 additions and 31 deletions
@@ -1,16 +1,111 @@
{% extends 'default.html.twig' %}
{% block content %}
{% set weather_icons = {
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
'Snow': '❄️', 'Thunderstorm': '⛈️'
} %}
<article class="entry">
<header class="entry-header">
<time class="entry-date" datetime="{{ page.date|date('Y-m-d') }}">
{{ page.date|date('l, d F Y') }}
</time>
{% if page.header.location_city or page.header.location_country %}
<p class="entry-location">
📍
{% if page.header.location_city %}{{ page.header.location_city }}{% endif %}
{% if page.header.location_city and page.header.location_country %}, {% endif %}
{% if page.header.location_country %}{{ page.header.location_country }}{% endif %}
</p>
{% endif %}
{% if page.header.weather_desc or page.header.weather_temp_c %}
<p class="entry-weather">
{% if page.header.weather_desc %}
{{ weather_icons[page.header.weather_desc] ?? '🌡️' }} {{ page.header.weather_desc }}
{% endif %}
{% if page.header.weather_temp_c %}
· {{ page.header.weather_temp_c|round }}°C
{% endif %}
</p>
{% endif %}
<h1 class="entry-title">{{ page.title }}</h1>
</header>
<div class="entry-body">
{{ page.content }}
</div>
{% set images = page.media.images %}
{% if images|length > 0 %}
<div class="entry-gallery" id="entry-gallery">
{% for image in images %}
<button class="gallery-thumb" data-full="{{ image.url }}" data-alt="{{ image.filename }}" aria-label="View {{ image.filename }}">
<img src="{{ image.cropResize(300, 300).url }}" alt="{{ image.filename }}" loading="lazy">
</button>
{% endfor %}
</div>
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
<button class="lightbox-close" id="lb-close" aria-label="Close">✕</button>
<button class="lightbox-prev" id="lb-prev" aria-label="Previous"></button>
<img class="lightbox-img" id="lb-img" src="" alt="">
<button class="lightbox-next" id="lb-next" aria-label="Next"></button>
</div>
<script>
(function() {
var gallery = document.getElementById('entry-gallery');
var lightbox = document.getElementById('lightbox');
var lbImg = document.getElementById('lb-img');
var thumbs = Array.from(gallery.querySelectorAll('.gallery-thumb'));
var current = 0;
function open(index) {
current = index;
var btn = thumbs[index];
lbImg.src = btn.dataset.full;
lbImg.alt = btn.dataset.alt;
lightbox.hidden = false;
document.body.style.overflow = 'hidden';
document.getElementById('lb-close').focus();
}
function close() {
lightbox.hidden = true;
document.body.style.overflow = '';
thumbs[current].focus();
}
function prev() { open((current - 1 + thumbs.length) % thumbs.length); }
function next() { open((current + 1) % thumbs.length); }
thumbs.forEach(function(btn, i) {
btn.addEventListener('click', function() { open(i); });
});
document.getElementById('lb-close').addEventListener('click', close);
document.getElementById('lb-prev').addEventListener('click', prev);
document.getElementById('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>
{% endif %}
<footer class="entry-footer">
<a href="{{ base_url_absolute }}/tracker">← Back to journal</a>
</footer>
@@ -4,11 +4,29 @@
<div class="post-form-wrap">
<h1>{{ page.title }}</h1>
{% include 'forms/form.html.twig' ignore missing %}
<button type="button" id="get-location" class="btn-location">Get Current Location</button>
<p id="location-status" class="location-status"></p>
<div class="form-actions-extra">
<button type="button" id="get-location" class="btn-extra">📍 Get Current Location</button>
<button type="button" id="get-weather" class="btn-extra">🌤 Get Weather</button>
</div>
<p id="location-status" class="field-status"></p>
<p id="weather-status" class="field-status"></p>
</div>
<script>
var WMO_MAP = {
0:'Sunny',1:'Partly cloudy',2:'Partly cloudy',3:'Cloudy',
45:'Foggy',48:'Foggy',
51:'Drizzle',53:'Drizzle',55:'Drizzle',56:'Drizzle',57:'Drizzle',
61:'Rain',63:'Rain',65:'Rain',66:'Rain',67:'Rain',80:'Rain',81:'Rain',82:'Rain',
71:'Snow',73:'Snow',75:'Snow',77:'Snow',85:'Snow',86:'Snow',
95:'Thunderstorm',96:'Thunderstorm',99:'Thunderstorm'
};
function getField(name) {
return document.querySelector('input[name="data[' + name + ']"]');
}
document.getElementById('get-location').addEventListener('click', function() {
var status = document.getElementById('location-status');
status.textContent = 'Getting location…';
@@ -19,14 +37,45 @@ document.getElementById('get-location').addEventListener('click', function() {
navigator.geolocation.getCurrentPosition(function(pos) {
var lat = pos.coords.latitude.toFixed(6);
var lng = pos.coords.longitude.toFixed(6);
var latField = document.querySelector('input[name="data[lat]"]');
var lngField = document.querySelector('input[name="data[lng]"]');
var latField = getField('lat');
var lngField = getField('lng');
if (latField) latField.value = lat;
if (lngField) lngField.value = lng;
status.textContent = 'Location set: ' + lat + ', ' + lng;
status.textContent = '📍 Location set: ' + lat + ', ' + lng;
}, function(err) {
status.textContent = 'Could not get location: ' + err.message;
});
});
document.getElementById('get-weather').addEventListener('click', function() {
var status = document.getElementById('weather-status');
var latField = getField('lat');
var lngField = getField('lng');
var lat = latField ? latField.value.trim() : '';
var lng = lngField ? lngField.value.trim() : '';
if (!lat || !lng) {
status.textContent = 'Enter or get coordinates first.';
return;
}
status.textContent = 'Fetching weather…';
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat +
'&longitude=' + lng +
'&current=temperature_2m,weather_code&temperature_unit=celsius';
fetch(url)
.then(function(r) { return r.json(); })
.then(function(data) {
var temp = Math.round(data.current.temperature_2m);
var code = data.current.weather_code;
var desc = WMO_MAP[code] || 'Cloudy';
var tempField = getField('weather_temp_c');
var descField = getField('weather_desc');
if (tempField) tempField.value = temp;
if (descField) descField.value = desc;
status.textContent = '🌤 Weather set: ' + desc + ' · ' + temp + '°C';
})
.catch(function() {
status.textContent = 'Could not fetch weather — enter manually if needed.';
});
});
</script>
{% endblock %}
+30 -8
View File
@@ -6,17 +6,39 @@
{% if entries|length > 0 %}
{% for entry in entries %}
<article class="entry-card">
<time class="entry-date" datetime="{{ entry.date|date('Y-m-d') }}">
{{ entry.date|date('d M Y') }}
</time>
{% 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 %}
{% if hero %}
<div class="entry-thumb">
<a href="{{ entry.url }}">
<img src="{{ hero.cropResize(680, 383).url }}" alt="{{ entry.title }}" loading="lazy">
</a>
</div>
{% endif %}
<div class="entry-card-meta">
<time class="entry-date" datetime="{{ entry.date|date('Y-m-d') }}">
{{ entry.date|date('d M Y') }}
</time>
{% if entry.header.location_city or entry.header.location_country %}
<span class="entry-location entry-location--card">
📍
{% if entry.header.location_city %}{{ entry.header.location_city|slice(0,25) }}{% 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>
<h2 class="entry-title">
<a href="{{ entry.url }}">{{ entry.title }}</a>
</h2>
{% if entry.header.hero_image %}
<div class="entry-thumb">
<img src="{{ entry.media[entry.header.hero_image].url }}" alt="{{ entry.title }}">
</div>
{% endif %}
<div class="entry-excerpt">
{{ entry.summary }}
</div>