diff --git a/frontend/public/js/location.js b/frontend/public/js/location.js
new file mode 100644
index 0000000..53442fd
--- /dev/null
+++ b/frontend/public/js/location.js
@@ -0,0 +1,80 @@
+// Single source of truth for "where is the user" across the home hero map,
+// /map, and Browse Events' location filter — storage, place search, and
+// GPS lookup used to be copy-pasted across all three, which is exactly how
+// one of them (a Nominatim result not being saved) silently drifted out of
+// sync with the other two. Each page still owns what happens once a
+// location resolves (recenter a map vs. navigate to a new URL) — that part
+// genuinely differs per page — but the operations themselves live here once.
+(function (window) {
+ var LAST_LOCATION_KEY = 'vindn:lastLocation';
+
+ function load() {
+ 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 save(lat, lon, name) {
+ try {
+ if (window.localStorage) localStorage.setItem(LAST_LOCATION_KEY, JSON.stringify({ lat: lat, lon: lon, name: name || null }));
+ } catch (e) { /* localStorage disabled/unavailable — nothing to persist to */ }
+ }
+
+ function clear() {
+ try {
+ if (window.localStorage) localStorage.removeItem(LAST_LOCATION_KEY);
+ } catch (e) { /* localStorage disabled/unavailable */ }
+ }
+
+ // Nominatim: explicit submit only, never as-you-type — required by its
+ // usage policy (max ~1 req/s, no autocomplete-style querying). Every
+ // "search a place" form on the site should call this instead of its own
+ // fetch, so they all behave identically (and a fix here fixes all three).
+ function searchPlace(query, callbacks) {
+ callbacks = callbacks || {};
+ if (callbacks.onStart) callbacks.onStart(query);
+ 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 (callbacks.onNotFound) callbacks.onNotFound(query);
+ return;
+ }
+ if (callbacks.onFound) callbacks.onFound(parseFloat(results[0].lat), parseFloat(results[0].lon), results[0].display_name);
+ })
+ .catch(function () {
+ if (callbacks.onError) callbacks.onError();
+ });
+ }
+
+ // "Use my location" — shared so every page reports the same fixed label
+ // for a GPS-derived point (no reverse geocoding happens) and the same
+ // error/unsupported messaging.
+ function useMyLocation(callbacks) {
+ callbacks = callbacks || {};
+ if (!navigator.geolocation) {
+ if (callbacks.onUnsupported) callbacks.onUnsupported();
+ return;
+ }
+ if (callbacks.onStart) callbacks.onStart();
+ navigator.geolocation.getCurrentPosition(function (pos) {
+ if (callbacks.onFound) callbacks.onFound(pos.coords.latitude, pos.coords.longitude, 'Your location');
+ }, function () {
+ if (callbacks.onError) callbacks.onError();
+ }, { timeout: 8000 });
+ }
+
+ window.VindnLocation = {
+ load: load,
+ save: save,
+ clear: clear,
+ searchPlace: searchPlace,
+ useMyLocation: useMyLocation
+ };
+})(window);
diff --git a/frontend/public/js/map.js b/frontend/public/js/map.js
index 8238632..1dbd3db 100644
--- a/frontend/public/js/map.js
+++ b/frontend/public/js/map.js
@@ -12,28 +12,9 @@
// 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, name) {
- try {
- if (window.localStorage) localStorage.setItem(LAST_LOCATION_KEY, JSON.stringify({ lat: lat, lon: lon, name: name || null }));
- } catch (e) { /* localStorage disabled/unavailable — nothing to persist to */ }
- }
+ // Storage/search/GPS lookups live in window.VindnLocation (location.js,
+ // loaded globally in base.html.twig) — shared with Browse Events' own
+ // location filter too, so all three stay in sync by construction.
// Mirrors whatever setUserLocation/setSource just resolved (a searched
// place name, or nothing for GPS/a map click) into the location search
@@ -490,9 +471,13 @@
});
}
+ function statusForLimit() {
+ return limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you';
+ }
+
function setUserLocation(lat, lon, statusText, placeName) {
state.userLoc = [lat, lon];
- saveLastLocation(lat, lon, placeName);
+ window.VindnLocation.save(lat, lon, placeName);
setLocationInputValue(placeName);
if (limit) rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit));
@@ -512,26 +497,22 @@
function requestLocation(isAutoCheck) {
var statusEl = document.getElementById('locateStatus');
- if (!navigator.geolocation) {
- setLocateStatus(statusEl, 'Location not supported by your browser', true);
- return;
- }
- setLocateStatus(statusEl, 'Finding you…', false);
- navigator.geolocation.getCurrentPosition(function (pos) {
- // 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', 'Your location');
- }, function () {
- setLocateStatus(statusEl, "Couldn't get your location", true);
- }, { timeout: 8000 });
+ window.VindnLocation.useMyLocation({
+ onUnsupported: function () { setLocateStatus(statusEl, 'Location not supported by your browser', true); },
+ onStart: function () { setLocateStatus(statusEl, 'Finding you…', false); },
+ onFound: function (lat, lon, name) {
+ // A manual search/click may have already happened while this
+ // (silent, permission-check-triggered) lookup was still resolving.
+ if (isAutoCheck && manualLocationSet) return;
+ setUserLocation(lat, lon, statusForLimit(), name);
+ },
+ onError: function () { setLocateStatus(statusEl, "Couldn't get your location", true); }
+ });
}
var locateBtn = document.getElementById('locateBtn');
if (locateBtn) locateBtn.addEventListener('click', function () { manualLocationSet = true; requestLocation(false); });
- // 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) {
@@ -541,29 +522,22 @@
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', results[0].display_name);
- })
- .catch(function () {
- setLocateStatus(statusEl, 'Location search failed', true);
- });
+ window.VindnLocation.searchPlace(query, {
+ onStart: function () { setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); },
+ onNotFound: function () { setLocateStatus(statusEl, 'Could not find "' + query + '"', true); },
+ onFound: function (lat, lon, name) { setUserLocation(lat, lon, statusForLimit(), name); },
+ onError: 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();
+ var lastLocation = window.VindnLocation.load();
if (lastLocation) {
manualLocationSet = true;
- setUserLocation(lastLocation.lat, lastLocation.lon, limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you', lastLocation.name);
+ setUserLocation(lastLocation.lat, lastLocation.lon, statusForLimit(), lastLocation.name);
} 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
@@ -730,7 +704,7 @@
// conservative: reload whenever full coverage isn't guaranteed,
// rather than risk silently showing an incomplete result.
function setSource(lat, lon, placeName) {
- saveLastLocation(lat, lon, placeName);
+ window.VindnLocation.save(lat, lon, placeName);
setLocationInputValue(placeName);
var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon);
if (distFromOriginal + state.rangeKm > maxRangeKm) {
@@ -781,8 +755,6 @@
});
}
- // 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) {
@@ -792,37 +764,28 @@
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;
- }
- setSource(parseFloat(results[0].lat), parseFloat(results[0].lon), results[0].display_name);
- })
- .catch(function () {
- setLocateStatus(statusEl, 'Location search failed', true);
- });
+ window.VindnLocation.searchPlace(query, {
+ onStart: function () { setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); },
+ onNotFound: function () { setLocateStatus(statusEl, 'Could not find "' + query + '"', true); },
+ onFound: function (lat, lon, name) { setSource(lat, lon, name); },
+ onError: function () { setLocateStatus(statusEl, 'Location search failed', true); }
+ });
});
}
function requestLocation(isAutoCheck) {
var statusEl = document.getElementById('locateStatus');
- if (!navigator.geolocation) {
- setLocateStatus(statusEl, 'Location not supported by your browser', true);
- return;
- }
- 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, 'Your location');
- }, function () {
- setLocateStatus(statusEl, "Couldn't get your location", true);
- }, { timeout: 8000 });
+ window.VindnLocation.useMyLocation({
+ onUnsupported: function () { setLocateStatus(statusEl, 'Location not supported by your browser', true); },
+ onStart: function () { setLocateStatus(statusEl, 'Finding you…', false); },
+ onFound: function (lat, lon, name) {
+ // A manual search/click/drag may have already happened while this
+ // (silent, permission-check-triggered) lookup was still resolving.
+ if (isAutoCheck && manualLocationSet) return;
+ setSource(lat, lon, name);
+ },
+ onError: function () { setLocateStatus(statusEl, "Couldn't get your location", true); }
+ });
}
var locateBtn = document.getElementById('locateBtn');
@@ -832,21 +795,26 @@
// own reload-threshold redirect, or a shared link) can't carry the
// place name in the URL — fill the field back in from localStorage
// regardless of whether that load also needs the redirect below.
- var savedForName = loadLastLocation();
+ var savedForName = window.VindnLocation.load();
if (savedForName && savedForName.name) setLocationInputValue(savedForName.name);
- // 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.
+ // An explicit ?lat=/&lon= in the URL (e.g. a shared link, or this same
+ // page reloading itself after setSource's own redirect) always wins —
+ // and, crucially, also skips the auto-geolocation check below entirely.
+ // That check used to run on *every* fresh page load regardless of the
+ // URL, which meant searching for a place far enough to trigger the
+ // redirect immediately got overwritten by GPS again the moment the
+ // reloaded page re-ran this same check with a clean (unset)
+ // manualLocationSet. 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 can't clobber it.
var urlHasExplicitLocation = /[?&]lat=/.test(window.location.search);
var lastLocation = urlHasExplicitLocation ? null : savedForName;
if (lastLocation) {
manualLocationSet = true;
setSource(lastLocation.lat, lastLocation.lon, lastLocation.name);
- } else if (navigator.permissions && navigator.permissions.query) {
+ } else if (!urlHasExplicitLocation && navigator.permissions && navigator.permissions.query) {
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
if (status.state === 'granted') requestLocation(true);
}).catch(function () {});
diff --git a/frontend/templates/base.html.twig b/frontend/templates/base.html.twig
index 39230c2..2bd8b9a 100644
--- a/frontend/templates/base.html.twig
+++ b/frontend/templates/base.html.twig
@@ -59,6 +59,7 @@
+
{% block scripts %}{% endblock %}