Files
vindn.de/frontend/templates/events/list.html.twig
T

243 lines
12 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html.twig' %}
{% block title %}Browse events — VINDN{% endblock %}
{% 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 %}
{% block body %}
<section class="section" style="padding-bottom:0;">
<div class="wrap">
<p class="eyebrow" style="margin-bottom:10px;">All events</p>
<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>
<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', 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 %}
</div>
<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>
<section class="section" style="padding-top:0;">
<div class="wrap">
<div class="card-grid">
{% for event in events %}
{% 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 those filters yet — try clearing one, or check back once the importer has run.</p>
</div>
{% endfor %}
</div>
{% include '_pager.html.twig' with {
currentPage: page,
totalPages: totalPages,
routeName: 'event_list',
routeParams: filters
} %}
</div>
</section>
{% endblock %}
{% block scripts %}
<script>
(function(){
var currentLat = {{ hasLocation ? lat : 'null' }};
var currentLon = {{ hasLocation ? lon : 'null' }};
var rangeSelect = document.getElementById('rangeSelect');
// 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.
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;
}
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();
}
function setStatus(text, isError) {
var el = document.getElementById('locationStatus');
if (!el) return;
el.textContent = text;
el.classList.toggle('is-error', !!isError);
}
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;
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); }
});
});
}
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); }
});
});
}
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 () { window.VindnLocation.clear(); });
}
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 = 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);
}
})();
</script>
{% endblock %}