From 36ed3880e83ca6c3e029a87c18b6bde402bf1a23 Mon Sep 17 00:00:00 2001 From: Hendrik Behme Date: Fri, 9 Oct 2026 16:26:56 +0200 Subject: [PATCH] added date selection and filter popup --- frontend/public/js/dates.js | 136 ++++++ frontend/public/js/map.js | 104 +++- frontend/public/styles.css | 20 + frontend/src/Controller/EventController.php | 46 +- frontend/src/Repository/EventRepository.php | 68 +-- frontend/src/Service/EventMapSerializer.php | 6 + frontend/src/Twig/GermanFormatExtension.php | 58 +++ frontend/templates/_datepicker_head.html.twig | 2 + .../templates/_datepicker_scripts.html.twig | 3 + frontend/templates/_map_stage.html.twig | 17 +- frontend/templates/base.html.twig | 1 + frontend/templates/events/_card.html.twig | 4 +- frontend/templates/events/list.html.twig | 446 +++++++++++++----- frontend/templates/events/show.html.twig | 6 +- frontend/templates/home.html.twig | 2 + frontend/templates/map.html.twig | 2 + 16 files changed, 741 insertions(+), 180 deletions(-) create mode 100644 frontend/public/js/dates.js create mode 100644 frontend/src/Twig/GermanFormatExtension.php create mode 100644 frontend/templates/_datepicker_head.html.twig create mode 100644 frontend/templates/_datepicker_scripts.html.twig 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