improved maps and browse events, removed search
This commit is contained in:
1 parent
55e28fd0af
commit
75d5d0bb0f
11 files changed
+387
-392
No files matched your search
@@ -16,6 +16,11 @@
|
||||
{% endif %}
|
||||
<h3>{{ event.title }}</h3>
|
||||
<p class="when">{{ event.startDateTime ? event.startDateTime|date('D, M j · H:i') : 'Date TBD' }}</p>
|
||||
<p class="place">{{ [event.street, event.city]|filter(v => v)|join(', ') ?: event.country }}</p>
|
||||
<p class="place">
|
||||
{{ [event.street, event.city]|filter(v => v)|join(', ') ?: event.country }}
|
||||
{% if dist is defined and dist is not null %}
|
||||
· {{ dist < 1 ? (dist * 1000)|round ~ ' m' : (dist < 10 ? dist|round(1) : dist|round) ~ ' km' }} away
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
@@ -5,6 +5,18 @@
|
||||
{% block head_extra %}
|
||||
<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:20px; }
|
||||
.search-input-bar input{ flex:1; 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; }
|
||||
.location-filter{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
.location-filter form{ display:flex; gap:6px; }
|
||||
.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 select{ padding:7px 8px; border:1px solid var(--line-strong); border-radius:6px; font-family:var(--f-mono); font-size:12px; background:#fff; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,21 +25,81 @@
|
||||
<div class="wrap">
|
||||
<p class="eyebrow" style="margin-bottom:10px;">All events</p>
|
||||
<div class="toolbar">
|
||||
<h1 style="font-size:clamp(28px,4vw,40px);">What's on{{ activeCategory ? ' — ' ~ activeCategory : '' }}</h1>
|
||||
<p class="mono" style="font-size:13px; color:var(--muted);">{{ total }} event{{ total == 1 ? '' : 's' }}</p>
|
||||
<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>
|
||||
<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>
|
||||
{% if hasLocation %}
|
||||
<option value="nearest" data-href="{{ path('event_list', filters|merge({sort: 'nearest'})) }}" {{ 'nearest' == sort ? 'selected' : '' }}>Sort: Nearest</option>
|
||||
{% endif %}
|
||||
<option value="az" data-href="{{ path('event_list', filters|merge({sort: 'az'})) }}" {{ 'az' == sort ? 'selected' : '' }}>Sort: A–Z</option>
|
||||
<option value="za" data-href="{{ path('event_list', filters|merge({sort: 'za'})) }}" {{ 'za' == sort ? 'selected' : '' }}>Sort: Z–A</option>
|
||||
</select>
|
||||
</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 hasLocation %}
|
||||
<input type="hidden" name="lat" value="{{ lat }}">
|
||||
<input type="hidden" name="lon" value="{{ lon }}">
|
||||
<input type="hidden" name="range" value="{{ rangeKm }}">
|
||||
{% endif %}
|
||||
<label for="q" style="display:none;">Search events</label>
|
||||
<input id="q" name="q" type="text" value="{{ q }}" placeholder="Search by event, artist or venue…">
|
||||
<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') }}" class="chip {{ activeCategory is null ? 'is-active' : '' }}">All</a>
|
||||
<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', {category: category}) }}" class="chip {{ activeCategory == category ? 'is-active' : '' }}">{{ category }}</a>
|
||||
<a href="{{ path('event_list', filters|merge({category: category})) }}" class="chip {{ activeCategory == category ? 'is-active' : '' }}">{{ category }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<label for="catsToggle" class="chip-toggle-btn" style="margin-bottom:32px;">
|
||||
<label for="catsToggle" class="chip-toggle-btn" style="margin-bottom:20px;">
|
||||
<span class="show-more">Show all categories ↑</span>
|
||||
<span class="show-less">Show fewer categories ↑</span>
|
||||
</label>
|
||||
|
||||
<div class="filter-row">
|
||||
<div class="filter-group-inline">
|
||||
<span class="filter-label">When</span>
|
||||
<div class="chip-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group-inline">
|
||||
<span class="filter-label">Near a location</span>
|
||||
<div class="location-filter">
|
||||
<form id="locationSearchForm">
|
||||
<label for="locationSearchInput" style="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>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="useMyLocationBtn">Use my location</button>
|
||||
<select id="rangeSelect">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -35,11 +107,11 @@
|
||||
<div class="wrap">
|
||||
<div class="card-grid">
|
||||
{% for event in events %}
|
||||
{% include 'events/_card.html.twig' with { event: event } %}
|
||||
{% include 'events/_card.html.twig' with { event: event, dist: hasLocation ? distances[event.id]|default(null) : null } %}
|
||||
{% else %}
|
||||
<div class="no-results" style="grid-column:1/-1; padding:60px 20px; text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--r-card);">
|
||||
<p class="eyebrow" style="margin-bottom:8px;">No events</p>
|
||||
<p>Nothing matches this filter yet — try "All" or check back once the importer has run.</p>
|
||||
<p>Nothing matches those filters yet — try clearing one, or check back once the importer has run.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -48,8 +120,155 @@
|
||||
currentPage: page,
|
||||
totalPages: totalPages,
|
||||
routeName: 'event_list',
|
||||
routeParams: { category: activeCategory }
|
||||
routeParams: filters
|
||||
} %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
var currentLat = {{ hasLocation ? lat : 'null' }};
|
||||
var currentLon = {{ hasLocation ? lon : 'null' }};
|
||||
var rangeSelect = document.getElementById('rangeSelect');
|
||||
|
||||
// Shared with the home hero map and /map (see map.js) so a search/
|
||||
// geolocation made on any of the three sticks when moving between them.
|
||||
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, name) {
|
||||
try {
|
||||
if (window.localStorage) localStorage.setItem(LAST_LOCATION_KEY, JSON.stringify({ lat: lat, lon: lon, name: name || null }));
|
||||
} catch (e) { /* localStorage disabled/unavailable — nothing to persist to */ }
|
||||
}
|
||||
|
||||
function currentRangeKm() {
|
||||
return rangeSelect ? parseInt(rangeSelect.value, 10) : {{ rangeKm }};
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
saveLastLocation(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();
|
||||
}
|
||||
|
||||
function setStatus(text, isError) {
|
||||
var el = document.getElementById('locationStatus');
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.classList.toggle('is-error', !!isError);
|
||||
}
|
||||
|
||||
// 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('locationSearchInput');
|
||||
var query = (input.value || '').trim();
|
||||
if (!query) return;
|
||||
setStatus('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) {
|
||||
setStatus('Could not find "' + query + '"', true);
|
||||
return;
|
||||
}
|
||||
navigateWithLocation(parseFloat(results[0].lat), parseFloat(results[0].lon), currentRangeKm(), results[0].display_name);
|
||||
})
|
||||
.catch(function () {
|
||||
setStatus('Location search failed', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var useMyLocationBtn = document.getElementById('useMyLocationBtn');
|
||||
if (useMyLocationBtn) {
|
||||
useMyLocationBtn.addEventListener('click', function () {
|
||||
if (!navigator.geolocation) {
|
||||
setStatus('Location not supported by your browser', true);
|
||||
return;
|
||||
}
|
||||
setStatus('Finding you…', false);
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
navigateWithLocation(pos.coords.latitude, pos.coords.longitude, currentRangeKm(), 'Your location');
|
||||
}, function () {
|
||||
setStatus("Couldn't get your location", true);
|
||||
}, { timeout: 8000 });
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
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 () {
|
||||
try { window.localStorage && localStorage.removeItem(LAST_LOCATION_KEY); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
var sortSelect = document.getElementById('sortSelect');
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener('change', function () {
|
||||
var opt = sortSelect.options[sortSelect.selectedIndex];
|
||||
window.location.href = opt.getAttribute('data-href');
|
||||
});
|
||||
}
|
||||
|
||||
var saved = loadLastLocation();
|
||||
|
||||
// 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);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user