Compare commits
57 Commits
936662e35c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c1d22859 | |||
| 68b328dabc | |||
| 817bd17959 | |||
| 77dd99ee2b | |||
| 857f33be54 | |||
| 320a98893a | |||
| e07fb3a72a | |||
| 1bb588d1d2 | |||
| aa1cb7411c | |||
| 5fe8c015f1 | |||
| 6f9538053c | |||
| 5e503cf3a5 | |||
| ce860cfef9 | |||
| 989755d33c | |||
| 9ddf52c635 | |||
| 9b62f79301 | |||
| 64aa9ec023 | |||
| 9bfd96af2c | |||
| 000af6934f | |||
| c94e36a861 | |||
| 2c831628b2 | |||
| 02fc666661 | |||
| c3cb224402 | |||
| 2b8ea1963b | |||
| f94880e758 | |||
| b6142cee44 | |||
| 53bfe5955d | |||
| 9f503c011d | |||
| 415d95ed47 | |||
| e787544a2b | |||
| 9f94164c61 | |||
| 608ccfdecd | |||
| 933652fd57 | |||
| fdaed1033a | |||
| bc77baca2e | |||
| 7c9a55224a | |||
| 85ba3747b1 | |||
| 71f8629d18 | |||
| b1492918d5 | |||
| 95ea38d250 | |||
| 81be69f08d | |||
| 71eaa3e788 | |||
| 5c75f1416f | |||
| 3379e50503 | |||
| 30c8937566 | |||
| 770a96b099 | |||
| 604ba00c70 | |||
| b6c9d0b2ac | |||
| 51ab99b839 | |||
| 2f733e5ffc | |||
| d7e3162f55 | |||
| 8e127e7e3a | |||
| e853cb543a | |||
| e29953ab90 | |||
| 366974475f | |||
| fa29888578 | |||
| 31f3c6fb2f |
@@ -4,3 +4,6 @@
|
|||||||
!/plugins/story-blocks/
|
!/plugins/story-blocks/
|
||||||
/data/
|
/data/
|
||||||
/pages/01.trips/italy-2026-demo/
|
/pages/01.trips/italy-2026-demo/
|
||||||
|
/pages/02.post/*ui-test*/
|
||||||
|
/config/plugins/git-sync.yaml
|
||||||
|
/config/security.yaml
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
salt: HlC0NrX9QsYq1S
|
|
||||||
@@ -6,4 +6,4 @@ metadata:
|
|||||||
description: 'Into the East — travel journal'
|
description: 'Into the East — travel journal'
|
||||||
description: 'A travel blog by Mischa'
|
description: 'A travel blog by Mischa'
|
||||||
active_trip: /trips/us-canada-mex-2024
|
active_trip: /trips/us-canada-mex-2024
|
||||||
travelling: true
|
travelling: false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Last Beer Before the Foreign Land'
|
title: 'Last Beer Before the Foreign Land'
|
||||||
date: '2023-08-29 10:29'
|
date: '2023-08-29 10:29'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '52.36402'
|
lat: '52.36402'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'The UAZ Buchanka Counter Begins'
|
title: 'The UAZ Buchanka Counter Begins'
|
||||||
date: '2023-08-30 18:06'
|
date: '2023-08-30 18:06'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: plane
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '51.140108'
|
lat: '51.140108'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Baiterek: Bird of Happiness in Astana'
|
title: 'Baiterek: Bird of Happiness in Astana'
|
||||||
date: '2023-08-31 16:45'
|
date: '2023-08-31 16:45'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: walking
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '51.128246'
|
lat: '51.128246'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Doshirak and Politics on the Night Train'
|
title: 'Doshirak and Politics on the Night Train'
|
||||||
date: '2023-09-02 15:47'
|
date: '2023-09-02 15:47'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '43.2220'
|
lat: '43.2220'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Plov and Street Art in Almaty'
|
title: 'Plov and Street Art in Almaty'
|
||||||
date: '2023-09-03 16:38'
|
date: '2023-09-03 16:38'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '43.2220'
|
lat: '43.2220'
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
title: 'Rain in Charyn Canyon, Manti for Dinner'
|
title: 'Rain in Charyn Canyon, Manti for Dinner'
|
||||||
date: '2023-09-04 15:50'
|
date: '2023-09-04 15:50'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: photo-1.jpg
|
||||||
lat: '43.35102'
|
lat: '43.35102'
|
||||||
lng: '79.08010'
|
lng: '79.08010'
|
||||||
location_city: 'Charyn Canyon'
|
location_city: 'Charyn Canyon'
|
||||||
location_country: 'Kazakhstan'
|
location_country: Kazakhstan
|
||||||
weather_temp_c: '18'
|
weather_temp_c: '18'
|
||||||
weather_desc: 'cloudy with showers'
|
weather_desc: 'cloudy with showers'
|
||||||
|
featured: true
|
||||||
---
|
---
|
||||||
|
|
||||||
The adventure started, a big jeep, few adventurous tourists and our guide Aybek.
|
The adventure started, a big jeep, few adventurous tourists and our guide Aybek.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Kurt, Kumis and a UAZ Dream Ride'
|
title: 'Kurt, Kumis and a UAZ Dream Ride'
|
||||||
date: '2023-09-05 15:50'
|
date: '2023-09-05 15:50'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '43.068370'
|
lat: '43.068370'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'First Hike Up Toward Ala Kol'
|
title: 'First Hike Up Toward Ala Kol'
|
||||||
date: '2023-09-07 17:00'
|
date: '2023-09-07 17:00'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '42.4900'
|
lat: '42.4900'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Tea Trails and No Seatbelts in Kyrgyzstan'
|
title: 'Tea Trails and No Seatbelts in Kyrgyzstan'
|
||||||
date: '2023-09-10 06:16'
|
date: '2023-09-10 06:16'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: bus
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '42.876640'
|
lat: '42.876640'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: "Stuck in No Man's Land at 4655m"
|
title: "Stuck in No Man's Land at 4655m"
|
||||||
date: '2023-09-18 03:20'
|
date: '2023-09-18 03:20'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '39.384414'
|
lat: '39.384414'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Black Water Lake on the Pamir Highway'
|
title: 'Black Water Lake on the Pamir Highway'
|
||||||
date: '2023-09-19 05:50'
|
date: '2023-09-19 05:50'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '39.01250'
|
lat: '39.01250'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Warm Soup in a Village of Hundreds'
|
title: 'Warm Soup in a Village of Hundreds'
|
||||||
date: '2023-09-19 06:04'
|
date: '2023-09-19 06:04'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '37.755579'
|
lat: '37.755579'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Afghanistan Just Across the Wakhan River'
|
title: 'Afghanistan Just Across the Wakhan River'
|
||||||
date: '2023-09-23 08:41'
|
date: '2023-09-23 08:41'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '37.032071'
|
lat: '37.032071'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'The Night the Beer Finally Arrived'
|
title: 'The Night the Beer Finally Arrived'
|
||||||
date: '2023-09-23 08:29'
|
date: '2023-09-23 08:29'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '37.755660'
|
lat: '37.755660'
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Farewell Vodka Under the World's Tallest Flag"
|
title: "Farewell Vodka Under the World's Tallest Flag"
|
||||||
date: '2023-09-20 16:19'
|
date: '2023-10-01 18:00'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '38.5598'
|
lat: '38.5598'
|
||||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -2,6 +2,7 @@
|
|||||||
title: 'Hot Springs and a Pamiri Homestay'
|
title: 'Hot Springs and a Pamiri Homestay'
|
||||||
date: '2023-10-01 10:40'
|
date: '2023-10-01 10:40'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '36.983527'
|
lat: '36.983527'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'What Is Normal? Reflections from Khorog'
|
title: 'What Is Normal? Reflections from Khorog'
|
||||||
date: '2023-10-01 10:51'
|
date: '2023-10-01 10:51'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: car
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '37.49046'
|
lat: '37.49046'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Millionaires and Minarets in Bukhara'
|
title: 'Millionaires and Minarets in Bukhara'
|
||||||
date: '2023-10-02 00:00'
|
date: '2023-10-02 00:00'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '39.775957'
|
lat: '39.775957'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Four Weeks in Central Asia, Barely Enough'
|
title: 'Four Weeks in Central Asia, Barely Enough'
|
||||||
date: '2023-10-03 07:58'
|
date: '2023-10-03 07:58'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '40.37660'
|
lat: '40.37660'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: "Timur's Samarkand and a Badly Parked Truck"
|
title: "Timur's Samarkand and a Badly Parked Truck"
|
||||||
date: '2023-10-03 07:38'
|
date: '2023-10-03 07:38'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: train
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '39.65841'
|
lat: '39.65841'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: 'Hunting the Mother of Georgia from Above'
|
title: 'Hunting the Mother of Georgia from Above'
|
||||||
date: '2023-10-18 07:38'
|
date: '2023-10-18 07:38'
|
||||||
template: entry
|
template: entry
|
||||||
|
transport_mode: plane
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: '41.6938'
|
lat: '41.6938'
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ date_start: '2025-10-11'
|
|||||||
date_end: '2025-10-16'
|
date_end: '2025-10-16'
|
||||||
cover_image: ''
|
cover_image: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ date: '2024-05-28 07:03'
|
|||||||
template: entry
|
template: entry
|
||||||
published: true
|
published: true
|
||||||
hero_image: 'photo-1.jpg'
|
hero_image: 'photo-1.jpg'
|
||||||
lat: ''
|
lat: '45.5285'
|
||||||
lng: ''
|
lng: '13.5680'
|
||||||
location_city: ''
|
location_city: Piran
|
||||||
location_country: ''
|
location_country: Slovenia
|
||||||
weather_temp_c: ''
|
weather_temp_c: '21'
|
||||||
weather_desc: ''
|
weather_desc: sunny
|
||||||
---
|
---
|
||||||
|
|
||||||
A sunny day in Piran. We drove from Ljubljana through the beautiful Slovenian countryside. The more west we went, the more Mediterranean the landscape felt. Piran is a cute, Mediterranean harbor town, with little streets, squares and no cars. The view from the old fortification walls was great and the climb in the warm weather gave us a sense of accomplishment which we rewarded with a well deserved ice cream.
|
A sunny day in Piran. We drove from Ljubljana through the beautiful Slovenian countryside. The more west we went, the more Mediterranean the landscape felt. Piran is a cute, Mediterranean harbor town, with little streets, squares and no cars. The view from the old fortification walls was great and the climb in the warm weather gave us a sense of accomplishment which we rewarded with a well deserved ice cream.
|
||||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 302 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: 'The Journey'
|
||||||
|
template: dailies
|
||||||
|
content:
|
||||||
|
items: '@self.children'
|
||||||
|
order:
|
||||||
|
by: date
|
||||||
|
dir: desc
|
||||||
|
filter:
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Trip Map'
|
||||||
|
template: map
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Trip Stats'
|
||||||
|
template: stats
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Stories
|
||||||
|
template: stories
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: 'Slovenia 2024'
|
||||||
|
template: trip
|
||||||
|
date: '2024-05-28'
|
||||||
|
date_start: '2024-05-28'
|
||||||
|
date_end: '2024-05-28'
|
||||||
|
cover_image: ''
|
||||||
|
---
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'Northern America 2024'
|
title: 'Northern America 2024'
|
||||||
template: trip
|
template: trip
|
||||||
date: '2024-05-28'
|
date: '2024-07-21'
|
||||||
date_start: '2024-05-28'
|
date_start: '2024-07-21'
|
||||||
date_end: '2024-08-07'
|
date_end: '2024-08-07'
|
||||||
cover_image: ''
|
cover_image: ''
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ HTML;
|
|||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div class="pgallery">
|
<div class="pgallery">
|
||||||
<div class="pgallery__frame" role="region" aria-label="Photo gallery">
|
<div class="pgallery__frame" role="region" aria-label="Photo gallery" tabindex="0">
|
||||||
{$slidesHtml}
|
{$slidesHtml}
|
||||||
<div class="pgallery__dots" aria-hidden="true">{$dotsHtml}</div>
|
<div class="pgallery__dots" aria-hidden="true">{$dotsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ form:
|
|||||||
'bus': 'Bus'
|
'bus': 'Bus'
|
||||||
'train': 'Train'
|
'train': 'Train'
|
||||||
'car': 'Car'
|
'car': 'Car'
|
||||||
|
'plane': 'Plane'
|
||||||
|
|
||||||
header.force_connect:
|
header.force_connect:
|
||||||
type: toggle
|
type: toggle
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
@@ -87,7 +89,7 @@ body::after {
|
|||||||
|
|
||||||
/* ── Feed ────────────────────────────────────────────────────────────────────── */
|
/* ── Feed ────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.feed { display: flex; flex-direction: column; gap: var(--space-12); }
|
.feed { display: flex; flex-direction: column; gap: var(--space-8); }
|
||||||
.feed-empty { color: var(--color-ink-muted); font-style: italic; }
|
.feed-empty { color: var(--color-ink-muted); font-style: italic; }
|
||||||
|
|
||||||
.entry-card {
|
.entry-card {
|
||||||
@@ -95,8 +97,9 @@ body::after {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
padding-bottom: var(--space-12);
|
padding-bottom: var(--space-8);
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
|
scroll-margin-top: calc(var(--site-header-height) + var(--space-4));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card: photo variant */
|
/* Card: photo variant */
|
||||||
@@ -162,8 +165,8 @@ body::after {
|
|||||||
|
|
||||||
.journal-post {
|
.journal-post {
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
padding-bottom: var(--space-12);
|
padding-bottom: var(--space-8);
|
||||||
margin-bottom: var(--space-12);
|
scroll-margin-top: calc(var(--site-header-height) + var(--space-4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-post-header {
|
.journal-post-header {
|
||||||
@@ -202,13 +205,21 @@ body::after {
|
|||||||
color: var(--color-ink-muted);
|
color: var(--color-ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.journal-photo-wrap {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
.journal-photo-strip {
|
.journal-photo-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
border-radius: var(--radius-md);
|
height: 100%;
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-strip::-webkit-scrollbar { display: none; }
|
.journal-photo-strip::-webkit-scrollbar { display: none; }
|
||||||
@@ -216,34 +227,76 @@ body::after {
|
|||||||
.journal-photo-slide {
|
.journal-photo-slide {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
aspect-ratio: 3 / 2;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-slide::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: var(--thumb);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(24px) brightness(0.75);
|
||||||
|
transform: scale(1.15);
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-slide img {
|
.journal-photo-slide img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-dots {
|
.journal-photo-expand {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-3);
|
||||||
|
right: var(--space-3);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 2;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-photo-expand:active { background: rgba(0,0,0,0.7); }
|
||||||
|
|
||||||
|
.journal-photo-dots {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-3);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-4);
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-dot {
|
.journal-photo-dot {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: var(--color-border);
|
background: rgba(255,255,255,0.5);
|
||||||
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 1px 3px rgba(0,0,0,0.4);
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-dot.is-active {
|
.journal-photo-dot.is-active {
|
||||||
background: var(--color-ink-muted);
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strip-controls {
|
.strip-controls {
|
||||||
@@ -254,6 +307,10 @@ body::after {
|
|||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.strip-controls { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
.strip-prev,
|
.strip-prev,
|
||||||
.strip-next {
|
.strip-next {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -282,7 +339,8 @@ body::after {
|
|||||||
.journal-post-body p:last-child { margin-bottom: 0; }
|
.journal-post-body p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
.journal-post.is-highlighted,
|
.journal-post.is-highlighted,
|
||||||
.entry-card.is-highlighted {
|
.entry-card.is-highlighted,
|
||||||
|
.story-card.is-highlighted {
|
||||||
animation: card-highlight 0.7s ease-out forwards;
|
animation: card-highlight 0.7s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,53 +453,28 @@ body::after {
|
|||||||
|
|
||||||
.gallery-thumb:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
.gallery-thumb:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||||
|
|
||||||
/* ── Lightbox ────────────────────────────────────────────────────────────────── */
|
/* ── PhotoSwipe overrides ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.lightbox {
|
.pswp__bg { background: #000; }
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
/* pswp.css loads in <body> after this stylesheet, so !important is needed to win */
|
||||||
background: rgba(0,0,0,0.94);
|
.pswp { height: 100dvh !important; }
|
||||||
z-index: 1000;
|
/* Pin bg directly to viewport so it can't be cut short by parent height rounding */
|
||||||
display: flex;
|
.pswp__bg { position: fixed !important; inset: 0 !important; }
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
/* Keyboard arrow navigation slide-in animations */
|
||||||
|
.pswp-key-from-right { animation: pswpKeyFromRight 0.35s cubic-bezier(0.4, 0, 0.22, 1) both; }
|
||||||
|
.pswp-key-from-left { animation: pswpKeyFromLeft 0.35s cubic-bezier(0.4, 0, 0.22, 1) both; }
|
||||||
|
|
||||||
|
@keyframes pswpKeyFromRight {
|
||||||
|
from { transform: translateX(48px); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
@keyframes pswpKeyFromLeft {
|
||||||
.lightbox[hidden] { display: none; }
|
from { transform: translateX(-48px); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
.lightbox-img {
|
|
||||||
max-width: 92vw;
|
|
||||||
max-height: 90vh;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightbox-close,
|
|
||||||
.lightbox-prev,
|
|
||||||
.lightbox-next {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(255,255,255,0.12);
|
|
||||||
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;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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.26); }
|
|
||||||
|
|
||||||
/* ── Map page ────────────────────────────────────────────────────────────────── */
|
/* ── Map page ────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.map-page .site-main { max-width: none; padding: 0; }
|
.map-page .site-main { max-width: none; padding: 0; }
|
||||||
@@ -571,7 +604,7 @@ body::after {
|
|||||||
.stat-value {
|
.stat-value {
|
||||||
display: block;
|
display: block;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: var(--text-3xl);
|
font-size: clamp(2rem, 6vw, var(--text-3xl));
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
@@ -615,6 +648,7 @@ body::after {
|
|||||||
/* ── Mini-map on tracker feed ────────────────────────────────────────────────── */
|
/* ── Mini-map on tracker feed ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.feed-map-wrap {
|
.feed-map-wrap {
|
||||||
|
position: relative;
|
||||||
margin-bottom: var(--space-10);
|
margin-bottom: var(--space-10);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -623,6 +657,7 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feed-map {
|
.feed-map {
|
||||||
|
position: relative;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -631,6 +666,53 @@ body::after {
|
|||||||
.feed-map { height: 300px; }
|
.feed-map { height: 300px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-map-fullscreen-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-2);
|
||||||
|
right: var(--space-2);
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background: var(--color-canvas);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-ink);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-map-fullscreen-btn:hover { background: var(--color-paper); }
|
||||||
|
|
||||||
|
.feed-map-fs-close { display: none; font-size: 1rem; line-height: 1; }
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.feed-map-fullscreen-btn { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-map-wrap.is-fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-map-wrap.is-fullscreen .feed-map {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-map-wrap.is-fullscreen .feed-map-link { display: none; }
|
||||||
|
|
||||||
|
.feed-map-wrap.is-fullscreen .feed-map-fs-open,
|
||||||
|
.home-map-col.is-fullscreen .feed-map-fs-open { display: none; }
|
||||||
|
.feed-map-wrap.is-fullscreen .feed-map-fs-close,
|
||||||
|
.home-map-col.is-fullscreen .feed-map-fs-close { display: block; }
|
||||||
|
|
||||||
.feed-map-link {
|
.feed-map-link {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -852,10 +934,22 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-map {
|
.home-map {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-map-col.is-fullscreen {
|
||||||
|
position: fixed !important;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
height: 100dvh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-map-col.is-fullscreen .home-map {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
.home-feed-col {
|
.home-feed-col {
|
||||||
padding: var(--space-8) var(--space-8);
|
padding: var(--space-8) var(--space-8);
|
||||||
}
|
}
|
||||||
@@ -881,6 +975,12 @@ body::after {
|
|||||||
|
|
||||||
/* ── Trip page filter bar ────────────────────────────────────────────────────── */
|
/* ── Trip page filter bar ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.feed-sort-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.trip-filter-bar {
|
.trip-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -923,6 +1023,46 @@ body::after {
|
|||||||
background: var(--color-accent-light);
|
background: var(--color-accent-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trip-panel-toggles {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-5);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-toggle:hover {
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-color: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
.trip-panel-toggle.is-active {
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-caret {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75em;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-toggle.is-active .trip-panel-caret { transform: rotate(180deg); }
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.home-layout { display: flex; flex-direction: column; }
|
.home-layout { display: flex; flex-direction: column; }
|
||||||
.home-map-col { position: static; height: 40vh; align-self: stretch; }
|
.home-map-col { position: static; height: 40vh; align-self: stretch; }
|
||||||
@@ -1073,12 +1213,51 @@ body::after {
|
|||||||
|
|
||||||
/* ── Trip page inline stats block ───────────────────────────────────────────── */
|
/* ── Trip page inline stats block ───────────────────────────────────────────── */
|
||||||
|
|
||||||
.trip-stats-block {
|
.trip-stats-block,
|
||||||
|
.trip-cycling-block {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0;
|
||||||
|
transition: max-height 0.4s ease, margin-bottom 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-stats-block.is-open,
|
||||||
|
.trip-cycling-block.is-open {
|
||||||
|
max-height: 600px;
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-inner {
|
||||||
background: var(--color-canvas);
|
background: var(--color-canvas);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
margin-bottom: var(--space-6);
|
}
|
||||||
|
|
||||||
|
.trip-panel-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip-panel-close:hover {
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-color: var(--color-ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.trip-panel-close { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.trip-stats-grid {
|
.trip-stats-grid {
|
||||||
@@ -1089,7 +1268,7 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.trip-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
.trip-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.trip-stats-countries {
|
.trip-stats-countries {
|
||||||
@@ -1105,13 +1284,6 @@ body::after {
|
|||||||
|
|
||||||
/* ── Trip page cycling panel ─────────────────────────────────────────────────── */
|
/* ── Trip page cycling panel ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.trip-cycling-block {
|
|
||||||
background: var(--color-canvas);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: var(--space-6);
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trip-cycling-header {
|
.trip-cycling-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1137,7 +1309,8 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.trip-cycling-grid { grid-template-columns: repeat(2, 1fr); }
|
.trip-cycling-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.trip-cycling-grid .stat-block:last-child:nth-child(odd) { grid-column: 1 / -1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Story pages ─────────────────────────────────────────────────────────── */
|
/* ── Story pages ─────────────────────────────────────────────────────────── */
|
||||||
@@ -1708,12 +1881,18 @@ body::after {
|
|||||||
|
|
||||||
/* ── Stories listing ──────────────────────────────────────── */
|
/* ── Stories listing ──────────────────────────────────────── */
|
||||||
.stories-listing { padding: var(--space-10) 0; }
|
.stories-listing { padding: var(--space-10) 0; }
|
||||||
|
.stories-listing__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-10);
|
||||||
|
}
|
||||||
.stories-listing__heading {
|
.stories-listing__heading {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
margin-bottom: var(--space-10);
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.stories-grid {
|
.stories-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -10,28 +10,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Catmull-Rom spline through waypoints → dense interpolated coords.
|
* Centripetal Catmull-Rom spline (α=0.5) through waypoints → dense coords.
|
||||||
* Produces a smooth curve that passes through every entry dot.
|
* Parameterising by √chord-length prevents the large bow that uniform
|
||||||
* steps: interpolated points per segment (16 is plenty for daily entries).
|
* parameterisation produces when consecutive points are very close together
|
||||||
|
* but their neighbours are far away (e.g. two entries both in Berlin).
|
||||||
*/
|
*/
|
||||||
function catmullRomSpline(coords, steps) {
|
function catmullRomSpline(coords, steps) {
|
||||||
if (coords.length < 2) return coords;
|
if (coords.length < 2) return coords;
|
||||||
steps = steps || 16;
|
steps = steps || 16;
|
||||||
|
var alpha = 0.5;
|
||||||
var out = [];
|
var out = [];
|
||||||
|
|
||||||
for (var i = 0; i < coords.length - 1; i++) {
|
/* Phantom endpoints via reflection so the spline reaches the first and last real points */
|
||||||
var p0 = coords[Math.max(i - 1, 0)];
|
var ext = [
|
||||||
var p1 = coords[i];
|
[2*coords[0][0] - coords[1][0], 2*coords[0][1] - coords[1][1]]
|
||||||
var p2 = coords[i + 1];
|
].concat(coords).concat([
|
||||||
var p3 = coords[Math.min(i + 2, coords.length - 1)];
|
[2*coords[coords.length-1][0] - coords[coords.length-2][0],
|
||||||
|
2*coords[coords.length-1][1] - coords[coords.length-2][1]]
|
||||||
|
]);
|
||||||
|
|
||||||
|
function segT(a, b) {
|
||||||
|
var dx = b[0]-a[0], dy = b[1]-a[1];
|
||||||
|
return Math.pow(Math.max(Math.sqrt(dx*dx + dy*dy), 1e-10), alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i < ext.length - 2; i++) {
|
||||||
|
var p0 = ext[i-1], p1 = ext[i], p2 = ext[i+1], p3 = ext[i+2];
|
||||||
|
|
||||||
|
var t0 = 0;
|
||||||
|
var t1 = t0 + segT(p0, p1);
|
||||||
|
var t2 = t1 + segT(p1, p2);
|
||||||
|
var t3 = t2 + segT(p2, p3);
|
||||||
|
|
||||||
for (var s = 0; s < steps; s++) {
|
for (var s = 0; s < steps; s++) {
|
||||||
var t = s / steps;
|
var t = t1 + (t2 - t1) * s / steps;
|
||||||
var t2 = t * t;
|
|
||||||
var t3 = t2 * t;
|
var a1x = (t1-t)/(t1-t0)*p0[0] + (t-t0)/(t1-t0)*p1[0];
|
||||||
|
var a1y = (t1-t)/(t1-t0)*p0[1] + (t-t0)/(t1-t0)*p1[1];
|
||||||
|
var a2x = (t2-t)/(t2-t1)*p1[0] + (t-t1)/(t2-t1)*p2[0];
|
||||||
|
var a2y = (t2-t)/(t2-t1)*p1[1] + (t-t1)/(t2-t1)*p2[1];
|
||||||
|
var a3x = (t3-t)/(t3-t2)*p2[0] + (t-t2)/(t3-t2)*p3[0];
|
||||||
|
var a3y = (t3-t)/(t3-t2)*p2[1] + (t-t2)/(t3-t2)*p3[1];
|
||||||
|
|
||||||
|
var b1x = (t2-t)/(t2-t0)*a1x + (t-t0)/(t2-t0)*a2x;
|
||||||
|
var b1y = (t2-t)/(t2-t0)*a1y + (t-t0)/(t2-t0)*a2y;
|
||||||
|
var b2x = (t3-t)/(t3-t1)*a2x + (t-t1)/(t3-t1)*a3x;
|
||||||
|
var b2y = (t3-t)/(t3-t1)*a2y + (t-t1)/(t3-t1)*a3y;
|
||||||
|
|
||||||
out.push([
|
out.push([
|
||||||
0.5 * ((2*p1[0]) + (-p0[0]+p2[0])*t + (2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*t2 + (-p0[0]+3*p1[0]-3*p2[0]+p3[0])*t3),
|
(t2-t)/(t2-t1)*b1x + (t-t1)/(t2-t1)*b2x,
|
||||||
0.5 * ((2*p1[1]) + (-p0[1]+p2[1])*t + (2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*t2 + (-p0[1]+3*p1[1]-3*p2[1]+p3[1])*t3)
|
(t2-t)/(t2-t1)*b1y + (t-t1)/(t2-t1)*b2y
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'default.html.twig' %}
|
{% extends 'default.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
|
||||||
{% set journal_entries = page.collection() %}
|
{% set journal_entries = page.collection() %}
|
||||||
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
|
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
|
||||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
||||||
@@ -13,7 +14,8 @@
|
|||||||
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
|
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{# No sort needed: page.collection() returns journal entries date-descending per dailies.md config. Dailies has no stories, so no re-merge sort is needed. #}
|
{# page.collection() returns date-descending; reverse to match ascending default on trip page. #}
|
||||||
|
{% set all_items = all_items|reverse %}
|
||||||
|
|
||||||
{# Collect GPS entries for mini-map #}
|
{# Collect GPS entries for mini-map #}
|
||||||
{% set map_entries = [] %}
|
{% set map_entries = [] %}
|
||||||
@@ -33,134 +35,112 @@
|
|||||||
|
|
||||||
{% set trip_page = page.parent() %}
|
{% set trip_page = page.parent() %}
|
||||||
|
|
||||||
{% if map_entries|length > 0 %}
|
{% include 'partials/feed-map.html.twig' with {
|
||||||
<div class="feed-map-wrap">
|
'map_entries': map_entries,
|
||||||
<div class="feed-map" id="feed-map"></div>
|
'map_id': 'feed-map',
|
||||||
<a class="feed-map-link" href="{{ page.parent().url }}/map">View full map →</a>
|
'map_var': 'feedMap',
|
||||||
|
'link_href': page.parent().url ~ '/map',
|
||||||
|
'card_prefix': 'entry-',
|
||||||
|
'trip_page': trip_page,
|
||||||
|
'show_journey': true
|
||||||
|
} only %}
|
||||||
|
|
||||||
|
<div class="feed-sort-bar">
|
||||||
|
<button class="trip-stats-btn" id="feed-sort-toggle" aria-label="Sort: oldest first">↑ Oldest first</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
|
||||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
var FEED_ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
{% set _ac = trip_page.header.autoconnect ?? 'on' %}
|
|
||||||
var AUTOCONNECT = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
|
|
||||||
|
|
||||||
var feedMap = new maplibregl.Map({
|
|
||||||
container: 'feed-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
feedMap.on('load', function () {
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
FEED_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === FEED_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(feedMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () { window.location.href = entry.url; });
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(feedMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (FEED_ENTRIES.length === 1) {
|
|
||||||
feedMap.jumpTo({ center: [parseFloat(FEED_ENTRIES[0].lng), parseFloat(FEED_ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { connectMode: AUTOCONNECT });
|
|
||||||
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="feed">
|
<div class="feed">
|
||||||
{% if all_items|length > 0 %}
|
{% if all_items|length > 0 %}
|
||||||
{% for item in all_items %}
|
{% for item in all_items %}
|
||||||
{% set entry = item.page %}
|
{% set entry = item.page %}
|
||||||
|
|
||||||
{% if item.type == 'journal' %}
|
{% if item.type == 'journal' %}
|
||||||
{% set weather_icons = {
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
|
||||||
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
|
||||||
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
|
||||||
} %}
|
|
||||||
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
|
||||||
<header class="journal-post-header">
|
|
||||||
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
{% if entry.header.location_city or entry.header.location_country %}
|
|
||||||
<span class="journal-post-location">
|
|
||||||
· 📍
|
|
||||||
{%- set _loc = [] -%}
|
|
||||||
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
|
||||||
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
|
||||||
{{ _loc|join(', ') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.header.weather_desc %}
|
|
||||||
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% set images = entry.media.images %}
|
|
||||||
{% if images|length > 0 %}
|
|
||||||
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
|
||||||
{% for img in images %}
|
|
||||||
<div class="journal-photo-slide">
|
|
||||||
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if images|length > 1 %}
|
|
||||||
<div class="journal-photo-dots" aria-hidden="true">
|
|
||||||
{% for img in images %}
|
|
||||||
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set hero = null %}
|
{% include 'partials/entry-story.html.twig' %}
|
||||||
{% 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 %}
|
|
||||||
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story" href="{{ entry.url }}">
|
|
||||||
{% if hero %}
|
|
||||||
<div class="entry-card-photo entry-card-photo--story">
|
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="entry-card-body">
|
|
||||||
<span class="story-badge">✦ Story</span>
|
|
||||||
<h2 class="entry-title">{{ entry.title }}</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
|
||||||
|
|
||||||
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '.pswp-gallery',
|
||||||
|
children: 'a.journal-photo-slide',
|
||||||
|
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
|
||||||
|
});
|
||||||
|
lightbox.on('afterOpen', function () {
|
||||||
|
var pswp = lightbox.pswp;
|
||||||
|
var keyDir = 0;
|
||||||
|
var clearTimer = null;
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.key === 'ArrowRight') keyDir = 1;
|
||||||
|
else if (e.key === 'ArrowLeft') keyDir = -1;
|
||||||
|
else keyDir = 0;
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey, true);
|
||||||
|
pswp.on('change', function () {
|
||||||
|
if (!keyDir) return;
|
||||||
|
var dir = keyDir;
|
||||||
|
keyDir = 0;
|
||||||
|
var el = pswp.currSlide && pswp.currSlide.container;
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
|
||||||
|
el.offsetWidth;
|
||||||
|
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
|
||||||
|
});
|
||||||
|
pswp.on('close', function () {
|
||||||
|
document.removeEventListener('keydown', onKey, true);
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
lightbox.init();
|
||||||
|
|
||||||
|
/* Per-strip: dot sync + expand button → tap the visible slide to trigger pswp */
|
||||||
|
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
|
||||||
|
var strip = wrap.querySelector('.journal-photo-strip');
|
||||||
|
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
|
||||||
|
var expandBtn = wrap.querySelector('.journal-photo-expand');
|
||||||
|
var article = wrap.closest('article');
|
||||||
|
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
|
||||||
|
var visibleIdx = 0;
|
||||||
|
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
if (!e.isIntersecting) return;
|
||||||
|
visibleIdx = slides.indexOf(e.target);
|
||||||
|
dots.forEach(function (d) { d.classList.remove('is-active'); });
|
||||||
|
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
|
||||||
|
});
|
||||||
|
}, { root: strip, threshold: 0.5 });
|
||||||
|
slides.forEach(function (s) { io.observe(s); });
|
||||||
|
|
||||||
|
if (expandBtn && slides.length) {
|
||||||
|
expandBtn.addEventListener('click', function () {
|
||||||
|
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var sortBtn = document.getElementById('feed-sort-toggle');
|
||||||
|
if (!sortBtn) return;
|
||||||
|
var feed = document.querySelector('.feed');
|
||||||
|
var ascending = true;
|
||||||
|
|
||||||
|
sortBtn.addEventListener('click', function() {
|
||||||
|
ascending = !ascending;
|
||||||
|
var entries = Array.from(feed.querySelectorAll('[data-type]'));
|
||||||
|
entries.reverse().forEach(function(el) { feed.appendChild(el); });
|
||||||
|
sortBtn.textContent = ascending ? '↑ Oldest first' : '↓ Newest first';
|
||||||
|
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
|
||||||
|
sortBtn.classList.toggle('is-active', !ascending);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<form class="gpx-upload-form" data-trip-route="{{ trip.route }}">
|
<form class="gpx-upload-form" data-trip-route="{{ trip.route }}">
|
||||||
<label class="gpx-upload-label">
|
<label class="gpx-upload-label">
|
||||||
|
<span class="gpx-upload-label__text">Choose GPX file</span>
|
||||||
<input type="file" accept=".gpx,application/gpx+xml" name="file" class="gpx-file-input">
|
<input type="file" accept=".gpx,application/gpx+xml" name="file" class="gpx-file-input">
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="gpx-upload-btn">Upload</button>
|
<button type="submit" class="gpx-upload-btn">Upload</button>
|
||||||
@@ -34,13 +35,13 @@
|
|||||||
.gpx-trip { border: 1px solid #e0ddd6; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
|
.gpx-trip { border: 1px solid #e0ddd6; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
|
||||||
.gpx-trip__name { font-size: 1.1rem; font-weight: 600; margin: 0 0 1rem; }
|
.gpx-trip__name { font-size: 1.1rem; font-weight: 600; margin: 0 0 1rem; }
|
||||||
.gpx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 1rem; }
|
.gpx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||||
.gpx-table th { text-align: left; color: #666; font-weight: 500; padding: 0.25rem 0.5rem; border-bottom: 1px solid #e0ddd6; }
|
.gpx-table th { text-align: left; color: #999; font-weight: 500; padding: 0.25rem 0.5rem; border-bottom: 1px solid #e0ddd6; }
|
||||||
.gpx-table td { padding: 0.5rem; border-bottom: 1px solid #f0ede8; }
|
.gpx-table td { padding: 0.5rem; border-bottom: 1px solid #f0ede8; }
|
||||||
.gpx-empty, .gpx-loading { color: #888; font-size: 0.875rem; margin-bottom: 0.75rem; }
|
.gpx-empty, .gpx-loading { color: #888; font-size: 0.875rem; margin-bottom: 0.75rem; }
|
||||||
.gpx-upload-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
.gpx-upload-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
||||||
.gpx-upload-btn { background: #1F6B5A; color: #fff; border: none; border-radius: 5px; padding: 0.4rem 1rem; font-size: 0.875rem; cursor: pointer; }
|
.gpx-upload-btn { background: #1F6B5A; color: #fff; border: none; border-radius: 5px; padding: 0.4rem 1rem; font-size: 0.875rem; cursor: pointer; }
|
||||||
.gpx-upload-btn:disabled { opacity: 0.5; cursor: default; }
|
.gpx-upload-btn:disabled { opacity: 0.5; cursor: default; }
|
||||||
.gpx-delete { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer; color: #c0392b; }
|
.gpx-delete { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer; color: #e07070; }
|
||||||
.gpx-delete:disabled { opacity: 0.5; }
|
.gpx-delete:disabled { opacity: 0.5; }
|
||||||
.gpx-status { font-size: 0.8rem; color: #555; }
|
.gpx-status { font-size: 0.8rem; color: #555; }
|
||||||
.gpx-status.error { color: #c0392b; }
|
.gpx-status.error { color: #c0392b; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
|
||||||
{% set trip_route = config.site.active_trip %}
|
{% set trip_route = config.site.active_trip %}
|
||||||
{% set trip = grav.pages.find(trip_route) %}
|
{% set trip = grav.pages.find(trip_route) %}
|
||||||
|
|
||||||
@@ -65,73 +66,10 @@
|
|||||||
{% if all_items|length > 0 %}
|
{% if all_items|length > 0 %}
|
||||||
{% for item in all_items %}
|
{% for item in all_items %}
|
||||||
{% set entry = item.page %}
|
{% set entry = item.page %}
|
||||||
|
|
||||||
{% if item.type == 'journal' %}
|
{% if item.type == 'journal' %}
|
||||||
{% set weather_icons = {
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
|
||||||
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
|
||||||
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
|
||||||
} %}
|
|
||||||
<article class="journal-post" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
|
||||||
<header class="journal-post-header">
|
|
||||||
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
{% if entry.header.location_city or entry.header.location_country %}
|
|
||||||
<span class="journal-post-location">
|
|
||||||
· 📍
|
|
||||||
{%- set _loc = [] -%}
|
|
||||||
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
|
||||||
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
|
||||||
{{ _loc|join(', ') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.header.weather_desc %}
|
|
||||||
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% set images = entry.media.images %}
|
|
||||||
{% if images|length > 0 %}
|
|
||||||
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
|
||||||
{% for img in images %}
|
|
||||||
<div class="journal-photo-slide">
|
|
||||||
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if images|length > 1 %}
|
|
||||||
<div class="journal-photo-dots" aria-hidden="true">
|
|
||||||
{% for img in images %}
|
|
||||||
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set hero = null %}
|
{% include 'partials/entry-story.html.twig' %}
|
||||||
{% 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 %}
|
|
||||||
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" href="{{ entry.url }}">
|
|
||||||
{% if hero %}
|
|
||||||
<div class="entry-card-photo entry-card-photo--story">
|
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="entry-card-body">
|
|
||||||
<span class="story-badge">✦ Story</span>
|
|
||||||
<h2 class="entry-title">{{ entry.title }}</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -180,7 +118,8 @@ homeMap.on('load', function () {
|
|||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
el.addEventListener('mouseleave', function () { popup.remove(); });
|
||||||
el.addEventListener('click', function () {
|
el.addEventListener('click', function () {
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
var card = document.getElementById('entry-' + entry.slug);
|
||||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (!card) return;
|
||||||
|
window.location.hash = 'entry-' + entry.slug;
|
||||||
});
|
});
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
||||||
@@ -199,6 +138,68 @@ homeMap.on('load', function () {
|
|||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
|
||||||
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '.pswp-gallery',
|
||||||
|
children: 'a.journal-photo-slide',
|
||||||
|
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
|
||||||
|
});
|
||||||
|
lightbox.on('afterOpen', function () {
|
||||||
|
var pswp = lightbox.pswp;
|
||||||
|
var keyDir = 0;
|
||||||
|
var clearTimer = null;
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.key === 'ArrowRight') keyDir = 1;
|
||||||
|
else if (e.key === 'ArrowLeft') keyDir = -1;
|
||||||
|
else keyDir = 0;
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey, true);
|
||||||
|
pswp.on('change', function () {
|
||||||
|
if (!keyDir) return;
|
||||||
|
var dir = keyDir;
|
||||||
|
keyDir = 0;
|
||||||
|
var el = pswp.currSlide && pswp.currSlide.container;
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
|
||||||
|
el.offsetWidth;
|
||||||
|
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
|
||||||
|
});
|
||||||
|
pswp.on('close', function () {
|
||||||
|
document.removeEventListener('keydown', onKey, true);
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
lightbox.init();
|
||||||
|
|
||||||
|
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
|
||||||
|
var strip = wrap.querySelector('.journal-photo-strip');
|
||||||
|
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
|
||||||
|
var expandBtn = wrap.querySelector('.journal-photo-expand');
|
||||||
|
var article = wrap.closest('article');
|
||||||
|
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
|
||||||
|
var visibleIdx = 0;
|
||||||
|
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
if (!e.isIntersecting) return;
|
||||||
|
visibleIdx = slides.indexOf(e.target);
|
||||||
|
dots.forEach(function (d) { d.classList.remove('is-active'); });
|
||||||
|
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
|
||||||
|
});
|
||||||
|
}, { root: strip, threshold: 0.5 });
|
||||||
|
slides.forEach(function (s) { io.observe(s); });
|
||||||
|
|
||||||
|
if (expandBtn && slides.length) {
|
||||||
|
expandBtn.addEventListener('click', function () {
|
||||||
|
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
controls.className = 'strip-controls';
|
controls.className = 'strip-controls';
|
||||||
controls.appendChild(prev);
|
controls.appendChild(prev);
|
||||||
controls.appendChild(next);
|
controls.appendChild(next);
|
||||||
dots.insertAdjacentElement('afterend', controls);
|
var wrap = strip.closest('.journal-photo-wrap');
|
||||||
|
(wrap || dots).insertAdjacentElement('afterend', controls);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% set weather_icons = {
|
||||||
|
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
||||||
|
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
||||||
|
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
||||||
|
} %}
|
||||||
|
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||||
|
<header class="journal-post-header">
|
||||||
|
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
{% if entry.header.location_city or entry.header.location_country %}
|
||||||
|
<span class="journal-post-location">
|
||||||
|
· 📍
|
||||||
|
{%- set _loc = [] -%}
|
||||||
|
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||||
|
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||||
|
{{ _loc|join(', ') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.header.weather_desc %}
|
||||||
|
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% set images = entry.media.images %}
|
||||||
|
{% if images|length > 0 %}
|
||||||
|
{% set firstImg = images|first %}
|
||||||
|
{% set wrapRatio = firstImg.height > firstImg.width ? '4 / 5' : '4 / 3' %}
|
||||||
|
<div class="journal-photo-wrap" style="aspect-ratio: {{ wrapRatio }}">
|
||||||
|
<div class="journal-photo-strip pswp-gallery" id="gallery-{{ entry.slug }}" data-slides="{{ images|length }}">
|
||||||
|
{% for img in images %}
|
||||||
|
<a class="journal-photo-slide"
|
||||||
|
href="{{ img.url }}"
|
||||||
|
data-pswp-width="{{ img.width }}"
|
||||||
|
data-pswp-height="{{ img.height }}"
|
||||||
|
style="--thumb: url('{{ img.cropResize(900, 675).url }}')"
|
||||||
|
target="_blank">
|
||||||
|
<img src="{{ img.cropResize(900, 675).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if images|length > 1 %}
|
||||||
|
<div class="journal-photo-dots" aria-hidden="true">
|
||||||
|
{% for img in images %}
|
||||||
|
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<button class="journal-photo-expand" aria-label="View full size">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
||||||
|
</article>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{% 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 %}
|
||||||
|
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story" href="{{ entry.url }}">
|
||||||
|
{% if hero %}
|
||||||
|
<div class="entry-card-photo entry-card-photo--story">
|
||||||
|
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="entry-card-body">
|
||||||
|
<span class="story-badge">✦ Story</span>
|
||||||
|
<h2 class="entry-title">{{ entry.title }}</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{#
|
||||||
|
Feed mini-map partial — shared by dailies.html.twig and stories.html.twig.
|
||||||
|
|
||||||
|
Required variables (via {% include ... with {...} only %}):
|
||||||
|
map_entries — array: [{lat, lng, title, slug, url, type, force_connect, transport_mode}]
|
||||||
|
map_id — string: HTML id for the map div (e.g. 'feed-map', 'stories-map')
|
||||||
|
map_var — string: JS variable name for the MapLibre Map (e.g. 'feedMap', 'storiesMap')
|
||||||
|
link_href — string|null: URL for "View full map" link; null/empty hides the link
|
||||||
|
card_prefix — string: prefix for scroll-to card IDs ('entry-' or 'story-')
|
||||||
|
trip_page — Grav page: trip page for autoconnect setting (used when show_journey is true)
|
||||||
|
show_journey — bool: whether to draw the route connector line between markers
|
||||||
|
#}
|
||||||
|
{% if map_entries|length > 0 %}
|
||||||
|
<div class="feed-map-wrap">
|
||||||
|
<div class="feed-map" id="{{ map_id }}">
|
||||||
|
<button class="feed-map-fullscreen-btn" id="{{ map_id }}-fullscreen" aria-label="Expand map">
|
||||||
|
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
|
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% if link_href %}
|
||||||
|
<a class="feed-map-link" href="{{ link_href }}">View full map →</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||||
|
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
{% set js_suffix = map_id|replace({'-': '_'})|upper %}
|
||||||
|
var MAP_ENTRIES_{{ js_suffix }} = {{ map_entries|json_encode|raw }};
|
||||||
|
{% if show_journey %}
|
||||||
|
{% set _ac = trip_page ? (trip_page.header.autoconnect ?? 'on') : 'on' %}
|
||||||
|
var AUTOCONNECT_{{ js_suffix }} = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
var {{ map_var }} = new maplibregl.Map({
|
||||||
|
container: '{{ map_id }}',
|
||||||
|
style: MapUtils.MAP_STYLE,
|
||||||
|
center: [20, 20],
|
||||||
|
zoom: 2,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
{{ map_var }}.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
||||||
|
|
||||||
|
{{ map_var }}.on('load', function () {
|
||||||
|
var attrib = {{ map_var }}.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
||||||
|
if (attrib) attrib.removeAttribute('open');
|
||||||
|
|
||||||
|
var bounds = new maplibregl.LngLatBounds();
|
||||||
|
var entries = MAP_ENTRIES_{{ js_suffix }};
|
||||||
|
|
||||||
|
entries.forEach(function (entry, i) {
|
||||||
|
var isLatest = (entry.type !== 'story') && (i === entries.length - 1);
|
||||||
|
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||||
|
bounds.extend(lngLat);
|
||||||
|
|
||||||
|
var el = entry.type === 'story' ? MapUtils.createStoryMarker() : MapUtils.createDotMarker(isLatest);
|
||||||
|
el.dataset.url = entry.url;
|
||||||
|
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
||||||
|
.setLngLat(lngLat)
|
||||||
|
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
||||||
|
el.addEventListener('mouseenter', function () { popup.addTo({{ map_var }}); });
|
||||||
|
el.addEventListener('mouseleave', function () { popup.remove(); });
|
||||||
|
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
var card = document.getElementById('{{ card_prefix }}' + entry.slug);
|
||||||
|
var mapWrap = document.querySelector('.feed-map-wrap');
|
||||||
|
var isFs = mapWrap && mapWrap.classList.contains('is-fullscreen');
|
||||||
|
function scrollAndHighlight() {
|
||||||
|
if (!card) { window.location.href = entry.url; return; }
|
||||||
|
window.location.hash = '{{ card_prefix }}' + entry.slug;
|
||||||
|
setTimeout(function () {
|
||||||
|
card.classList.add('is-highlighted');
|
||||||
|
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
if (isFs) {
|
||||||
|
var fsBtn = document.getElementById('{{ map_id }}-fullscreen');
|
||||||
|
if (fsBtn) fsBtn.click();
|
||||||
|
setTimeout(scrollAndHighlight, 450);
|
||||||
|
} else {
|
||||||
|
scrollAndHighlight();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo({{ map_var }});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
{{ map_var }}.jumpTo({ center: [parseFloat(entries[0].lng), parseFloat(entries[0].lat)], zoom: 10 });
|
||||||
|
} else {
|
||||||
|
{{ map_var }}.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
||||||
|
}
|
||||||
|
|
||||||
|
{% if show_journey %}
|
||||||
|
var segments = MapUtils.buildJourneySegments(entries, { connectMode: AUTOCONNECT_{{ js_suffix }} });
|
||||||
|
MapUtils.addJourneySegments({{ map_var }}, segments, '{{ map_id }}-journey');
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var fsBtn = document.getElementById('{{ map_id }}-fullscreen');
|
||||||
|
var mapWrap = document.querySelector('.feed-map-wrap');
|
||||||
|
if (!fsBtn || !mapWrap) return;
|
||||||
|
fsBtn.addEventListener('click', function() {
|
||||||
|
var isFs = mapWrap.classList.toggle('is-fullscreen');
|
||||||
|
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
||||||
|
document.body.style.overflow = isFs ? 'hidden' : '';
|
||||||
|
setTimeout(function() { typeof {{ map_var }} !== 'undefined' && {{ map_var }}.resize(); }, 50);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
@@ -3,8 +3,40 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% set stories = page.children.published().order('date', 'asc') %}
|
{% set stories = page.children.published().order('date', 'asc') %}
|
||||||
|
|
||||||
|
{# Collect stories that have coordinates for the mini-map #}
|
||||||
|
{% set map_entries = [] %}
|
||||||
|
{% for story in stories %}
|
||||||
|
{% if story.header.lat is not empty and story.header.lng is not empty %}
|
||||||
|
{% set map_entries = map_entries|merge([{
|
||||||
|
'lat': story.header.lat,
|
||||||
|
'lng': story.header.lng,
|
||||||
|
'title': story.title,
|
||||||
|
'slug': story.slug,
|
||||||
|
'url': story.url,
|
||||||
|
'type': 'story',
|
||||||
|
'force_connect': false,
|
||||||
|
'transport_mode': null
|
||||||
|
}]) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% set trip_page = page.parent() %}
|
||||||
|
|
||||||
|
{% include 'partials/feed-map.html.twig' with {
|
||||||
|
'map_entries': map_entries,
|
||||||
|
'map_id': 'stories-map',
|
||||||
|
'map_var': 'storiesMap',
|
||||||
|
'link_href': null,
|
||||||
|
'card_prefix': 'story-',
|
||||||
|
'trip_page': trip_page,
|
||||||
|
'show_journey': false
|
||||||
|
} only %}
|
||||||
|
|
||||||
<div class="stories-listing">
|
<div class="stories-listing">
|
||||||
|
<div class="stories-listing__header">
|
||||||
<h1 class="stories-listing__heading">Stories</h1>
|
<h1 class="stories-listing__heading">Stories</h1>
|
||||||
|
<button class="trip-stats-btn" id="feed-sort-toggle" aria-label="Sort: oldest first">↑ Oldest first</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if stories|length > 0 %}
|
{% if stories|length > 0 %}
|
||||||
<div class="stories-grid">
|
<div class="stories-grid">
|
||||||
@@ -19,7 +51,7 @@
|
|||||||
{% set date_str = story.date|date('d M') ~ '–' ~ story.header.end_date|date('d M Y') %}
|
{% set date_str = story.date|date('d M') ~ '–' ~ story.header.end_date|date('d M Y') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="story-card" href="{{ story.url }}">
|
<a class="story-card" id="story-{{ story.slug }}" href="{{ story.url }}">
|
||||||
{% if hero %}
|
{% if hero %}
|
||||||
<div class="story-card__photo">
|
<div class="story-card__photo">
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ story.title }}" loading="lazy">
|
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ story.title }}" loading="lazy">
|
||||||
@@ -42,4 +74,22 @@
|
|||||||
<p class="stories-empty">No stories yet — check back soon.</p>
|
<p class="stories-empty">No stories yet — check back soon.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var sortBtn = document.getElementById('feed-sort-toggle');
|
||||||
|
if (!sortBtn) return;
|
||||||
|
var grid = document.querySelector('.stories-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
var ascending = true;
|
||||||
|
|
||||||
|
sortBtn.addEventListener('click', function() {
|
||||||
|
ascending = !ascending;
|
||||||
|
var cards = Array.from(grid.querySelectorAll('.story-card'));
|
||||||
|
cards.reverse().forEach(function(el) { grid.appendChild(el); });
|
||||||
|
sortBtn.textContent = ascending ? '↑ Oldest first' : '↓ Newest first';
|
||||||
|
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
|
||||||
|
sortBtn.classList.toggle('is-active', !ascending);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
|
||||||
{% set dailies_page = grav.pages.find(page.route ~ '/dailies') %}
|
{% set dailies_page = grav.pages.find(page.route ~ '/dailies') %}
|
||||||
{% set stories_page = grav.pages.find(page.route ~ '/stories') %}
|
{% set stories_page = grav.pages.find(page.route ~ '/stories') %}
|
||||||
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
|
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
|
||||||
@@ -104,7 +105,14 @@
|
|||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
<div class="home-map-col">
|
||||||
<div class="home-map" id="trip-map"></div>
|
<div class="home-map" id="trip-map">
|
||||||
|
<button class="feed-map-fullscreen-btn" id="trip-map-fullscreen" aria-label="Expand map">
|
||||||
|
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
|
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-feed-col">
|
<div class="home-feed-col">
|
||||||
@@ -126,16 +134,18 @@
|
|||||||
<button class="trip-filter-btn" data-filter="journal" aria-pressed="false">Journal</button>
|
<button class="trip-filter-btn" data-filter="journal" aria-pressed="false">Journal</button>
|
||||||
<button class="trip-filter-btn" data-filter="story" aria-pressed="false">Stories</button>
|
<button class="trip-filter-btn" data-filter="story" aria-pressed="false">Stories</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="trip-filter-group">
|
<button class="trip-stats-btn" id="trip-sort-toggle" aria-label="Sort: oldest first">↑</button>
|
||||||
<button class="trip-stats-btn" id="trip-stats-toggle" aria-expanded="false" aria-controls="trip-stats-block">Stats</button>
|
</div>
|
||||||
|
<div class="trip-panel-toggles">
|
||||||
|
<button class="trip-panel-toggle" id="trip-stats-toggle" aria-expanded="false" aria-controls="trip-stats-block">Stats <span class="trip-panel-caret" aria-hidden="true">▾</span></button>
|
||||||
{% if has_gpx %}
|
{% if has_gpx %}
|
||||||
<button class="trip-stats-btn" id="trip-cycling-toggle" aria-expanded="false" aria-controls="trip-cycling-block">Cycling</button>
|
<button class="trip-panel-toggle" id="trip-cycling-toggle" aria-expanded="false" aria-controls="trip-cycling-block">Cycling <span class="trip-panel-caret" aria-hidden="true">▾</span></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="trip-stats-block" class="trip-stats-block" style="display:none">
|
<div id="trip-stats-block" class="trip-stats-block">
|
||||||
|
<div class="trip-panel-inner">
|
||||||
<div class="trip-stats-grid">
|
<div class="trip-stats-grid">
|
||||||
<div class="stat-block">
|
<div class="stat-block">
|
||||||
<span class="stat-value">{{ days_on_road }}</span>
|
<span class="stat-value">{{ days_on_road }}</span>
|
||||||
@@ -170,10 +180,13 @@
|
|||||||
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
|
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
||||||
|
<button class="trip-panel-close" data-toggle="trip-stats-toggle">↑ Close stats</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if has_gpx %}
|
{% if has_gpx %}
|
||||||
<div id="trip-cycling-block" class="trip-cycling-block" style="display:none">
|
<div id="trip-cycling-block" class="trip-cycling-block">
|
||||||
|
<div class="trip-panel-inner">
|
||||||
<div class="trip-cycling-header">
|
<div class="trip-cycling-header">
|
||||||
<span class="trip-cycling-icon">🚴</span>
|
<span class="trip-cycling-icon">🚴</span>
|
||||||
<span class="trip-cycling-title">Cycling Stats</span>
|
<span class="trip-cycling-title">Cycling Stats</span>
|
||||||
@@ -208,6 +221,8 @@
|
|||||||
<span class="stat-label">km/h avg speed</span>
|
<span class="stat-label">km/h avg speed</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -215,73 +230,10 @@
|
|||||||
{% if all_items|length > 0 %}
|
{% if all_items|length > 0 %}
|
||||||
{% for item in all_items %}
|
{% for item in all_items %}
|
||||||
{% set entry = item.page %}
|
{% set entry = item.page %}
|
||||||
|
|
||||||
{% if item.type == 'journal' %}
|
{% if item.type == 'journal' %}
|
||||||
{% set weather_icons = {
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
|
||||||
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
|
||||||
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
|
||||||
} %}
|
|
||||||
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
|
||||||
<header class="journal-post-header">
|
|
||||||
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
{% if entry.header.location_city or entry.header.location_country %}
|
|
||||||
<span class="journal-post-location">
|
|
||||||
· 📍
|
|
||||||
{%- set _loc = [] -%}
|
|
||||||
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
|
||||||
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
|
||||||
{{ _loc|join(', ') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.header.weather_desc %}
|
|
||||||
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% set images = entry.media.images %}
|
|
||||||
{% if images|length > 0 %}
|
|
||||||
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
|
||||||
{% for img in images %}
|
|
||||||
<div class="journal-photo-slide">
|
|
||||||
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if images|length > 1 %}
|
|
||||||
<div class="journal-photo-dots" aria-hidden="true">
|
|
||||||
{% for img in images %}
|
|
||||||
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set hero = null %}
|
{% include 'partials/entry-story.html.twig' %}
|
||||||
{% 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 %}
|
|
||||||
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story" href="{{ entry.url }}">
|
|
||||||
{% if hero %}
|
|
||||||
<div class="entry-card-photo entry-card-photo--story">
|
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="entry-card-body">
|
|
||||||
<span class="story-badge">✦ Story</span>
|
|
||||||
<h2 class="entry-title">{{ entry.title }}</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -306,8 +258,10 @@ var tripMap = new maplibregl.Map({
|
|||||||
container: 'trip-map',
|
container: 'trip-map',
|
||||||
style: MapUtils.MAP_STYLE,
|
style: MapUtils.MAP_STYLE,
|
||||||
center: [20, 20],
|
center: [20, 20],
|
||||||
zoom: 2
|
zoom: 2,
|
||||||
|
attributionControl: false
|
||||||
});
|
});
|
||||||
|
tripMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
||||||
|
|
||||||
tripMap.on('load', function () {
|
tripMap.on('load', function () {
|
||||||
if (TRIP_ENTRIES.length === 0) {
|
if (TRIP_ENTRIES.length === 0) {
|
||||||
@@ -333,11 +287,22 @@ tripMap.on('load', function () {
|
|||||||
el.addEventListener('click', function () {
|
el.addEventListener('click', function () {
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
var card = document.getElementById('entry-' + entry.slug);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
var mapCol = document.querySelector('.home-map-col');
|
||||||
|
var isFs = mapCol && mapCol.classList.contains('is-fullscreen');
|
||||||
|
function scrollAndHighlight() {
|
||||||
|
window.location.hash = 'entry-' + entry.slug;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
card.classList.add('is-highlighted');
|
card.classList.add('is-highlighted');
|
||||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||||
}, 350);
|
}, 350);
|
||||||
|
}
|
||||||
|
if (isFs) {
|
||||||
|
var fsBtn = document.getElementById('trip-map-fullscreen');
|
||||||
|
if (fsBtn) fsBtn.click();
|
||||||
|
setTimeout(scrollAndHighlight, 450);
|
||||||
|
} else {
|
||||||
|
scrollAndHighlight();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(tripMap);
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(tripMap);
|
||||||
@@ -352,9 +317,25 @@ tripMap.on('load', function () {
|
|||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
/* ── GPX tracks + journey segments ─────────────────────────── */
|
||||||
MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
|
MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
|
||||||
|
|
||||||
|
// Collapse attribution <details> which MapLibre may open on load
|
||||||
|
var attrib = tripMap.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
||||||
|
if (attrib) attrib.removeAttribute('open');
|
||||||
});
|
});
|
||||||
setTimeout(function () { tripMap.resize(); }, 100);
|
setTimeout(function () { tripMap.resize(); }, 100);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var fsBtn = document.getElementById('trip-map-fullscreen');
|
||||||
|
var mapCol = document.querySelector('.home-map-col');
|
||||||
|
if (!fsBtn || !mapCol) return;
|
||||||
|
fsBtn.addEventListener('click', function() {
|
||||||
|
var isFs = mapCol.classList.toggle('is-fullscreen');
|
||||||
|
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
||||||
|
document.body.style.overflow = isFs ? 'hidden' : '';
|
||||||
|
setTimeout(function() { tripMap.resize(); }, 50);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var filterBtns = document.querySelectorAll('.trip-filter-btn');
|
var filterBtns = document.querySelectorAll('.trip-filter-btn');
|
||||||
var cards = document.querySelectorAll('[data-type]');
|
var cards = document.querySelectorAll('[data-type]');
|
||||||
@@ -389,6 +370,23 @@ setTimeout(function () { tripMap.resize(); }, 100);
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var sortBtn = document.getElementById('trip-sort-toggle');
|
||||||
|
if (!sortBtn) return;
|
||||||
|
var feed = document.querySelector('.feed');
|
||||||
|
var emptyMsg = document.getElementById('feed-filter-empty');
|
||||||
|
var ascending = true;
|
||||||
|
|
||||||
|
sortBtn.addEventListener('click', function() {
|
||||||
|
ascending = !ascending;
|
||||||
|
var entries = Array.from(feed.querySelectorAll('[data-type]'));
|
||||||
|
entries.reverse().forEach(function(el) { feed.insertBefore(el, emptyMsg); });
|
||||||
|
sortBtn.textContent = ascending ? '↑' : '↓';
|
||||||
|
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
|
||||||
|
sortBtn.classList.toggle('is-active', !ascending);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
var STATS_GPS = {{ gps_points|json_encode|raw }};
|
var STATS_GPS = {{ gps_points|json_encode|raw }};
|
||||||
var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
|
var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
|
||||||
|
|
||||||
@@ -522,29 +520,108 @@ function parseGpxFiles(urls, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats toggle
|
function makePanelToggle(toggleId, blockId) {
|
||||||
var statsToggle = document.getElementById('trip-stats-toggle');
|
var toggle = document.getElementById(toggleId);
|
||||||
var statsBlock = document.getElementById('trip-stats-block');
|
var block = document.getElementById(blockId);
|
||||||
if (statsToggle && statsBlock) {
|
if (!toggle || !block) return;
|
||||||
statsToggle.addEventListener('click', function() {
|
toggle.addEventListener('click', function() {
|
||||||
var isOpen = statsBlock.style.display !== 'none';
|
var isOpen = block.classList.contains('is-open');
|
||||||
statsBlock.style.display = isOpen ? 'none' : '';
|
block.classList.toggle('is-open', !isOpen);
|
||||||
statsToggle.classList.toggle('is-active', !isOpen);
|
toggle.classList.toggle('is-active', !isOpen);
|
||||||
statsToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
makePanelToggle('trip-stats-toggle', 'trip-stats-block');
|
||||||
|
makePanelToggle('trip-cycling-toggle', 'trip-cycling-block');
|
||||||
|
|
||||||
// Cycling toggle (only present when has_gpx)
|
// Close buttons inside panels (mobile only via CSS)
|
||||||
var cycToggle = document.getElementById('trip-cycling-toggle');
|
document.querySelectorAll('.trip-panel-close').forEach(function(btn) {
|
||||||
var cycBlock = document.getElementById('trip-cycling-block');
|
var toggleBtn = document.getElementById(btn.getAttribute('data-toggle'));
|
||||||
if (cycToggle && cycBlock) {
|
if (toggleBtn) btn.addEventListener('click', function() { toggleBtn.click(); });
|
||||||
cycToggle.addEventListener('click', function() {
|
});
|
||||||
var isOpen = cycBlock.style.display !== 'none';
|
})();
|
||||||
cycBlock.style.display = isOpen ? 'none' : '';
|
|
||||||
cycToggle.classList.toggle('is-active', !isOpen);
|
/* ── Back to top ─────────────────────────────────────────── */
|
||||||
cycToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var btn = document.getElementById('trip-totop');
|
||||||
|
if (!btn) return;
|
||||||
|
var threshold = window.innerHeight * 0.8;
|
||||||
|
var shown = false;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
history.pushState(null, '', window.location.pathname + window.location.search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
window.addEventListener('scroll', function () {
|
||||||
|
var shouldShow = window.scrollY > threshold;
|
||||||
|
if (shouldShow !== shown) {
|
||||||
|
shown = shouldShow;
|
||||||
|
btn.classList.toggle('is-visible', shown);
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="story-totop" id="trip-totop" aria-label="Back to top">↑ Top</button>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
|
||||||
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '.pswp-gallery',
|
||||||
|
children: 'a.journal-photo-slide',
|
||||||
|
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
|
||||||
|
});
|
||||||
|
lightbox.on('afterOpen', function () {
|
||||||
|
var pswp = lightbox.pswp;
|
||||||
|
var keyDir = 0;
|
||||||
|
var clearTimer = null;
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.key === 'ArrowRight') keyDir = 1;
|
||||||
|
else if (e.key === 'ArrowLeft') keyDir = -1;
|
||||||
|
else keyDir = 0;
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey, true);
|
||||||
|
pswp.on('change', function () {
|
||||||
|
if (!keyDir) return;
|
||||||
|
var dir = keyDir;
|
||||||
|
keyDir = 0;
|
||||||
|
var el = pswp.currSlide && pswp.currSlide.container;
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
|
||||||
|
el.offsetWidth;
|
||||||
|
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
|
||||||
|
});
|
||||||
|
pswp.on('close', function () {
|
||||||
|
document.removeEventListener('keydown', onKey, true);
|
||||||
|
clearTimeout(clearTimer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
lightbox.init();
|
||||||
|
|
||||||
|
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
|
||||||
|
var strip = wrap.querySelector('.journal-photo-strip');
|
||||||
|
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
|
||||||
|
var expandBtn = wrap.querySelector('.journal-photo-expand');
|
||||||
|
var article = wrap.closest('article');
|
||||||
|
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
|
||||||
|
var visibleIdx = 0;
|
||||||
|
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
if (!e.isIntersecting) return;
|
||||||
|
visibleIdx = slides.indexOf(e.target);
|
||||||
|
dots.forEach(function (d) { d.classList.remove('is-active'); });
|
||||||
|
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
|
||||||
|
});
|
||||||
|
}, { root: strip, threshold: 0.5 });
|
||||||
|
slides.forEach(function (s) { io.observe(s); });
|
||||||
|
|
||||||
|
if (expandBtn && slides.length) {
|
||||||
|
expandBtn.addEventListener('click', function () {
|
||||||
|
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||