mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 12:51:09 -05:00
Implement park search suggestions: add autocomplete functionality and improve search input handling
This commit is contained in:
@@ -48,18 +48,43 @@
|
||||
<div class="mb-6">
|
||||
<div class="max-w-3xl mx-auto relative mb-8">
|
||||
<label for="search" class="sr-only">Search parks</label>
|
||||
<input type="search"
|
||||
name="search"
|
||||
id="search"
|
||||
class="block w-full rounded-md border-gray-300 bg-white py-3 pl-4 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Search parks by name or location..."
|
||||
hx-get="{% url 'parks:search_parks' %}?view_mode={{ view_mode|default:'grid' }}"
|
||||
hx-trigger="input delay:300ms, search"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#search-indicator"
|
||||
value="{{ request.GET.search|default:'' }}"
|
||||
aria-label="Search parks">
|
||||
<div class="relative">
|
||||
<div x-data="{
|
||||
open: false,
|
||||
query: '{{ request.GET.search|default:'' }}',
|
||||
focusedIndex: -1,
|
||||
suggestions: []
|
||||
}"
|
||||
@click.away="open = false"
|
||||
x-init="$watch('query', value => { console.log('query:', value); console.log('open:', open) })"
|
||||
class="relative">
|
||||
<input type="search"
|
||||
name="search"
|
||||
id="search"
|
||||
class="block w-full rounded-md border-gray-300 bg-white py-3 pl-4 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Search parks by name or location..."
|
||||
x-model="query"
|
||||
hx-get="{% url 'parks:search_parks' %}?view_mode={{ view_mode|default:'grid' }}"
|
||||
hx-trigger="input delay:500ms, search"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#search-indicator"
|
||||
aria-label="Search parks"
|
||||
@keydown.down.prevent="focusedIndex = Math.min(focusedIndex + 1, $refs.suggestionsList?.children.length - 1 || 0)"
|
||||
@keydown.up.prevent="focusedIndex = Math.max(focusedIndex - 1, -1)"
|
||||
@keydown.enter.prevent="if (focusedIndex >= 0) $refs.suggestionsList?.children[focusedIndex]?.click()">
|
||||
<div class="relative">
|
||||
<div hx-get="{% url 'parks:suggest_parks' %}?view_mode={{ view_mode|default:'grid' }}"
|
||||
hx-trigger="input[target.value.length > 1] delay:300ms from:input"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="closest input[name=search]"
|
||||
x-ref="suggestionsList"
|
||||
@htmx:afterRequest="open = detail.xhr.response.trim().length > 0"
|
||||
@htmx:beforeRequest="open = false"
|
||||
class="absolute top-full left-0 right-0 mt-1 z-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<div id="search-indicator" class="htmx-indicator">
|
||||
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -94,6 +119,13 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="{% static 'parks/js/search.js' %}"></script>
|
||||
{% endblock %}
|
||||
29
parks/templates/parks/partials/search_suggestions.html
Normal file
29
parks/templates/parks/partials/search_suggestions.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% if suggestions %}
|
||||
<div class="w-full bg-white rounded-md shadow-lg border border-gray-200 max-h-96 overflow-y-auto"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
@keydown.escape.window="open = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95">
|
||||
{% for park in suggestions %}
|
||||
<a href="{% url 'parks:park_detail' slug=park.slug %}"
|
||||
class="block px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer flex items-center justify-between"
|
||||
:class="{ 'bg-gray-100': focusedIndex === {{ forloop.counter0 }} }"
|
||||
hx-get="{% url 'parks:search_parks' %}?search={{ park.name }}&view_mode={{ view_mode|default:'grid' }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
@mousedown.prevent
|
||||
@click="query = '{{ park.name }}'; open = false">
|
||||
<span class="font-medium">{{ park.name }}</span>
|
||||
<span class="text-gray-500">
|
||||
{% if park.location.first.city %}{{ park.location.first.city }}, {% endif %}
|
||||
{% if park.location.first.state %}{{ park.location.first.state }}{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.urls import path, include
|
||||
from . import views
|
||||
from . import views, views_search
|
||||
from rides.views import ParkSingleCategoryListView
|
||||
|
||||
app_name = "parks"
|
||||
@@ -18,6 +18,8 @@ urlpatterns = [
|
||||
|
||||
# Areas and search endpoints for HTMX
|
||||
path("areas/", views.get_park_areas, name="get_park_areas"),
|
||||
path("suggestions/", views_search.suggest_parks, name="suggest_parks"),
|
||||
|
||||
path("search/", views.search_parks, name="search_parks"),
|
||||
|
||||
# Park detail and related views
|
||||
|
||||
32
parks/views_search.py
Normal file
32
parks/views_search.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from .filters import ParkFilter
|
||||
from .querysets import get_base_park_queryset
|
||||
|
||||
def suggest_parks(request: HttpRequest) -> HttpResponse:
|
||||
"""Return park search suggestions as a dropdown"""
|
||||
try:
|
||||
query = request.GET.get('search', '').strip()
|
||||
if not query or len(query) < 2:
|
||||
return HttpResponse('')
|
||||
|
||||
# Get current view mode from request
|
||||
current_view_mode = request.GET.get('view_mode', 'grid')
|
||||
park_filter = ParkFilter({
|
||||
'search': query
|
||||
}, queryset=get_base_park_queryset())
|
||||
|
||||
parks = park_filter.qs[:8] # Limit to 8 suggestions
|
||||
|
||||
response = render(
|
||||
request,
|
||||
'parks/partials/search_suggestions.html',
|
||||
{
|
||||
'suggestions': parks,
|
||||
'view_mode': current_view_mode
|
||||
}
|
||||
)
|
||||
response['HX-Trigger'] = 'showSuggestions'
|
||||
return response
|
||||
except Exception as e:
|
||||
return HttpResponse(f'Error getting suggestions: {str(e)}')
|
||||
Reference in New Issue
Block a user