@@ -3,20 +3,74 @@
{% block title %} Browse events — VINDN {% endblock %}
{% block head_extra %}
{% include '_datepicker_head.html.twig' %}
<style>
.toolbar { display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap; margin-bottom:20px; }
.search-input-bar { display:flex; gap:10px; margin-bottom:20 px; }
.search-input-bar input { flex:1; padding:16px 20px; border:1px solid var(--line-strong); border-radius: var(--r-pill); font-size:15px; }
.search-input-bar { display:flex; gap:10px; margin-bottom:14 px; }
.search-input-bar input { flex:1; min-width:0; padding:16px 20px; border:1px solid var(--line-strong); border-radius: var(--r-pill); font-size:15px; }
.filter-label {
display:block; font-family:var(--f-mono); font-size:11px; text-transform:uppercase;
letter-spacing:.06em; color:var(--muted); margin:0 0 8px;
}
.filter-row { display:flex; align-items:flex-start; gap:24px; flex-wrap:wrap; margin-bottom:18px; }
.filter-group-inline { display:flex; flex-direction:column; }
/* summary of what's active, under the search bar */
.filter-bar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:20px; }
.filter-bar .count { display:inline-block; min-width:18px; padding:1px 6px; margin-left:4px; border-radius:var(--r-pill); background:var(--ink); color:var(--paper); font-family:var(--f-mono); font-size:11px; text-align:center; }
.filter-pill { display:inline-flex; align-items:center; gap:8px; padding:6px 8px 6px 12px; border:1px solid var(--line-strong); border-radius:var(--r-pill); background:#fff; font-family:var(--f-mono); font-size:12px; color:var(--ink); }
.filter-pill:hover { border-color:var(--ink); }
.filter-pill .x { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; border-radius:50%; background:var(--paper-dim); font-size:13px; line-height:1; }
.filter-clear-all { font-family:var(--f-mono); font-size:12px; color:var(--accent); text-decoration:underline; margin-left:4px; }
/* the filter popup */
.filter-dialog { width:min(640px, calc(100vw - 24px)); padding:0; overflow:hidden; border:1px solid var(--line-strong); border-radius:var(--r-card); background:var(--paper); color:var(--ink); }
.filter-dialog::backdrop { background:rgba(20,20,20,.25); backdrop-filter:blur(5px) saturate(0); -webkit-backdrop-filter:blur(5px) saturate(0); }
/* Open/close transition. Opening uses @starting-style as the "from" state;
closing adds .is-closing first (see the script) and only calls close()
once the fade has finished, so it works without the newer
display/overlay transition features. */
.filter-dialog, .filter-dialog::backdrop {
transition: opacity .22s ease, transform .22s ease, background-color .22s ease,
backdrop-filter .22s ease, -webkit-backdrop-filter .22s ease;
}
.filter-dialog.is-closing { opacity:0; transform:translateY(14px) scale(.98); }
.filter-dialog.is-closing::backdrop { background-color:rgba(20,20,20,0); backdrop-filter:blur(0) saturate(1); -webkit-backdrop-filter:blur(0) saturate(1); }
@starting-style {
.filter-dialog[open] { opacity:0; transform:translateY(14px) scale(.98); }
.filter-dialog[open]::backdrop { background-color:rgba(20,20,20,0); backdrop-filter:blur(0) saturate(1); -webkit-backdrop-filter:blur(0) saturate(1); }
}
@media (prefers-reduced-motion: reduce) {
.filter-dialog, .filter-dialog::backdrop { transition-duration:.01s; }
}
body:has(dialog[open]) { overflow:hidden; }
.filter-dialog form { display:flex; flex-direction:column; max-height:calc(100vh - 48px); max-height:calc(100dvh - 48px); }
.fd-head, .fd-foot { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:16px 24px; }
.fd-head { border-bottom:1px solid var(--line); }
.fd-head h2 { font-size:20px; margin:0; }
.fd-close { border:0; background:none; font-size:26px; line-height:1; cursor:pointer; color:var(--ink); padding:0 4px; }
.fd-body { flex:1; overflow-y:auto; padding:20px 24px 8px; }
.fd-foot { border-top:1px solid var(--line); }
.fd-section { margin-bottom:24px; }
.fd-section > .filter-label { margin-bottom:10px; }
.pick-row { display:flex; flex-wrap:wrap; gap:8px; max-height:176px; overflow-y:auto; padding:2px; }
.pick { position:relative; cursor:pointer; }
.pick input { position:absolute; opacity:0; pointer-events:none; }
.pick span { display:inline-block; font-family:var(--f-mono); font-size:12px; padding:7px 14px; border-radius:var(--r-pill); border:1px solid var(--line-strong); background:#fff; color:var(--ink-soft); white-space:nowrap; }
.pick input:checked + span { background:var(--ink); color:var(--paper); border-color:var(--ink); }
.pick input:focus-visible + span { outline:2px solid var(--accent); outline-offset:2px; }
.when-filter { display:flex; align-items:center; gap:10px 20px; flex-wrap:wrap; }
.when-filter .chip-row-tight { max-height:none; overflow:visible; }
.when-filter button.chip-sm { cursor:pointer; }
.when-filter .date-range { margin-top:0; }
.location-filter { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.location-filter form { display:flex; gap:6 px; }
.location-filter input[type=text] { min-width:0; width:180px; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:13px; }
.location-filter input[type=text] { min-width:0; flex:1 1 160px; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:13 px; }
.location-filter select { padding:7px 8px; border:1px solid var(--line-strong); border-radius:6px; font-family:var(--f-mono); font-size:12px; background:#fff; }
.locate-status { max-width:none; }
.fd-link { border:0; background:none; cursor:pointer; padding:0; color:var(--accent); text-decoration:underline; font-family:var(--f-mono); font-size:12px; }
@media (max-width:560px) {
.fd-head, .fd-foot { padding:14px 16px; }
.fd-body { padding:16px 16px 8px; }
}
</style>
{% endblock %}
@@ -27,7 +81,7 @@
<div class="toolbar">
<h1 style="font-size:clamp(28px,4vw,40px);"> {{ q ? 'Searching: "' ~ q ~ '"' : "What's on" }} {{ activeCategory ? ' — ' ~ activeCategory : '' }} </h1>
<div style="display:flex; align-items:center; gap:12px;">
<p class="mono" style="font-size:13px; color:var(--muted); margin:0;"> {{ total }} event {{ total == 1 ? '' : 's' }} </p>
<p class="mono" style="font-size:13px; color:var(--muted); margin:0;"> {{ total | de_number }} event {{ total == 1 ? '' : 's' }} </p>
<label for="sortSelect" style="display:none;">Sort by</label>
<select id="sortSelect" style="padding:7px 8px; border:1px solid var(--line-strong); border-radius:6px; font-family:var(--f-mono); font-size:12px; background:#fff;">
<option value="soonest" data-href=" {{ path ( 'event_list' , filters | merge ( { sort : 'soonest' } ) ) }} " {{ 'soonest' == sort ? 'selected' : '' }} >Sort: Soonest</option>
@@ -40,10 +94,10 @@
</div>
</div>
<span class="filter-label">Search</span>
<form class="search-input-bar" method="get" action=" {{ path ( 'event_list' ) }} ">
{% if activeCategory %} <input type="hidden" name="category" value=" {{ activeCategory }} "> {% endif %}
{% if 'any' != when %} <input type="hidden" name="when " value=" {{ when }} "> {% endif %}
{% if dateFrom %} <input type="hidden" name="from " value=" {{ dateFrom }} "> {% endif %}
{% if dateTo %} <input type="hidden" name="to" value=" {{ dateTo }} "> {% endif %}
{% if hasLocation %}
<input type="hidden" name="lat" value=" {{ lat }} ">
<input type="hidden" name="lon" value=" {{ lon }} ">
@@ -54,54 +108,95 @@
<button class="btn btn-primary" type="submit">Search</button>
</form>
<span class="filter-label">Category</span>
<input type="checkbox" id="catsToggle" class="chip-toggle-checkbox ">
<div class="chip-row chip-row-collapsible" >
<a href=" {{ path ( 'event_list' , filters | merge ( { category : null } ) ) }} " class="chip {{ activeCategory is null ? 'is-active' : '' }} ">All</a>
{% for category in categories %}
<a href=" {{ path ( 'event_list' , filters | merge ( { category : category } ) ) }} " class="chip {{ activeCategory == category ? 'is-active' : '' }} "> {{ category } }</a>
{% endfor %}
{% set activeCount = ( activeCategory ? 1 : 0 ) + ( dateFrom or dateTo ? 1 : 0 ) + ( hasLocation ? 1 : 0 ) %}
<div class="filter-bar ">
<button type="button" class="btn btn-ghost btn-sm" id="openFilters" aria-haspopup="dialog">Filters {% if activeCount %} <span class="count"> {{ activeCount }} </span> {% endif %} </button >
{% if activeCategory %}
<a class="filter-pill" href=" {{ path ( 'event_list' , filters | merge ( { category : null } ) ) }} " aria-label="Remove category filter {{ activeCategory }} "> {{ activeCategory }} <span class="x" aria-hidden="true">× </span></a>
{% endif % }
{% if dateFrom or dateTo %}
{% if dateFrom == today and dateTo == today %} {% set dateLabel = 'Today' %}
{% elseif dateFrom == today and dateTo == weekEnd %} {% set dateLabel = 'Next 7 days' %}
{% elseif dateFrom and dateTo and dateFrom == dateTo %} {% set dateLabel = dateFrom | date ( 'd.m.Y' ) %}
{% elseif dateFrom and dateTo %} {% set dateLabel = dateFrom | date ( 'd.m.Y' ) ~ ' – ' ~ dateTo | date ( 'd.m.Y' ) %}
{% elseif dateFrom %} {% set dateLabel = 'From ' ~ dateFrom | date ( 'd.m.Y' ) %}
{% else %} {% set dateLabel = 'Until ' ~ dateTo | date ( 'd.m.Y' ) %} {% endif %}
<a class="filter-pill" data-forget="dates" href=" {{ path ( 'event_list' , filters | merge ( { from : null , to : null } ) ) }} " aria-label="Remove date filter {{ dateLabel }} "> {{ dateLabel }} <span class="x" aria-hidden="true">× </span></a>
{% endif %}
{% if hasLocation %}
<a class="filter-pill" id="locationPill" data-forget="location" data-lat=" {{ lat }} " data-lon=" {{ lon }} " href=" {{ path ( 'event_list' , filters | merge ( { lat : null , lon : null , range : null } ) ) }} " aria-label="Remove location filter"><span class="label">Within {{ rangeKm }} km of {{ lat | de_number ( 3 ) }} , {{ lon | de_number ( 3 ) }} </span><span class="x" aria-hidden="true">× </span></a>
{% endif %}
{% if activeCount %}
<a class="filter-clear-all" data-forget="all" href=" {{ path ( 'event_list' , { q : q ? : null , sort : filters .sort } ) }} ">Clear all</a>
{% endif %}
</div>
<label for="catsToggle" class="chip-toggle-btn" style="margin-bottom:20px;" >
<span class="show-more">Show all categories ↑</spa n>
<span class="show-less">Show fewer categories ↑</span>
</label>
</div >
</sectio n>
<div class="filter-row ">
<div class="filter-group-inline ">
<span class="filter-label">When</span>
<div class ="c hip-row-tight">
<a href=" {{ path ( 'event_list' , filters | merge ( { when : null } ) ) }} " class="chip-sm {{ 'any' == when ? 'is-active' : '' }} ">Any date</a >
<a href=" {{ path ( 'event_list' , filters | merge ( { when : 'today' } ) ) }} " class="chip-sm {{ 'today' == when ? 'is-active' : '' }} ">Today</a >
<a href=" {{ path ( 'event_list' , filters | merge ( { when : 'week' } ) ) }} " class="chip-sm {{ 'week' == when ? 'is-active' : '' }} ">Next 7 days</a >
<dialog class="filter-dialog" id="filterDialog" aria-labelledby="filterDialogTitle ">
<form method="get" action=" {{ path ( 'event_list' ) }} " id="filterForm ">
{% if q %} <input type="hidden" name="q" value=" {{ q }} "> {% endif %}
{% if filters .sort %} <input type ="hidden" name="sort" value=" {{ filters .sort }} "> {% endif %}
<div class="fd-head" >
<h2 id="filterDialogTitle">Filters</h2 >
<button type="button" class="fd-close" id="closeFilters" aria-label="Close">× </button >
</div>
<div class="fd-body">
<div class="fd-section">
<span class="filter-label">Category</span>
<div class="pick-row">
<label class="pick"><input type="radio" name="category" value="" {{ activeCategory is null ? 'checked' : '' }} ><span>All</span></label>
{% for category in categories %}
<label class="pick"><input type="radio" name="category" value=" {{ category }} " {{ activeCategory == category ? 'checked' : '' }} ><span> {{ category }} </span></label>
{% endfor %}
</div>
</div>
<div class="filter-group-inline ">
<div class="fd-section ">
<span class="filter-label">When</span>
<div class="when-filter">
<div class="chip-row-tight" id="datePresets">
<button type="button" class="chip-sm" data-from="" data-to="">Any date</button>
<button type="button" class="chip-sm" data-from=" {{ today }} " data-to=" {{ today }} ">Today</button>
<button type="button" class="chip-sm" data-from=" {{ today }} " data-to=" {{ weekEnd }} ">Next 7 days</button>
</div>
<div class="date-range" role="group" aria-label="Date range">
<label for="dateFrom">From</label>
<input type="date" id="dateFrom" name="from" aria-label="From date" min=" {{ today }} " value=" {{ dateFrom }} ">
<label for="dateTo">to</label>
<input type="date" id="dateTo" name="to" aria-label="To date" min=" {{ today }} " value=" {{ dateTo }} ">
</div>
</div>
</div>
<div class="fd-section">
<span class="filter-label">Near a location</span>
<input type="hidden" name="lat" id="fLat" value=" {{ hasLocation ? lat : '' }} ">
<input type="hidden" name="lon" id="fLon" value=" {{ hasLocation ? lon : '' }} ">
<div class="location-filter">
<form id ="locationSearchForm" >
<label for ="locationSearchInput" s tyl e="display:none;">Search a place</label >
<input id="locationSearchInput" type="text" placeholder="Search a place…" autocomplete="off" >
<button class="btn btn-ghost btn-sm" type="submit" aria-label="Search near this place">Go</button>
</form>
<label for ="locationSearchInput" style="display:none;">Search a place</label >
<input id ="locationSearchInput" typ e="text" placeholder="Search a place…" autocomplete="off" >
<button class="btn btn-ghost btn-sm" type="button" id="locationGo">Go</button >
<button type="button" class="btn btn-ghost btn-sm" id="useMyLocationBtn">Use my location</button>
<select id ="rangeSelect">
<label for ="rangeSelect" style="display:none;">Radius</label >
<select id="rangeSelect" name="range">
{% for km in rangeOptions %}
<option value=" {{ km }} " {{ km == rangeKm ? 'selected' : '' }} > {{ km }} km</option>
{% endfor %}
</select>
{% if hasLocation % }
<a href=" {{ path ( 'event_list' , filters | merge ( { lat : null , lon : null , range : null } ) ) }} " id="clearLocationLink" style="color:var(--accent); text-decoration:underline; font-family:var(--f-mono); font-size:12px;">clear</a>
{% endif %}
<button type="button" class="fd-link" id="clearLocation" {{ hasLocation ? '' : 'hidden' } }>clear</button>
</div>
<p class="locate-status" id="locationStatus"> {% if hasLocation %} Within {{ rangeKm }} km of {{ lat | de_number ( 3 ) }} , {{ lon | de_number ( 3 ) }} {% endif %} </p>
</div>
</div>
<p class="locate-status" id="locationStatus">
{% if hasLocation %} Showing events within {{ rangeKm }} km of {{ lat | number_format ( 3 ) }} , {{ lon | number_format ( 3 ) }} {% endif %}
</p >
</div >
</section >
<div class="fd-foot">
<button type="button" class="fd-link" id="resetFilters">Reset all</button >
<button type="submit" class="btn btn-primary btn-sm">Show events</button >
</div >
</form>
</dialog>
<section class="section" style="padding-top:0;">
<div class="wrap">
@@ -127,91 +222,188 @@
{% endblock %}
{% block scripts %}
{% include '_datepicker_scripts.html.twig' %}
<script>
(function() {
var currentLat = {{ hasLocation ? lat : 'null' }} ;
var currentLon = {{ hasLocation ? lon : 'null' }} ;
// Storage/search/GPS lookups live in window.VindnLocation (location.js)
// and the range handling in window.VindnDates (dates.js), both loaded
// globally in base.html.twig and shared with the home hero map and /map,
// so a place or date range chosen on any of the three sticks when moving
// between them.
var dialog = document.getElementById('filterDialog');
var form = document.getElementById('filterForm');
var latEl = document.getElementById('fLat');
var lonEl = document.getElementById('fLon');
var placeInput = document.getElementById('locationSearchInput');
var rangeSelect = document.getElementById('rangeSelect');
var clearLocationBtn = document.getElementById('clearLocation');
var statusEl = document.getElementById('locationStatus');
var dateFrom = document.getElementById('dateFrom');
var dateTo = document.getElementById('dateTo');
// Storage/search/GPS lookups live in window.VindnLocation (location.js,
// loaded globally in base.html.twig) — shared with the home hero map and
// /map, so a search/geolocation made on any of the three sticks when
// moving between them.
var saved = window.VindnLocation.load();
var savedDates = window.VindnDates.load();
var search = window.location.search;
function currentRangeKm() {
return rangeSelect ? parseInt(rangeSelect.value, 10) : {{ rangeKm }} ;
}
// ---- popup open/close ----
document.getElementById('openFilters').addEventListener('click', function () { dialog.showModal(); }) ;
// Full reloads happen after every location change here, so there's no
// point setting the input's value client-side before navigating — the
// restore-on-load block below re-reads it from localStorage instead,
// right after this same call has just written it there.
function navigateWithLocation(lat, lon, rangeKm, name) {
// Preserve whatever's currently shown (e.g. a range-only change)
// unless a new place/geolocation explicitly overrides it.
if (name === undefined) {
var locInput = document.getElementById('locationSearchInput') ;
name = locInput ? locInput.value : null ;
// Fade out first, then close — and do the same for Esc (the native
// "cancel") so every way of closing animates alike.
function closeDialog() {
if (!dialog.open || dialog.classList.contains('is-closing')) return;
dialog.classList.add('is-closing');
var finished = false;
function finish() {
if (finished) return;
finished = true ;
dialog.classList.remove('is-closing') ;
dialog.close();
}
window.VindnLocation.save(lat, lon, name);
var url = new URL(window.location.href) ;
url.searchParams.set('lat', lat.toFixed(5) );
url.searchParams.set('lon', lon.toFixed(5) );
url.searchParams.set('range', rangeKm || url.searchParams.get('range') || 25 );
url.searchParams.delete('page'); // the result set changed, page N may no longer exist
window.location.href = url.toString();
dialog.addEventListener('transitionend', function onEnd(e) {
if (e.target !== dialog) return ;
dialog.removeEventListener('transitionend', onEnd );
finish( );
} );
setTimeout(finish, 400); // in case no transition runs (reduced motion, background tab)
}
document.getElementById('closeFilters').addEventListener('click', closeDialog);
dialog.addEventListener('cancel', function (e) { e.preventDefault(); closeDialog(); });
// a click on the backdrop lands on the <dialog> element itself
dialog.addEventListener('click', function (e) { if (e.target === dialog) closeDialog(); });
// ---- dates ----
var setDates = window.VindnDates.bindInputs(dateFrom, dateTo, markPresets);
var presets = document.querySelectorAll('#datePresets button');
function markPresets() {
presets.forEach(function (btn) {
var on = btn.getAttribute('data-from') === dateFrom.value && btn.getAttribute('data-to') === dateTo.value;
btn.classList.toggle('is-active', on);
});
}
presets.forEach(function (btn) {
btn.addEventListener('click', function () {
setDates(btn.getAttribute('data-from'), btn.getAttribute('data-to'));
markPresets();
});
});
markPresets();
// A range that arrived via the URL is remembered too, so the maps pick it
// up like they would one chosen here.
var urlDateFrom = {{ dateFrom | json_encode | raw }} ;
var urlDateTo = {{ dateTo | json_encode | raw }} ;
if (urlDateFrom || urlDateTo) window.VindnDates.save(urlDateFrom, urlDateTo);
// ---- location ----
var resolvedName = '';
if (saved && latEl.value && Math.abs(saved.lat - parseFloat(latEl.value)) < 1e-4 && Math.abs(saved.lon - parseFloat(lonEl.value)) < 1e-4 && saved.name) {
resolvedName = saved.name;
placeInput.value = saved.name;
// the summary pill can only show coordinates server-side
var pillLabel = document.querySelector('#locationPill .label');
if (pillLabel) pillLabel.textContent = 'Within ' + rangeSelect.value + ' km of ' + saved.name.split(',').slice(0, 2).join(',');
}
function setStatus(text, isError) {
var el = document.getElementById('locationStatus') ;
if (!el) return ;
el.textContent = text;
el.classList.toggle('is-error', !!isError);
statusEl.textContent = text ;
statusEl.classList.toggle('is-error', !!isError) ;
}
function setLocation(lat, lon, name) {
latEl.value = lat.toFixed(5);
lonEl.value = lon.toFixed(5);
placeInput.value = name || '';
resolvedName = name || '';
clearLocationBtn.hidden = false;
setStatus('Found: ' + (name || lat.toFixed(3) + ', ' + lon.toFixed(3)), false);
}
function clearLocation() {
latEl.value = '';
lonEl.value = '';
placeInput.value = '';
resolvedName = '';
clearLocationBtn.hidden = true;
setStatus('', false);
}
function lookup(query, done) {
window.VindnLocation.searchPlace(query, {
onStart: function () { setStatus('Looking up "' + query + '"…', false); },
onNotFound: function () { setStatus('Could not find "' + query + '"', true); },
onFound: function (lat, lon, name) { setLocation(lat, lon, name); if (done) done(); },
onError: function () { setStatus('Location search failed', true); }
});
}
var locationForm = document.getElementById('locationSearchForm');
if (locationForm) {
locationForm.addEventListener('submit', function (e) {
document.getElementById('locationGo').addEventListener('click', function () {
var query = placeInput.value.trim();
if (query) lookup(query);
});
// Enter in the place field looks the place up rather than applying the
// whole popup half-finished.
placeInput.addEventListener('keydown', function (e) {
if (e.key !== 'Enter') return;
e.preventDefault();
var query = placeInput.value.trim();
if (query) lookup(query);
});
document.getElementById('useMyLocationBtn').addEventListener('click', function () {
window.VindnLocation.useMyLocation( {
onUnsupported: function () { setStatus('Location not supported by your browser', true); },
onStart: function () { setStatus('Finding you…', false); },
onFound: setLocation,
onError: function () { setStatus("Couldn't get your location", true); }
});
});
clearLocationBtn.addEventListener('click', clearLocation);
document.getElementById('resetFilters').addEventListener('click', function () {
form.querySelector('input[name=category][value=""]').checked = true;
setDates('', '');
markPresets();
clearLocation();
});
// ---- applying ----
var submitting = false;
form.addEventListener('submit', function (e) {
if (submitting) return;
// A place typed but never looked up ("Go" skipped): resolve it first.
var typed = placeInput.value.trim();
if (typed && typed !== resolvedName) {
e.preventDefault();
var input = document.getElementById('locationSearchInput' );
var query = (input.value || '').trim() ;
if (!query) return;
window.VindnLocation.searchPlace(query, {
onStart: function () { setStatus('Looking up "' + query + '"…', false); },
onNotFound: function () { setStatus('Could not find "' + query + '"', true); },
onFound: function (lat, lon, name) { navigateWithLocation(lat, lon, currentRangeKm(), name); },
onError: function () { setStatus('Location search failed', true); }
});
});
}
lookup(typed, function () { form.requestSubmit(); } );
return ;
}
submitting = true;
var useMyLocationBtn = document.getElementById('useMyLocationBtn' );
if (useMyLocationBtn) {
useMyLocationBtn.addEventListener('click', function () {
window.VindnLocation.useMyLocation( {
onUnsupported: function () { setStatus('Location not supported by your browser', true); },
onStart: function () { setStatus('Finding you…', false); },
onFound: function (lat, lon, name) { navigateWithLocation(lat, lon, currentRangeKm(), name); },
onError: function () { setStatus("Couldn't get your location", true); }
});
});
}
var range = window.VindnDates.normalize(dateFrom.value, dateTo.value );
dateFrom.value = range.from || '';
dateTo.value = range.to || '';
window.VindnDates.save(range.from, range.to);
if (rangeSelect ) {
rangeSelect.addEventListener('change', function () {
// No location chosen yet — just remember the preference for
// whichever search/geolocation happens next.
if (currentLat === null || currentLon === null) return ;
navigateWithLocation(currentLat, currentLon, currentRangeKm());
if (latEl.value && lonEl.value ) {
window.VindnLocation.save(parseFloat(latEl.value), parseFloat(lonEl.value), resolvedName || null);
} else {
window.VindnLocation.clear();
rangeSelect.disabled = true ;
}
// keep the URL to what's actually set
Array.prototype.forEach.call(form.elements, function (el) {
if (el.name && el.type !== 'radio' && el.value === '') el.disabled = true;
});
}
var cat = form.querySelector('input[name=category]:checked');
if (cat && cat.value === '') cat.disabled = true;
});
var clearLocationLink = document.getElementById('clearLocationLink');
if (clearLocationLink) {
// Otherwise the restore-on-load check below would immediately undo
// the clear on the very next visit.
clearLocationLink.addEventListener('click', function () { window.VindnLocation.clear(); } );
}
// Removing a filter via its pill must also forget it, otherwise the
// restore-on-load check below would bring it straight back.
document.querySelectorAll('[data-forget]').forEach(function (link) {
link.addEventListener('click', function () {
var what = link.getAttribute('data-forget' );
if (what === 'dates' || what === 'all') window.VindnDates.clear();
if (what === 'location' || what === 'all') window.VindnLocation.clear();
});
});
var sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
@@ -221,21 +413,25 @@
});
}
var saved = window.VindnLocation.load();
// Every full reload here (search, geolocation, range change) already
// lands with an explicit ?lat=, so this doesn't overwrite it — it only
// fills in the name text a fresh navigation can't carry in the URL.
if (saved && saved.name) {
var locInput = document.getElementById('locationSearchInput');
if (locInput && !locInput.value) locInput.value = saved.name ;
}
// A remembered search/geolocation from the home map or /map takes over
// here too — unless this URL already has its own explicit ?lat=, which
// always wins (e.g. a link shared with a location baked in).
if (!/[?&]lat=/.test(window.location.search) && saved ) {
navigateWithLocation(saved.lat, saved.lon, currentRangeKm(), saved.name );
// A remembered place and date range from the home map or /map take over
// here too — unless the URL already says something itself (an explicit
// ?lat=, or any of from/to/when), which always wins, e.g. a shared link.
// Both are restored in one redirect rather than two chained ones.
var restoreLocation = saved && !/[?&]lat=/.test(search);
var restoreDates = savedDates && !/[?&](from|to|when)=/.test(search);
if (restoreLocation || restoreDates) {
var restoreUrl = new URL(window.location.href) ;
if (restoreLocation) {
restoreUrl.searchParams.set('lat', saved.lat.toFixed(5));
restoreUrl.searchParams.set('lon', saved.lon.toFixed(5));
restoreUrl.searchParams.set('range', rangeSelect.value);
}
if (restoreDates ) {
if (savedDates.from) restoreUrl.searchParams.set('from', savedDates.from );
if (savedDates.to) restoreUrl.searchParams.set('to', savedDates.to);
}
restoreUrl.searchParams.delete('page');
window.location.href = restoreUrl.toString();
}
})();
</script>