From 55e28fd0affd5a385c617a88c9f1423c6aa6c0d8 Mon Sep 17 00:00:00 2001 From: Hendrik Behme Date: Mon, 17 Aug 2026 02:49:53 +0200 Subject: [PATCH] fixed map behaviur with searched locations and dev --- docker-compose.dev.yaml | 2 + frontend/config/packages/framework.yaml | 8 ++ frontend/public/js/map.js | 176 ++++++++++++++++++------ frontend/public/styles.css | 3 +- frontend/templates/_map_stage.html.twig | 12 +- 5 files changed, 151 insertions(+), 50 deletions(-) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 7d13ad1..e77edf8 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -27,6 +27,8 @@ services: restart: unless-stopped frontend: + env_file: + - .env-frontend build: ./frontend working_dir: /app ports: diff --git a/frontend/config/packages/framework.yaml b/frontend/config/packages/framework.yaml index 0194348..aa6a196 100644 --- a/frontend/config/packages/framework.yaml +++ b/frontend/config/packages/framework.yaml @@ -5,3 +5,11 @@ framework: php_errors: log: true assets: ~ + # Traefik terminates TLS and forwards plain HTTP to this container over + # the docker network, so Symfony has to be told to trust and read its + # X-Forwarded-* headers — otherwise isSecure()/generated absolute URLs + # think every request is plain HTTP. Docker's private bridge-network + # ranges, not a fixed IP, since Traefik's container address isn't stable + # across restarts. + trusted_proxies: '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port'] diff --git a/frontend/public/js/map.js b/frontend/public/js/map.js index fd7d6ba..39daae2 100644 --- a/frontend/public/js/map.js +++ b/frontend/public/js/map.js @@ -12,6 +12,35 @@ // so this only matters if that ever gets forgotten. var DEFAULT_CENTER = [50.9375, 6.9603]; + // Remembers the last place a user searched/geolocated to, shared between + // the home hero map and /map, so switching between them (or coming back + // later) doesn't reset to Köln and make them search again. + var LAST_LOCATION_KEY = 'vindn:lastLocation'; + + function loadLastLocation() { + try { + var raw = window.localStorage && localStorage.getItem(LAST_LOCATION_KEY); + if (!raw) return null; + var parsed = JSON.parse(raw); + if (typeof parsed.lat === 'number' && typeof parsed.lon === 'number' && isFinite(parsed.lat) && isFinite(parsed.lon)) { + return parsed; + } + } catch (e) { /* localStorage disabled/unavailable, or corrupt value — just skip it */ } + return null; + } + + function saveLastLocation(lat, lon) { + try { + if (window.localStorage) localStorage.setItem(LAST_LOCATION_KEY, JSON.stringify({ lat: lat, lon: lon })); + } catch (e) { /* localStorage disabled/unavailable — nothing to persist to */ } + } + + function setLocateStatus(statusEl, text, isError) { + if (!statusEl) return; + statusEl.textContent = text; + statusEl.classList.toggle('is-error', !!isError); + } + // Leaflet.awesome-markers only ships a fixed palette of named colors (no // arbitrary hex/hsl) — curated to the ones that read well against the // grayscale-filtered basemap. Approximate hex values below are only used @@ -311,6 +340,10 @@ var defaultCenter = opts.defaultCenter || DEFAULT_CENTER; var containerId = opts.containerId || 'mapCanvas'; var state = { cat: 'all', userLoc: null }; + // getCurrentPosition() can take a few seconds; if the user searches for + // a place before it resolves, its result must not overwrite whatever + // they deliberately chose in the meantime. + var manualLocationSet = false; var shown = limit ? nearest(defaultCenter, allEvents.slice(), limit) : allEvents; @@ -449,48 +482,86 @@ }); } - function requestLocation() { + function setUserLocation(lat, lon, statusText) { + state.userLoc = [lat, lon]; + saveLastLocation(lat, lon); + + if (limit) rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit)); + + var statusEl = document.getElementById('locateStatus'); + setLocateStatus(statusEl, statusText, false); + + if (map) { + if (userMarker) map.removeLayer(userMarker); + userMarker = L.marker([lat, lon], { icon: dotIcon('#2440FF', true), zIndexOffset: 1000 }).addTo(map); + if (limit) fitToMarkers(map, markers); + else map.setView([lat, lon], 13); + } + + renderList(); + } + + function requestLocation(isAutoCheck) { var statusEl = document.getElementById('locateStatus'); if (!navigator.geolocation) { - if (statusEl) statusEl.textContent = 'Location not supported by your browser'; + setLocateStatus(statusEl, 'Location not supported by your browser', true); return; } - if (statusEl) statusEl.textContent = 'Finding you…'; + setLocateStatus(statusEl, 'Finding you…', false); navigator.geolocation.getCurrentPosition(function (pos) { - var lat = pos.coords.latitude; - var lon = pos.coords.longitude; - state.userLoc = [lat, lon]; - - if (limit) { - rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit)); - if (statusEl) statusEl.textContent = 'Showing the ' + limit + ' nearest events'; - } else if (statusEl) { - statusEl.textContent = 'Showing events near you'; - } - - if (map) { - if (userMarker) map.removeLayer(userMarker); - userMarker = L.marker([lat, lon], { icon: dotIcon('#2440FF', true), zIndexOffset: 1000 }).addTo(map); - if (limit) fitToMarkers(map, markers); - else map.setView([lat, lon], 13); - } - - renderList(); + // A manual search/click may have already happened while this + // (silent, permission-check-triggered) lookup was still resolving. + if (isAutoCheck && manualLocationSet) return; + setUserLocation(pos.coords.latitude, pos.coords.longitude, limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you'); }, function () { - if (statusEl) statusEl.textContent = "Couldn't get your location"; + setLocateStatus(statusEl, "Couldn't get your location", true); }, { timeout: 8000 }); } var locateBtn = document.getElementById('locateBtn'); - if (locateBtn) locateBtn.addEventListener('click', requestLocation); + if (locateBtn) locateBtn.addEventListener('click', function () { manualLocationSet = true; requestLocation(false); }); - // If the user already granted location access (on this or an earlier - // visit), center on it automatically — this only *reads* the current - // permission state, it never triggers the browser's permission prompt - // itself, so it's safe to check unconditionally on load. - if (navigator.permissions && navigator.permissions.query) { + // Nominatim: explicit submit only, never as-you-type — required by its + // usage policy (max ~1 req/s, no autocomplete-style querying). + var locationForm = document.getElementById('locationSearchForm'); + if (locationForm) { + locationForm.addEventListener('submit', function (e) { + e.preventDefault(); + var input = document.getElementById('locationSearch'); + var query = (input.value || '').trim(); + if (!query) return; + manualLocationSet = true; + var statusEl = document.getElementById('locateStatus'); + setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); + fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(query)) + .then(function (res) { return res.json(); }) + .then(function (results) { + if (!results || !results.length) { + setLocateStatus(statusEl, 'Could not find "' + query + '"', true); + return; + } + setUserLocation(parseFloat(results[0].lat), parseFloat(results[0].lon), limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you'); + }) + .catch(function () { + setLocateStatus(statusEl, 'Location search failed', true); + }); + }); + } + + // A remembered search/geolocation takes priority over asking the + // browser again — restoring it counts as the user's own manual choice, + // so a slower-to-resolve auto-geolocation check below can't clobber it. + var lastLocation = loadLastLocation(); + if (lastLocation) { + manualLocationSet = true; + setUserLocation(lastLocation.lat, lastLocation.lon, limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you'); + } else if (navigator.permissions && navigator.permissions.query) { + // If the user already granted location access (on this or an earlier + // visit), center on it automatically — this only *reads* the current + // permission state, it never triggers the browser's permission prompt + // itself, so it's safe to check unconditionally on load. navigator.permissions.query({ name: 'geolocation' }).then(function (status) { - if (status.state === 'granted') requestLocation(); + if (status.state === 'granted') requestLocation(true); }).catch(function () {}); } @@ -522,6 +593,10 @@ var containerId = opts.containerId || 'mapCanvas'; var state = { source: originalCenter.slice(), rangeKm: opts.rangeKm || 25, cat: 'all' }; + // getCurrentPosition() can take a few seconds; if the user searches or + // clicks the map before it resolves, its result must not overwrite + // whatever they deliberately chose in the meantime. + var manualLocationSet = false; var result = renderPinMap(containerId, [], { zoomControl: true, @@ -601,7 +676,7 @@ } var statusEl = document.getElementById('locateStatus'); - if (statusEl) statusEl.textContent = 'Showing events within ' + state.rangeKm + ' km'; + setLocateStatus(statusEl, 'Showing events within ' + state.rangeKm + ' km', false); var countEl = document.getElementById('resultCount'); if (countEl) countEl.textContent = visible.length + (visible.length === 1 ? ' event' : ' events'); @@ -650,6 +725,7 @@ // conservative: reload whenever full coverage isn't guaranteed, // rather than risk silently showing an incomplete result. function setSource(lat, lon) { + saveLastLocation(lat, lon); var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon); if (distFromOriginal + state.rangeKm > maxRangeKm) { var url = new URL(window.location.href); @@ -667,10 +743,11 @@ } if (map) { - map.on('click', function (e) { setSource(e.latlng.lat, e.latlng.lng); }); + map.on('click', function (e) { manualLocationSet = true; setSource(e.latlng.lat, e.latlng.lng); }); } if (sourceMarker) { sourceMarker.on('dragend', function () { + manualLocationSet = true; var ll = sourceMarker.getLatLng(); setSource(ll.lat, ll.lng); }); @@ -705,43 +782,58 @@ var input = document.getElementById('locationSearch'); var query = (input.value || '').trim(); if (!query) return; + manualLocationSet = true; var statusEl = document.getElementById('locateStatus'); - if (statusEl) statusEl.textContent = 'Looking up "' + query + '"…'; + setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(query)) .then(function (res) { return res.json(); }) .then(function (results) { if (!results || !results.length) { - if (statusEl) statusEl.textContent = 'Could not find "' + query + '"'; + setLocateStatus(statusEl, 'Could not find "' + query + '"', true); return; } setSource(parseFloat(results[0].lat), parseFloat(results[0].lon)); }) .catch(function () { - if (statusEl) statusEl.textContent = 'Location search failed'; + setLocateStatus(statusEl, 'Location search failed', true); }); }); } - function requestLocation() { + function requestLocation(isAutoCheck) { var statusEl = document.getElementById('locateStatus'); if (!navigator.geolocation) { - if (statusEl) statusEl.textContent = 'Location not supported by your browser'; + setLocateStatus(statusEl, 'Location not supported by your browser', true); return; } - if (statusEl) statusEl.textContent = 'Finding you…'; + setLocateStatus(statusEl, 'Finding you…', false); navigator.geolocation.getCurrentPosition(function (pos) { + // A manual search/click/drag may have already happened while this + // (silent, permission-check-triggered) lookup was still resolving. + if (isAutoCheck && manualLocationSet) return; setSource(pos.coords.latitude, pos.coords.longitude); }, function () { - if (statusEl) statusEl.textContent = "Couldn't get your location"; + setLocateStatus(statusEl, "Couldn't get your location", true); }, { timeout: 8000 }); } var locateBtn = document.getElementById('locateBtn'); - if (locateBtn) locateBtn.addEventListener('click', requestLocation); + if (locateBtn) locateBtn.addEventListener('click', function () { manualLocationSet = true; requestLocation(false); }); - if (navigator.permissions && navigator.permissions.query) { + // An explicit ?lat=/&lon= in the URL (e.g. a shared link) always wins. + // Otherwise, a remembered search/geolocation takes priority over asking + // the browser again — restoring it counts as the user's own manual + // choice, so a slower-to-resolve auto-geolocation check below can't + // clobber it. If it's far enough from this page's own server-resolved + // center, setSource's own reload-threshold logic kicks in as usual. + var urlHasExplicitLocation = /[?&]lat=/.test(window.location.search); + var lastLocation = urlHasExplicitLocation ? null : loadLastLocation(); + if (lastLocation) { + manualLocationSet = true; + setSource(lastLocation.lat, lastLocation.lon); + } else if (navigator.permissions && navigator.permissions.query) { navigator.permissions.query({ name: 'geolocation' }).then(function (status) { - if (status.state === 'granted') requestLocation(); + if (status.state === 'granted') requestLocation(true); }).catch(function () {}); } diff --git a/frontend/public/styles.css b/frontend/public/styles.css index 624fa5d..62da7fd 100644 --- a/frontend/public/styles.css +++ b/frontend/public/styles.css @@ -373,6 +373,7 @@ h1,h2,h3{ font-family: var(--f-display); font-weight:700; letter-spacing:-0.01em font-family:var(--f-mono); font-size:12px; background:#fff; color:var(--ink); } .locate-status{ font-family:var(--f-mono); font-size:11px; color:var(--muted); margin-top:8px; max-width:200px; } +.locate-status.is-error{ color:var(--warn); font-weight:600; } .legend{ position:absolute; left:18px; bottom:18px; z-index:30; padding:10px 14px; @@ -422,7 +423,7 @@ h1,h2,h3{ font-family: var(--f-display); font-weight:700; letter-spacing:-0.01em @media (max-width: 900px){ .stage{ height:auto; } .stage-compact{ height:auto; } - #mapCanvas{ position:static; height:62vh; } + #mapCanvas{ position:static; height:calc(100dvh - 76px); } .panel, .stage-compact .panel{ position:static; width:auto; height:auto; max-height:50vh; margin-top:14px; } .map-overlay-top{ position:absolute; } .legend{ display:none; } diff --git a/frontend/templates/_map_stage.html.twig b/frontend/templates/_map_stage.html.twig index 2561acb..96f4c86 100644 --- a/frontend/templates/_map_stage.html.twig +++ b/frontend/templates/_map_stage.html.twig @@ -30,13 +30,11 @@ {% endif %}
- {% if not compact %} -
- - - -
- {% endif %} +
+ + + +