diff --git a/frontend/public/js/dates.js b/frontend/public/js/dates.js
new file mode 100644
index 0000000..42ef68e
--- /dev/null
+++ b/frontend/public/js/dates.js
@@ -0,0 +1,136 @@
+// Shared date-range handling for Browse Events and both maps — remembered
+// across pages the way location.js remembers the place, validated in one
+// spot, and bound to German-format (dd.mm.yyyy) date pickers. Pages still
+// own what a range *does* (a server-side filter on Browse, a client-side
+// one on the maps); this only owns the range itself.
+(function (window) {
+ var KEY = 'vindn:dateRange';
+ var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
+
+ // "Y-m-d" for today in Germany — the site's audience and data are
+ // German-local, so "today" must not flip at UTC midnight. sv-SE happens
+ // to format dates as ISO, which is the only reason it's used here.
+ function today() {
+ return new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' });
+ }
+
+ // Valid "Y-m-d" strings or null for each end, and reversed ends swapped.
+ function normalize(from, to) {
+ from = DAY_RE.test(from || '') ? from : null;
+ to = DAY_RE.test(to || '') ? to : null;
+ if (from && to && from > to) {
+ var swap = from;
+ from = to;
+ to = swap;
+ }
+ return { from: from, to: to };
+ }
+
+ // The remembered range, or null. A range that already ended is dropped
+ // (nothing it could match is shown anymore), and a start in the past is
+ // moved up to today so the pickers never show a date they can't select.
+ function load() {
+ try {
+ var raw = window.localStorage && localStorage.getItem(KEY);
+ if (!raw) return null;
+ var parsed = JSON.parse(raw);
+ var range = normalize(parsed.from, parsed.to);
+ if (!range.from && !range.to) return null;
+ var now = today();
+ if (range.to && range.to < now) {
+ clear();
+ return null;
+ }
+ if (range.from && range.from < now) range.from = now;
+ return range;
+ } catch (e) { /* localStorage disabled/unavailable, or corrupt value — just skip it */ }
+ return null;
+ }
+
+ function save(from, to) {
+ var range = normalize(from, to);
+ if (!range.from && !range.to) {
+ clear();
+ return;
+ }
+ try {
+ if (window.localStorage) localStorage.setItem(KEY, JSON.stringify(range));
+ } catch (e) { /* localStorage disabled/unavailable — nothing to persist to */ }
+ }
+
+ function clear() {
+ try {
+ if (window.localStorage) localStorage.removeItem(KEY);
+ } catch (e) { /* localStorage disabled/unavailable */ }
+ }
+
+ // Turns a pair of into German-format pickers when
+ // flatpickr is loaded. A native date input displays in the *browser's*
+ // locale and can't be forced to dd.mm.yyyy, so the visible field is
+ // flatpickr's alt input (d.m.Y, German calendar) while the original input
+ // keeps holding the machine-readable "Y-m-d" value everything else reads.
+ // Without flatpickr (e.g. its CDN is unreachable) the native inputs keep
+ // working as they are.
+ //
+ // onChange(from, to) fires when the *user* edits either field — never for
+ // updates made through the returned setter, so applying a remembered
+ // range doesn't trigger the page's own change handling.
+ function bindInputs(fromEl, toEl, onChange) {
+ var pickers = [];
+ if (window.flatpickr) {
+ var options = {
+ locale: 'de',
+ dateFormat: 'Y-m-d',
+ altInput: true,
+ altFormat: 'd.m.Y',
+ allowInput: true,
+ disableMobile: true, // the native mobile picker would bring the device locale back
+ static: true, // rendered inside the card, so it also shows in a fullscreen map
+ minDate: 'today',
+ // The popup renders inside its card/dialog; in a scrolling container
+ // (the Browse filter popup) it can open partly out of view.
+ onOpen: function (selected, str, instance) {
+ setTimeout(function () {
+ instance.calendarContainer.scrollIntoView({ block: 'nearest' });
+ }, 0);
+ }
+ };
+ pickers = [fromEl, toEl].map(function (el) {
+ var picker = window.flatpickr(el, options);
+ // The original input turns into a hidden one, so the visible alt
+ // input has to take over its id and label — otherwise the