mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 18:11:08 -05:00
Implement RideUpdateView and refactor search suggestion handling for improved modularity and error management
This commit is contained in:
@@ -1,4 +1,17 @@
|
|||||||
# Keep all imports and previous classes up to RideCreateView
|
# Keep all imports and previous classes up to RideCreateView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views.generic import UpdateView
|
||||||
|
from companies.models import Manufacturer
|
||||||
|
from designers.models import Designer
|
||||||
|
from moderation.mixins import EditSubmissionMixin
|
||||||
|
from apps.parks.mixins.base import ParkContextRequired # type: ignore
|
||||||
|
from moderation.models import EditSubmission
|
||||||
|
from parks.models import Park
|
||||||
|
from rides.forms import RideForm # type: ignore
|
||||||
|
from .models import Ride, RideModel
|
||||||
|
|
||||||
class RideUpdateView(LoginRequiredMixin, ParkContextRequired, EditSubmissionMixin, UpdateView):
|
class RideUpdateView(LoginRequiredMixin, ParkContextRequired, EditSubmissionMixin, UpdateView):
|
||||||
"""View for updating an existing ride"""
|
"""View for updating an existing ride"""
|
||||||
|
|||||||
@@ -116,53 +116,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
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 response = await this.fetchSuggestions(controller, requestId);
|
||||||
const url = `/rides/search-suggestions/?q=${encodeURIComponent(this.searchQuery)}${parkSlug ? '&park_slug=' + parkSlug : ''}`;
|
await this.handleSuggestionResponse(response, requestId);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
'X-Request-ID': requestId.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
// Only update if this is still the most recent request
|
|
||||||
if (requestId === this.lastRequestId && this.searchQuery === document.getElementById('search').value) {
|
|
||||||
const suggestionsEl = document.getElementById('search-suggestions');
|
|
||||||
suggestionsEl.innerHTML = html;
|
|
||||||
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') {
|
this.handleSuggestionError(error, requestId);
|
||||||
console.warn('Search suggestion request timed out or cancelled');
|
|
||||||
} else {
|
|
||||||
console.error('Error fetching suggestions:', error);
|
|
||||||
if (requestId === this.lastRequestId) {
|
|
||||||
const suggestionsEl = document.getElementById('search-suggestions');
|
|
||||||
suggestionsEl.innerHTML = `
|
|
||||||
<div class="p-2 text-sm text-red-600 dark:text-red-400" role="alert">
|
|
||||||
Failed to load suggestions. Please try again.
|
|
||||||
</div>`;
|
|
||||||
this.showSuggestions = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (this.currentRequest === controller) {
|
if (this.currentRequest === controller) {
|
||||||
@@ -171,6 +128,64 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchSuggestions(controller, requestId) {
|
||||||
|
const parkSlug = document.querySelector('input[name="park_slug"]')?.value;
|
||||||
|
const url = `/rides/search-suggestions/?q=${encodeURIComponent(this.searchQuery)}${parkSlug ? '&park_slug=' + parkSlug : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'X-Request-ID': requestId.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleSuggestionResponse(response, requestId) {
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
if (requestId === this.lastRequestId && this.searchQuery === document.getElementById('search').value) {
|
||||||
|
const suggestionsEl = document.getElementById('search-suggestions');
|
||||||
|
suggestionsEl.innerHTML = html;
|
||||||
|
this.showSuggestions = Boolean(html.trim());
|
||||||
|
|
||||||
|
this.updateAriaAttributes(suggestionsEl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAriaAttributes(suggestionsEl) {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSuggestionError(error, requestId) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('Search suggestion request timed out or cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Error fetching suggestions:', error);
|
||||||
|
if (requestId === this.lastRequestId) {
|
||||||
|
const suggestionsEl = document.getElementById('search-suggestions');
|
||||||
|
suggestionsEl.innerHTML = `
|
||||||
|
<div class="p-2 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||||
|
Failed to load suggestions. Please try again.
|
||||||
|
</div>`;
|
||||||
|
this.showSuggestions = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Handle input changes with debounce
|
// Handle input changes with debounce
|
||||||
async handleInput() {
|
async handleInput() {
|
||||||
clearTimeout(this.suggestionTimeout);
|
clearTimeout(this.suggestionTimeout);
|
||||||
@@ -277,24 +292,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- HTMX Loading Indicator Styles -->
|
|
||||||
<style>
|
|
||||||
.htmx-indicator {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 200ms ease-in;
|
|
||||||
}
|
|
||||||
.htmx-request .htmx-indicator {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Loading Indicator */
|
|
||||||
.loading-indicator {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
z-index: 50;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
performCleanup() {
|
performCleanup() {
|
||||||
@@ -324,6 +321,23 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- HTMX Loading Indicator Styles -->
|
||||||
|
<style>
|
||||||
|
.htmx-indicator {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in;
|
||||||
|
}
|
||||||
|
.htmx-request .htmx-indicator {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Loading Indicator */
|
||||||
|
.loading-indicator {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user