Refactor photo management and upload functionality to use HTMX for asynchronous requests

- 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.
This commit is contained in:
pacnpal
2025-09-26 10:18:56 -04:00
parent 8aa56c463a
commit 12deafaa09
18 changed files with 1103 additions and 577 deletions

View File

@@ -123,18 +123,30 @@ Features:
if (search.length >= 2) {
{% if autocomplete_url %}
loading = true;
fetch('{{ autocomplete_url }}?q=' + encodeURIComponent(search))
.then(response => response.json())
.then(data => {
// 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(() => {
} catch (error) {
loading = false;
open = false;
});
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
{% endif %}
} else {
open = false;

View File

@@ -147,15 +147,28 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Handle map clicks
map.on('click', async function(e) {
map.on('click', function(e) {
const { lat, lng } = e.latlng;
try {
const response = await fetch(`/parks/search/reverse-geocode/?lat=${lat}&lon=${lng}`);
const data = await response.json();
updateLocation(lat, lng, data);
} catch (error) {
console.error('Reverse geocoding failed:', error);
}
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
tempForm.setAttribute('hx-vals', JSON.stringify({lat: lat, lon: lng}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
updateLocation(lat, lng, data);
} catch (error) {
console.error('Reverse geocoding failed:', error);
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
});
}
@@ -172,43 +185,55 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
searchTimeout = setTimeout(function() {
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '/parks/search/location/');
tempForm.setAttribute('hx-vals', JSON.stringify({q: query}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</div>
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
}
} catch (error) {
console.error('Search failed:', error);
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}, 300);
});

View File

@@ -533,12 +533,22 @@ class NearbyMap {
}
showLocationDetails(type, id) {
htmx.ajax('GET', `{% url 'maps:htmx_location_detail' 'type' 0 %}`.replace('type', type).replace('0', id), {
target: '#location-modal',
swap: 'innerHTML'
}).then(() => {
document.getElementById('location-modal').classList.remove('hidden');
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', `{% url 'maps:htmx_location_detail' 'type' 0 %}`.replace('type', type).replace('0', id));
tempForm.setAttribute('hx-target', '#location-modal');
tempForm.setAttribute('hx-swap', 'innerHTML');
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
document.getElementById('location-modal').classList.remove('hidden');
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
}
@@ -578,4 +588,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -375,38 +375,61 @@ class ParkMap {
});
}
async loadMapData() {
loadMapData() {
try {
document.getElementById('map-loading').style.display = 'flex';
const formData = new FormData(document.getElementById('park-filters'));
const params = new URLSearchParams();
const queryParams = {};
// Add form data to params
for (let [key, value] of formData.entries()) {
params.append(key, value);
queryParams[key] = value;
}
// Add map bounds
const bounds = this.map.getBounds();
params.append('north', bounds.getNorth());
params.append('south', bounds.getSouth());
params.append('east', bounds.getEast());
params.append('west', bounds.getWest());
params.append('zoom', this.map.getZoom());
queryParams.north = bounds.getNorth();
queryParams.south = bounds.getSouth();
queryParams.east = bounds.getEast();
queryParams.west = bounds.getWest();
queryParams.zoom = this.map.getZoom();
const response = await fetch(`{{ map_api_urls.locations }}?${params}`);
const data = await response.json();
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '{{ map_api_urls.locations }}');
tempForm.setAttribute('hx-vals', JSON.stringify(queryParams));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
if (data.status === 'success') {
this.updateMarkers(data.data);
this.updateStats(data.data);
} else {
console.error('Park data error:', data.message);
}
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.status === 'success') {
this.updateMarkers(data.data);
this.updateStats(data.data);
} else {
console.error('Park data error:', data.message);
}
} catch (error) {
console.error('Failed to load park data:', error);
} finally {
document.getElementById('map-loading').style.display = 'none';
document.body.removeChild(tempForm);
}
});
tempForm.addEventListener('htmx:error', (event) => {
console.error('Failed to load park data:', event.detail.error);
document.getElementById('map-loading').style.display = 'none';
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (error) {
console.error('Failed to load park data:', error);
} finally {
document.getElementById('map-loading').style.display = 'none';
}
}
@@ -536,12 +559,27 @@ class ParkMap {
}
showParkDetails(parkId) {
htmx.ajax('GET', `{% url 'maps:htmx_location_detail' 'park' 0 %}`.replace('0', parkId), {
target: '#location-modal',
swap: 'innerHTML'
}).then(() => {
document.getElementById('location-modal').classList.remove('hidden');
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', `{% url 'maps:htmx_location_detail' 'park' 0 %}`.replace('0', parkId));
tempForm.setAttribute('hx-target', '#location-modal');
tempForm.setAttribute('hx-swap', 'innerHTML');
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.successful) {
document.getElementById('location-modal').classList.remove('hidden');
}
document.body.removeChild(tempForm);
});
tempForm.addEventListener('htmx:error', (event) => {
console.error('Failed to load park details:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
updateMapBounds() {
@@ -615,4 +653,4 @@ document.addEventListener('DOMContentLoaded', function() {
border-color: #374151;
}
</style>
{% endblock %}
{% endblock %}

View File

@@ -471,11 +471,12 @@ window.shareLocation = function(type, id) {
});
} else {
// Fallback: copy to clipboard
navigator.clipboard.writeText(url).then(() => {
try {
navigator.clipboard.writeText(url);
showPopupFeedback('Link copied to clipboard!', 'success');
}).catch(() => {
} catch (error) {
showPopupFeedback('Could not copy link', 'error');
});
}
}
};
@@ -527,4 +528,4 @@ if (!document.getElementById('popup-animations')) {
`;
document.head.appendChild(style);
}
</script>
</script>

View File

@@ -293,37 +293,60 @@ class ThrillWikiMap {
});
}
async loadMapData() {
loadMapData() {
try {
document.getElementById('map-loading').style.display = 'flex';
const formData = new FormData(document.getElementById('map-filters'));
const params = new URLSearchParams();
const queryParams = {};
// Add form data to params
for (let [key, value] of formData.entries()) {
params.append(key, value);
queryParams[key] = value;
}
// Add map bounds
const bounds = this.map.getBounds();
params.append('north', bounds.getNorth());
params.append('south', bounds.getSouth());
params.append('east', bounds.getEast());
params.append('west', bounds.getWest());
params.append('zoom', this.map.getZoom());
queryParams.north = bounds.getNorth();
queryParams.south = bounds.getSouth();
queryParams.east = bounds.getEast();
queryParams.west = bounds.getWest();
queryParams.zoom = this.map.getZoom();
const response = await fetch(`{{ map_api_urls.locations }}?${params}`);
const data = await response.json();
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '{{ map_api_urls.locations }}');
tempForm.setAttribute('hx-vals', JSON.stringify(queryParams));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
if (data.status === 'success') {
this.updateMarkers(data.data);
} else {
console.error('Map data error:', data.message);
}
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.status === 'success') {
this.updateMarkers(data.data);
} else {
console.error('Map data error:', data.message);
}
} catch (error) {
console.error('Failed to load map data:', error);
} finally {
document.getElementById('map-loading').style.display = 'none';
document.body.removeChild(tempForm);
}
});
tempForm.addEventListener('htmx:error', (event) => {
console.error('Failed to load map data:', event.detail.error);
document.getElementById('map-loading').style.display = 'none';
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (error) {
console.error('Failed to load map data:', error);
} finally {
document.getElementById('map-loading').style.display = 'none';
}
}
@@ -410,12 +433,27 @@ class ThrillWikiMap {
}
showLocationDetails(type, id) {
htmx.ajax('GET', `{% url 'maps:htmx_location_detail' 'TYPE' 0 %}`.replace('TYPE', type).replace('0', id), {
target: '#location-modal',
swap: 'innerHTML'
}).then(() => {
document.getElementById('location-modal').classList.remove('hidden');
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', `{% url 'maps:htmx_location_detail' 'TYPE' 0 %}`.replace('TYPE', type).replace('0', id));
tempForm.setAttribute('hx-target', '#location-modal');
tempForm.setAttribute('hx-swap', 'innerHTML');
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.successful) {
document.getElementById('location-modal').classList.remove('hidden');
}
document.body.removeChild(tempForm);
});
tempForm.addEventListener('htmx:error', (event) => {
console.error('Failed to load location details:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
updateMapBounds() {
@@ -501,4 +539,4 @@ document.addEventListener('DOMContentLoaded', function() {
border-color: #374151;
}
</style>
{% endblock %}
{% endblock %}

View File

@@ -126,7 +126,7 @@ document.addEventListener('alpine:init', () => {
error: null,
showSuccess: false,
async handleFileSelect(event) {
handleFileSelect(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
@@ -146,23 +146,83 @@ document.addEventListener('alpine:init', () => {
formData.append('object_id', objectId);
try {
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
},
body: formData
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', uploadUrl);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.enctype = 'multipart/form-data';
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
// Add form data
const imageInput = document.createElement('input');
imageInput.type = 'file';
imageInput.name = 'image';
const dt = new DataTransfer();
dt.items.add(file);
imageInput.files = dt.files;
tempForm.appendChild(imageInput);
const appLabelInput = document.createElement('input');
appLabelInput.type = 'hidden';
appLabelInput.name = 'app_label';
appLabelInput.value = contentType.split('.')[0];
tempForm.appendChild(appLabelInput);
const modelInput = document.createElement('input');
modelInput.type = 'hidden';
modelInput.name = 'model';
modelInput.value = contentType.split('.')[1];
tempForm.appendChild(modelInput);
const objectIdInput = document.createElement('input');
objectIdInput.type = 'hidden';
objectIdInput.name = 'object_id';
objectIdInput.value = objectId;
tempForm.appendChild(objectIdInput);
// Use HTMX event listeners instead of Promise
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
const photo = JSON.parse(event.detail.xhr.responseText);
this.photos.push(photo);
completedFiles++;
this.uploadProgress = (completedFiles / totalFiles) * 100;
if (completedFiles === totalFiles) {
this.uploading = false;
this.showSuccess = true;
setTimeout(() => {
this.showSuccess = false;
}, 3000);
}
} else {
const data = JSON.parse(event.detail.xhr.responseText);
this.error = data.error || 'Upload failed';
this.uploading = false;
}
} catch (err) {
this.error = err.message || 'Upload failed';
this.uploading = false;
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Upload failed');
}
const photo = await response.json();
this.photos.push(photo);
completedFiles++;
this.uploadProgress = (completedFiles / totalFiles) * 100;
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Upload failed';
this.uploading = false;
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to upload photo. Please try again.';
console.error('Upload error:', err);
@@ -181,72 +241,125 @@ document.addEventListener('alpine:init', () => {
}
},
async updateCaption(photo) {
updateCaption(photo) {
try {
const response = await fetch(`${uploadUrl}${photo.id}/caption/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
caption: photo.caption
})
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/caption/`);
tempForm.setAttribute('hx-vals', JSON.stringify({ caption: photo.caption }));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status < 200 || event.detail.xhr.status >= 300) {
this.error = 'Failed to update caption';
console.error('Caption update error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to update caption');
}
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to update caption';
console.error('Caption update error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to update caption';
console.error('Caption update error:', err);
}
},
async togglePrimary(photo) {
togglePrimary(photo) {
try {
const response = await fetch(`${uploadUrl}${photo.id}/primary/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/primary/`);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
// Update local state
this.photos = this.photos.map(p => ({
...p,
is_primary: p.id === photo.id
}));
} else {
this.error = 'Failed to update primary photo';
console.error('Primary photo update error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to update primary photo');
}
// Update local state
this.photos = this.photos.map(p => ({
...p,
is_primary: p.id === photo.id
}));
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to update primary photo';
console.error('Primary photo update error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to update primary photo';
console.error('Primary photo update error:', err);
}
},
async deletePhoto(photo) {
deletePhoto(photo) {
if (!confirm('Are you sure you want to delete this photo?')) {
return;
}
try {
const response = await fetch(`${uploadUrl}${photo.id}/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-delete', `${uploadUrl}${photo.id}/`);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
// Update local state
this.photos = this.photos.filter(p => p.id !== photo.id);
} else {
this.error = 'Failed to delete photo';
console.error('Delete error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to delete photo');
}
// Update local state
this.photos = this.photos.filter(p => p.id !== photo.id);
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to delete photo';
console.error('Delete error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to delete photo';
console.error('Delete error:', err);

View File

@@ -128,7 +128,7 @@ document.addEventListener('alpine:init', () => {
return this.photos.length < maxFiles;
},
async handleFileSelect(event) {
handleFileSelect(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
@@ -152,23 +152,79 @@ document.addEventListener('alpine:init', () => {
formData.append('object_id', objectId);
try {
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
},
body: formData
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', uploadUrl);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.enctype = 'multipart/form-data';
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
// Add form data
const imageInput = document.createElement('input');
imageInput.type = 'file';
imageInput.name = 'image';
const dt = new DataTransfer();
dt.items.add(file);
imageInput.files = dt.files;
tempForm.appendChild(imageInput);
const appLabelInput = document.createElement('input');
appLabelInput.type = 'hidden';
appLabelInput.name = 'app_label';
appLabelInput.value = contentType.split('.')[0];
tempForm.appendChild(appLabelInput);
const modelInput = document.createElement('input');
modelInput.type = 'hidden';
modelInput.name = 'model';
modelInput.value = contentType.split('.')[1];
tempForm.appendChild(modelInput);
const objectIdInput = document.createElement('input');
objectIdInput.type = 'hidden';
objectIdInput.name = 'object_id';
objectIdInput.value = objectId;
tempForm.appendChild(objectIdInput);
// Use HTMX event listeners instead of Promise
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
const photo = JSON.parse(event.detail.xhr.responseText);
this.photos.push(photo);
completedFiles++;
this.uploadProgress = (completedFiles / totalFiles) * 100;
if (completedFiles === totalFiles) {
this.uploading = false;
}
} else {
const data = JSON.parse(event.detail.xhr.responseText);
this.error = data.error || 'Upload failed';
this.uploading = false;
}
} catch (err) {
this.error = err.message || 'Upload failed';
this.uploading = false;
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Upload failed');
}
const photo = await response.json();
this.photos.push(photo);
completedFiles++;
this.uploadProgress = (completedFiles / totalFiles) * 100;
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Upload failed';
this.uploading = false;
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to upload photo. Please try again.';
console.error('Upload error:', err);
@@ -179,25 +235,43 @@ document.addEventListener('alpine:init', () => {
event.target.value = ''; // Reset file input
},
async togglePrimary(photo) {
togglePrimary(photo) {
try {
const response = await fetch(`${uploadUrl}${photo.id}/primary/`, { // Added trailing slash
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/primary/`);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
// Update local state
this.photos = this.photos.map(p => ({
...p,
is_primary: p.id === photo.id
}));
} else {
this.error = 'Failed to update primary photo';
console.error('Primary photo update error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to update primary photo');
}
// Update local state
this.photos = this.photos.map(p => ({
...p,
is_primary: p.id === photo.id
}));
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to update primary photo';
console.error('Primary photo update error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to update primary photo';
console.error('Primary photo update error:', err);
@@ -209,57 +283,92 @@ document.addEventListener('alpine:init', () => {
this.showCaptionModal = true;
},
async saveCaption() {
saveCaption() {
try {
const response = await fetch(`${uploadUrl}${this.editingPhoto.id}/caption/`, { // Added trailing slash
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
caption: this.editingPhoto.caption
})
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', `${uploadUrl}${this.editingPhoto.id}/caption/`);
tempForm.setAttribute('hx-vals', JSON.stringify({ caption: this.editingPhoto.caption }));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
// Update local state
this.photos = this.photos.map(p =>
p.id === this.editingPhoto.id
? { ...p, caption: this.editingPhoto.caption }
: p
);
this.showCaptionModal = false;
this.editingPhoto = { caption: '' };
} else {
this.error = 'Failed to update caption';
console.error('Caption update error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to update caption');
}
// Update local state
this.photos = this.photos.map(p =>
p.id === this.editingPhoto.id
? { ...p, caption: this.editingPhoto.caption }
: p
);
this.showCaptionModal = false;
this.editingPhoto = { caption: '' };
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to update caption';
console.error('Caption update error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to update caption';
console.error('Caption update error:', err);
}
},
async deletePhoto(photo) {
deletePhoto(photo) {
if (!confirm('Are you sure you want to delete this photo?')) {
return;
}
try {
const response = await fetch(`${uploadUrl}${photo.id}/`, { // Added trailing slash
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-delete', `${uploadUrl}${photo.id}/`);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
// Update local state
this.photos = this.photos.filter(p => p.id !== photo.id);
} else {
this.error = 'Failed to delete photo';
console.error('Delete error');
}
document.body.removeChild(tempForm);
});
if (!response.ok) {
throw new Error('Failed to delete photo');
}
// Update local state
this.photos = this.photos.filter(p => p.id !== photo.id);
tempForm.addEventListener('htmx:error', (event) => {
this.error = 'Failed to delete photo';
console.error('Delete error:', event.detail.error);
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (err) {
this.error = err.message || 'Failed to delete photo';
console.error('Delete error:', err);

View File

@@ -360,16 +360,30 @@ function searchGlobal() {
this.isSearching = true;
// Use HTMX to fetch search results
htmx.ajax('GET', `/api/v1/search/global/?q=${encodeURIComponent(this.searchQuery)}`, {
target: '#search-results-container',
swap: 'innerHTML'
}).then(() => {
this.isSearching = false;
this.showResults = true;
}).catch(() => {
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', `/api/v1/search/global/?q=${encodeURIComponent(this.searchQuery)}`);
tempForm.setAttribute('hx-target', '#search-results-container');
tempForm.setAttribute('hx-swap', 'innerHTML');
tempForm.setAttribute('hx-trigger', 'submit');
// Add HTMX event listeners
tempForm.addEventListener('htmx:afterRequest', (event) => {
this.isSearching = false;
if (event.detail.successful) {
this.showResults = true;
}
document.body.removeChild(tempForm);
});
tempForm.addEventListener('htmx:responseError', (event) => {
this.isSearching = false;
document.body.removeChild(tempForm);
});
// Execute HTMX request
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
</script>
{% endblock %}
@@ -412,4 +426,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -259,60 +259,128 @@ function parkForm() {
this.previews.splice(index, 1);
},
async uploadPhotos() {
uploadPhotos() {
if (!this.previews.length) return true;
this.uploading = true;
let allUploaded = true;
let uploadPromises = [];
for (let preview of this.previews) {
if (preview.uploaded || preview.error) continue;
preview.uploading = true;
const formData = new FormData();
formData.append('image', preview.file);
formData.append('app_label', 'parks');
formData.append('model', 'park');
formData.append('object_id', '{{ park.id }}');
try {
const response = await fetch('/photos/upload/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
body: formData
});
if (!response.ok) throw new Error('Upload failed');
const result = await response.json();
preview.uploading = false;
preview.uploaded = true;
} catch (error) {
console.error('Upload failed:', error);
preview.uploading = false;
preview.error = true;
allUploaded = false;
}
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', '/photos/upload/');
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.enctype = 'multipart/form-data';
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value;
tempForm.appendChild(csrfInput);
// Add form data
const imageInput = document.createElement('input');
imageInput.type = 'file';
imageInput.name = 'image';
imageInput.files = this.createFileList([preview.file]);
tempForm.appendChild(imageInput);
const appLabelInput = document.createElement('input');
appLabelInput.type = 'hidden';
appLabelInput.name = 'app_label';
appLabelInput.value = 'parks';
tempForm.appendChild(appLabelInput);
const modelInput = document.createElement('input');
modelInput.type = 'hidden';
modelInput.name = 'model';
modelInput.value = 'park';
tempForm.appendChild(modelInput);
const objectIdInput = document.createElement('input');
objectIdInput.type = 'hidden';
objectIdInput.name = 'object_id';
objectIdInput.value = '{{ park.id }}';
tempForm.appendChild(objectIdInput);
// Track upload completion with event listeners
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
if (event.detail.xhr.status === 200) {
const result = JSON.parse(event.detail.xhr.responseText);
preview.uploading = false;
preview.uploaded = true;
} else {
throw new Error('Upload failed');
}
} catch (error) {
console.error('Upload failed:', error);
preview.uploading = false;
preview.error = true;
allUploaded = false;
}
// Track completion
completedUploads++;
if (completedUploads === totalUploads) {
this.uploading = false;
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
this.uploading = false;
return allUploaded;
// Initialize completion tracking
let completedUploads = 0;
const totalUploads = this.previews.filter(p => !p.uploaded && !p.error).length;
if (totalUploads === 0) {
this.uploading = false;
return true;
}
return true; // Return immediately, completion handled by event listeners
},
createFileList(files) {
const dt = new DataTransfer();
files.forEach(file => dt.items.add(file));
return dt.files;
},
removePhoto(photoId) {
if (confirm('Are you sure you want to remove this photo?')) {
fetch(`/photos/${photoId}/delete/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
}).then(response => {
if (response.ok) {
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-delete', `/photos/${photoId}/delete/`);
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value;
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.xhr.status === 200) {
window.location.reload();
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
}
}

View File

@@ -380,17 +380,28 @@ class TripPlanner {
});
}
async loadAllParks() {
try {
const response = await fetch('{{ map_api_urls.locations }}?types=park&limit=1000');
const data = await response.json();
if (data.status === 'success' && data.data.locations) {
this.allParks = data.data.locations;
loadAllParks() {
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '{{ map_api_urls.locations }}');
tempForm.setAttribute('hx-vals', JSON.stringify({types: 'park', limit: 1000}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.status === 'success' && data.data.locations) {
this.allParks = data.data.locations;
}
} catch (error) {
console.error('Failed to load parks:', error);
}
} catch (error) {
console.error('Failed to load parks:', error);
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
initDragDrop() {
@@ -570,38 +581,50 @@ class TripPlanner {
}
}
async optimizeRoute() {
optimizeRoute() {
if (this.tripParks.length < 2) return;
try {
const parkIds = this.tripParks.map(p => p.id);
const response = await fetch('{% url "parks:htmx_optimize_route" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ park_ids: parkIds })
});
const data = await response.json();
if (data.status === 'success' && data.optimized_order) {
// Reorder parks based on optimization
const optimizedParks = data.optimized_order.map(id =>
this.tripParks.find(p => p.id === id)
).filter(Boolean);
const parkIds = this.tripParks.map(p => p.id);
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', '{% url "parks:htmx_optimize_route" %}');
tempForm.setAttribute('hx-vals', JSON.stringify({ park_ids: parkIds }));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = '{{ csrf_token }}';
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
this.tripParks = optimizedParks;
this.updateTripDisplay();
this.updateTripMarkers();
if (data.status === 'success' && data.optimized_order) {
// Reorder parks based on optimization
const optimizedParks = data.optimized_order.map(id =>
this.tripParks.find(p => p.id === id)
).filter(Boolean);
this.tripParks = optimizedParks;
this.updateTripDisplay();
this.updateTripMarkers();
}
} catch (error) {
console.error('Route optimization failed:', error);
}
} catch (error) {
console.error('Route optimization failed:', error);
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
async calculateRoute() {
calculateRoute() {
if (this.tripParks.length < 2) return;
// Remove existing route
@@ -733,38 +756,49 @@ class TripPlanner {
document.getElementById('trip-summary').classList.add('hidden');
}
async saveTrip() {
saveTrip() {
if (this.tripParks.length === 0) return;
const tripName = prompt('Enter a name for this trip:');
if (!tripName) return;
try {
const response = await fetch('{% url "parks:htmx_save_trip" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
name: tripName,
park_ids: this.tripParks.map(p => p.id)
})
});
const data = await response.json();
if (data.status === 'success') {
alert('Trip saved successfully!');
// Refresh saved trips
htmx.trigger('#saved-trips', 'refresh');
} else {
alert('Failed to save trip: ' + (data.message || 'Unknown error'));
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-post', '{% url "parks:htmx_save_trip" %}');
tempForm.setAttribute('hx-vals', JSON.stringify({
name: tripName,
park_ids: this.tripParks.map(p => p.id)
}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = '{{ csrf_token }}';
tempForm.appendChild(csrfInput);
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.status === 'success') {
alert('Trip saved successfully!');
// Refresh saved trips using HTMX
htmx.trigger(document.getElementById('saved-trips'), 'refresh');
} else {
alert('Failed to save trip: ' + (data.message || 'Unknown error'));
}
} catch (error) {
console.error('Save trip failed:', error);
alert('Failed to save trip');
}
} catch (error) {
console.error('Save trip failed:', error);
alert('Failed to save trip');
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}
}
@@ -785,4 +819,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -12,14 +12,22 @@
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
selectDesigner(data.id, data.name);
});
// Handle HTMX response using event listeners
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
if (event.detail.pathInfo.requestPath === '/rides/designers/create/') {
document.removeEventListener('htmx:afterRequest', handleResponse);
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
const data = JSON.parse(event.detail.xhr.response);
if (typeof selectDesigner === 'function') {
selectDesigner(data.id, data.name);
}
$dispatch('close-designer-modal');
}
submitting = false;
}
$dispatch('close-designer-modal');
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}

View File

@@ -12,14 +12,22 @@
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
selectManufacturer(data.id, data.name);
});
// Handle HTMX response using event listeners
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
if (event.detail.pathInfo.requestPath === '/rides/manufacturers/create/') {
document.removeEventListener('htmx:afterRequest', handleResponse);
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
const data = JSON.parse(event.detail.xhr.response);
if (typeof selectManufacturer === 'function') {
selectManufacturer(data.id, data.name);
}
$dispatch('close-manufacturer-modal');
}
submitting = false;
}
$dispatch('close-manufacturer-modal');
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}

View File

@@ -24,20 +24,28 @@
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
selectRideModel(data.id, data.name);
}
const parentForm = document.querySelector('[x-data]');
if (parentForm) {
const parentData = Alpine.$data(parentForm);
if (parentData && parentData.setRideModelModal) {
parentData.setRideModelModal(false);
});
// Handle HTMX response using event listeners
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
if (event.detail.pathInfo.requestPath === '/rides/models/create/') {
document.removeEventListener('htmx:afterRequest', handleResponse);
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
const data = JSON.parse(event.detail.xhr.response);
if (typeof selectRideModel === 'function') {
selectRideModel(data.id, data.name);
}
const parentForm = document.querySelector('[x-data]');
if (parentForm) {
const parentData = Alpine.$data(parentForm);
if (parentData && parentData.setRideModelModal) {
parentData.setRideModelModal(false);
}
}
}
submitting = false;
}
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}

View File

@@ -98,7 +98,7 @@ document.addEventListener('alpine:init', () => {
lastRequestId: 0,
currentRequest: null,
async getSearchSuggestions() {
getSearchSuggestions() {
if (this.searchQuery.length < 2) {
this.showSuggestions = false;
return;
@@ -115,40 +115,79 @@ document.addEventListener('alpine:init', () => {
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
try {
const response = await this.fetchSuggestions(controller, requestId);
await this.handleSuggestionResponse(response, requestId);
} catch (error) {
this.handleSuggestionError(error, requestId);
} finally {
this.fetchSuggestions(controller, requestId, () => {
clearTimeout(timeoutId);
if (this.currentRequest === controller) {
this.currentRequest = null;
}
}
});
},
async fetchSuggestions(controller, requestId) {
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 queryParams = {
q: this.searchQuery
};
if (parkSlug) {
queryParams.park_slug = parkSlug;
}
const response = await fetch(url, {
signal: controller.signal,
headers: {
'X-Request-ID': requestId.toString()
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '/rides/search-suggestions/');
tempForm.setAttribute('hx-vals', JSON.stringify(queryParams));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add request ID header simulation
tempForm.setAttribute('hx-headers', JSON.stringify({
'X-Request-ID': requestId.toString()
}));
// Handle abort signal
if (controller.signal.aborted) {
this.handleSuggestionError(new Error('AbortError'), requestId);
return;
}
const abortHandler = () => {
if (document.body.contains(tempForm)) {
document.body.removeChild(tempForm);
}
this.handleSuggestionError(new Error('AbortError'), requestId);
};
controller.signal.addEventListener('abort', abortHandler);
tempForm.addEventListener('htmx:afterRequest', (event) => {
controller.signal.removeEventListener('abort', abortHandler);
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
this.handleSuggestionResponse(event.detail.xhr, requestId);
} else {
this.handleSuggestionError(new Error(`HTTP error! status: ${event.detail.xhr.status}`), requestId);
}
if (document.body.contains(tempForm)) {
document.body.removeChild(tempForm);
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
tempForm.addEventListener('htmx:error', (event) => {
controller.signal.removeEventListener('abort', abortHandler);
this.handleSuggestionError(new Error(`HTTP error! status: ${event.detail.xhr.status || 'unknown'}`), requestId);
if (document.body.contains(tempForm)) {
document.body.removeChild(tempForm);
}
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
},
async handleSuggestionResponse(response, requestId) {
const html = await response.text();
handleSuggestionResponse(xhr, requestId) {
if (requestId === this.lastRequestId && this.searchQuery === document.getElementById('search').value) {
const html = xhr.responseText || '';
const suggestionsEl = document.getElementById('search-suggestions');
suggestionsEl.innerHTML = html;
this.showSuggestions = Boolean(html.trim());
@@ -187,7 +226,7 @@ document.addEventListener('alpine:init', () => {
},
// Handle input changes with debounce
async handleInput() {
handleInput() {
clearTimeout(this.suggestionTimeout);
this.suggestionTimeout = setTimeout(() => {
this.getSearchSuggestions();
@@ -413,4 +452,4 @@ window.addEventListener('beforeunload', () => {
timeouts.forEach(timeoutId => clearTimeout(timeoutId));
timeouts.clear();
});
</script>
</script>