Phase 4 M1: Entry enrichment — location, weather, gallery, hero image
This commit is contained in:
@@ -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 +
|
||||
'¤t=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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user