Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924cfc18e2 | ||
|
|
e2ba8f413a | ||
|
|
19a8fbcbf0 | ||
|
|
d79a67cbe6 | ||
|
|
29535e39b8 | ||
|
|
75da83d6e1 | ||
|
|
fa1d77f444 | ||
|
|
827429c551 | ||
|
|
fd41512ba8 | ||
|
|
b92111b853 | ||
|
|
d7a772ae02 | ||
|
|
ac38ae2a54 | ||
|
|
9fa1988211 | ||
|
|
f6ee4944f9 | ||
|
|
b017d2ff64 | ||
|
|
3345c076a9 | ||
|
|
ed25907545 | ||
|
|
476c2c17ef | ||
|
|
24ffa42499 | ||
|
|
04bcab2d55 | ||
|
|
1458c24aa3 | ||
|
|
ede5ce1914 | ||
|
|
0b73cf5048 | ||
|
|
10354a0760 | ||
|
|
632a3028fa | ||
|
|
53d873b4c7 | ||
|
|
a640294e9b | ||
|
|
f8a6b9eb96 | ||
|
|
5923ba431a | ||
|
|
75aaa61640 | ||
|
|
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 |
@@ -4,3 +4,7 @@
|
|||||||
!/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
|
||||||
|
/themes/intotheeast/node_modules/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Journal
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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,13 @@
|
|||||||
|
---
|
||||||
|
title: Journal
|
||||||
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
|
content:
|
||||||
|
items: '@self.children'
|
||||||
|
order:
|
||||||
|
by: date
|
||||||
|
dir: desc
|
||||||
|
filter:
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Stories
|
||||||
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
|
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,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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: ''
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,25 +13,39 @@
|
|||||||
--color-accent-on: #FFFFFF; /* text on accent surfaces */
|
--color-accent-on: #FFFFFF; /* text on accent surfaces */
|
||||||
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */
|
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */
|
||||||
--color-ink-inverse: #17171A; /* text on accent-coloured buttons */
|
--color-ink-inverse: #17171A; /* text on accent-coloured buttons */
|
||||||
|
--color-error: #c0392b; /* validation errors, form error status */
|
||||||
|
|
||||||
|
/* ── Glass overlays (paper colour at opacity, for story components) ── */
|
||||||
|
--color-paper-glass-low: color-mix(in srgb, var(--color-paper) 8%, transparent);
|
||||||
|
--color-paper-glass-mid: color-mix(in srgb, var(--color-paper) 25%, transparent);
|
||||||
|
--color-paper-glass-high: color-mix(in srgb, var(--color-paper) 55%, transparent);
|
||||||
|
--color-paper-glass-hover: color-mix(in srgb, var(--color-paper) 80%, transparent);
|
||||||
|
|
||||||
/* ── Fonts ───────────────────────────────────────────────── */
|
/* ── Fonts ───────────────────────────────────────────────── */
|
||||||
--font-display: 'DM Serif Display', Georgia, serif;
|
--font-display: 'DM Serif Display', Georgia, serif;
|
||||||
--font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
--font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
|
||||||
/* ── Type scale ──────────────────────────────────────────── */
|
/* ── Type scale ──────────────────────────────────────────── */
|
||||||
--text-xs: 0.75rem;
|
--text-xs: 0.75rem;
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-base: 1rem;
|
--text-base: 1rem;
|
||||||
--text-md: 1.125rem;
|
--text-md: 1.125rem;
|
||||||
--text-lg: 1.375rem;
|
--text-lg: 1.375rem;
|
||||||
--text-xl: 1.75rem;
|
--text-xl: 1.75rem;
|
||||||
--text-2xl: 2.25rem;
|
--text-2xl: 2.25rem;
|
||||||
--text-3xl: 3rem;
|
--text-3xl: 3rem;
|
||||||
|
--text-story: 1.0625rem; /* long-form reading size: story body + scrolly steps */
|
||||||
|
|
||||||
/* ── Leading ─────────────────────────────────────────────── */
|
/* ── Leading ─────────────────────────────────────────────── */
|
||||||
--leading-tight: 1.2;
|
--leading-tight: 1.2;
|
||||||
--leading-snug: 1.35;
|
--leading-snug: 1.35;
|
||||||
--leading-normal: 1.65;
|
--leading-normal: 1.65;
|
||||||
|
--leading-story: 1.85; /* relaxed reading rhythm for story long-form */
|
||||||
|
|
||||||
|
/* ── Tracking ────────────────────────────────────────────── */
|
||||||
|
--tracking-caps: 0.07em; /* uppercase UI labels: dates, badges, sidebar headings */
|
||||||
|
--tracking-display: -0.015em; /* display headings: text-xl and text-2xl */
|
||||||
|
--tracking-display-sm: -0.01em; /* display headings: text-lg and smaller; site logotype */
|
||||||
|
|
||||||
/* ── Spacing (4px grid) ──────────────────────────────────── */
|
/* ── Spacing (4px grid) ──────────────────────────────────── */
|
||||||
--space-1: 0.25rem;
|
--space-1: 0.25rem;
|
||||||
|
|||||||
@@ -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 out = [];
|
var alpha = 0.5;
|
||||||
|
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
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,9 +330,228 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse one or more GPX files and compute aggregate cycling statistics.
|
||||||
|
* urls: array of GPX file URL strings
|
||||||
|
* callback: called once with { distance, eleGain, eleLoss, highest, lowest, movingTime, avgSpeed }
|
||||||
|
* or { error: 'no files' } if urls is empty.
|
||||||
|
*
|
||||||
|
* distance/eleGain/eleLoss in raw units (km / metres).
|
||||||
|
* movingTime: "H:MM" string. avgSpeed: km/h number.
|
||||||
|
*/
|
||||||
|
function parseGpxFiles(urls, callback) {
|
||||||
|
var pending = urls.length;
|
||||||
|
var fileResults = new Array(urls.length);
|
||||||
|
if (pending === 0) { callback({ error: 'no files' }); return; }
|
||||||
|
|
||||||
|
urls.forEach(function (url, idx) {
|
||||||
|
fetch(url)
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (text) {
|
||||||
|
var xml = new DOMParser().parseFromString(text, 'text/xml');
|
||||||
|
var pts = [];
|
||||||
|
xml.querySelectorAll('trkpt').forEach(function (pt) {
|
||||||
|
var eleEl = pt.querySelector('ele');
|
||||||
|
var timeEl = pt.querySelector('time');
|
||||||
|
pts.push({
|
||||||
|
lat: parseFloat(pt.getAttribute('lat')),
|
||||||
|
lon: parseFloat(pt.getAttribute('lon')),
|
||||||
|
ele: eleEl ? parseFloat(eleEl.textContent) : NaN,
|
||||||
|
time: timeEl ? timeEl.textContent : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fileResults[idx] = pts;
|
||||||
|
if (--pending === 0) computeAndCallback();
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.warn('GPX load failed:', url, err);
|
||||||
|
fileResults[idx] = [];
|
||||||
|
if (--pending === 0) computeAndCallback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function computeAndCallback() {
|
||||||
|
var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
|
||||||
|
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
|
||||||
|
|
||||||
|
fileResults.forEach(function (pts) {
|
||||||
|
if (!pts || pts.length < 2) return;
|
||||||
|
/* Include first point of each file in elevation range */
|
||||||
|
if (!isNaN(pts[0].ele)) {
|
||||||
|
if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele;
|
||||||
|
if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele;
|
||||||
|
}
|
||||||
|
for (var i = 1; i < pts.length; i++) {
|
||||||
|
var p0 = pts[i - 1], p1 = pts[i];
|
||||||
|
totalDistance += haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
|
||||||
|
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
|
||||||
|
var dEle = p1.ele - p0.ele;
|
||||||
|
if (dEle > 0) totalEleGain += dEle;
|
||||||
|
if (dEle < 0) totalEleLoss += (-dEle);
|
||||||
|
if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele;
|
||||||
|
if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele;
|
||||||
|
}
|
||||||
|
if (p0.time && p1.time) {
|
||||||
|
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
|
||||||
|
if (dtHrs > 0 && (haversineKm(p0.lat, p0.lon, p1.lat, p1.lon) / dtHrs) >= 1) {
|
||||||
|
totalMovingTime += dtHrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0;
|
||||||
|
var movHours = Math.floor(totalMovingTime);
|
||||||
|
var movMins = Math.round((totalMovingTime - movHours) * 60);
|
||||||
|
if (movMins === 60) { movHours++; movMins = 0; }
|
||||||
|
|
||||||
|
callback({
|
||||||
|
distance: totalDistance,
|
||||||
|
eleGain: totalEleGain,
|
||||||
|
eleLoss: totalEleLoss,
|
||||||
|
highest: globalHighest,
|
||||||
|
lowest: globalLowest,
|
||||||
|
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
|
||||||
|
avgSpeed: avgSpeed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Config-driven entry-map initialisation — the single orchestration shared by
|
||||||
|
* the trip page and both home views. Mirrors the trip page's canonical handler:
|
||||||
|
* compact collapsed attribution bottom-left, an on('load') marker loop with a
|
||||||
|
* map-tip hover popup, bounds fit (jumpTo for one entry), GPX journey rendering,
|
||||||
|
* and the fullscreen toggle.
|
||||||
|
*
|
||||||
|
* opts:
|
||||||
|
* container string — map div id ('trip-map', 'home-map')
|
||||||
|
* entries array — parsed map_entries [{lat,lng,slug,title,url,type?,force_connect?}, ...]
|
||||||
|
* cardPrefix string? — e.g. 'entry-'; when set and a card #prefix+slug exists,
|
||||||
|
* click scrolls+flashes that card; otherwise click navigates to entry.url
|
||||||
|
* storyMarkers bool — render createStoryMarker() for entry.type === 'story' (default: dot markers)
|
||||||
|
* markLatest bool — enlarge the final non-story entry's dot (default true; pass false for
|
||||||
|
* unordered sets like home highlights so no marker is singled out)
|
||||||
|
* fullscreen object? — { btnId, colSelector }; wires the fullscreen toggle + fullscreen-aware click
|
||||||
|
* gpx object? — { urls, use, autoconnect, sourcePrefix, journeyId }; forwarded to renderGpxJourney
|
||||||
|
* fit object? — { padding=60, maxZoom=11, singleZoom=10 }
|
||||||
|
*
|
||||||
|
* Returns the maplibregl.Map instance.
|
||||||
|
*/
|
||||||
|
function initEntryMap(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var entries = opts.entries || [];
|
||||||
|
var fit = opts.fit || {};
|
||||||
|
var padding = fit.padding != null ? fit.padding : 60;
|
||||||
|
var maxZoom = fit.maxZoom != null ? fit.maxZoom : 11;
|
||||||
|
var singleZoom = fit.singleZoom != null ? fit.singleZoom : 10;
|
||||||
|
var markLatest = opts.markLatest !== false;
|
||||||
|
|
||||||
|
var map = new maplibregl.Map({
|
||||||
|
container: opts.container,
|
||||||
|
style: MAP_STYLE,
|
||||||
|
center: [20, 20],
|
||||||
|
zoom: 2,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
||||||
|
|
||||||
|
map.on('load', function () {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
map.jumpTo({ center: [0, 20], zoom: 2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = new maplibregl.LngLatBounds();
|
||||||
|
|
||||||
|
entries.forEach(function (entry, i) {
|
||||||
|
var isLatest = markLatest && (entry.type !== 'story') && (i === entries.length - 1);
|
||||||
|
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||||
|
bounds.extend(lngLat);
|
||||||
|
|
||||||
|
var el = (opts.storyMarkers && entry.type === 'story')
|
||||||
|
? createStoryMarker()
|
||||||
|
: 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); });
|
||||||
|
el.addEventListener('mouseleave', function () { popup.remove(); });
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
var card = opts.cardPrefix ? document.getElementById(opts.cardPrefix + entry.slug) : null;
|
||||||
|
if (!card) { window.location.href = entry.url; return; }
|
||||||
|
var hashId = opts.cardPrefix + entry.slug;
|
||||||
|
function scrollAndHighlight() {
|
||||||
|
window.location.hash = hashId;
|
||||||
|
setTimeout(function () {
|
||||||
|
card.classList.add('is-highlighted');
|
||||||
|
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
var isFs = col && col.classList.contains('is-fullscreen');
|
||||||
|
if (isFs) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
if (fsBtn) fsBtn.click();
|
||||||
|
setTimeout(scrollAndHighlight, 450);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollAndHighlight();
|
||||||
|
});
|
||||||
|
|
||||||
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
map.jumpTo({ center: [parseFloat(entries[0].lng), parseFloat(entries[0].lat)], zoom: singleZoom });
|
||||||
|
} else {
|
||||||
|
map.fitBounds(bounds, { padding: padding, maxZoom: maxZoom });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.gpx) {
|
||||||
|
renderGpxJourney(
|
||||||
|
map,
|
||||||
|
opts.gpx.use ? (opts.gpx.urls || []) : [],
|
||||||
|
entries,
|
||||||
|
opts.gpx.sourcePrefix,
|
||||||
|
opts.gpx.journeyId,
|
||||||
|
{ connectMode: opts.gpx.autoconnect }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse attribution <details> which MapLibre may open on load */
|
||||||
|
var attrib = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
||||||
|
if (attrib) attrib.removeAttribute('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () { map.resize(); }, 100);
|
||||||
|
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
if (fsBtn && col) {
|
||||||
|
fsBtn.addEventListener('click', function () {
|
||||||
|
var isFs = col.classList.toggle('is-fullscreen');
|
||||||
|
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
||||||
|
document.body.style.overflow = isFs ? 'hidden' : '';
|
||||||
|
setTimeout(function () { map.resize(); }, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
global.MapUtils = {
|
global.MapUtils = {
|
||||||
MAP_STYLE: MAP_STYLE,
|
MAP_STYLE: MAP_STYLE,
|
||||||
|
initEntryMap: initEntryMap,
|
||||||
ACCENT: ACCENT,
|
ACCENT: ACCENT,
|
||||||
|
haversineKm: haversineKm,
|
||||||
|
parseGpxFiles: parseGpxFiles,
|
||||||
addJourneyLine: addJourneyLine,
|
addJourneyLine: addJourneyLine,
|
||||||
addJourneySegments: addJourneySegments,
|
addJourneySegments: addJourneySegments,
|
||||||
buildJourneySegments: buildJourneySegments,
|
buildJourneySegments: buildJourneySegments,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* Mobile nav: hamburger toggle for the slide-down menu panel.
|
||||||
|
Standalone (not part of the esbuild bundle) — a small progressive
|
||||||
|
enhancement for the header. Loaded via base.html.twig. */
|
||||||
|
(function () {
|
||||||
|
var toggle = document.querySelector('.nav-toggle');
|
||||||
|
var nav = document.getElementById('site-nav');
|
||||||
|
if (!toggle || !nav) return;
|
||||||
|
|
||||||
|
function setOpen(open) {
|
||||||
|
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
|
||||||
|
nav.setAttribute('data-open', open ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Tapping a link closes the panel before navigating. */
|
||||||
|
nav.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('a')) setOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Close on outside tap or Escape. */
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!nav.contains(e.target) && !toggle.contains(e.target)) setOpen(false);
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape') setOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Reset to closed if the viewport grows back to desktop. */
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
if (window.innerWidth > 768) setOpen(false);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
/* ── Fonts ───────────────────────────────────────────────── */
|
||||||
|
import '@fontsource-variable/dm-sans';
|
||||||
|
import '@fontsource/dm-serif-display/400.css';
|
||||||
|
import '@fontsource/dm-serif-display/400-italic.css';
|
||||||
|
|
||||||
|
/* ── PhotoSwipe ──────────────────────────────────────────── */
|
||||||
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
|
import PhotoSwipe from 'photoswipe';
|
||||||
|
import 'photoswipe/style.css';
|
||||||
|
|
||||||
|
/* ── Scrollama (used by story.html.twig inline script) ────── */
|
||||||
|
import scrollama from 'scrollama';
|
||||||
|
window.scrollama = scrollama;
|
||||||
|
|
||||||
|
/* ── Photo strip: prev/next buttons + scroll-based dot sync ─ */
|
||||||
|
function initPhotoStrip() {
|
||||||
|
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
|
||||||
|
strip.setAttribute('role', 'region');
|
||||||
|
strip.setAttribute('aria-label', 'Photo strip');
|
||||||
|
strip.setAttribute('tabindex', '0');
|
||||||
|
|
||||||
|
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
|
||||||
|
var dots = strip.nextElementSibling;
|
||||||
|
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
|
||||||
|
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
|
||||||
|
|
||||||
|
strip.addEventListener('scroll', function () {
|
||||||
|
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
|
||||||
|
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
if (slideCount < 2) return;
|
||||||
|
|
||||||
|
var prev = document.createElement('button');
|
||||||
|
prev.className = 'strip-prev';
|
||||||
|
prev.setAttribute('aria-label', 'Previous photo');
|
||||||
|
prev.textContent = '‹';
|
||||||
|
prev.addEventListener('click', function () {
|
||||||
|
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
var next = document.createElement('button');
|
||||||
|
next.className = 'strip-next';
|
||||||
|
next.setAttribute('aria-label', 'Next photo');
|
||||||
|
next.textContent = '›';
|
||||||
|
next.addEventListener('click', function () {
|
||||||
|
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
var controls = document.createElement('div');
|
||||||
|
controls.className = 'strip-controls';
|
||||||
|
controls.appendChild(prev);
|
||||||
|
controls.appendChild(next);
|
||||||
|
var wrap = strip.closest('.journal-photo-wrap');
|
||||||
|
(wrap || dots).insertAdjacentElement('afterend', controls);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PhotoSwipe lightbox + IntersectionObserver dot sync ──── */
|
||||||
|
function initPhotoSwipe() {
|
||||||
|
if (!document.querySelector('.pswp-gallery')) return;
|
||||||
|
|
||||||
|
var lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '.pswp-gallery',
|
||||||
|
children: 'a.journal-photo-slide',
|
||||||
|
pswpModule: PhotoSwipe
|
||||||
|
});
|
||||||
|
|
||||||
|
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; /* force reflow */
|
||||||
|
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: IntersectionObserver dot sync + expand button */
|
||||||
|
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
|
||||||
|
var strip = wrap.querySelector('.journal-photo-strip');
|
||||||
|
if (!strip) return;
|
||||||
|
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 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sort button ─────────────────────────────────────────────
|
||||||
|
btnId: element id of the sort toggle button
|
||||||
|
containerSel: CSS selector for the list container
|
||||||
|
itemSel: CSS selector for sortable items within container
|
||||||
|
withLabel: true = button shows "↑ Oldest first" / "↓ Newest first"
|
||||||
|
false = button shows "↑" / "↓" only
|
||||||
|
──────────────────────────────────────────────────────────── */
|
||||||
|
function initSortButton(btnId, containerSel, itemSel, withLabel) {
|
||||||
|
var btn = document.getElementById(btnId);
|
||||||
|
if (!btn) return;
|
||||||
|
var container = document.querySelector(containerSel);
|
||||||
|
if (!container) return;
|
||||||
|
var sentinel = container.querySelector('#feed-filter-empty');
|
||||||
|
var ascending = true;
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
ascending = !ascending;
|
||||||
|
var items = Array.from(container.querySelectorAll(itemSel));
|
||||||
|
items.reverse().forEach(function (el) {
|
||||||
|
if (sentinel) container.insertBefore(el, sentinel);
|
||||||
|
else container.appendChild(el);
|
||||||
|
});
|
||||||
|
btn.textContent = withLabel
|
||||||
|
? (ascending ? '↑ Oldest first' : '↓ Newest first')
|
||||||
|
: (ascending ? '↑' : '↓');
|
||||||
|
btn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
|
||||||
|
btn.classList.toggle('is-active', !ascending);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Filter bar (trip page: All / Journal / Stories) ─────── */
|
||||||
|
function initFilterBar() {
|
||||||
|
var filterBtns = document.querySelectorAll('.trip-filter-btn');
|
||||||
|
if (!filterBtns.length) return;
|
||||||
|
var cards = document.querySelectorAll('[data-type]');
|
||||||
|
var filterEmpty = document.getElementById('feed-filter-empty');
|
||||||
|
|
||||||
|
filterBtns.forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
filterBtns.forEach(function (b) {
|
||||||
|
b.classList.remove('is-active');
|
||||||
|
b.setAttribute('aria-pressed', 'false');
|
||||||
|
});
|
||||||
|
btn.classList.add('is-active');
|
||||||
|
btn.setAttribute('aria-pressed', 'true');
|
||||||
|
|
||||||
|
var filter = btn.getAttribute('data-filter');
|
||||||
|
var visible = 0;
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
var show = filter === 'all' || card.getAttribute('data-type') === filter;
|
||||||
|
card.style.display = show ? '' : 'none';
|
||||||
|
if (show) visible++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filterEmpty) {
|
||||||
|
if (visible === 0) {
|
||||||
|
filterEmpty.textContent = filter === 'story'
|
||||||
|
? 'No stories yet for this trip.'
|
||||||
|
: 'No entries yet.';
|
||||||
|
filterEmpty.style.display = '';
|
||||||
|
} else {
|
||||||
|
filterEmpty.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Back to top ─────────────────────────────────────────── */
|
||||||
|
function initBackToTop(btnId) {
|
||||||
|
var btn = document.getElementById(btnId);
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Panel toggles (trip stats / cycling panels) ─────────── */
|
||||||
|
function initPanelToggles() {
|
||||||
|
document.querySelectorAll('.trip-panel-toggle').forEach(function (toggle) {
|
||||||
|
var blockId = toggle.getAttribute('aria-controls');
|
||||||
|
var block = blockId ? document.getElementById(blockId) : null;
|
||||||
|
if (!block) return;
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
var isOpen = block.classList.contains('is-open');
|
||||||
|
block.classList.toggle('is-open', !isOpen);
|
||||||
|
toggle.classList.toggle('is-active', !isOpen);
|
||||||
|
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.trip-panel-close').forEach(function (btn) {
|
||||||
|
var toggleBtn = document.getElementById(btn.getAttribute('data-toggle'));
|
||||||
|
if (toggleBtn) btn.addEventListener('click', function () { toggleBtn.click(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trip stats / cycling computation (trip + home-active) ───
|
||||||
|
config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool }
|
||||||
|
No-op if #stat-distance is absent (page rendered no stats panel).
|
||||||
|
No-GPX fallback: if gpsPoints.length < 2, write '—' and return (no '~0'). */
|
||||||
|
function initTripStats(config) {
|
||||||
|
var distEl = document.getElementById('stat-distance');
|
||||||
|
if (!distEl) return;
|
||||||
|
|
||||||
|
var gpxUrls = config.gpxUrls || [];
|
||||||
|
var gpsPoints = config.gpsPoints || [];
|
||||||
|
|
||||||
|
if (config.hasGpx) {
|
||||||
|
MapUtils.parseGpxFiles(gpxUrls, function (result) {
|
||||||
|
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
|
||||||
|
function setText(id, val) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = val;
|
||||||
|
}
|
||||||
|
setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—');
|
||||||
|
setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—');
|
||||||
|
setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—');
|
||||||
|
setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—');
|
||||||
|
setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—');
|
||||||
|
setText('cyc-moving-time', result.movingTime || '—');
|
||||||
|
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (gpsPoints.length < 2) {
|
||||||
|
distEl.textContent = '—';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 1; i < gpsPoints.length; i++) {
|
||||||
|
total += MapUtils.haversineKm(
|
||||||
|
parseFloat(gpsPoints[i-1][0]), parseFloat(gpsPoints[i-1][1]),
|
||||||
|
parseFloat(gpsPoints[i][0]), parseFloat(gpsPoints[i][1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
distEl.textContent = '~' + Math.round(total).toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.initTripStats = initTripStats;
|
||||||
|
|
||||||
|
/* ── Boot ────────────────────────────────────────────────── */
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initPhotoStrip();
|
||||||
|
initPhotoSwipe();
|
||||||
|
initFilterBar();
|
||||||
|
/* Sort buttons — each call is silent if its button/container isn't on this page */
|
||||||
|
initSortButton('trip-sort-toggle', '.feed', '[data-type]', false);
|
||||||
|
initSortButton('feed-sort-toggle', '.feed', '[data-type]', true);
|
||||||
|
initSortButton('feed-sort-toggle', '.stories-grid', '.story-card', true);
|
||||||
|
initBackToTop('story-totop');
|
||||||
|
initBackToTop('trip-totop');
|
||||||
|
initPanelToggles();
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import toGeoJSON from '@mapbox/togeojson';
|
||||||
|
|
||||||
|
/* maplibre-utils.js attaches MapUtils to window as a side effect */
|
||||||
|
import '../maplibre-utils.js';
|
||||||
|
|
||||||
|
window.maplibregl = maplibregl;
|
||||||
|
window.toGeoJSON = toGeoJSON;
|
||||||
@@ -0,0 +1,972 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource-variable/dm-sans": "latest",
|
||||||
|
"@fontsource/dm-serif-display": "latest",
|
||||||
|
"@mapbox/togeojson": "^0.16.2",
|
||||||
|
"maplibre-gl": "^4",
|
||||||
|
"photoswipe": "^5",
|
||||||
|
"scrollama": "^3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.21",
|
||||||
|
"lucide-static": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource-variable/dm-sans": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/dm-serif-display": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/dm-serif-display/-/dm-serif-display-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-GYSDSlGU6vyhv9a5MwaiVNf9HCuSVpK8hEFRyG4NNDHCDeHiX7YHDAcWsaoLKKcfXLgWG9YkBkk9T3SxM4rAjQ==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/geojson-rewind": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"get-stream": "^6.0.1",
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"geojson-rewind": "geojson-rewind"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/point-geometry": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/tiny-sdf": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/togeojson": {
|
||||||
|
"version": "0.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/togeojson/-/togeojson-0.16.2.tgz",
|
||||||
|
"integrity": "sha512-DcApudmw4g/grOrpM5gYPZfts6Kr8litBESN6n/27sDsjR2f+iJhx4BA0J2B+XrLlnHyJkKztYApe6oCUZpzFA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.8.10",
|
||||||
|
"concat-stream": "~2.0.0",
|
||||||
|
"minimist": "1.2.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"togeojson": "togeojson"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/unitbezier": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/vector-tile": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "~0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/whoots-js": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
|
"version": "20.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
|
||||||
|
"integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"quickselect": "^2.0.0",
|
||||||
|
"rw": "^1.3.3",
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gl-style-format": "dist/gl-style-format.mjs",
|
||||||
|
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||||
|
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/geojson-vt": {
|
||||||
|
"version": "3.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||||
|
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mapbox__point-geometry": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/mapbox__vector-tile": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*",
|
||||||
|
"@types/mapbox__point-geometry": "*",
|
||||||
|
"@types/pbf": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/pbf": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/supercluster": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
|
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.21.5",
|
||||||
|
"@esbuild/android-arm": "0.21.5",
|
||||||
|
"@esbuild/android-arm64": "0.21.5",
|
||||||
|
"@esbuild/android-x64": "0.21.5",
|
||||||
|
"@esbuild/darwin-arm64": "0.21.5",
|
||||||
|
"@esbuild/darwin-x64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-arm64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-x64": "0.21.5",
|
||||||
|
"@esbuild/linux-arm": "0.21.5",
|
||||||
|
"@esbuild/linux-arm64": "0.21.5",
|
||||||
|
"@esbuild/linux-ia32": "0.21.5",
|
||||||
|
"@esbuild/linux-loong64": "0.21.5",
|
||||||
|
"@esbuild/linux-mips64el": "0.21.5",
|
||||||
|
"@esbuild/linux-ppc64": "0.21.5",
|
||||||
|
"@esbuild/linux-riscv64": "0.21.5",
|
||||||
|
"@esbuild/linux-s390x": "0.21.5",
|
||||||
|
"@esbuild/linux-x64": "0.21.5",
|
||||||
|
"@esbuild/netbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/openbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/sunos-x64": "0.21.5",
|
||||||
|
"@esbuild/win32-arm64": "0.21.5",
|
||||||
|
"@esbuild/win32-ia32": "0.21.5",
|
||||||
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/geojson-vt": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-jR1MwkLaZGa8Zftct9ZFruyWFrdl9ZyD2OliXNy9Qq5bBPeg5wHVpBQF9p5GjnicSDQqvBVpysxTPKmWdsfWMA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/global-prefix": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ini": "^4.1.3",
|
||||||
|
"kind-of": "^6.0.3",
|
||||||
|
"which": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-stringify-pretty-compact": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/kdbush": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/kind-of": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lucide-static": {
|
||||||
|
"version": "1.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.21.0.tgz",
|
||||||
|
"integrity": "sha512-6248z2/4sEyKkYAPPUYxOPiB2RCfMmLdMHuoOhsTFnoD40ixAoHmTVhOPux8ADa1NTBmzpEKF7WNePm+Ms503Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/maplibre-gl": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/geojson-rewind": "^0.5.2",
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
|
"@mapbox/point-geometry": "^0.1.0",
|
||||||
|
"@mapbox/tiny-sdf": "^2.0.6",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"@mapbox/vector-tile": "^1.3.1",
|
||||||
|
"@mapbox/whoots-js": "^3.1.0",
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
|
||||||
|
"@types/geojson": "^7946.0.14",
|
||||||
|
"@types/geojson-vt": "3.2.5",
|
||||||
|
"@types/mapbox__point-geometry": "^0.1.4",
|
||||||
|
"@types/mapbox__vector-tile": "^1.3.4",
|
||||||
|
"@types/pbf": "^3.0.5",
|
||||||
|
"@types/supercluster": "^7.1.3",
|
||||||
|
"earcut": "^3.0.0",
|
||||||
|
"geojson-vt": "^4.0.2",
|
||||||
|
"gl-matrix": "^3.4.3",
|
||||||
|
"global-prefix": "^4.0.0",
|
||||||
|
"kdbush": "^4.0.2",
|
||||||
|
"murmurhash-js": "^1.0.0",
|
||||||
|
"pbf": "^3.3.0",
|
||||||
|
"potpack": "^2.0.0",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"supercluster": "^8.0.1",
|
||||||
|
"tinyqueue": "^3.0.0",
|
||||||
|
"vt-pbf": "^3.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.14.0",
|
||||||
|
"npm": ">=8.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/murmurhash-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pbf": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ieee754": "^1.1.12",
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/photoswipe": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/protocol-buffers-schema": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-protobuf-schema": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"protocol-buffers-schema": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rw": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/scrollama": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scrollama/-/scrollama-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-PIPwB1kYBnbw/ezvPBJa5dCN5qEwokfpAkI3BmpZWAwcVID4nDf1qH6WV16A2fQaJmsKx0un5S/zhxN+PQeKDQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supercluster": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vt-pbf": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "0.1.0",
|
||||||
|
"@mapbox/vector-tile": "^1.3.1",
|
||||||
|
"pbf": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/which.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; }"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource-variable/dm-sans": "latest",
|
||||||
|
"@fontsource/dm-serif-display": "latest",
|
||||||
|
"@mapbox/togeojson": "^0.16.2",
|
||||||
|
"maplibre-gl": "^4",
|
||||||
|
"photoswipe": "^5",
|
||||||
|
"scrollama": "^3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.21",
|
||||||
|
"lucide-static": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
'sunny': 'sun',
|
||||||
|
'partly cloudy': 'cloud-sun',
|
||||||
|
'cloudy': 'cloud',
|
||||||
|
'foggy': 'cloud-fog',
|
||||||
|
'drizzle': 'cloud-drizzle',
|
||||||
|
'rain': 'cloud-rain',
|
||||||
|
'snow': 'cloud-snow',
|
||||||
|
'thunderstorm': 'cloud-lightning'
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapAttrs = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
|
||||||
|
|
||||||
|
function buildSvg(iconName) {
|
||||||
|
const src = path.resolve(__dirname, '..', 'node_modules', 'lucide-static', 'icons', iconName + '.svg');
|
||||||
|
const raw = fs.readFileSync(src, 'utf8');
|
||||||
|
const inner = raw
|
||||||
|
.replace(/^[\s\S]*?<svg[^>]*>/, '')
|
||||||
|
.replace(/<\/svg>[\s\S]*$/, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
return `<svg ${wrapAttrs}>${inner}</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(iconMap);
|
||||||
|
const lines = entries.map(([condition, iconName], i) => {
|
||||||
|
const svg = buildSvg(iconName).replace(/'/g, "\\'");
|
||||||
|
const pad = ' '.repeat(Math.max(0, 16 - condition.length));
|
||||||
|
const comma = i < entries.length - 1 ? ',' : '';
|
||||||
|
return ` '${condition}':${pad}'${svg}'${comma}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const twig = [
|
||||||
|
'{# Auto-generated by scripts/gen-weather-icons.js — do not edit directly #}',
|
||||||
|
'{% set weather_icons = {',
|
||||||
|
...lines,
|
||||||
|
'} %}'
|
||||||
|
].join('\n') + '\n';
|
||||||
|
|
||||||
|
const dest = path.resolve(__dirname, '..', 'templates', 'partials', 'weather-icons.html.twig');
|
||||||
|
fs.writeFileSync(dest, twig);
|
||||||
|
console.log('Generated ' + path.relative(path.resolve(__dirname, '..'), dest));
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
{% extends 'default.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set journal_entries = page.collection() %}
|
|
||||||
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
|
|
||||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{% set all_items = [] %}
|
|
||||||
{% for e in journal_entries %}
|
|
||||||
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.date}]) %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for s in story_entries %}
|
|
||||||
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
|
|
||||||
{% 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. #}
|
|
||||||
|
|
||||||
{# Collect GPS entries for mini-map #}
|
|
||||||
{% set map_entries = [] %}
|
|
||||||
{% for item in all_items %}
|
|
||||||
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
|
||||||
{% set map_entries = map_entries|merge([{
|
|
||||||
'lat': item.page.header.lat,
|
|
||||||
'lng': item.page.header.lng,
|
|
||||||
'title': item.page.title,
|
|
||||||
'slug': item.page.slug,
|
|
||||||
'url': item.page.url,
|
|
||||||
'force_connect': item.page.header.force_connect ? true : false,
|
|
||||||
'transport_mode': item.page.header.transport_mode ? item.page.header.transport_mode : null
|
|
||||||
}]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
|
|
||||||
{% if map_entries|length > 0 %}
|
|
||||||
<div class="feed-map-wrap">
|
|
||||||
<div class="feed-map" id="feed-map"></div>
|
|
||||||
<a class="feed-map-link" href="{{ page.parent().url }}/map">View full map →</a>
|
|
||||||
</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">
|
|
||||||
{% if all_items|length > 0 %}
|
|
||||||
{% for item in all_items %}
|
|
||||||
{% set entry = item.page %}
|
|
||||||
|
|
||||||
{% if item.type == 'journal' %}
|
|
||||||
{% 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 %}
|
|
||||||
<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 %}
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,129 +1,12 @@
|
|||||||
{% extends 'default.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set weather_icons = {
|
<a class="back-pill entry-back-fixed" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
|
||||||
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
|
|
||||||
'Snow': '❄️', 'Thunderstorm': '⛈️'
|
|
||||||
} %}
|
|
||||||
|
|
||||||
{% set hero = null %}
|
{% set entry = page %}
|
||||||
{% if page.header.hero_image and page.media[page.header.hero_image] is defined %}
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
{% set hero = page.media[page.header.hero_image] %}
|
|
||||||
{% elseif page.media.images|length > 0 %}
|
|
||||||
{% set hero = page.media.images|first %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a class="back-pill entry-back-fixed" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<footer class="entry-footer">
|
||||||
|
<a class="back-pill" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
<article class="entry">
|
</footer>
|
||||||
{% if hero %}
|
|
||||||
<div class="entry-hero">
|
|
||||||
<img src="{{ hero.cropResize(1440, 720).url }}" alt="{{ page.title }}" loading="eager">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<header class="entry-header">
|
|
||||||
<div class="entry-header-meta">
|
|
||||||
<time class="entry-date" datetime="{{ page.date|date('Y-m-d') }}">
|
|
||||||
{{ page.date|date('l, d F Y') }}
|
|
||||||
</time>
|
|
||||||
{% if page.header.location_city or page.header.location_country %}
|
|
||||||
<p class="entry-location">
|
|
||||||
{%- set _loc = [] -%}
|
|
||||||
{%- if page.header.location_city -%}{%- set _loc = _loc|merge([page.header.location_city]) -%}{%- endif -%}
|
|
||||||
{%- if page.header.location_country -%}{%- set _loc = _loc|merge([page.header.location_country]) -%}{%- endif -%}
|
|
||||||
📍 {{ _loc|join(', ') }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if page.header.weather_desc or page.header.weather_temp_c %}
|
|
||||||
<p class="entry-weather">
|
|
||||||
{% if page.header.weather_desc %}
|
|
||||||
{{ weather_icons[page.header.weather_desc] ?? '🌡️' }} {{ page.header.weather_desc }}
|
|
||||||
{% endif %}
|
|
||||||
{% if page.header.weather_temp_c %}
|
|
||||||
· {{ page.header.weather_temp_c|round }}°C
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="entry-title">{{ page.title }}</h1>
|
|
||||||
<div class="entry-title-rule"></div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="entry-body">
|
|
||||||
{{ page.content|raw }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% set images = page.media.images %}
|
|
||||||
{% if images|length > 0 %}
|
|
||||||
<div class="entry-gallery" id="entry-gallery">
|
|
||||||
{% for image in images %}
|
|
||||||
<button class="gallery-thumb" data-full="{{ image.url }}" data-alt="{{ image.filename }}" aria-label="View {{ image.filename }}">
|
|
||||||
<img src="{{ image.cropResize(300, 300).url }}" alt="{{ image.filename }}" loading="lazy">
|
|
||||||
</button>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
|
|
||||||
<button class="lightbox-close" id="lb-close" aria-label="Close">✕</button>
|
|
||||||
<button class="lightbox-prev" id="lb-prev" aria-label="Previous">‹</button>
|
|
||||||
<img class="lightbox-img" id="lb-img" src="" alt="">
|
|
||||||
<button class="lightbox-next" id="lb-next" aria-label="Next">›</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var gallery = document.getElementById('entry-gallery');
|
|
||||||
var lightbox = document.getElementById('lightbox');
|
|
||||||
var lbImg = document.getElementById('lb-img');
|
|
||||||
var thumbs = Array.from(gallery.querySelectorAll('.gallery-thumb'));
|
|
||||||
var current = 0;
|
|
||||||
|
|
||||||
function open(index) {
|
|
||||||
current = index;
|
|
||||||
var btn = thumbs[index];
|
|
||||||
lbImg.src = btn.dataset.full;
|
|
||||||
lbImg.alt = btn.dataset.alt;
|
|
||||||
lightbox.hidden = false;
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
document.getElementById('lb-close').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
lightbox.hidden = true;
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
thumbs[current].focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prev() { open((current - 1 + thumbs.length) % thumbs.length); }
|
|
||||||
function next() { open((current + 1) % thumbs.length); }
|
|
||||||
|
|
||||||
thumbs.forEach(function(btn, i) {
|
|
||||||
btn.addEventListener('click', function() { open(i); });
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('lb-close').addEventListener('click', close);
|
|
||||||
document.getElementById('lb-prev').addEventListener('click', prev);
|
|
||||||
document.getElementById('lb-next').addEventListener('click', next);
|
|
||||||
|
|
||||||
lightbox.addEventListener('click', function(e) {
|
|
||||||
if (e.target === lightbox) close();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (lightbox.hidden) return;
|
|
||||||
if (e.key === 'Escape') close();
|
|
||||||
if (e.key === 'ArrowLeft') prev();
|
|
||||||
if (e.key === 'ArrowRight') next();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<footer class="entry-footer">
|
|
||||||
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block map_assets %}
|
||||||
|
{% do assets.addCss('theme://css-compiled/map.css') %}
|
||||||
|
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% 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) %}
|
||||||
@@ -24,6 +29,13 @@
|
|||||||
{% set journal_count = journal_entries|length %}
|
{% set journal_count = journal_entries|length %}
|
||||||
{% set story_count = story_entries|length %}
|
{% set story_count = story_entries|length %}
|
||||||
|
|
||||||
|
{% set gps_points = [] %}
|
||||||
|
{% for entry in journal_entries %}
|
||||||
|
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
||||||
|
{% set gps_points = gps_points|merge([[entry.header.lat, entry.header.lng]]) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% set map_entries = [] %}
|
{% set map_entries = [] %}
|
||||||
{% for item in all_items %}
|
{% for item in all_items %}
|
||||||
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
||||||
@@ -48,157 +60,38 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
{% include 'partials/entry-map.html.twig' with {
|
||||||
<div class="home-map" id="home-map"></div>
|
map_id: 'home-map',
|
||||||
</div>
|
map_global: 'homeMap',
|
||||||
|
entries: map_entries,
|
||||||
|
card_prefix: 'entry-',
|
||||||
|
story_markers: false,
|
||||||
|
gpx_urls: home_gpx_urls,
|
||||||
|
use_gpx: trip and trip.header.use_gpx is not null ? trip.header.use_gpx : true,
|
||||||
|
autoconnect: trip ? (trip.header.autoconnect ?? 'on') : 'on',
|
||||||
|
gpx_source_prefix: 'home-gpx',
|
||||||
|
journey_id: 'home-journey'
|
||||||
|
} only %}
|
||||||
|
|
||||||
<div class="home-feed-col">
|
{% if all_items|length == 0 %}
|
||||||
<div class="home-trip-header">
|
{% include 'partials/home-predeparture.html.twig' with {
|
||||||
<h1 class="home-trip-name">{{ trip ? trip.title : trip_route }}</h1>
|
trip_page: trip
|
||||||
<span class="home-trip-counts">
|
} only %}
|
||||||
{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }}
|
{% else %}
|
||||||
{% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
|
{% include 'partials/trip-feed-col.html.twig' with {
|
||||||
</span>
|
trip_page: trip,
|
||||||
</div>
|
all_items: all_items,
|
||||||
|
journal_entries: journal_entries,
|
||||||
<div class="feed">
|
journal_count: journal_count,
|
||||||
{% if all_items|length > 0 %}
|
story_count: story_count,
|
||||||
{% for item in all_items %}
|
has_gpx: home_gpx_urls|length > 0,
|
||||||
{% set entry = item.page %}
|
gpx_urls: home_gpx_urls,
|
||||||
|
gps_points: gps_points,
|
||||||
{% if item.type == 'journal' %}
|
show_sort: false
|
||||||
{% set weather_icons = {
|
} only %}
|
||||||
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
|
{% endif %}
|
||||||
'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 %}
|
|
||||||
{% 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 }}" 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 %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if map_entries|length > 0 %}
|
|
||||||
<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>
|
|
||||||
{% if home_gpx_urls|length > 0 %}
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var HOME_GPX_URLS = {{ home_gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ trip and trip.header.use_gpx is not null ? (trip.header.use_gpx ? 'true' : 'false') : 'true' }};
|
|
||||||
var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
|
|
||||||
|
|
||||||
var homeMap = new maplibregl.Map({
|
|
||||||
container: 'home-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
homeMap.on('load', function () {
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
var coords = [];
|
|
||||||
|
|
||||||
HOME_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === HOME_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
coords.push(lngLat);
|
|
||||||
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(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HOME_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: coords[0], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
|
|
||||||
MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
||||||
|
|
||||||
@@ -304,50 +197,17 @@ homeMap.on('load', function () {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if highlights_map_entries|length > 0 %}
|
{% if highlights_map_entries|length > 0 %}
|
||||||
<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>
|
<script>
|
||||||
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
||||||
|
|
||||||
var homeMap = new maplibregl.Map({
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
container: 'home-map',
|
window.homeMap = MapUtils.initEntryMap({
|
||||||
style: MapUtils.MAP_STYLE,
|
container: 'home-map',
|
||||||
center: [20, 20],
|
entries: HIGHLIGHTS_ENTRIES,
|
||||||
zoom: 2
|
markLatest: false,
|
||||||
});
|
fit: { padding: 60, maxZoom: 8, singleZoom: 8 }
|
||||||
|
|
||||||
homeMap.on('load', function () {
|
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 0) return;
|
|
||||||
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
HIGHLIGHTS_ENTRIES.forEach(function (entry) {
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(false);
|
|
||||||
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(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('highlight-' + entry.slug);
|
|
||||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
});
|
||||||
|
}); // DOMContentLoaded
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: [parseFloat(HIGHLIGHTS_ENTRIES[0].lng), parseFloat(HIGHLIGHTS_ENTRIES[0].lat)], zoom: 8 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 8 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{% macro cycling_panel() %}
|
||||||
|
<div id="trip-cycling-block" class="trip-cycling-block">
|
||||||
|
<div class="trip-panel-inner">
|
||||||
|
<div class="trip-cycling-header">
|
||||||
|
<span class="trip-cycling-icon">🚴</span>
|
||||||
|
<span class="trip-cycling-title">Cycling Stats</span>
|
||||||
|
</div>
|
||||||
|
<div class="trip-cycling-grid">
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-distance">—</span>
|
||||||
|
<span class="stat-label">km distance</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-ele-gain">—</span>
|
||||||
|
<span class="stat-label">m ↑ gain</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-ele-loss">—</span>
|
||||||
|
<span class="stat-label">m ↓ loss</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-highest">—</span>
|
||||||
|
<span class="stat-label">m highest</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-lowest">—</span>
|
||||||
|
<span class="stat-label">m lowest</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-moving-time">—</span>
|
||||||
|
<span class="stat-label">moving time</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="cyc-avg-speed">—</span>
|
||||||
|
<span class="stat-label">km/h avg speed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{%- macro format_date_range(start_date, end_date) -%}
|
||||||
|
{%- if end_date is not empty and end_date|date('Y-m-d') != start_date|date('Y-m-d') -%}
|
||||||
|
{%- set sd = start_date|date('d') -%}
|
||||||
|
{%- set sm = start_date|date('M') -%}
|
||||||
|
{%- set sy = start_date|date('Y') -%}
|
||||||
|
{%- set ed = end_date|date('d') -%}
|
||||||
|
{%- set em = end_date|date('M') -%}
|
||||||
|
{%- set ey = end_date|date('Y') -%}
|
||||||
|
{%- if sy == ey and sm == em -%}
|
||||||
|
{{- sd ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
|
||||||
|
{%- elseif sy == ey -%}
|
||||||
|
{{- sd ~ ' ' ~ sm ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
|
||||||
|
{%- else -%}
|
||||||
|
{{- sd ~ ' ' ~ sm ~ ' ' ~ sy ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- else -%}
|
||||||
|
{{- start_date|date('d M Y') -}}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endmacro -%}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{% macro stats_panel(journal_entries, page, journal_count, has_gpx) %}
|
||||||
|
{% set days_on_road = 0 %}
|
||||||
|
{% if page.header.date_end is not empty %}
|
||||||
|
{% set start_ts = page.header.date_start|date('U') %}
|
||||||
|
{% set end_ts = page.header.date_end|date('U') %}
|
||||||
|
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
|
||||||
|
{% else %}
|
||||||
|
{% set first_ts = null %}
|
||||||
|
{% for entry in journal_entries %}
|
||||||
|
{% set ts = entry.date|date('U') %}
|
||||||
|
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if first_ts is not null %}
|
||||||
|
{% set diff_seconds = "now"|date('U') - first_ts %}
|
||||||
|
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
|
||||||
|
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set seen_lower = [] %}
|
||||||
|
{% set country_display = [] %}
|
||||||
|
{% for entry in journal_entries %}
|
||||||
|
{% if entry.header.location_country is not empty %}
|
||||||
|
{% set lower = entry.header.location_country|trim|lower %}
|
||||||
|
{% if lower not in seen_lower %}
|
||||||
|
{% set seen_lower = seen_lower|merge([lower]) %}
|
||||||
|
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% set seen_city_lower = [] %}
|
||||||
|
{% set city_display = [] %}
|
||||||
|
{% for entry in journal_entries %}
|
||||||
|
{% if entry.header.location_city is not empty %}
|
||||||
|
{% set lower = entry.header.location_city|trim|lower %}
|
||||||
|
{% if lower not in seen_city_lower %}
|
||||||
|
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
|
||||||
|
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% set temp_min = null %}
|
||||||
|
{% set temp_max = null %}
|
||||||
|
{% for entry in journal_entries %}
|
||||||
|
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
|
||||||
|
{% set t = entry.header.weather_temp_c %}
|
||||||
|
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
|
||||||
|
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div id="trip-stats-block" class="trip-stats-block">
|
||||||
|
<div class="trip-panel-inner">
|
||||||
|
<div class="trip-stats-grid">
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value">{{ days_on_road }}</span>
|
||||||
|
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value">{{ journal_count }}</span>
|
||||||
|
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value">{{ country_display|length }}</span>
|
||||||
|
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value">{{ city_display|length }}</span>
|
||||||
|
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
<span class="stat-value" id="stat-distance">—</span>
|
||||||
|
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-block">
|
||||||
|
{% if temp_min is not null %}
|
||||||
|
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="stat-value">—</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="stat-label">°C range</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if country_display|length > 0 %}
|
||||||
|
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<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>
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
{% set tracker_page = grav.pages.find(page.parent().route ~ '/dailies') %}
|
|
||||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{% set gpx_urls = [] %}
|
|
||||||
{% for name, media in trip_page.media.all %}
|
|
||||||
{% if name|split('.')|last == 'gpx' %}
|
|
||||||
{% set gpx_urls = gpx_urls|merge([trip_page.url ~ '/' ~ name]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set map_entries = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
|
||||||
{% set hero_url = null %}
|
|
||||||
{% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %}
|
|
||||||
{% set hero_url = entry.media[entry.header.hero_image].cropResize(240, 135).url %}
|
|
||||||
{% elseif entry.media.images|length > 0 %}
|
|
||||||
{% set hero_url = entry.media.images|first.cropResize(240, 135).url %}
|
|
||||||
{% endif %}
|
|
||||||
{% set map_entries = map_entries|merge([{
|
|
||||||
'lat': entry.header.lat|number_format(6, '.', ''),
|
|
||||||
'lng': entry.header.lng|number_format(6, '.', ''),
|
|
||||||
'title': entry.title,
|
|
||||||
'date': entry.date|date('d M Y'),
|
|
||||||
'url': entry.url,
|
|
||||||
'hero': hero_url,
|
|
||||||
'force_connect': entry.header.force_connect ? true : false,
|
|
||||||
'transport_mode': entry.header.transport_mode ? entry.header.transport_mode : null
|
|
||||||
}]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="map-container" id="trip-map"></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="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
|
||||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ trip_page.header.use_gpx ?? true ? 'true' : 'false' }};
|
|
||||||
var AUTOCONNECT = "{{ trip_page.header.autoconnect ?? 'on' }}";
|
|
||||||
|
|
||||||
var map = new maplibregl.Map({
|
|
||||||
container: 'trip-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
|
||||||
|
|
||||||
if (ENTRIES.length === 0) {
|
|
||||||
var empty = document.createElement('div');
|
|
||||||
empty.className = 'map-empty';
|
|
||||||
empty.textContent = 'No locations yet — entries with GPS will appear here.';
|
|
||||||
document.getElementById('trip-map').appendChild(empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on('load', function () {
|
|
||||||
if (ENTRIES.length === 0) return;
|
|
||||||
|
|
||||||
/* ── Markers + bounds ──────────────────────────────────────── */
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === 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(map); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () { window.location.href = entry.url; });
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Fit bounds ─────────────────────────────────────────────── */
|
|
||||||
if (ENTRIES.length === 1) {
|
|
||||||
map.jumpTo({ center: [parseFloat(ENTRIES[0].lng), parseFloat(ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
map.fitBounds(bounds, { padding: 100, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
|
||||||
MapUtils.renderGpxJourney(map, USE_GPX ? GPX_URLS : [], ENTRIES, 'gpx', 'journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -4,20 +4,26 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
|
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
{% do assets.addCss('theme://css/tokens.css') %}
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
{% do assets.addCss('theme://css/style.css') %}
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet">
|
{% do assets.addCss('theme://css-compiled/main.css') %}
|
||||||
<link rel="stylesheet" href="{{ url('theme://css/tokens.css') }}">
|
{% do assets.addJs('theme://js/main.js', {group: 'bottom'}) %}
|
||||||
<link rel="stylesheet" href="{{ url('theme://css/style.css') }}">
|
{% do assets.addJs('theme://js/nav.js', {group: 'bottom'}) %}
|
||||||
|
{% block map_assets %}{% endblock %}
|
||||||
{{ assets.css()|raw }}
|
{{ assets.css()|raw }}
|
||||||
{{ assets.js()|raw }}
|
{{ assets.js()|raw }}
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if page.template == 'map' %}map-page{% endif %}{% if page.template == 'home' or page.template == 'trip' %} home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
|
<body class="{% if page.template == 'home' or page.template == 'trip' %}home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
|
||||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a class="site-title" href="{{ base_url_absolute }}">into the east</a>
|
<a class="site-title" href="{{ base_url_absolute }}">into the east</a>
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<nav class="site-nav" aria-label="Main navigation">
|
<button class="nav-toggle" type="button" aria-label="Open menu" aria-controls="site-nav" aria-expanded="false">
|
||||||
|
<span class="nav-toggle__bar"></span>
|
||||||
|
<span class="nav-toggle__bar"></span>
|
||||||
|
<span class="nav-toggle__bar"></span>
|
||||||
|
</button>
|
||||||
|
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
|
||||||
<a href="{{ base_url_absolute }}"{% if page.template == 'home' %} aria-current="page"{% endif %}>Home</a>
|
<a href="{{ base_url_absolute }}"{% if page.template == 'home' %} aria-current="page"{% endif %}>Home</a>
|
||||||
<a href="{{ base_url_absolute }}/trips"{% if page.template == 'trips' %} aria-current="page"{% endif %}>Past Trips</a>
|
<a href="{{ base_url_absolute }}/trips"{% if page.template == 'trips' %} aria-current="page"{% endif %}>Past Trips</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -27,48 +33,5 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
{{ assets.js('bottom')|raw }}
|
{{ assets.js('bottom')|raw }}
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
|
|
||||||
strip.setAttribute('role', 'region');
|
|
||||||
strip.setAttribute('aria-label', 'Photo strip');
|
|
||||||
strip.setAttribute('tabindex', '0');
|
|
||||||
|
|
||||||
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
|
|
||||||
var dots = strip.nextElementSibling;
|
|
||||||
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
|
|
||||||
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
|
|
||||||
|
|
||||||
strip.addEventListener('scroll', function () {
|
|
||||||
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
|
|
||||||
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
if (slideCount < 2) return;
|
|
||||||
|
|
||||||
var prev = document.createElement('button');
|
|
||||||
prev.className = 'strip-prev';
|
|
||||||
prev.setAttribute('aria-label', 'Previous photo');
|
|
||||||
prev.textContent = '‹';
|
|
||||||
prev.addEventListener('click', function () {
|
|
||||||
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
|
|
||||||
});
|
|
||||||
|
|
||||||
var next = document.createElement('button');
|
|
||||||
next.className = 'strip-next';
|
|
||||||
next.setAttribute('aria-label', 'Next photo');
|
|
||||||
next.textContent = '›';
|
|
||||||
next.addEventListener('click', function () {
|
|
||||||
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
|
|
||||||
});
|
|
||||||
|
|
||||||
var controls = document.createElement('div');
|
|
||||||
controls.className = 'strip-controls';
|
|
||||||
controls.appendChild(prev);
|
|
||||||
controls.appendChild(next);
|
|
||||||
dots.insertAdjacentElement('afterend', controls);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% include 'partials/weather-icons.html.twig' %}
|
||||||
|
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||||
|
<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">
|
||||||
|
· <svg width="11" height="13" viewBox="0 0 12 14" fill="currentColor" aria-hidden="true"><path d="M6 0C3.24 0 1 2.24 1 5c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 6.75A1.75 1.75 0 1 1 6 3.25a1.75 1.75 0 0 1 0 3.5z"/></svg>
|
||||||
|
{%- 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|lower]|default('')|raw }} {{ 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,49 @@
|
|||||||
|
{#
|
||||||
|
Entry-map partial — shared by trip.html.twig and home.html.twig (active-trip branch).
|
||||||
|
|
||||||
|
Renders the `.home-map-col` map column plus the thin MapUtils.initEntryMap()
|
||||||
|
invocation. The map engine itself lives in js/map.js (MapUtils.initEntryMap);
|
||||||
|
this partial only supplies markup + a parameterised call. Callers register the
|
||||||
|
map assets (map.css / map.js) themselves via their own {% block map_assets %}.
|
||||||
|
|
||||||
|
The map column always renders (home shows an empty map in pre-departure); the
|
||||||
|
init script only runs when there are entries to plot.
|
||||||
|
|
||||||
|
Required variables (via {% include ... with {...} only %}):
|
||||||
|
map_id — string: map div id + fullscreen-btn id prefix ('trip-map' | 'home-map')
|
||||||
|
map_global — string: window global the Map is assigned to ('tripMap' | 'homeMap')
|
||||||
|
entries — array: map entries [{lat, lng, slug, title, url, type?, force_connect, ...}]
|
||||||
|
card_prefix — string: scroll-to card id prefix ('entry-')
|
||||||
|
story_markers — bool: render story entries as diamond markers (trip: true, home: false)
|
||||||
|
gpx_urls — array: GPX file URLs
|
||||||
|
use_gpx — bool: whether to draw GPX tracks
|
||||||
|
autoconnect — string: journey connect mode ('on' | 'off' | 'manual' | ...)
|
||||||
|
gpx_source_prefix — string: MapLibre GPX source id prefix ('gpx' | 'home-gpx')
|
||||||
|
journey_id — string: journey layer id ('trip-journey' | 'home-journey')
|
||||||
|
#}
|
||||||
|
<div class="home-map-col">
|
||||||
|
<div class="home-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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if entries|length > 0 %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.{{ map_global }} = MapUtils.initEntryMap({
|
||||||
|
container: '{{ map_id }}',
|
||||||
|
entries: {{ entries|json_encode|raw }},
|
||||||
|
cardPrefix: '{{ card_prefix }}',
|
||||||
|
{% if story_markers %}storyMarkers: true,
|
||||||
|
{% endif %}fullscreen: { btnId: '{{ map_id }}-fullscreen', colSelector: '.home-map-col' },
|
||||||
|
gpx: { urls: {{ gpx_urls|json_encode|raw }}, use: {{ use_gpx ? 'true' : 'false' }}, autoconnect: "{{ autoconnect }}", sourcePrefix: '{{ gpx_source_prefix }}', journeyId: '{{ journey_id }}' },
|
||||||
|
fit: { padding: 60, maxZoom: 11, singleZoom: 10 }
|
||||||
|
});
|
||||||
|
}); // DOMContentLoaded
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
@@ -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,16 @@
|
|||||||
|
<div class="home-feed-col">
|
||||||
|
<div class="home-trip-header">
|
||||||
|
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
|
||||||
|
{% if trip_page.header.date_start %}
|
||||||
|
<p class="trip-dates">Departing {{ trip_page.header.date_start|date('d M Y') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<span class="home-trip-counts">Coming soon</span>
|
||||||
|
</div>
|
||||||
|
<div class="feed">
|
||||||
|
<hr class="home-predeparture-divider">
|
||||||
|
<p class="feed-empty">The journey hasn't begun yet — check back once we're on the road.</p>
|
||||||
|
<div class="home-highlights-cta-wrap">
|
||||||
|
<a class="home-highlights-cta" href="/trips">In the meantime, explore my other trips →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{% import 'macros/stats.html.twig' as stats_m %}
|
||||||
|
{% import 'macros/cycling.html.twig' as cycling_m %}
|
||||||
|
<div class="home-feed-col">
|
||||||
|
<div class="home-trip-header">
|
||||||
|
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
|
||||||
|
{% if trip_page.header.date_start %}
|
||||||
|
<p class="trip-dates">
|
||||||
|
{{ trip_page.header.date_start|date('d M Y') }}
|
||||||
|
{% if trip_page.header.date_end %} — {{ trip_page.header.date_end|date('d M Y') }}{% else %} — Ongoing{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<span class="home-trip-counts">
|
||||||
|
{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }}
|
||||||
|
{% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<div class="trip-filter-bar">
|
||||||
|
<div class="trip-filter-group">
|
||||||
|
<button class="trip-filter-btn is-active" data-filter="all" aria-pressed="true">All content</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>
|
||||||
|
</div>
|
||||||
|
{% if show_sort %}
|
||||||
|
<button class="trip-stats-btn" id="trip-sort-toggle" aria-label="Sort: oldest first">↑</button>
|
||||||
|
{% endif %}
|
||||||
|
</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 %}
|
||||||
|
<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 %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ stats_m.stats_panel(journal_entries, trip_page, journal_count, has_gpx) }}
|
||||||
|
|
||||||
|
{% if has_gpx %}
|
||||||
|
{{ cycling_m.cycling_panel() }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="feed">
|
||||||
|
{% if all_items|length > 0 %}
|
||||||
|
{% for item in all_items %}
|
||||||
|
{% set entry = item.page %}
|
||||||
|
{% if item.type == 'journal' %}
|
||||||
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'partials/entry-story.html.twig' %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
||||||
|
{% endif %}
|
||||||
|
<p id="feed-filter-empty" class="feed-empty" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initTripStats({
|
||||||
|
gpxUrls: {{ gpx_urls|json_encode|raw }},
|
||||||
|
gpsPoints: {{ gps_points|json_encode|raw }},
|
||||||
|
hasGpx: {{ has_gpx ? 'true' : 'false' }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{# Auto-generated by scripts/gen-weather-icons.js — do not edit directly #}
|
||||||
|
{% set weather_icons = {
|
||||||
|
'sunny': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4" /> <path d="M12 2v2" /> <path d="M12 20v2" /> <path d="m4.93 4.93 1.41 1.41" /> <path d="m17.66 17.66 1.41 1.41" /> <path d="M2 12h2" /> <path d="M20 12h2" /> <path d="m6.34 17.66-1.41 1.41" /> <path d="m19.07 4.93-1.41 1.41" /></svg>',
|
||||||
|
'partly cloudy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v2" /> <path d="m4.93 4.93 1.41 1.41" /> <path d="M20 12h2" /> <path d="m19.07 4.93-1.41 1.41" /> <path d="M15.947 12.65a4 4 0 0 0-5.925-4.128" /> <path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z" /></svg>',
|
||||||
|
'cloudy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></svg>',
|
||||||
|
'foggy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M16 17H7" /> <path d="M17 21H9" /></svg>',
|
||||||
|
'drizzle': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M8 19v1" /> <path d="M8 14v1" /> <path d="M16 19v1" /> <path d="M16 14v1" /> <path d="M12 21v1" /> <path d="M12 16v1" /></svg>',
|
||||||
|
'rain': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M16 14v6" /> <path d="M8 14v6" /> <path d="M12 16v6" /></svg>',
|
||||||
|
'snow': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M8 15h.01" /> <path d="M8 19h.01" /> <path d="M12 17h.01" /> <path d="M12 21h.01" /> <path d="M16 15h.01" /> <path d="M16 19h.01" /></svg>',
|
||||||
|
'thunderstorm': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973" /> <path d="m13 12-3 5h4l-3 5" /></svg>'
|
||||||
|
} %}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
{% set tracker_page = grav.pages.find(trip_page.route ~ '/dailies') %}
|
|
||||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{# Basic counts #}
|
|
||||||
{% set entry_count = all_entries|length %}
|
|
||||||
|
|
||||||
{# Days on road — past trip uses declared date_end; active trip uses first entry to now #}
|
|
||||||
{% set days_on_road = 0 %}
|
|
||||||
{% if trip_page.header.date_end is not empty %}
|
|
||||||
{# Past trip: use declared end date #}
|
|
||||||
{% set start_ts = trip_page.header.date_start|date('U') %}
|
|
||||||
{% set end_ts = trip_page.header.date_end|date('U') %}
|
|
||||||
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
|
|
||||||
{% else %}
|
|
||||||
{# Active trip: first entry to now #}
|
|
||||||
{% set first_ts = null %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% set ts = entry.date|date('U') %}
|
|
||||||
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if first_ts is not null %}
|
|
||||||
{% set diff_seconds = "now"|date('U') - first_ts %}
|
|
||||||
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
|
|
||||||
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Countries — unique, case-insensitive dedup, preserve original casing #}
|
|
||||||
{% set seen_lower = [] %}
|
|
||||||
{% set country_display = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.location_country is not empty %}
|
|
||||||
{% set lower = entry.header.location_country|trim|lower %}
|
|
||||||
{% if lower not in seen_lower %}
|
|
||||||
{% set seen_lower = seen_lower|merge([lower]) %}
|
|
||||||
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# Cities — unique, case-insensitive dedup, preserve original casing #}
|
|
||||||
{% set seen_city_lower = [] %}
|
|
||||||
{% set city_display = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.location_city is not empty %}
|
|
||||||
{% set lower = entry.header.location_city|trim|lower %}
|
|
||||||
{% if lower not in seen_city_lower %}
|
|
||||||
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
|
|
||||||
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# Temperature range #}
|
|
||||||
{% set temp_min = null %}
|
|
||||||
{% set temp_max = null %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
|
|
||||||
{% set t = entry.header.weather_temp_c %}
|
|
||||||
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
|
|
||||||
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# GPS points for distance — collect as JSON for JS computation #}
|
|
||||||
{% set gps_points = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
|
||||||
{% set gps_points = gps_points|merge([[entry.header.lat, entry.header.lng]]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# GPX detection — trip has GPX files if any .gpx media exists on the trip page #}
|
|
||||||
{% set gpx_urls = [] %}
|
|
||||||
{% for name, media in trip_page.media.all %}
|
|
||||||
{% if name|split('.')|last == 'gpx' %}
|
|
||||||
{% set gpx_urls = gpx_urls|merge([trip_page.url ~ '/' ~ name]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% set has_gpx = gpx_urls|length > 0 %}
|
|
||||||
|
|
||||||
<div class="stats-page">
|
|
||||||
<h1 class="stats-heading">Trip Statistics</h1>
|
|
||||||
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ days_on_road }}</span>
|
|
||||||
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ entry_count }}</span>
|
|
||||||
<span class="stat-label">{{ entry_count == 1 ? 'entry' : 'entries' }} posted</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ country_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ city_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="stat-distance">—</span>
|
|
||||||
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
{% if temp_min is not null %}
|
|
||||||
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="stat-value">—</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="stat-label">°C range</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if country_display|length > 0 %}
|
|
||||||
<div class="stats-countries">
|
|
||||||
<span class="stats-countries-label">Countries visited</span>
|
|
||||||
{{ country_display|join(' · ') }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var GPS_POINTS = {{ gps_points|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
|
|
||||||
function haversine(lat1, lng1, lat2, lng2) {
|
|
||||||
var R = 6371;
|
|
||||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
|
||||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
|
||||||
var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
|
|
||||||
Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*
|
|
||||||
Math.sin(dLng/2)*Math.sin(dLng/2);
|
|
||||||
return R * 2 * Math.asin(Math.sqrt(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
var distEl = document.getElementById('stat-distance');
|
|
||||||
|
|
||||||
if (GPX_URLS.length > 0) {
|
|
||||||
// Mode A: sum haversine between all GPX trackpoints (deterministic order)
|
|
||||||
var pending = GPX_URLS.length;
|
|
||||||
var fileResults = new Array(GPX_URLS.length);
|
|
||||||
GPX_URLS.forEach(function(url, idx) {
|
|
||||||
fetch(url)
|
|
||||||
.then(function(r) { return r.text(); })
|
|
||||||
.then(function(text) {
|
|
||||||
var xml = new DOMParser().parseFromString(text, 'text/xml');
|
|
||||||
var trkpts = xml.querySelectorAll('trkpt');
|
|
||||||
var pts = [];
|
|
||||||
trkpts.forEach(function(pt) {
|
|
||||||
pts.push({
|
|
||||||
lat: parseFloat(pt.getAttribute('lat')),
|
|
||||||
lon: parseFloat(pt.getAttribute('lon'))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
fileResults[idx] = pts;
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeTotalDistance(); }
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.warn('GPX load failed:', url, err);
|
|
||||||
fileResults[idx] = [];
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeTotalDistance(); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function computeTotalDistance() {
|
|
||||||
var total = 0;
|
|
||||||
fileResults.forEach(function(pts) {
|
|
||||||
for (var i = 1; i < pts.length; i++) {
|
|
||||||
total += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
distEl.textContent = total > 0 ? Math.round(total).toLocaleString() : '—';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mode B: sum haversine between consecutive entry lat/lng points
|
|
||||||
var total = 0;
|
|
||||||
for (var i = 1; i < GPS_POINTS.length; i++) {
|
|
||||||
total += haversine(
|
|
||||||
parseFloat(GPS_POINTS[i-1][0]), parseFloat(GPS_POINTS[i-1][1]),
|
|
||||||
parseFloat(GPS_POINTS[i][0]), parseFloat(GPS_POINTS[i][1])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
distEl.textContent = GPS_POINTS.length < 2 ? '—' : '~' + Math.round(total).toLocaleString();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set stories = page.children.published().order('date', 'asc') %}
|
|
||||||
|
|
||||||
<div class="stories-listing">
|
|
||||||
<h1 class="stories-listing__heading">Stories</h1>
|
|
||||||
|
|
||||||
{% if stories|length > 0 %}
|
|
||||||
<div class="stories-grid">
|
|
||||||
{% for story in stories %}
|
|
||||||
{% set hero = null %}
|
|
||||||
{% if story.header.hero_image and story.media[story.header.hero_image] is defined %}
|
|
||||||
{% set hero = story.media[story.header.hero_image] %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set date_str = story.date|date('d M Y') %}
|
|
||||||
{% if story.header.end_date %}
|
|
||||||
{% set date_str = story.date|date('d M') ~ '–' ~ story.header.end_date|date('d M Y') %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a class="story-card" href="{{ story.url }}">
|
|
||||||
{% if hero %}
|
|
||||||
<div class="story-card__photo">
|
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ story.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="story-card__photo story-card__photo--empty"></div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="story-card__body">
|
|
||||||
<time class="story-card__date" datetime="{{ story.date|date('Y-m-d') }}">{{ date_str }}</time>
|
|
||||||
{% if story.header.location_name %}
|
|
||||||
<span class="story-card__location">📍 {{ story.header.location_name }}{% if story.header.location_country %}, {{ story.header.location_country }}{% endif %}</span>
|
|
||||||
{% endif %}
|
|
||||||
<h2 class="story-card__title">{{ story.title }}</h2>
|
|
||||||
<span class="story-card__cta">Read story →</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="stories-empty">No stories yet — check back soon.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<a class="story-escape" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="story-escape" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
|
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="story-progress" id="story-progress"></div>
|
||||||
|
{% import 'macros/date-range.html.twig' as dr_m %}
|
||||||
{% set hero_url = null %}
|
{% set hero_url = null %}
|
||||||
{% if page.header.hero_image %}
|
{% if page.header.hero_image %}
|
||||||
{% if page.media[page.header.hero_image] is defined %}
|
{% if page.media[page.header.hero_image] is defined %}
|
||||||
@@ -16,22 +18,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set date_str = page.date|date('d M Y') %}
|
{% set date_str = dr_m.format_date_range(page.date, page.header.end_date ?? null) %}
|
||||||
{% if page.header.end_date and page.header.end_date|date('Y-m-d') != page.date|date('Y-m-d') %}
|
|
||||||
{% set sd = page.date|date('d') %}
|
|
||||||
{% set sm = page.date|date('M') %}
|
|
||||||
{% set sy = page.date|date('Y') %}
|
|
||||||
{% set ed = page.header.end_date|date('d') %}
|
|
||||||
{% set em = page.header.end_date|date('M') %}
|
|
||||||
{% set ey = page.header.end_date|date('Y') %}
|
|
||||||
{% if sy == ey and sm == em %}
|
|
||||||
{% set date_str = sd ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
|
||||||
{% elseif sy == ey %}
|
|
||||||
{% set date_str = sd ~ ' ' ~ sm ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
|
||||||
{% else %}
|
|
||||||
{% set date_str = sd ~ ' ' ~ sm ~ ' ' ~ sy ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set location = page.header.location_name ?? '' %}
|
{% set location = page.header.location_name ?? '' %}
|
||||||
|
|
||||||
@@ -60,16 +47,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="story-body">
|
<div class="story-body">
|
||||||
|
{% if location or date_str %}
|
||||||
|
<div class="story-opener">
|
||||||
|
<span class="story-opener__text">
|
||||||
|
{{- date_str -}}
|
||||||
|
{%- if location and date_str %} · {% endif -%}
|
||||||
|
{{- location -}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{{ page.content|raw }}
|
{{ page.content|raw }}
|
||||||
|
|
||||||
<footer class="story-footer">
|
<footer class="story-footer">
|
||||||
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="back-pill" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="story-totop" id="story-totop" aria-label="Back to top">↑ Top</button>
|
<button class="story-totop" id="story-totop" aria-label="Back to top">↑ Top</button>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/scrollama@3/build/scrollama.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
/* ── Hero scroll effect ──────────────────────────────────── */
|
/* ── Hero scroll effect ──────────────────────────────────── */
|
||||||
(function () {
|
(function () {
|
||||||
@@ -202,6 +197,25 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* ── Reading progress bar ────────────────────────────────── */
|
||||||
|
(function () {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
var bar = document.getElementById('story-progress');
|
||||||
|
var body = document.querySelector('.story-body');
|
||||||
|
if (!bar || !body) return;
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var rect = body.getBoundingClientRect();
|
||||||
|
var total = body.offsetHeight - window.innerHeight;
|
||||||
|
var scrolled = -rect.top;
|
||||||
|
var pct = total > 0 ? Math.min(100, Math.max(0, (scrolled / total) * 100)) : 0;
|
||||||
|
bar.style.width = pct.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', update, { passive: true });
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
|
||||||
/* ── ScrollySection (Scrollama) ──────────────────────────── */
|
/* ── ScrollySection (Scrollama) ──────────────────────────── */
|
||||||
(function () {
|
(function () {
|
||||||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% import 'macros/stats.html.twig' as stats_m %}
|
||||||
|
{% import 'macros/cycling.html.twig' as cycling_m %}
|
||||||
|
{% block map_assets %}
|
||||||
|
{% do assets.addCss('theme://css-compiled/map.css') %}
|
||||||
|
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
|
||||||
|
{% endblock %}
|
||||||
{% 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() : [] %}
|
||||||
@@ -18,59 +24,6 @@
|
|||||||
{% set journal_count = journal_entries|length %}
|
{% set journal_count = journal_entries|length %}
|
||||||
{% set story_count = story_entries|length %}
|
{% set story_count = story_entries|length %}
|
||||||
|
|
||||||
{# Stats computation #}
|
|
||||||
{% set days_on_road = 0 %}
|
|
||||||
{% if page.header.date_end is not empty %}
|
|
||||||
{% set start_ts = page.header.date_start|date('U') %}
|
|
||||||
{% set end_ts = page.header.date_end|date('U') %}
|
|
||||||
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
|
|
||||||
{% else %}
|
|
||||||
{% set first_ts = null %}
|
|
||||||
{% for entry in journal_entries %}
|
|
||||||
{% set ts = entry.date|date('U') %}
|
|
||||||
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if first_ts is not null %}
|
|
||||||
{% set diff_seconds = "now"|date('U') - first_ts %}
|
|
||||||
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
|
|
||||||
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set seen_lower = [] %}
|
|
||||||
{% set country_display = [] %}
|
|
||||||
{% for entry in journal_entries %}
|
|
||||||
{% if entry.header.location_country is not empty %}
|
|
||||||
{% set lower = entry.header.location_country|trim|lower %}
|
|
||||||
{% if lower not in seen_lower %}
|
|
||||||
{% set seen_lower = seen_lower|merge([lower]) %}
|
|
||||||
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set seen_city_lower = [] %}
|
|
||||||
{% set city_display = [] %}
|
|
||||||
{% for entry in journal_entries %}
|
|
||||||
{% if entry.header.location_city is not empty %}
|
|
||||||
{% set lower = entry.header.location_city|trim|lower %}
|
|
||||||
{% if lower not in seen_city_lower %}
|
|
||||||
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
|
|
||||||
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set temp_min = null %}
|
|
||||||
{% set temp_max = null %}
|
|
||||||
{% for entry in journal_entries %}
|
|
||||||
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
|
|
||||||
{% set t = entry.header.weather_temp_c %}
|
|
||||||
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
|
|
||||||
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set gps_points = [] %}
|
{% set gps_points = [] %}
|
||||||
{% for entry in journal_entries %}
|
{% for entry in journal_entries %}
|
||||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
||||||
@@ -103,448 +56,32 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
{% include 'partials/entry-map.html.twig' with {
|
||||||
<div class="home-map" id="trip-map"></div>
|
map_id: 'trip-map',
|
||||||
</div>
|
map_global: 'tripMap',
|
||||||
|
entries: map_entries,
|
||||||
|
card_prefix: 'entry-',
|
||||||
|
story_markers: true,
|
||||||
|
gpx_urls: gpx_urls,
|
||||||
|
use_gpx: page.header.use_gpx ?? true,
|
||||||
|
autoconnect: page.header.autoconnect ?? 'on',
|
||||||
|
gpx_source_prefix: 'gpx',
|
||||||
|
journey_id: 'trip-journey'
|
||||||
|
} only %}
|
||||||
|
|
||||||
<div class="home-feed-col">
|
{% include 'partials/trip-feed-col.html.twig' with {
|
||||||
<div class="home-trip-header">
|
trip_page: page,
|
||||||
<h1 class="home-trip-name">{{ page.title }}</h1>
|
all_items: all_items,
|
||||||
{% if page.header.date_start %}
|
journal_entries: journal_entries,
|
||||||
<p class="trip-dates" style="font-size:var(--text-sm);color:var(--color-ink-muted);margin:var(--space-1) 0 var(--space-2);">
|
journal_count: journal_count,
|
||||||
{{ page.header.date_start|date('d M Y') }}
|
story_count: story_count,
|
||||||
{% if page.header.date_end %} — {{ page.header.date_end|date('d M Y') }}{% else %} — Ongoing{% endif %}
|
has_gpx: has_gpx,
|
||||||
</p>
|
gpx_urls: gpx_urls,
|
||||||
{% endif %}
|
gps_points: gps_points,
|
||||||
<span class="home-trip-counts">
|
show_sort: true
|
||||||
{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }}
|
} only %}
|
||||||
{% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
|
|
||||||
</span>
|
|
||||||
<div class="trip-filter-bar">
|
|
||||||
<div class="trip-filter-group">
|
|
||||||
<button class="trip-filter-btn is-active" data-filter="all" aria-pressed="true">All content</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>
|
|
||||||
</div>
|
|
||||||
<div class="trip-filter-group">
|
|
||||||
<button class="trip-stats-btn" id="trip-stats-toggle" aria-expanded="false" aria-controls="trip-stats-block">Stats</button>
|
|
||||||
{% if has_gpx %}
|
|
||||||
<button class="trip-stats-btn" id="trip-cycling-toggle" aria-expanded="false" aria-controls="trip-cycling-block">Cycling</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="trip-stats-block" class="trip-stats-block" style="display:none">
|
|
||||||
<div class="trip-stats-grid">
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ days_on_road }}</span>
|
|
||||||
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ journal_count }}</span>
|
|
||||||
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ country_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ city_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="stat-distance">—</span>
|
|
||||||
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
{% if temp_min is not null %}
|
|
||||||
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="stat-value">—</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="stat-label">°C range</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if country_display|length > 0 %}
|
|
||||||
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if has_gpx %}
|
|
||||||
<div id="trip-cycling-block" class="trip-cycling-block" style="display:none">
|
|
||||||
<div class="trip-cycling-header">
|
|
||||||
<span class="trip-cycling-icon">🚴</span>
|
|
||||||
<span class="trip-cycling-title">Cycling Stats</span>
|
|
||||||
</div>
|
|
||||||
<div class="trip-cycling-grid">
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-distance">—</span>
|
|
||||||
<span class="stat-label">km distance</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-ele-gain">—</span>
|
|
||||||
<span class="stat-label">m ↑ gain</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-ele-loss">—</span>
|
|
||||||
<span class="stat-label">m ↓ loss</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-highest">—</span>
|
|
||||||
<span class="stat-label">m highest</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-lowest">—</span>
|
|
||||||
<span class="stat-label">m lowest</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-moving-time">—</span>
|
|
||||||
<span class="stat-label">moving time</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="cyc-avg-speed">—</span>
|
|
||||||
<span class="stat-label">km/h avg speed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="feed">
|
|
||||||
{% if all_items|length > 0 %}
|
|
||||||
{% for item in all_items %}
|
|
||||||
{% set entry = item.page %}
|
|
||||||
|
|
||||||
{% if item.type == 'journal' %}
|
|
||||||
{% 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 %}
|
|
||||||
<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 %}
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
|
||||||
{% endif %}
|
|
||||||
<p id="feed-filter-empty" class="feed-empty" style="display:none;"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
<button class="story-totop" id="trip-totop" aria-label="Back to top">↑ Top</button>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
|
||||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ page.header.use_gpx ?? true ? 'true' : 'false' }};
|
|
||||||
var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
|
|
||||||
|
|
||||||
var tripMap = new maplibregl.Map({
|
|
||||||
container: 'trip-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
tripMap.on('load', function () {
|
|
||||||
if (TRIP_ENTRIES.length === 0) {
|
|
||||||
tripMap.jumpTo({ center: [0, 20], zoom: 2 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Markers + bounds ──────────────────────────────────────── */
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
TRIP_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (entry.type !== 'story') && (i === TRIP_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(tripMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (!card) return;
|
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
setTimeout(function () {
|
|
||||||
card.classList.add('is-highlighted');
|
|
||||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
|
||||||
}, 350);
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(tripMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Fit bounds ─────────────────────────────────────────────── */
|
|
||||||
if (TRIP_ENTRIES.length === 1) {
|
|
||||||
tripMap.jumpTo({ center: [parseFloat(TRIP_ENTRIES[0].lng), parseFloat(TRIP_ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
tripMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
|
||||||
MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
setTimeout(function () { tripMap.resize(); }, 100);
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var filterBtns = document.querySelectorAll('.trip-filter-btn');
|
|
||||||
var cards = document.querySelectorAll('[data-type]');
|
|
||||||
var filterEmpty = document.getElementById('feed-filter-empty');
|
|
||||||
|
|
||||||
filterBtns.forEach(function(btn) {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
filterBtns.forEach(function(b) { b.classList.remove('is-active'); b.setAttribute('aria-pressed', 'false'); });
|
|
||||||
btn.classList.add('is-active');
|
|
||||||
btn.setAttribute('aria-pressed', 'true');
|
|
||||||
|
|
||||||
var filter = btn.getAttribute('data-filter');
|
|
||||||
var visible = 0;
|
|
||||||
|
|
||||||
cards.forEach(function(card) {
|
|
||||||
var show = filter === 'all' || card.getAttribute('data-type') === filter;
|
|
||||||
card.style.display = show ? '' : 'none';
|
|
||||||
if (show) visible++;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filterEmpty) {
|
|
||||||
if (visible === 0) {
|
|
||||||
filterEmpty.textContent = filter === 'story'
|
|
||||||
? 'No stories yet for this trip.'
|
|
||||||
: 'No entries yet.';
|
|
||||||
filterEmpty.style.display = '';
|
|
||||||
} else {
|
|
||||||
filterEmpty.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
var STATS_GPS = {{ gps_points|json_encode|raw }};
|
|
||||||
var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
|
|
||||||
|
|
||||||
function haversineKm(lat1, lng1, lat2, lng2) {
|
|
||||||
var R = 6371;
|
|
||||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
|
||||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
|
||||||
var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
|
|
||||||
Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*
|
|
||||||
Math.sin(dLng/2)*Math.sin(dLng/2);
|
|
||||||
return R * 2 * Math.asin(Math.sqrt(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGpxFiles(urls, callback) {
|
|
||||||
var pending = urls.length;
|
|
||||||
var fileResults = new Array(urls.length);
|
|
||||||
if (pending === 0) { callback({ error: 'no files' }); return; }
|
|
||||||
urls.forEach(function(url, idx) {
|
|
||||||
fetch(url)
|
|
||||||
.then(function(r) { return r.text(); })
|
|
||||||
.then(function(text) {
|
|
||||||
var xml = new DOMParser().parseFromString(text, 'text/xml');
|
|
||||||
var trkpts = xml.querySelectorAll('trkpt');
|
|
||||||
var pts = [];
|
|
||||||
trkpts.forEach(function(pt) {
|
|
||||||
var eleEl = pt.querySelector('ele');
|
|
||||||
var timeEl = pt.querySelector('time');
|
|
||||||
pts.push({
|
|
||||||
lat: parseFloat(pt.getAttribute('lat')),
|
|
||||||
lon: parseFloat(pt.getAttribute('lon')),
|
|
||||||
ele: eleEl ? parseFloat(eleEl.textContent) : NaN,
|
|
||||||
time: timeEl ? timeEl.textContent : null
|
|
||||||
});
|
|
||||||
});
|
|
||||||
fileResults[idx] = pts;
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeAndCallback(); }
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.warn('GPX load failed:', url, err);
|
|
||||||
fileResults[idx] = [];
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeAndCallback(); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function computeAndCallback() {
|
|
||||||
var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
|
|
||||||
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
|
|
||||||
|
|
||||||
fileResults.forEach(function(pts) {
|
|
||||||
if (!pts || pts.length < 2) return;
|
|
||||||
for (var i = 1; i < pts.length; i++) {
|
|
||||||
var p0 = pts[i-1], p1 = pts[i];
|
|
||||||
var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
|
|
||||||
totalDistance += d;
|
|
||||||
|
|
||||||
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
|
|
||||||
var dEle = p1.ele - p0.ele;
|
|
||||||
if (dEle > 0) totalEleGain += dEle;
|
|
||||||
if (dEle < 0) totalEleLoss += (-dEle);
|
|
||||||
if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele;
|
|
||||||
if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p0.time && p1.time) {
|
|
||||||
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
|
|
||||||
if (dtHrs > 0) {
|
|
||||||
var speed = d / dtHrs;
|
|
||||||
if (speed >= 1) totalMovingTime += dtHrs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// include first point of each file in elevation range
|
|
||||||
if (pts.length > 0 && !isNaN(pts[0].ele)) {
|
|
||||||
if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele;
|
|
||||||
if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0;
|
|
||||||
var movHours = Math.floor(totalMovingTime);
|
|
||||||
var movMins = Math.round((totalMovingTime - movHours) * 60);
|
|
||||||
if (movMins === 60) { movHours++; movMins = 0; }
|
|
||||||
|
|
||||||
callback({
|
|
||||||
distance: totalDistance,
|
|
||||||
eleGain: totalEleGain,
|
|
||||||
eleLoss: totalEleLoss,
|
|
||||||
highest: globalHighest,
|
|
||||||
lowest: globalLowest,
|
|
||||||
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
|
|
||||||
avgSpeed: avgSpeed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var distEl = document.getElementById('stat-distance');
|
|
||||||
|
|
||||||
if (HAS_GPX) {
|
|
||||||
parseGpxFiles(GPX_URLS, function(result) {
|
|
||||||
// Mode A: update distance stat
|
|
||||||
if (distEl) {
|
|
||||||
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
|
|
||||||
}
|
|
||||||
// Populate cycling panel
|
|
||||||
function setText(id, val) {
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (el) el.textContent = val;
|
|
||||||
}
|
|
||||||
setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—');
|
|
||||||
setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—');
|
|
||||||
setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—');
|
|
||||||
setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—');
|
|
||||||
setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—');
|
|
||||||
setText('cyc-moving-time', result.movingTime || '—');
|
|
||||||
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Mode B: haversine between entry points
|
|
||||||
var total = 0;
|
|
||||||
for (var i = 1; i < STATS_GPS.length; i++) {
|
|
||||||
total += haversineKm(
|
|
||||||
parseFloat(STATS_GPS[i-1][0]), parseFloat(STATS_GPS[i-1][1]),
|
|
||||||
parseFloat(STATS_GPS[i][0]), parseFloat(STATS_GPS[i][1])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (distEl) {
|
|
||||||
distEl.textContent = STATS_GPS.length < 2 ? '—' : '~' + Math.round(total).toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats toggle
|
|
||||||
var statsToggle = document.getElementById('trip-stats-toggle');
|
|
||||||
var statsBlock = document.getElementById('trip-stats-block');
|
|
||||||
if (statsToggle && statsBlock) {
|
|
||||||
statsToggle.addEventListener('click', function() {
|
|
||||||
var isOpen = statsBlock.style.display !== 'none';
|
|
||||||
statsBlock.style.display = isOpen ? 'none' : '';
|
|
||||||
statsToggle.classList.toggle('is-active', !isOpen);
|
|
||||||
statsToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cycling toggle (only present when has_gpx)
|
|
||||||
var cycToggle = document.getElementById('trip-cycling-toggle');
|
|
||||||
var cycBlock = document.getElementById('trip-cycling-block');
|
|
||||||
if (cycToggle && cycBlock) {
|
|
||||||
cycToggle.addEventListener('click', function() {
|
|
||||||
var isOpen = cycBlock.style.display !== 'none';
|
|
||||||
cycBlock.style.display = isOpen ? 'none' : '';
|
|
||||||
cycToggle.classList.toggle('is-active', !isOpen);
|
|
||||||
cycToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -13,6 +13,20 @@
|
|||||||
{% set journal_count = dailies_page ? dailies_page.children.published()|length : 0 %}
|
{% set journal_count = dailies_page ? dailies_page.children.published()|length : 0 %}
|
||||||
{% set story_count = stories_page ? stories_page.children.published()|length : 0 %}
|
{% set story_count = stories_page ? stories_page.children.published()|length : 0 %}
|
||||||
<a class="trip-card" href="{{ trip.url }}">
|
<a class="trip-card" href="{{ trip.url }}">
|
||||||
|
{% set cover = null %}
|
||||||
|
{% if trip.header.cover_image and trip.media[trip.header.cover_image] is defined %}
|
||||||
|
{% set cover = trip.media[trip.header.cover_image] %}
|
||||||
|
{% elseif dailies_page %}
|
||||||
|
{% set first_entry = dailies_page.children.published()|first %}
|
||||||
|
{% if first_entry and first_entry.media.images|length > 0 %}
|
||||||
|
{% set cover = first_entry.media.images|first %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if cover %}
|
||||||
|
<div class="trip-card-cover">
|
||||||
|
<img src="{{ cover.cropResize(720, 240).url }}" alt="{{ trip.title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="trip-card-title">{{ trip.title }}</div>
|
<div class="trip-card-title">{{ trip.title }}</div>
|
||||||
<div class="trip-card-meta">
|
<div class="trip-card-meta">
|
||||||
{% if trip.header.date_start %}
|
{% if trip.header.date_start %}
|
||||||
|
|||||||