mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:31:09 -05:00
Enhance search suggestions with request tracking and cleanup handlers
This commit is contained in:
@@ -27,15 +27,53 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.showError('An error occurred while searching. Please try again.');
|
this.showError('An error occurred while searching. Please try again.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind to popstate for browser navigation
|
// Store bound handlers for cleanup
|
||||||
window.addEventListener('popstate', () => {
|
this.boundHandlers = new Map();
|
||||||
|
|
||||||
|
// Create handler functions
|
||||||
|
const popstateHandler = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
this.searchQuery = urlParams.get('search') || '';
|
this.searchQuery = urlParams.get('search') || '';
|
||||||
this.syncFormWithUrl();
|
this.syncFormWithUrl();
|
||||||
});
|
};
|
||||||
|
this.boundHandlers.set('popstate', popstateHandler);
|
||||||
|
|
||||||
|
const errorHandler = (evt) => {
|
||||||
|
console.error('HTMX Error:', evt.detail.error);
|
||||||
|
this.showError('An error occurred while searching. Please try again.');
|
||||||
|
};
|
||||||
|
this.boundHandlers.set('htmx:error', errorHandler);
|
||||||
|
|
||||||
|
// Bind event listeners
|
||||||
|
window.addEventListener('popstate', popstateHandler);
|
||||||
|
document.body.addEventListener('htmx:error', errorHandler);
|
||||||
|
|
||||||
// Restore filters from localStorage if no URL params exist
|
// Restore filters from localStorage if no URL params exist
|
||||||
const savedFilters = localStorage.getItem('rideFilters');
|
const savedFilters = localStorage.getItem('rideFilters');
|
||||||
|
|
||||||
|
// Set up destruction handler
|
||||||
|
this.$cleanup = () => {
|
||||||
|
// Remove all bound event listeners
|
||||||
|
this.boundHandlers.forEach((handler, event) => {
|
||||||
|
if (event === 'popstate') {
|
||||||
|
window.removeEventListener(event, handler);
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener(event, handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.boundHandlers.clear();
|
||||||
|
|
||||||
|
// Cancel any pending requests
|
||||||
|
if (this.currentRequest) {
|
||||||
|
this.currentRequest.abort();
|
||||||
|
this.currentRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any pending timeouts
|
||||||
|
if (this.suggestionTimeout) {
|
||||||
|
clearTimeout(this.suggestionTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
if (savedFilters) {
|
if (savedFilters) {
|
||||||
const filters = JSON.parse(savedFilters);
|
const filters = JSON.parse(savedFilters);
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
@@ -77,21 +115,37 @@ document.addEventListener('alpine:init', () => {
|
|||||||
document.querySelector('form').dispatchEvent(new Event('change'));
|
document.querySelector('form').dispatchEvent(new Event('change'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get search suggestions
|
// Get search suggestions with request tracking
|
||||||
|
lastRequestId: 0,
|
||||||
|
currentRequest: null,
|
||||||
|
|
||||||
async getSearchSuggestions() {
|
async getSearchSuggestions() {
|
||||||
if (this.searchQuery.length < 2) {
|
if (this.searchQuery.length < 2) {
|
||||||
this.showSuggestions = false;
|
this.showSuggestions = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel any pending request
|
||||||
|
if (this.currentRequest) {
|
||||||
|
this.currentRequest.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++this.lastRequestId;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
this.currentRequest = controller;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parkSlug = document.querySelector('input[name="park_slug"]')?.value;
|
const parkSlug = document.querySelector('input[name="park_slug"]')?.value;
|
||||||
const url = `/rides/search-suggestions/?q=${encodeURIComponent(this.searchQuery)}${parkSlug ? '&park_slug=' + parkSlug : ''}`;
|
const url = `/rides/search-suggestions/?q=${encodeURIComponent(this.searchQuery)}${parkSlug ? '&park_slug=' + parkSlug : ''}`;
|
||||||
|
|
||||||
const response = await fetch(url, { signal: controller.signal });
|
const response = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'X-Request-ID': requestId.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
@@ -99,25 +153,42 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
||||||
// Ensure the response is for the current query
|
// Only update if this is still the most recent request
|
||||||
if (this.searchQuery === document.getElementById('search').value) {
|
if (requestId === this.lastRequestId && this.searchQuery === document.getElementById('search').value) {
|
||||||
document.getElementById('search-suggestions').innerHTML = html;
|
const suggestionsEl = document.getElementById('search-suggestions');
|
||||||
|
suggestionsEl.innerHTML = html;
|
||||||
this.showSuggestions = html.trim() ? true : false;
|
this.showSuggestions = html.trim() ? true : false;
|
||||||
|
|
||||||
|
// Set proper ARIA attributes
|
||||||
|
const searchInput = document.getElementById('search');
|
||||||
|
searchInput.setAttribute('aria-expanded', this.showSuggestions.toString());
|
||||||
|
searchInput.setAttribute('aria-controls', 'search-suggestions');
|
||||||
|
if (this.showSuggestions) {
|
||||||
|
suggestionsEl.setAttribute('role', 'listbox');
|
||||||
|
suggestionsEl.querySelectorAll('button').forEach(btn => {
|
||||||
|
btn.setAttribute('role', 'option');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
console.warn('Search suggestion request timed out');
|
console.warn('Search suggestion request timed out or cancelled');
|
||||||
} else {
|
} else {
|
||||||
console.error('Error fetching suggestions:', error);
|
console.error('Error fetching suggestions:', error);
|
||||||
// Show error state in UI
|
if (requestId === this.lastRequestId) {
|
||||||
document.getElementById('search-suggestions').innerHTML = `
|
const suggestionsEl = document.getElementById('search-suggestions');
|
||||||
<div class="p-2 text-sm text-red-600 dark:text-red-400">
|
suggestionsEl.innerHTML = `
|
||||||
Failed to load suggestions. Please try again.
|
<div class="p-2 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||||
</div>`;
|
Failed to load suggestions. Please try again.
|
||||||
this.showSuggestions = true;
|
</div>`;
|
||||||
|
this.showSuggestions = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
if (this.currentRequest === controller) {
|
||||||
|
this.currentRequest = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user