mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:11:09 -05:00
- Updated photo upload handling in `photo_manager.html` and `photo_upload.html` to utilize HTMX for file uploads, improving user experience and reducing reliance on Promises. - Refactored caption update and primary photo toggle methods to leverage HTMX for state updates without full page reloads. - Enhanced error handling and success notifications using HTMX events. - Replaced fetch API calls with HTMX forms in various templates, including `homepage.html`, `park_form.html`, and `roadtrip_planner.html`, to streamline AJAX interactions. - Improved search suggestion functionality in `search_script.html` by implementing HTMX for fetching suggestions, enhancing performance and user experience. - Updated designer, manufacturer, and ride model forms to handle responses with HTMX, ensuring better integration and user feedback.
282 lines
13 KiB
HTML
282 lines
13 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;
|
|
// Trigger search update if HTMX is available
|
|
if (typeof htmx !== 'undefined' && this.$refs.searchInput) {
|
|
htmx.trigger(this.$refs.searchInput, 'keyup');
|
|
}
|
|
},
|
|
selectSuggestion(suggestion) {
|
|
this.search = suggestion.name || suggestion;
|
|
this.open = false;
|
|
this.selectedIndex = -1;
|
|
// Trigger search update if HTMX is available
|
|
if (typeof htmx !== 'undefined' && this.$refs.searchInput) {
|
|
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 with ARIA -->
|
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" aria-hidden="true">
|
|
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<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 with Enhanced Accessibility -->
|
|
<input
|
|
x-ref="searchInput"
|
|
id="park-search"
|
|
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;
|
|
|
|
// Create temporary form for HTMX request
|
|
const tempForm = document.createElement('form');
|
|
tempForm.setAttribute('hx-get', '{{ autocomplete_url }}');
|
|
tempForm.setAttribute('hx-vals', JSON.stringify({q: search}));
|
|
tempForm.setAttribute('hx-trigger', 'submit');
|
|
tempForm.setAttribute('hx-swap', 'none');
|
|
|
|
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
|
try {
|
|
const data = JSON.parse(event.detail.xhr.responseText);
|
|
suggestions = data.suggestions || [];
|
|
open = suggestions.length > 0;
|
|
loading = false;
|
|
selectedIndex = -1;
|
|
} catch (error) {
|
|
loading = false;
|
|
open = false;
|
|
}
|
|
document.body.removeChild(tempForm);
|
|
});
|
|
|
|
document.body.appendChild(tempForm);
|
|
htmx.trigger(tempForm, 'submit');
|
|
{% endif %}
|
|
} else {
|
|
open = false;
|
|
suggestions = [];
|
|
selectedIndex = -1;
|
|
}
|
|
"
|
|
autocomplete="off"
|
|
role="combobox"
|
|
aria-expanded="false"
|
|
:aria-expanded="open"
|
|
aria-autocomplete="list"
|
|
aria-controls="search-suggestions"
|
|
aria-describedby="search-help-text search-live-region"
|
|
:aria-activedescendant="selectedIndex >= 0 ? `suggestion-${selectedIndex}` : null"
|
|
/>
|
|
|
|
<!-- Loading Spinner with ARIA -->
|
|
<div id="search-spinner" class="absolute inset-y-0 right-0 pr-3 flex items-center htmx-indicator" aria-hidden="true">
|
|
<svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
<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 with Enhanced Accessibility -->
|
|
<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 min-w-[44px] min-h-[44px] justify-center"
|
|
aria-label="Clear search input"
|
|
title="Clear search"
|
|
tabindex="0"
|
|
>
|
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Autocomplete Dropdown with ARIA -->
|
|
<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;"
|
|
role="listbox"
|
|
aria-label="Search suggestions"
|
|
id="search-suggestions"
|
|
>
|
|
<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 min-h-[44px] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
:class="{ 'bg-gray-100 dark:bg-gray-700': selectedIndex === index }"
|
|
@click="selectSuggestion(suggestion)"
|
|
@mouseenter="selectedIndex = index"
|
|
role="option"
|
|
:id="`suggestion-${index}`"
|
|
:aria-selected="selectedIndex === index"
|
|
:aria-label="`Select ${suggestion.name || suggestion}${suggestion.type ? ' - ' + suggestion.type : ''}`"
|
|
>
|
|
<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>
|
|
|
|
<!-- Screen Reader Support Elements -->
|
|
<div id="search-help-text" class="sr-only">
|
|
Type to search parks. Use arrow keys to navigate suggestions, Enter to select, or Escape to close.
|
|
</div>
|
|
|
|
<!-- Live Region for Screen Reader Announcements -->
|
|
<div id="search-live-region"
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
class="sr-only"
|
|
x-text="open && suggestions.length > 0 ?
|
|
`${suggestions.length} suggestion${suggestions.length !== 1 ? 's' : ''} available. Use arrow keys to navigate.` :
|
|
(search.length >= 2 && !loading && suggestions.length === 0 ? 'No suggestions found.' : '')">
|
|
</div>
|
|
</div>
|