Merge commit 'b3b4f77'

# Conflicts:
#	themes/intotheeast/templates/partials/trip-feed-col.html.twig
#	themes/intotheeast/templates/trip.html.twig
This commit is contained in:
2026-07-08 00:08:37 +02:00
77 changed files with 3246 additions and 151 deletions
@@ -0,0 +1,18 @@
{# intotheeast override of Grav's datetime field.
Grav's stock forms/fields/datetime/datetime.html.twig is DEPRECATED and just
extends the text field, so `type: datetime` renders a plain <input type="text">
— the owner sees a raw box (the literal word "now" from `default: now`) with no
way to pick a date/time. Here we render a native <input type="datetime-local">
instead: a real calendar + clock picker, and an excellent one on mobile.
The value is prefilled to the current local time by post-form.js (JS knows the
traveller's timezone; the server doesn't), and the existing client-side
validator now requires this field — so an empty or invalid date can no longer
reach the server and destroy the FilePond photo list on a re-render. #}
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="datetime-local"
{{ parent() }}
{% endblock %}
+15 -3
View File
@@ -14,11 +14,20 @@
{% set dailies_page = grav.pages.find(trip_route ~ '/dailies') %}
{% set stories_page = grav.pages.find(trip_route ~ '/stories') %}
{# published-only — feeds the map, stats and counts (drafts excluded, R5) #}
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
{# This branch IS the active trip, so the owner gate is just owner identity
(KTD8). The super-admin tester authenticates too, so gate on owner_username. #}
{% set owner_can_edit = grav.user.authenticated
and grav.user.username == grav.config.site.owner_username %}
{# Owner-aware feed list: owner sees drafts; everyone else published only #}
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
{% set all_items = [] %}
{% for e in journal_entries %}
{% for e in journal_feed %}
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.header.date}]) %}
{% endfor %}
{% for s in story_entries %}
@@ -38,7 +47,8 @@
{% 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 %}
{# drafts render as a feed card only — never a map marker (R5) #}
{% if item.type == 'journal' and item.page.published 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|number_format(6, '.', ''),
'lng': item.page.header.lng|number_format(6, '.', ''),
@@ -87,7 +97,9 @@
has_gpx: home_gpx_urls|length > 0,
gpx_urls: home_gpx_urls,
gps_points: gps_points,
show_sort: false
show_sort: false,
owner_can_edit: owner_can_edit,
feed_return_url: page.url
} only %}
{% endif %}
</div>
@@ -10,6 +10,7 @@
{% do assets.addJs('theme://js/main.js', {group: 'bottom'}) %}
{% do assets.addJs('theme://js/nav.js', {group: 'bottom'}) %}
{% block map_assets %}{% endblock %}
{% block head_assets %}{% endblock %}
{{ assets.css()|raw }}
{{ assets.js()|raw }}
</head>
@@ -26,6 +27,10 @@
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
<a href="{{ base_url_absolute }}"{% if page.template == 'home' %} aria-current="page"{% endif %}>Home</a>
<a href="{{ base_url_absolute }}/trips"{% if page.template == 'trips' %} aria-current="page"{% endif %}>Past Trips</a>
{% if grav.user.authenticated %}
<a href="{{ base_url_absolute }}/post"{% if page.template == 'post-form' %} aria-current="page"{% endif %}>New Post</a>
<a href="{{ base_url_absolute }}/gpx-manager"{% if page.template == 'gpx-manager' %} aria-current="page"{% endif %}>GPX Manager</a>
{% endif %}
</nav>
{% endblock %}
</header>
@@ -1,7 +1,22 @@
{% include 'partials/weather-icons.html.twig' %}
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
{% set owner_can_edit = owner_can_edit ?? false %}
{% set feed_return_url = feed_return_url ?? trip_page.url ?? '/' %}
<article class="journal-post{% if not entry.published %} is-draft{% endif %}" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}"{% if owner_can_edit %} data-entry-route="{{ entry.route }}"{% endif %}>
<header class="journal-post-header">
<h2 class="journal-post-title">{{ entry.title }}</h2>
<div class="journal-post-titlerow">
<h2 class="journal-post-title">{{ entry.title }}{% if not entry.published %} <span class="journal-draft-badge">Draft</span>{% endif %}</h2>
{% if owner_can_edit %}
<div class="journal-post-actions" data-entry-route="{{ entry.route }}">
<a class="entry-action entry-action--edit" href="/post?edit={{ entry.route|url_encode }}&return={{ feed_return_url|url_encode }}">Edit</a>
<button class="entry-action entry-action--delete" type="button" data-delete-start>Delete</button>
<span class="entry-delete-confirm" hidden>
<button class="entry-action entry-action--cancel" type="button" data-delete-cancel>Cancel</button>
<button class="entry-action entry-action--confirm" type="button" data-delete-confirm>Confirm delete</button>
</span>
<span class="entry-delete-msg" role="status" aria-live="polite"></span>
</div>
{% endif %}
</div>
<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>
@@ -5,6 +5,10 @@
caller only. Home's active-trip include omits it (`only`), so it defaults off
and that header renders exactly as before (KTD4 / R12). #}
{% set trip_header_extras = trip_header_extras|default(false) %}
{# owner_can_edit gates the Draft badge + Edit/Delete controls on each card
(threaded into entry-journal below). Default false so any caller that doesn't
pass it renders a read-only feed. #}
{% set owner_can_edit = owner_can_edit ?? false %}
<div class="home-feed-col">
<div class="home-trip-header">
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
+17 -115
View File
@@ -1,126 +1,28 @@
{% extends 'default.html.twig' %}
{% block head_assets %}
{# FilePond's own CSS must load from the <head>. The filepond field registers
it via assets.addCss() during body rendering, which is too late for the
theme's head-only {{ assets.css() }} — so the widget renders unstyled
(giant overlapping tiles) unless we add it here. #}
{% do assets.addCss('plugin://form/assets/filepond/filepond.min.css') %}
{% do assets.addCss('plugin://form/assets/filepond/filepond-plugin-image-preview.min.css') %}
{% do assets.addCss('theme://css-compiled/post-form.css') %}
<script type="module" src="{{ url('theme://js/post/post-form.js') }}"></script>
{% endblock %}
{% block content %}
<div class="post-form-wrap">
<div class="post-form-wrap" data-trip-url="{{ config.site.active_trip }}">
<h1>New Entry</h1>
{% include 'forms/form.html.twig' ignore missing %}
<div class="form-action-row">
<button type="button" id="get-location" class="btn-action">📍 Get Location</button>
<button type="button" id="get-weather" class="btn-action">🌤 Get Weather</button>
<button type="button" id="get-weather" class="btn-action" disabled>🌤 Get Weather</button>
</div>
<p id="location-status" class="form-status"></p>
<p id="weather-status" class="form-status"></p>
<p id="location-status" class="form-status" role="status"></p>
<p id="weather-status" class="form-status" role="status"></p>
</div>
<script>
// Custom validation — form uses novalidate so we handle it here
(function() {
var REQUIRED = ['title', 'content'];
var form = document.querySelector('form[name="new-entry"]');
if (!form) return;
function clearErrors() {
form.querySelectorAll('.field-error').forEach(function(el) { el.remove(); });
form.querySelectorAll('.field-invalid').forEach(function(el) { el.classList.remove('field-invalid'); });
}
function showError(field, msg) {
field.classList.add('field-invalid');
var err = document.createElement('span');
err.className = 'field-error';
err.textContent = msg;
field.parentNode.insertBefore(err, field.nextSibling);
}
form.addEventListener('submit', function(e) {
clearErrors();
var firstInvalid = null;
REQUIRED.forEach(function(name) {
var field = form.querySelector('[name="data[' + name + ']"]');
if (field && !field.value.trim()) {
showError(field, name.charAt(0).toUpperCase() + name.slice(1) + ' is required.');
if (!firstInvalid) firstInvalid = field;
}
});
if (firstInvalid) {
e.preventDefault();
firstInvalid.focus();
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}());
</script>
<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.className = 'form-status';
status.textContent = 'Getting location…';
if (!navigator.geolocation) {
status.textContent = 'Geolocation not supported.';
return;
}
navigator.geolocation.getCurrentPosition(function(pos) {
var lat = pos.coords.latitude.toFixed(6);
var lng = pos.coords.longitude.toFixed(6);
var latField = getField('lat');
var lngField = getField('lng');
if (latField) latField.value = lat;
if (lngField) lngField.value = lng;
status.textContent = '✓ Location captured · ' + lat + ', ' + lng;
status.classList.add('form-status--ok');
}, function(err) {
status.textContent = '✗ Could not get location: ' + err.message;
status.classList.add('form-status--err');
});
});
document.getElementById('get-weather').addEventListener('click', function() {
var status = document.getElementById('weather-status');
status.className = 'form-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 = 'Get location first, then fetch weather.';
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';
status.classList.add('form-status--ok');
})
.catch(function() {
status.textContent = '✗ Could not fetch weather — enter manually if needed.';
status.classList.add('form-status--err');
});
});
</script>
{# Get Location / Get Weather, disclosure, and validation all live in the
page-scoped bundle (js/src/post-form.js), loaded via head_assets above. #}
{% endblock %}
+20 -3
View File
@@ -9,11 +9,25 @@
{% endblock %}
{% set dailies_page = grav.pages.find(page.route ~ '/dailies') %}
{% set stories_page = grav.pages.find(page.route ~ '/stories') %}
{# journal_entries stays published-only — it feeds the map, stats and counts,
which must never include drafts (R5). #}
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
{# Owner gate (KTD8): the site owner (not merely any login — the super-admin
tester also authenticates) viewing the ACTIVE trip. Drives draft visibility
in the feed and the Edit/Delete controls (threaded to the card partial). #}
{% set active_trip_slug = (grav.config.site.active_trip|default(''))|split('/')|last %}
{% set owner_can_edit = grav.user.authenticated
and grav.user.username == grav.config.site.owner_username
and page.slug == active_trip_slug %}
{# Feed list is owner-aware: the owner sees drafts (unpublished) too; everyone
else (and every non-active-trip view) sees published only (R5, KTD7). #}
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
{% set all_items = [] %}
{% for e in journal_entries %}
{% for e in journal_feed %}
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.header.date}]) %}
{% endfor %}
{% for s in story_entries %}
@@ -41,7 +55,8 @@
{% set map_entries = [] %}
{% for item in all_items %}
{% if item.page.header.lat is not empty and item.page.header.lng is not empty %}
{# drafts render as a feed card only — never a map marker (R5) #}
{% if item.page.published and item.page.header.lat is not empty and item.page.header.lng is not empty %}
{% set map_entries = map_entries|merge([{
'type': item.type,
'lat': item.page.header.lat|number_format(6, '.', ''),
@@ -79,7 +94,9 @@
gpx_urls: gpx_urls,
gps_points: gps_points,
show_sort: true,
trip_header_extras: true
trip_header_extras: true,
owner_can_edit: owner_can_edit,
feed_return_url: page.url
} only %}
</div>