fixed map behaviur with searched locations and dev
This commit is contained in:
1 parent
7f6b100506
commit
55e28fd0af
5 files changed
+136
-35
No files matched your search
@@ -27,6 +27,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
env_file:
|
||||||
|
- .env-frontend
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -5,3 +5,11 @@ framework:
|
|||||||
php_errors:
|
php_errors:
|
||||||
log: true
|
log: true
|
||||||
assets: ~
|
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']
|
||||||
+124
-32
@@ -12,6 +12,35 @@
|
|||||||
// so this only matters if that ever gets forgotten.
|
// so this only matters if that ever gets forgotten.
|
||||||
var DEFAULT_CENTER = [50.9375, 6.9603];
|
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
|
// Leaflet.awesome-markers only ships a fixed palette of named colors (no
|
||||||
// arbitrary hex/hsl) — curated to the ones that read well against the
|
// arbitrary hex/hsl) — curated to the ones that read well against the
|
||||||
// grayscale-filtered basemap. Approximate hex values below are only used
|
// grayscale-filtered basemap. Approximate hex values below are only used
|
||||||
@@ -311,6 +340,10 @@
|
|||||||
var defaultCenter = opts.defaultCenter || DEFAULT_CENTER;
|
var defaultCenter = opts.defaultCenter || DEFAULT_CENTER;
|
||||||
var containerId = opts.containerId || 'mapCanvas';
|
var containerId = opts.containerId || 'mapCanvas';
|
||||||
var state = { cat: 'all', userLoc: null };
|
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;
|
var shown = limit ? nearest(defaultCenter, allEvents.slice(), limit) : allEvents;
|
||||||
|
|
||||||
@@ -449,24 +482,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestLocation() {
|
function setUserLocation(lat, lon, statusText) {
|
||||||
var statusEl = document.getElementById('locateStatus');
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (statusEl) statusEl.textContent = 'Finding you…';
|
|
||||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
|
||||||
var lat = pos.coords.latitude;
|
|
||||||
var lon = pos.coords.longitude;
|
|
||||||
state.userLoc = [lat, lon];
|
state.userLoc = [lat, lon];
|
||||||
|
saveLastLocation(lat, lon);
|
||||||
|
|
||||||
if (limit) {
|
if (limit) rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit));
|
||||||
rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit));
|
|
||||||
if (statusEl) statusEl.textContent = 'Showing the ' + limit + ' nearest events';
|
var statusEl = document.getElementById('locateStatus');
|
||||||
} else if (statusEl) {
|
setLocateStatus(statusEl, statusText, false);
|
||||||
statusEl.textContent = 'Showing events near you';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map) {
|
if (map) {
|
||||||
if (userMarker) map.removeLayer(userMarker);
|
if (userMarker) map.removeLayer(userMarker);
|
||||||
@@ -476,21 +499,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderList();
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
}, function () {
|
}, function () {
|
||||||
if (statusEl) statusEl.textContent = "Couldn't get your location";
|
setLocateStatus(statusEl, "Couldn't get your location", true);
|
||||||
}, { timeout: 8000 });
|
}, { timeout: 8000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
var locateBtn = document.getElementById('locateBtn');
|
var locateBtn = document.getElementById('locateBtn');
|
||||||
if (locateBtn) locateBtn.addEventListener('click', requestLocation);
|
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) {
|
||||||
|
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
|
// If the user already granted location access (on this or an earlier
|
||||||
// visit), center on it automatically — this only *reads* the current
|
// visit), center on it automatically — this only *reads* the current
|
||||||
// permission state, it never triggers the browser's permission prompt
|
// permission state, it never triggers the browser's permission prompt
|
||||||
// itself, so it's safe to check unconditionally on load.
|
// itself, so it's safe to check unconditionally on load.
|
||||||
if (navigator.permissions && navigator.permissions.query) {
|
|
||||||
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
|
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
|
||||||
if (status.state === 'granted') requestLocation();
|
if (status.state === 'granted') requestLocation(true);
|
||||||
}).catch(function () {});
|
}).catch(function () {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +593,10 @@
|
|||||||
var containerId = opts.containerId || 'mapCanvas';
|
var containerId = opts.containerId || 'mapCanvas';
|
||||||
|
|
||||||
var state = { source: originalCenter.slice(), rangeKm: opts.rangeKm || 25, cat: 'all' };
|
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, [], {
|
var result = renderPinMap(containerId, [], {
|
||||||
zoomControl: true,
|
zoomControl: true,
|
||||||
@@ -601,7 +676,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var statusEl = document.getElementById('locateStatus');
|
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');
|
var countEl = document.getElementById('resultCount');
|
||||||
if (countEl) countEl.textContent = visible.length + (visible.length === 1 ? ' event' : ' events');
|
if (countEl) countEl.textContent = visible.length + (visible.length === 1 ? ' event' : ' events');
|
||||||
@@ -650,6 +725,7 @@
|
|||||||
// conservative: reload whenever full coverage isn't guaranteed,
|
// conservative: reload whenever full coverage isn't guaranteed,
|
||||||
// rather than risk silently showing an incomplete result.
|
// rather than risk silently showing an incomplete result.
|
||||||
function setSource(lat, lon) {
|
function setSource(lat, lon) {
|
||||||
|
saveLastLocation(lat, lon);
|
||||||
var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon);
|
var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon);
|
||||||
if (distFromOriginal + state.rangeKm > maxRangeKm) {
|
if (distFromOriginal + state.rangeKm > maxRangeKm) {
|
||||||
var url = new URL(window.location.href);
|
var url = new URL(window.location.href);
|
||||||
@@ -667,10 +743,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (map) {
|
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) {
|
if (sourceMarker) {
|
||||||
sourceMarker.on('dragend', function () {
|
sourceMarker.on('dragend', function () {
|
||||||
|
manualLocationSet = true;
|
||||||
var ll = sourceMarker.getLatLng();
|
var ll = sourceMarker.getLatLng();
|
||||||
setSource(ll.lat, ll.lng);
|
setSource(ll.lat, ll.lng);
|
||||||
});
|
});
|
||||||
@@ -705,43 +782,58 @@
|
|||||||
var input = document.getElementById('locationSearch');
|
var input = document.getElementById('locationSearch');
|
||||||
var query = (input.value || '').trim();
|
var query = (input.value || '').trim();
|
||||||
if (!query) return;
|
if (!query) return;
|
||||||
|
manualLocationSet = true;
|
||||||
var statusEl = document.getElementById('locateStatus');
|
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))
|
fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(query))
|
||||||
.then(function (res) { return res.json(); })
|
.then(function (res) { return res.json(); })
|
||||||
.then(function (results) {
|
.then(function (results) {
|
||||||
if (!results || !results.length) {
|
if (!results || !results.length) {
|
||||||
if (statusEl) statusEl.textContent = 'Could not find "' + query + '"';
|
setLocateStatus(statusEl, 'Could not find "' + query + '"', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSource(parseFloat(results[0].lat), parseFloat(results[0].lon));
|
setSource(parseFloat(results[0].lat), parseFloat(results[0].lon));
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.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');
|
var statusEl = document.getElementById('locateStatus');
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
|
setLocateStatus(statusEl, 'Location not supported by your browser', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statusEl) statusEl.textContent = 'Finding you…';
|
setLocateStatus(statusEl, 'Finding you…', false);
|
||||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
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);
|
setSource(pos.coords.latitude, pos.coords.longitude);
|
||||||
}, function () {
|
}, function () {
|
||||||
if (statusEl) statusEl.textContent = "Couldn't get your location";
|
setLocateStatus(statusEl, "Couldn't get your location", true);
|
||||||
}, { timeout: 8000 });
|
}, { timeout: 8000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
var locateBtn = document.getElementById('locateBtn');
|
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) {
|
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
|
||||||
if (status.state === 'granted') requestLocation();
|
if (status.state === 'granted') requestLocation(true);
|
||||||
}).catch(function () {});
|
}).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);
|
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{ 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{
|
.legend{
|
||||||
position:absolute; left:18px; bottom:18px; z-index:30; padding:10px 14px;
|
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){
|
@media (max-width: 900px){
|
||||||
.stage{ height:auto; }
|
.stage{ height:auto; }
|
||||||
.stage-compact{ 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; }
|
.panel, .stage-compact .panel{ position:static; width:auto; height:auto; max-height:50vh; margin-top:14px; }
|
||||||
.map-overlay-top{ position:absolute; }
|
.map-overlay-top{ position:absolute; }
|
||||||
.legend{ display:none; }
|
.legend{ display:none; }
|
||||||
|
|||||||
@@ -30,13 +30,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="float-card locate-card">
|
<div class="float-card locate-card">
|
||||||
{% if not compact %}
|
|
||||||
<form id="locationSearchForm">
|
<form id="locationSearchForm">
|
||||||
<label for="locationSearch" style="display:none;">Search a place</label>
|
<label for="locationSearch" style="display:none;">Search a place</label>
|
||||||
<input id="locationSearch" type="text" placeholder="Search a place…" autocomplete="off">
|
<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>
|
<button class="btn btn-ghost btn-sm" type="submit" aria-label="Center map on this place">Go</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
<button class="locate-btn" id="locateBtn" type="button">
|
<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>
|
<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
|
Use my location
|
||||||
|
|||||||
Reference in new issue
Block a user