fixed map behaviur with searched locations and dev
This commit is contained in:
1 parent
7f6b100506
commit
55e28fd0af
5 files changed
+151
-50
No files matched your search
@@ -27,6 +27,8 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
env_file:
|
||||
- .env-frontend
|
||||
build: ./frontend
|
||||
working_dir: /app
|
||||
ports:
|
||||
|
||||
@@ -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
@@ -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 () {});
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user