fixed map behaviur with searched locations and dev

This commit is contained in:
Hendrik Behme committed 2026-08-17 02:49:53 +02:00
1 parent 7f6b100506
commit 55e28fd0af
5 files changed
+151 -50

No files matched your search

+2
View File
@@ -27,6 +27,8 @@ services:
restart: unless-stopped
frontend:
env_file:
- .env-frontend
build: ./frontend
working_dir: /app
ports:
+8
View File
@@ -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']
+134 -42
View File
@@ -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 () {});
}
+2 -1
View File
@@ -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; }
+5 -7
View File
@@ -30,13 +30,11 @@
{% endif %}
</div>
<div class="float-card locate-card">
{% if not compact %}
<form id="locationSearchForm">
<label for="locationSearch" style="display:none;">Search a place</label>
<input id="locationSearch" type="text" placeholder="Search a place…" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="submit" aria-label="Center map on this place">Go</button>
</form>
{% endif %}
<form id="locationSearchForm">
<label for="locationSearch" style="display:none;">Search a place</label>
<input id="locationSearch" type="text" placeholder="Search a place…" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="submit" aria-label="Center map on this place">Go</button>
</form>
<button class="locate-btn" id="locateBtn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>
Use my location