mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:51:08 -05:00
Refactor search functionality: remove obsolete JavaScript and HTML templates; enhance error handling and response rendering for park search results
This commit is contained in:
@@ -1,21 +1,47 @@
|
|||||||
/* Loading indicator */
|
/* Loading indicator */
|
||||||
.htmx-indicator {
|
.htmx-indicator {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.htmx-request .htmx-indicator {
|
.htmx-request .htmx-indicator {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search results spacing */
|
/* Search results container */
|
||||||
#search-results {
|
#search-results {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search result items */
|
||||||
|
#search-results .border-b {
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results a {
|
||||||
|
display: block;
|
||||||
|
transition: background-color 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results a:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode adjustments */
|
/* Dark mode adjustments */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
#search-results .bg-white {
|
#search-results {
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
|
border-color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-results .text-gray-900 {
|
#search-results .text-gray-900 {
|
||||||
@@ -26,7 +52,11 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-results .hover\:bg-gray-50:hover {
|
#search-results .border-b {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results a:hover {
|
||||||
background-color: #374151;
|
background-color: #374151;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const searchInput = document.getElementById('search');
|
|
||||||
const searchResults = document.getElementById('search-results');
|
|
||||||
|
|
||||||
if (!searchInput || !searchResults) return;
|
|
||||||
|
|
||||||
// Clear search results when clicking outside
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchResults.contains(e.target) && e.target !== searchInput) {
|
|
||||||
searchResults.innerHTML = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear results on escape key
|
|
||||||
searchInput.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
searchResults.innerHTML = '';
|
|
||||||
searchInput.value = '';
|
|
||||||
searchInput.blur();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle back button
|
|
||||||
window.addEventListener('popstate', function() {
|
|
||||||
searchResults.innerHTML = '';
|
|
||||||
searchInput.value = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -38,19 +38,54 @@ Browse and filter amusement parks, theme parks, and water parks from around the
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
{# Quick Search #}
|
{# Quick Search #}
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto"
|
||||||
|
x-data="{
|
||||||
|
selectedIndex: -1,
|
||||||
|
results: [],
|
||||||
|
get hasResults() { return this.results.length > 0 },
|
||||||
|
init() {
|
||||||
|
this.$watch('results', () => this.selectedIndex = -1)
|
||||||
|
},
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (!this.hasResults) return
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1)
|
||||||
|
}
|
||||||
|
else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
else if (e.key === 'Enter' && this.selectedIndex >= 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.$refs[`result-${this.selectedIndex}`]?.click()
|
||||||
|
}
|
||||||
|
else if (e.key === 'Escape') {
|
||||||
|
this.results = []
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@click.away="results = []">
|
||||||
<label for="search" class="sr-only">Search parks</label>
|
<label for="search" class="sr-only">Search parks</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="search"
|
<input type="search"
|
||||||
name="search"
|
name="search"
|
||||||
id="search"
|
id="search"
|
||||||
|
x-ref="input"
|
||||||
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"
|
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..."
|
placeholder="Search parks by name or location..."
|
||||||
hx-get="{% url 'parks:search_parks' %}"
|
hx-get="{% url 'parks:search_parks' %}"
|
||||||
hx-trigger="keyup changed delay:300ms, search"
|
hx-trigger="keyup changed delay:300ms, search"
|
||||||
hx-target="#search-results"
|
hx-target="#search-results"
|
||||||
hx-indicator="#search-indicator"
|
hx-indicator="#search-indicator"
|
||||||
autocomplete="off">
|
@keydown="onKeyDown($event)"
|
||||||
|
autocomplete="off"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="search-results"
|
||||||
|
aria-autocomplete="list">
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
<div id="search-indicator" class="htmx-indicator">
|
<div id="search-indicator" class="htmx-indicator">
|
||||||
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
|
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
|
||||||
@@ -60,7 +95,10 @@ Browse and filter amusement parks, theme parks, and water parks from around the
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-results" class="mt-2"></div>
|
<div id="search-results"
|
||||||
|
class="mt-2"
|
||||||
|
role="listbox"
|
||||||
|
x-init="$watch('$el.children', value => results = Array.from(value))"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,7 +125,7 @@ Browse and filter amusement parks, theme parks, and water parks from around the
|
|||||||
{% block results_section %}
|
{% block results_section %}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{% for park in parks %}
|
{% for park in parks %}
|
||||||
{% include "search/partials/park_results.html" with park=park %}
|
{% include "parks/partials/park_list_item.html" with park=park %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
78
parks/templates/parks/partials/park_list_item.html
Normal file
78
parks/templates/parks/partials/park_list_item.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{% if error %}
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="inline-flex items-center px-4 py-2 rounded-md bg-red-50 text-red-700">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="divide-y">
|
||||||
|
{% for park in object_list|default:parks %}
|
||||||
|
<div role="option"
|
||||||
|
id="result-{{ forloop.counter0 }}"
|
||||||
|
:class="{ 'bg-gray-50 dark:bg-gray-800': selectedIndex === {{ forloop.counter0 }} }"
|
||||||
|
class="p-4 flex items-start space-x-4">
|
||||||
|
{% if park.photos.exists %}
|
||||||
|
<img src="{{ park.photos.first.image.url }}"
|
||||||
|
alt="{{ park.name }}"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg">
|
||||||
|
{% else %}
|
||||||
|
<div class="w-24 h-24 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-2xl font-medium text-gray-400">{{ park.name|first|upper }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="text-lg font-semibold truncate">
|
||||||
|
<a href="{% url 'parks:park_detail' park.slug %}"
|
||||||
|
x-ref="result-{{ forloop.counter0 }}"
|
||||||
|
:class="{ 'bg-gray-50 dark:bg-gray-800': selectedIndex === {{ forloop.counter0 }} }"
|
||||||
|
class="hover:text-blue-600 block w-full py-1 px-2 -mx-2 rounded">
|
||||||
|
{{ park.name }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mt-1 text-sm text-gray-500 truncate">
|
||||||
|
{% with location=park.location.first %}
|
||||||
|
{% if location %}
|
||||||
|
{{ location.city }}{% if location.state %}, {{ location.state }}{% endif %}{% if location.country %}, {{ location.country }}{% endif %}
|
||||||
|
{% else %}
|
||||||
|
Location unknown
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }}">
|
||||||
|
{{ park.get_status_display }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if park.opening_date %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||||
|
Opened {{ park.opening_date|date:"Y" }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if park.ride_count %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
{{ park.ride_count }} rides
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if park.coaster_count %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||||
|
{{ park.coaster_count }} coasters
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="p-4 text-sm text-gray-500 text-center">
|
||||||
|
No parks found matching your search.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{% if error %}
|
|
||||||
<div class="text-red-600 bg-red-50 p-4 rounded-md" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="space-y-4">
|
|
||||||
{% for park in parks %}
|
|
||||||
<div class="bg-white shadow sm:rounded-lg">
|
|
||||||
<a href="{% url 'parks:park_detail' park.slug %}" class="block px-4 py-4 hover:bg-gray-50">
|
|
||||||
<div class="flex items-start space-x-4">
|
|
||||||
{% if park.photos.exists %}
|
|
||||||
<img src="{{ park.photos.first.image.url }}"
|
|
||||||
alt="{{ park.name }}"
|
|
||||||
class="h-20 w-20 object-cover rounded-lg">
|
|
||||||
{% else %}
|
|
||||||
<div class="h-20 w-20 bg-gray-100 rounded-lg flex items-center justify-center">
|
|
||||||
<span class="text-2xl font-medium text-gray-400">{{ park.name|first|upper }}</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<h3 class="font-medium text-gray-900">{{ park.name }}</h3>
|
|
||||||
<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium {{ park.get_status_color }}">
|
|
||||||
{{ park.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% with location=park.location.first %}
|
|
||||||
{% if location %}
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
{{ location.city }}{% if location.state %}, {{ location.state }}{% endif %}
|
|
||||||
{% if location.country %}, {{ location.country }}{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<div class="mt-2 flex items-center space-x-2 text-sm text-gray-500">
|
|
||||||
{% if park.ride_count %}
|
|
||||||
<span>{{ park.ride_count }} rides</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if park.coaster_count %}
|
|
||||||
<span>•</span>
|
|
||||||
<span>{{ park.coaster_count }} coasters</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
<div class="text-center py-8 bg-white shadow sm:rounded-lg">
|
|
||||||
<p class="text-gray-500">No parks found matching your search.</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -74,17 +74,20 @@ def search_parks(request: HttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
parks = park_filter.qs[:8] # Limit to 8 suggestions
|
parks = park_filter.qs[:8] # Limit to 8 suggestions
|
||||||
|
|
||||||
response = render(request, "parks/partials/park_search_results.html", {
|
if not parks:
|
||||||
"parks": parks,
|
return HttpResponse(
|
||||||
"is_quick_search": True
|
'<div class="p-4 text-sm text-gray-500">No parks found matching your search.</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = render(request, "parks/partials/park_list_item.html", {
|
||||||
|
"object_list": parks # Use object_list to match template's for loop
|
||||||
})
|
})
|
||||||
response['HX-Trigger'] = 'searchComplete'
|
response['HX-Trigger'] = 'searchComplete'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
response = render(request, "parks/partials/park_search_results.html", {
|
response = render(request, "parks/partials/park_list_item.html", {
|
||||||
"error": f"Error performing search: {str(e)}",
|
"error": f"Error performing search: {str(e)}"
|
||||||
"is_quick_search": True
|
|
||||||
})
|
})
|
||||||
response['HX-Trigger'] = 'searchError'
|
response['HX-Trigger'] = 'searchError'
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<div class="divide-y">
|
|
||||||
{% if error %}
|
|
||||||
<div class="p-8 text-center">
|
|
||||||
<div class="inline-flex items-center px-4 py-2 rounded-md bg-red-50 text-red-700">
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% for park in object_list %}
|
|
||||||
<div class="p-4 flex items-start space-x-4">
|
|
||||||
{% if park.photos.exists %}
|
|
||||||
<img src="{{ park.photos.first.image.url }}"
|
|
||||||
alt="{{ park.name }}"
|
|
||||||
class="w-24 h-24 object-cover rounded-lg">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-lg font-semibold">
|
|
||||||
<a href="{{ park.get_absolute_url }}">{{ park.name }}</a>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="mt-1 text-sm text-gray-500">
|
|
||||||
{% with location=park.location.first %}
|
|
||||||
{% if location %}
|
|
||||||
{{ location.city }}{% if location.state %}, {{ location.state }}{% endif %}{% if location.country %}, {{ location.country }}{% endif %}
|
|
||||||
{% else %}
|
|
||||||
Location unknown
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }}">
|
|
||||||
{{ park.get_status_display }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{% if park.opening_date %}
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
||||||
Opened {{ park.opening_date|date:"Y" }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if park.total_rides %}
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
||||||
{{ park.total_rides }} rides
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if park.total_coasters %}
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
||||||
{{ park.total_coasters }} coasters
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if park.average_rating %}
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
{{ park.average_rating }} ★
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
<div class="p-8 text-center text-gray-500">
|
|
||||||
No parks found matching your criteria
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
Reference in New Issue
Block a user