231 lines
11 KiB
Twig
231 lines
11 KiB
Twig
{% extends 'base.html.twig' %}
|
|
|
|
{% block title %}Search — VINDN{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<style>
|
|
.search-shell{ display:grid; grid-template-columns: 260px 1fr; gap:36px; align-items:start; }
|
|
.filter-group{ padding-bottom:22px; margin-bottom:22px; border-bottom:1px solid var(--line); }
|
|
.filter-group:last-child{ border-bottom:none; }
|
|
.filter-group h4{ font-family:var(--f-mono); font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:0 0 14px; }
|
|
.filter-option{ display:flex; align-items:center; gap:10px; font-size:14px; padding:6px 0; cursor:pointer; }
|
|
.filter-option input{ accent-color: var(--accent); width:16px; height:16px; }
|
|
.results-toolbar{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; flex-wrap:wrap; gap:12px; }
|
|
.search-input-bar{ display:flex; gap:10px; margin-bottom:28px; }
|
|
.search-input-bar input{ flex:1; padding:16px 20px; border:1px solid var(--line-strong); border-radius: var(--r-pill); font-size:15px; }
|
|
.no-results{ padding:60px 20px; text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--r-card); }
|
|
#locationSearchForm{ display:flex; gap:6px; margin-bottom:10px; }
|
|
#locationSearchForm input{ flex:1; min-width:0; padding:9px 10px; border:1px solid var(--line-strong); border-radius:6px; font-size:13px; }
|
|
.range-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:10px; font-size:13px; }
|
|
.range-row select{ padding:6px 8px; border:1px solid var(--line-strong); border-radius:6px; font-family:var(--f-mono); font-size:12px; background:#fff; }
|
|
#locationStatus{ font-family:var(--f-mono); font-size:11px; color:var(--muted); margin-top:8px; }
|
|
@media (max-width: 860px){ .search-shell{ grid-template-columns:1fr; } }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<section class="section" style="padding-bottom:0;">
|
|
<div class="wrap">
|
|
<p class="eyebrow" style="margin-bottom:10px;">Search</p>
|
|
<h1 style="font-size:clamp(28px,4vw,40px); margin-bottom:24px;">
|
|
{{ q ? 'Searching: "' ~ q ~ '"' : 'Search events' }}
|
|
</h1>
|
|
|
|
<form class="search-input-bar" method="get" action="{{ path('search') }}">
|
|
{% if activeCategory %}<input type="hidden" name="category" value="{{ activeCategory }}">{% 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, venue or city…">
|
|
<button class="btn btn-primary" type="submit">Search</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section" style="padding-top:0;">
|
|
<div class="wrap">
|
|
<div class="search-shell">
|
|
|
|
<!-- FILTERS -->
|
|
<aside aria-label="Filters">
|
|
<div class="filter-group">
|
|
<h4>Near a location</h4>
|
|
<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" style="width:100%; justify-content:center;">Use my location</button>
|
|
{% if hasLocation %}
|
|
<div class="range-row">
|
|
<span>Within</span>
|
|
<select id="rangeSelect">
|
|
{% for km in rangeOptions %}
|
|
<option value="{{ km }}" {{ km == rangeKm ? 'selected' : '' }}>{{ km }} km</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<p id="locationStatus">
|
|
Near {{ lat|number_format(3) }}, {{ lon|number_format(3) }} —
|
|
<a href="{{ path('search', {q: q ?: null, category: activeCategory, when: when != 'any' ? when : null}) }}" style="color:var(--accent); text-decoration:underline;">clear</a>
|
|
</p>
|
|
{% else %}
|
|
<p id="locationStatus"></p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<form method="get" action="{{ path('search') }}" id="filterForm">
|
|
<input type="hidden" name="q" value="{{ q }}">
|
|
{% if hasLocation %}
|
|
<input type="hidden" name="lat" value="{{ lat }}">
|
|
<input type="hidden" name="lon" value="{{ lon }}">
|
|
<input type="hidden" name="range" value="{{ rangeKm }}">
|
|
{% endif %}
|
|
<div class="filter-group">
|
|
<h4>Category</h4>
|
|
<label class="filter-option">
|
|
<input type="radio" name="category" value="" {{ activeCategory ? '' : 'checked' }} onchange="this.form.submit()"> All
|
|
</label>
|
|
{% for category in categories %}
|
|
<label class="filter-option">
|
|
<input type="radio" name="category" value="{{ category }}" {{ activeCategory == category ? 'checked' : '' }} onchange="this.form.submit()"> {{ category }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="filter-group">
|
|
<h4>When</h4>
|
|
<label class="filter-option"><input type="radio" name="when" value="any" {{ when == 'any' ? 'checked' : '' }} onchange="this.form.submit()"> Any date</label>
|
|
<label class="filter-option"><input type="radio" name="when" value="today" {{ when == 'today' ? 'checked' : '' }} onchange="this.form.submit()"> Today</label>
|
|
<label class="filter-option"><input type="radio" name="when" value="week" {{ when == 'week' ? 'checked' : '' }} onchange="this.form.submit()"> Next 7 days</label>
|
|
</div>
|
|
<a href="{{ path('search') }}" class="btn btn-ghost btn-sm" style="width:100%; justify-content:center;">Clear filters</a>
|
|
</form>
|
|
</aside>
|
|
|
|
<!-- RESULTS -->
|
|
<div>
|
|
<div class="results-toolbar">
|
|
<p class="mono" style="font-size:13px; color:var(--muted);">
|
|
{{ total }} EVENT{{ total == 1 ? '' : 'S' }} MATCH{{ hasLocation ? ' · SORTED BY DISTANCE' : '' }}
|
|
</p>
|
|
</div>
|
|
|
|
{% if events|length %}
|
|
<div class="board" aria-label="Search results">
|
|
{% for event in events %}
|
|
<a class="board-row" href="{{ path('event_show', {id: event.id}) }}" aria-label="View {{ event.title }}">
|
|
<div class="board-date">
|
|
{{ event.startDateTime ? event.startDateTime|date('M j') : '—' }}
|
|
<span class="dow">{{ event.startDateTime ? event.startDateTime|date('D') : 'TBD' }}</span>
|
|
</div>
|
|
<div class="board-name">
|
|
{{ event.title }}
|
|
<span class="where">
|
|
{{ event.city }}{% if event.categoryNames|first %} · {{ event.categoryNames|first }}{% endif %}
|
|
{% if hasLocation and distances[event.id] is defined %}
|
|
{% set dist = distances[event.id] %}
|
|
· {{ dist < 1 ? (dist * 1000)|round ~ ' m' : (dist < 10 ? dist|round(1) : dist|round) ~ ' km' }} away
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="board-status">{{ event.startDateTime ? event.startDateTime|date('H:i') : '' }}</div>
|
|
<div class="board-go">→</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="no-results">
|
|
<p class="eyebrow" style="margin-bottom:8px;">No matches</p>
|
|
<p>Nothing fits those filters yet. Try clearing a filter or a different search term.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% include '_pager.html.twig' with {
|
|
currentPage: page,
|
|
totalPages: totalPages,
|
|
routeName: 'search',
|
|
routeParams: {
|
|
q: q ?: null,
|
|
category: activeCategory,
|
|
when: when != 'any' ? when : null,
|
|
lat: hasLocation ? lat : null,
|
|
lon: hasLocation ? lon : null,
|
|
range: hasLocation ? rangeKm : null
|
|
}
|
|
} %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
(function(){
|
|
var currentLat = {{ hasLocation ? lat : 'null' }};
|
|
var currentLon = {{ hasLocation ? lon : 'null' }};
|
|
|
|
function navigateWithLocation(lat, lon, rangeKm) {
|
|
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();
|
|
}
|
|
|
|
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;
|
|
var statusEl = document.getElementById('locationStatus');
|
|
if (statusEl) statusEl.textContent = 'Looking up "' + query + '"…';
|
|
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) {
|
|
if (statusEl) statusEl.textContent = 'Could not find "' + query + '"';
|
|
return;
|
|
}
|
|
navigateWithLocation(parseFloat(results[0].lat), parseFloat(results[0].lon));
|
|
})
|
|
.catch(function () {
|
|
if (statusEl) statusEl.textContent = 'Location search failed';
|
|
});
|
|
});
|
|
}
|
|
|
|
var useMyLocationBtn = document.getElementById('useMyLocationBtn');
|
|
if (useMyLocationBtn) {
|
|
useMyLocationBtn.addEventListener('click', function () {
|
|
var statusEl = document.getElementById('locationStatus');
|
|
if (!navigator.geolocation) {
|
|
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
|
|
return;
|
|
}
|
|
if (statusEl) statusEl.textContent = 'Finding you…';
|
|
navigator.geolocation.getCurrentPosition(function (pos) {
|
|
navigateWithLocation(pos.coords.latitude, pos.coords.longitude);
|
|
}, function () {
|
|
if (statusEl) statusEl.textContent = "Couldn't get your location";
|
|
}, { timeout: 8000 });
|
|
});
|
|
}
|
|
|
|
var rangeSelect = document.getElementById('rangeSelect');
|
|
if (rangeSelect) {
|
|
rangeSelect.addEventListener('change', function () {
|
|
if (currentLat === null || currentLon === null) return;
|
|
navigateWithLocation(currentLat, currentLon, parseInt(rangeSelect.value, 10));
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|