Tracker ordering fix + March–April fixture entries #1

Merged
m038 merged 34 commits from experimental-polar-steps into main 2026-06-18 22:41:55 +02:00
9 changed files with 531 additions and 31 deletions
Showing only changes of commit d3fcde9b0b - Show all commits
+10
View File
@@ -0,0 +1,10 @@
# Deny all direct web access to this folder and everything beneath it.
# Grav reads these files server-side; they must never be served over HTTP.
# This is a defense-in-depth backup for the rules in the site root .htaccess.
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
+36 -2
View File
@@ -41,20 +41,54 @@ form:
title: Location
fields:
header.location_city:
type: text
label: City
placeholder: 'e.g. Kyoto'
header.location_country:
type: text
label: Country
placeholder: 'e.g. Japan'
header.lat:
type: number
label: Latitude
help: 'GPS latitude (for map, Milestone 2)'
help: 'GPS latitude (for map)'
placeholder: '48.8566'
step: any
header.lng:
type: number
label: Longitude
help: 'GPS longitude (for map, Milestone 2)'
help: 'GPS longitude (for map)'
placeholder: '2.3522'
step: any
weather:
type: tab
title: Weather
fields:
header.weather_temp_c:
type: number
label: 'Temperature (°C)'
help: 'Auto-filled from post form. Edit if needed.'
step: 1
header.weather_desc:
type: select
label: 'Weather Condition'
options:
Sunny: '☀️ Sunny'
'Partly cloudy': '⛅ Partly cloudy'
Cloudy: '☁️ Cloudy'
Foggy: '🌫️ Foggy'
Drizzle: '🌦️ Drizzle'
Rain: '🌧️ Rain'
Snow: '❄️ Snow'
Thunderstorm: '⛈️ Thunderstorm'
publishing:
type: tab
title: Publishing
+10
View File
@@ -0,0 +1,10 @@
# Deny all direct web access to this folder and everything beneath it.
# Grav reads these files server-side; they must never be served over HTTP.
# This is a defense-in-depth backup for the rules in the site root .htaccess.
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
+6 -2
View File
@@ -4,8 +4,12 @@ date: '2026-06-17 10:00'
template: entry
published: true
hero_image: ''
lat: ''
lng: ''
lat: '52.3676'
lng: '4.9041'
location_city: 'Amsterdam'
location_country: 'Netherlands'
weather_temp_c: 19
weather_desc: 'Partly cloudy'
---
First entry. Bags are packed, passport is ready, the adventure starts here.
+24
View File
@@ -56,6 +56,26 @@ form:
type: text
placeholder: ''
-
name: location_city
label: City
type: text
placeholder: 'e.g. Kyoto'
-
name: location_country
label: Country
type: text
placeholder: 'e.g. Japan'
-
name: weather_temp_c
type: hidden
-
name: weather_desc
type: hidden
buttons:
-
type: submit
@@ -73,6 +93,10 @@ form:
date: '{{ form.value.date }}'
lat: '{{ form.value.lat }}'
lng: '{{ form.value.lng }}'
location_city: '{{ form.value.location_city }}'
location_country: '{{ form.value.location_country }}'
weather_temp_c: '{{ form.value.weather_temp_c }}'
weather_desc: '{{ form.value.weather_desc }}'
-
message: 'Entry posted successfully!'
-
+266 -14
View File
@@ -39,7 +39,8 @@ body {
padding: 1.5rem 1.25rem;
}
/* Feed */
/* ── Feed ──────────────────────────────────────────────────────────────────── */
.feed { display: flex; flex-direction: column; gap: 2rem; }
.entry-card {
@@ -53,15 +54,31 @@ body {
color: #666;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.4rem;
margin-bottom: 0.25rem;
}
.entry-card-meta {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.entry-card-meta .entry-date { margin-bottom: 0; }
.entry-card .entry-title { font-size: 1.3rem; margin-bottom: 0.75rem; }
.entry-card .entry-title a { color: inherit; text-decoration: none; }
.entry-card .entry-title a:hover { text-decoration: underline; }
.entry-thumb { margin-bottom: 0.75rem; }
.entry-thumb img { width: 100%; height: 200px; object-fit: cover; border-radius: 4px; }
.entry-thumb img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 8px;
display: block;
}
.entry-excerpt { color: #444; margin-bottom: 0.75rem; }
@@ -73,17 +90,231 @@ body {
.feed-empty { color: #666; font-style: italic; }
/* Single entry */
/* ── Location & Weather badges ─────────────────────────────────────────────── */
.entry-location {
font-size: 0.82rem;
color: #555;
display: inline-flex;
align-items: center;
gap: 0.2rem;
}
.entry-location--card {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.entry-weather {
font-size: 0.82rem;
color: #555;
margin-bottom: 0.25rem;
}
/* ── Single entry ───────────────────────────────────────────────────────────── */
.entry-header { margin-bottom: 1.5rem; }
.entry-header .entry-date { margin-bottom: 0.5rem; }
.entry .entry-title { font-size: 1.8rem; }
.entry-header .entry-date { margin-bottom: 0.3rem; }
.entry-header .entry-location { margin-bottom: 0.2rem; display: block; }
.entry-header .entry-weather { margin-bottom: 0.75rem; }
.entry .entry-title { font-size: 1.8rem; margin-top: 0.5rem; }
.entry-body { margin-bottom: 2rem; }
.entry-body p { margin-bottom: 1em; }
.entry-body img { max-width: 100%; height: auto; border-radius: 4px; }
.entry-footer { border-top: 1px solid #e5e5e5; padding-top: 1rem; }
.entry-footer a { color: #0066cc; text-decoration: none; font-size: 0.9rem; }
/* Login form */
/* ── Photo gallery ──────────────────────────────────────────────────────────── */
.entry-gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
margin-bottom: 2rem;
}
@media (min-width: 520px) {
.entry-gallery { grid-template-columns: repeat(3, 1fr); }
}
.gallery-thumb {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: block;
aspect-ratio: 1;
overflow: hidden;
border-radius: 4px;
}
.gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.15s;
}
.gallery-thumb:hover img,
.gallery-thumb:focus img { opacity: 0.85; }
.gallery-thumb:focus { outline: 2px solid #0066cc; outline-offset: 2px; }
/* ── Lightbox ───────────────────────────────────────────────────────────────── */
.lightbox {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.92);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox[hidden] { display: none; }
.lightbox-img {
max-width: 92vw;
max-height: 90vh;
object-fit: contain;
border-radius: 4px;
display: block;
}
.lightbox-close,
.lightbox-prev,
.lightbox-next {
position: absolute;
background: rgba(255,255,255,0.15);
border: none;
color: #fff;
cursor: pointer;
border-radius: 50%;
width: 44px;
height: 44px;
font-size: 1.4rem;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.lightbox-close { top: 1rem; right: 1rem; }
.lightbox-prev { left: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover { background: rgba(255,255,255,0.28); }
/* ── Map page ───────────────────────────────────────────────────────────────── */
.map-page .site-main { max-width: none; padding: 0; }
.map-container {
height: calc(100vh - 61px);
width: 100%;
}
.map-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
font-style: italic;
}
/* ── Stats page ─────────────────────────────────────────────────────────────── */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.stat-block {
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 1.25rem 1rem;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.stat-value {
display: block;
font-size: 2.5rem;
font-weight: 700;
color: #0066cc;
line-height: 1.1;
margin-bottom: 0.3rem;
}
.stat-label {
display: block;
font-size: 0.8rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.stats-countries {
font-size: 0.9rem;
color: #444;
text-align: center;
line-height: 1.8;
}
.stats-countries-label {
font-weight: 600;
display: block;
margin-bottom: 0.4rem;
color: #1a1a1a;
}
.stats-note {
font-size: 0.78rem;
color: #999;
text-align: center;
margin-top: 1.5rem;
}
/* ── Mini-map on tracker feed ───────────────────────────────────────────────── */
.feed-map-wrap {
margin-bottom: 2rem;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e5e5;
}
.feed-map {
height: 240px;
width: 100%;
}
@media (min-width: 520px) {
.feed-map { height: 320px; }
}
.feed-map-link {
display: block;
text-align: right;
font-size: 0.8rem;
color: #0066cc;
text-decoration: none;
padding: 0.4rem 0.6rem;
background: #fafafa;
border-top: 1px solid #e5e5e5;
}
/* ── Login form ─────────────────────────────────────────────────────────────── */
.login-form { max-width: 400px; margin: 2rem auto; padding: 0 1rem; }
.login-form .form-field { margin-bottom: 1.25rem; }
.login-form .form-label label { display: block; font-size: 0.9rem; font-weight: 600; margin-bottom: 0.4rem; }
@@ -101,12 +332,33 @@ body {
.login-form .button.secondary { background: #f0f0f0; color: #333; text-decoration: none; line-height: 44px; padding: 0 1rem; }
.login-form .rememberme { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
/* Post form */
/* ── Post form ──────────────────────────────────────────────────────────────── */
.post-form-wrap h1 { font-size: 1.4rem; margin-bottom: 1.5rem; }
.post-form-wrap .btn-location {
display: block; width: 100%; margin-top: 1rem;
padding: 0.85rem 1rem; min-height: 44px;
background: #f0f0f0; border: 1px solid #ccc;
border-radius: 6px; font-size: 1rem; cursor: pointer;
.form-actions-extra {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.btn-extra {
flex: 1;
padding: 0.75rem 0.5rem;
min-height: 44px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 0.95rem;
cursor: pointer;
text-align: center;
}
.btn-extra:hover { background: #e5e5e5; }
.field-status {
font-size: 0.85rem;
color: #555;
margin-top: 0.4rem;
min-height: 1.2em;
}
.post-form-wrap .location-status { font-size: 0.85rem; color: #666; margin-top: 0.5rem; text-align: center; }
@@ -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>