Skip to content

Commit 985909d

Browse files
authored
feat: Add interactive map to navigate countries (#251)
Interactive map that can be used to navigate the countries on the countries page, in addition to the list of countries. It's the foundation for the map and can be extended by indicating the FIP rating via background color, show additional information on hover or click etc. Resolves #135 In a later step, we could optimize the behavior and add the CSS classes to available countries in the SVG during build time, not with JavaScript during run time. However, this is more complex, we need to parse the SVG in Go templates.
1 parent db8da4b commit 985909d

File tree

13 files changed

+6033
-12
lines changed

13 files changed

+6033
-12
lines changed

assets/js/interactiveMap.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Panzoom from '@panzoom/panzoom';
2+
3+
window.initializeInteractiveMap = function() {
4+
const svg = document.querySelector('.o-interactive-map__container svg');
5+
if (svg) {
6+
// Ensure proper scaling
7+
svg.setAttribute('viewBox', '0 0 1300 1300');
8+
9+
const panzoom = Panzoom(svg, {
10+
maxScale: 5,
11+
minScale: 1,
12+
startScale: 1,
13+
contain: 'outside'
14+
});
15+
16+
// Enable mouse wheel zoom
17+
svg.parentElement.addEventListener('wheel', panzoom.zoomWithWheel);
18+
19+
// Enable double-click zoom
20+
svg.addEventListener('dblclick', (e) => {
21+
e.preventDefault();
22+
panzoom.zoomIn({ step: 0.5 });
23+
});
24+
25+
const zoomInBtn = document.querySelector('.o-interactive-map__zoom-in');
26+
const zoomOutBtn = document.querySelector('.o-interactive-map__zoom-out');
27+
const resetBtn = document.querySelector('.o-interactive-map__reset');
28+
29+
if (zoomInBtn) {
30+
zoomInBtn.addEventListener('click', (e) => {
31+
e.preventDefault();
32+
panzoom.zoomIn({ step: 0.5 });
33+
});
34+
}
35+
36+
if (zoomOutBtn) {
37+
zoomOutBtn.addEventListener('click', (e) => {
38+
e.preventDefault();
39+
panzoom.zoomOut({ step: 0.5 });
40+
});
41+
}
42+
43+
if (resetBtn) {
44+
resetBtn.addEventListener('click', (e) => {
45+
e.preventDefault();
46+
panzoom.reset();
47+
});
48+
}
49+
50+
// Add country click functionality
51+
const countries = svg.querySelectorAll('[id]');
52+
countries.forEach(country => {
53+
if (window.availableCountries && window.availableCountries.includes(country.id)) {
54+
country.style.cursor = 'pointer';
55+
country.classList.add('o-interactive-map__country--available');
56+
57+
country.addEventListener('click', (e) => {
58+
e.stopPropagation();
59+
window.location.href = `/${window.currentLanguage}/country/${country.id}/`;
60+
});
61+
}
62+
});
63+
}
64+
};

assets/js/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ import './resizeObserver.js';
44
import './mediaqueries.js';
55
import './highlightHeadline.js';
66
import './anchorlinks.js';
7+
import './interactiveMap.js';

assets/sass/interactiveMap.scss

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.o-interactive-map {
2+
&__wrapper {
3+
position: relative;
4+
}
5+
6+
&__container {
7+
border-radius: var(--border-radius-m);
8+
box-shadow: var(--box-shadow);
9+
width: 100%;
10+
aspect-ratio: 1 / 1;
11+
12+
svg {
13+
width: 100%;
14+
height: 100%;
15+
}
16+
}
17+
18+
&__controls {
19+
position: absolute;
20+
bottom: 1rem;
21+
right: 1rem;
22+
display: flex;
23+
flex-direction: column;
24+
gap: 0.5rem;
25+
z-index: 10;
26+
}
27+
28+
.o-interactive-map__country--available {
29+
path {
30+
fill: var(--link-default) !important;
31+
transition: all 0.2s ease;
32+
cursor: pointer;
33+
}
34+
35+
&:hover path {
36+
fill: var(--link-hovered) !important;
37+
}
38+
}
39+
40+
.o-interactive-map--loading {
41+
padding: 1rem;
42+
}
43+
44+
.o-interactive-map__controls button {
45+
background-color: var(--link-default);
46+
color: var(--bg-neutral);
47+
border-radius: var(--border-radius-m);
48+
border: 0;
49+
width: 4rem;
50+
aspect-ratio: 1 / 1;
51+
display: flex;
52+
align-items: center;
53+
justify-content: center;
54+
font-size: 3rem;
55+
transition: all 0.2s ease;
56+
57+
&:hover {
58+
background-color: var(--link-hovered);
59+
}
60+
}
61+
}

assets/sass/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616
@import "booking.scss";
1717
@import "button.scss";
1818
@import "startpage.scss";
19+
@import "interactiveMap.scss";

assets/sass/styles.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ img {
142142
align-items: center;
143143
}
144144

145+
.o-list__countries{
146+
display: grid;
147+
grid-template-columns: 1fr 1fr;
148+
gap: 1rem;
149+
150+
@media (max-width: #{$breakpoint-lg}) {
151+
display: flex;
152+
flex-direction: column;
153+
}
154+
}
155+
145156
@mixin add-columns($columns) {
146157
&--columns-#{$columns} {
147158
columns: $columns;

i18n/de.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ booking:
1414
reservation-costs: Reservierungskosten
1515
visit-additional-information-website: Weitere Informationen
1616
visit-booking-website: Zur Buchungsseite
17+
countries:
18+
overview: Alle Länder
19+
selection: Länderauswahl
1720
country:
1821
many: Länder
1922
one: Land
2023
other: Länder
21-
countryselection: Länderauswahl
2224
editPage: Seite bearbeiten
2325
footer-love:
2426
aria-label: Made with love in Aachen, Frankfurt, Köln & Paris
@@ -32,6 +34,12 @@ home-page-text: FIP Guide Startseite
3234
information-disclaimer-short: >-
3335
Diese Informationen sind inoffiziell und ohne Gewähr. Es besteht keine
3436
rechtliche Verbindung zu FIP oder Bahngesellschaften.
37+
interactiveMap:
38+
loading: Lade interaktive Karte...
39+
resetZoom: Zoom zurücksetzen
40+
title: Interaktive Länderkarte
41+
zoomIn: Hineinzoomen
42+
zoomOut: Herauszoomen
3543
menu-close: Schließen
3644
menu-open: Menü
3745
navigate-to-country: Gehe zu Land

i18n/en.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ booking:
1313
reservation-costs: Reservation Costs
1414
visit-additional-information-website: Additional Information
1515
visit-booking-website: To Booking Website
16+
countries:
17+
overview: All Countries
18+
selection: choose country
1619
country:
1720
many: countries
1821
one: country
1922
other: countries
20-
countryselection: choose country
2123
editPage: Edit page
2224
footer-love:
2325
aria-label: Made with love in Aachen, Frankfurt, Cologne & Paris
@@ -31,6 +33,12 @@ home-page-text: FIP Guide Home Page
3133
information-disclaimer-short: >-
3234
The information provided is unofficial and without guarantee. There is no
3335
legal connection to FIP or railway companies.
36+
interactiveMap:
37+
loading: Loading interactive map...
38+
resetZoom: Reset Zoom
39+
title: Interactive Country Map
40+
zoomIn: Zoom In
41+
zoomOut: Zoom Out
3442
menu-close: Close
3543
menu-open: Menu
3644
navigate-to-country: Navigate to country

i18n/fr.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ home-page-text: Page d'accueil du FIP Guide
3030
information-disclaimer-short: >-
3131
Les informations fournies sont non officielles et sans garantie. Il n'existe
3232
aucun lien juridique avec le FIP ou les compagnies ferroviaires.
33+
interactiveMap:
34+
loading: Chargement de la carte interactive...
35+
resetZoom: Réinitialiser le zoom
36+
title: Carte interactive des pays
37+
zoomIn: Zoom avant
38+
zoomOut: Zoom arrière
3339
menu-close: Fermer
3440
menu-open: Menu
3541
navigate-to-country: Aller au pays

layouts/country/list.html

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,63 @@
22
<article class="o-list o-list__container">
33
<h1 data-pagefind-meta="title">{{ .Title }}</h1>
44
{{ .Content }}
5-
<div class="o-list__list">
6-
{{ range .Pages }}
7-
<a href="{{ .RelPermalink }}" class="o-list__link">
8-
<div class="o-list__picture">
9-
{{ partial "flag" (dict "country" .File.ContentBaseName) }}
5+
<div class="o-list__countries">
6+
<div>
7+
<h2>{{ T "countries.overview" }}</h2>
8+
<div class="o-list__list">
9+
{{ range .Pages }}
10+
<a href="{{ .RelPermalink }}" class="o-list__link">
11+
<div class="o-list__picture">
12+
{{ partial "flag" (dict "country" .File.ContentBaseName) }}
13+
</div>
14+
<div>
15+
{{ .Title }}
16+
</div>
17+
</a>
18+
{{ end }}
1019
</div>
11-
<div>
12-
{{ .Title }}
20+
</div>
21+
<div class="o-interactive-map">
22+
<h2>{{ T "interactiveMap.title" }}</h2>
23+
<div class="o-interactive-map__wrapper">
24+
<div id="interactive-map__container" class="o-interactive-map__container">
25+
<p class="o-interactive-map--loading"><em>{{ T "interactiveMap.loading" }}</em></p>
26+
</div>
27+
<div class="o-interactive-map__controls">
28+
<button class="o-interactive-map__zoom-in" title="{{ T "interactiveMap.zoomIn" }}" aria-label="{{ T "interactiveMap.zoomIn" }}">
29+
{{ partial "icon" "add" }}
30+
</button>
31+
<button class="o-interactive-map__zoom-out" title="{{ T "interactiveMap.zoomOut" }}" aria-label="{{ T "interactiveMap.zoomOut" }}">
32+
{{ partial "icon" "remove" }}
33+
</button>
34+
<button class="o-interactive-map__reset" title="{{ T "interactiveMap.resetZoom" }}" aria-label="{{ T "interactiveMap.resetZoom" }}">
35+
{{ partial "icon" "crop_free" }}
36+
</button>
37+
</div>
1338
</div>
14-
</a>
15-
{{ end }}
39+
</div>
1640
</div>
1741
</article>
42+
43+
44+
<script>
45+
window.availableCountries = [
46+
{{- range .Pages -}}
47+
"{{ .File.ContentBaseName }}"{{ if not (eq . (index (last 1 $.Pages) 0)) }},{{ end }}
48+
{{- end -}}
49+
];
50+
51+
window.currentLanguage = "{{ .Language.Lang }}";
52+
53+
fetch('/map_europe.svg')
54+
.then(response => response.text())
55+
.then(svgContent => {
56+
document.getElementById('interactive-map__container').innerHTML = svgContent;
57+
58+
window.initializeInteractiveMap();
59+
})
60+
.catch(error => {
61+
console.error('Error loading SVG:', error);
62+
});
63+
</script>
1864
{{ end }}

layouts/partials/menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<menu>
2020
<li class="o-header__item">
2121
<button class="o-header__expand-button" aria-haspopup="true" aria-expanded="false">
22-
<span>{{ T "countryselection" }}</span>
22+
<span>{{ T "countries.selection" }}</span>
2323
{{ partial "icon" "keyboard_arrow_down" }}
2424
</button>
2525
<span id="menu-country-list-title">{{ T "country" }}</span>

0 commit comments

Comments
 (0)