Files
thrillwiki_django_no_react/templates/cotton/enhanced_search.html
pac7 d978217577 Enhance website's visual appeal and mobile responsiveness with style updates
Update CSS styles across various components to improve visual presentation and ensure better responsiveness on mobile devices, including adjustments to spacing, aspect ratios, and element sizing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c446bc9e-66df-438c-a86c-f53e6da13649
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
2025-09-23 22:11:05 +00:00

232 lines
9.9 KiB
HTML

{% comment %}
Enhanced Search Component - Django Cotton Version
Advanced search input with autocomplete suggestions, debouncing, and loading states.
Provides real-time search with suggestions and filtering integration.
Usage Examples:
<c-enhanced_search
placeholder="Search parks by name, location, or features..."
current_value=""
/>
<c-enhanced_search
placeholder="Find your perfect park..."
current_value="disney"
autocomplete_url="/parks/suggest/"
class="custom-class"
/>
Parameters:
- placeholder: Search input placeholder text (default: "Search parks...")
- current_value: Current search value (optional)
- autocomplete_url: URL for autocomplete suggestions (optional)
- debounce_delay: Debounce delay in milliseconds (default: 300)
- class: Additional CSS classes (optional)
Features:
- Real-time search with debouncing
- Autocomplete dropdown with suggestions
- Loading states and indicators
- HTMX integration for seamless search
- Keyboard navigation support
- Clear button functionality
{% endcomment %}
<c-vars
placeholder="Search parks..."
current_value=""
autocomplete_url=""
debounce_delay="300"
class=""
/>
<div class="relative w-full {{ class }}"
x-data="{
open: false,
search: '{{ current_value }}',
suggestions: [],
loading: false,
selectedIndex: -1,
clearSearch() {
this.search = '';
this.open = false;
this.suggestions = [];
this.selectedIndex = -1;
htmx.trigger(this.$refs.searchInput, 'keyup');
},
selectSuggestion(suggestion) {
this.search = suggestion.name || suggestion;
this.open = false;
this.selectedIndex = -1;
htmx.trigger(this.$refs.searchInput, 'keyup');
},
handleKeydown(event) {
if (!this.open) return;
switch(event.key) {
case 'ArrowDown':
event.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
break;
case 'Enter':
event.preventDefault();
if (this.selectedIndex >= 0 && this.suggestions[this.selectedIndex]) {
this.selectSuggestion(this.suggestions[this.selectedIndex]);
}
break;
case 'Escape':
this.open = false;
this.selectedIndex = -1;
break;
}
}
}"
@click.away="open = false">
<div class="relative">
<!-- Search Icon -->
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<!-- Search Input -->
<input
x-ref="searchInput"
type="text"
name="search"
x-model="search"
placeholder="{{ placeholder }}"
class="block w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-base sm:text-sm min-h-[44px] sm:min-h-0"
hx-get="{% url 'parks:park_list' %}"
hx-trigger="keyup changed delay:{{ debounce_delay }}ms"
hx-target="#park-results"
hx-include="[name='view_mode'], [name='status'], [name='operator'], [name='ordering']"
hx-indicator="#search-spinner"
hx-push-url="true"
@keydown="handleKeydown"
@input="
if (search.length >= 2) {
{% if autocomplete_url %}
loading = true;
fetch('{{ autocomplete_url }}?q=' + encodeURIComponent(search))
.then(response => response.json())
.then(data => {
suggestions = data.suggestions || [];
open = suggestions.length > 0;
loading = false;
selectedIndex = -1;
})
.catch(() => {
loading = false;
open = false;
});
{% endif %}
} else {
open = false;
suggestions = [];
selectedIndex = -1;
}
"
autocomplete="off"
/>
<!-- Loading Spinner -->
<div id="search-spinner" class="absolute inset-y-0 right-0 pr-3 flex items-center htmx-indicator">
<svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<!-- Clear Button -->
<button
x-show="search.length > 0"
@click="clearSearch()"
type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors htmx-indicator:hidden"
aria-label="Clear search"
title="Clear search"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Autocomplete Dropdown -->
<div
x-show="open && suggestions.length > 0"
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"
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 max-h-60 overflow-y-auto"
style="display: none;"
>
<div class="py-1">
<template x-for="(suggestion, index) in suggestions" :key="index">
<button
type="button"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 flex items-center justify-between"
:class="{ 'bg-gray-100 dark:bg-gray-700': selectedIndex === index }"
@click="selectSuggestion(suggestion)"
@mouseenter="selectedIndex = index"
>
<span x-text="suggestion.name || suggestion"></span>
<template x-if="suggestion.type">
<span class="text-xs text-gray-500 dark:text-gray-400 capitalize" x-text="suggestion.type"></span>
</template>
</button>
</template>
</div>
<!-- Quick Filters -->
{% if autocomplete_url %}
<div class="border-t border-gray-200 dark:border-gray-700 p-2">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Quick Filters:</div>
<div class="flex flex-wrap gap-1">
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?has_coasters=True"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Parks with Coasters
</button>
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?min_rating=4"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Highly Rated
</button>
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?park_type=disney"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Disney Parks
</button>
</div>
</div>
{% endif %}
</div>
</div>