added frontend and made it usable
This commit is contained in:
1 parent
96408f00d8
commit
b9b15152bc
50 files changed
+7974
-1
No files matched your search
@@ -0,0 +1,230 @@
|
||||
{% 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 %}
|
||||
Reference in new issue
Block a user