Merge commit 'b3b4f77'
# Conflicts: # themes/intotheeast/templates/partials/trip-feed-col.html.twig # themes/intotheeast/templates/trip.html.twig
This commit is contained in:
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 +
|
||||
'¤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';
|
||||
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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user