mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 11:11:09 -05:00
Refactor ride filters and forms to use AlpineJS for state management and HTMX for AJAX interactions
- Enhanced filter sidebar with AlpineJS for collapsible sections and localStorage persistence. - Removed custom JavaScript in favor of AlpineJS for managing filter states and interactions. - Updated ride form to utilize AlpineJS for handling manufacturer, designer, and ride model selections. - Simplified search script to leverage AlpineJS for managing search input and suggestions. - Improved error handling for HTMX requests with minimal JavaScript. - Refactored ride form data handling to encapsulate logic within an AlpineJS component.
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="location-widget" id="locationWidget">
|
||||
<div class="location-widget" id="locationWidget" x-data="locationWidget()">
|
||||
{# Search Form #}
|
||||
<div class="relative mb-4">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
@@ -31,9 +31,18 @@
|
||||
placeholder="Search for a location..."
|
||||
autocomplete="off"
|
||||
style="z-index: 10;">
|
||||
<div id="searchResults"
|
||||
<div x-show="showResults && searchResults.length > 0"
|
||||
x-transition
|
||||
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
|
||||
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
|
||||
class="w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
|
||||
<template x-for="(result, index) in searchResults" :key="index">
|
||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
@click="selectLocation(result)">
|
||||
<div class="font-medium text-gray-900 dark:text-white" x-text="result.display_name || result.name || ''"></div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400"
|
||||
x-text="[result.street, result.city || (result.address && (result.address.city || result.address.town || result.address.village)), result.state || (result.address && (result.address.state || result.address.region)), result.country || (result.address && result.address.country), result.postal_code || (result.address && result.address.postcode)].filter(Boolean).join(', ')"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,300 +113,275 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let map = null;
|
||||
let marker = null;
|
||||
const searchInput = document.getElementById('locationSearch');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
let searchTimeout;
|
||||
|
||||
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
|
||||
try {
|
||||
// Convert to string-3 with exact decimal places
|
||||
const rounded = Number(value).toFixed(decimalPlaces);
|
||||
|
||||
// Convert to string-3 without decimal point for digit counting
|
||||
const strValue = rounded.replace('.', '').replace('-', '');
|
||||
// Remove trailing zeros
|
||||
const strippedValue = strValue.replace(/0+$/, '');
|
||||
|
||||
// If total digits exceed maxDigits, round further
|
||||
if (strippedValue.length > maxDigits) {
|
||||
return Number(Number(value).toFixed(decimalPlaces - 1));
|
||||
}
|
||||
|
||||
// Return the string-3 representation to preserve exact decimal places
|
||||
return rounded;
|
||||
} catch (error) {
|
||||
console.error('Coordinate normalization failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function validateCoordinates(lat, lng) {
|
||||
// Normalize coordinates
|
||||
const normalizedLat = normalizeCoordinate(lat, 9, 6);
|
||||
const normalizedLng = normalizeCoordinate(lng, 10, 6);
|
||||
|
||||
if (normalizedLat === null || normalizedLng === null) {
|
||||
throw new Error('Invalid coordinate format');
|
||||
}
|
||||
|
||||
const parsedLat = parseFloat(normalizedLat);
|
||||
const parsedLng = parseFloat(normalizedLng);
|
||||
|
||||
if (parsedLat < -90 || parsedLat > 90) {
|
||||
throw new Error('Latitude must be between -90 and 90 degrees.');
|
||||
}
|
||||
if (parsedLng < -180 || parsedLng > 180) {
|
||||
throw new Error('Longitude must be between -180 and 180 degrees.');
|
||||
}
|
||||
|
||||
return { lat: normalizedLat, lng: normalizedLng };
|
||||
}
|
||||
|
||||
// Initialize map
|
||||
function initMap() {
|
||||
map = L.map('locationMap').setView([0, 0], 2);
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('locationWidget', () => ({
|
||||
map: null,
|
||||
marker: null,
|
||||
searchTimeout: null,
|
||||
searchResults: [],
|
||||
showResults: false,
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Initialize with existing coordinates if available
|
||||
const initialLat = document.getElementById('latitude').value;
|
||||
const initialLng = document.getElementById('longitude').value;
|
||||
if (initialLat && initialLng) {
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.initMap();
|
||||
this.setupEventListeners();
|
||||
});
|
||||
},
|
||||
|
||||
normalizeCoordinate(value, maxDigits, decimalPlaces) {
|
||||
try {
|
||||
const normalized = validateCoordinates(initialLat, initialLng);
|
||||
addMarker(normalized.lat, normalized.lng);
|
||||
const rounded = Number(value).toFixed(decimalPlaces);
|
||||
const strValue = rounded.replace('.', '').replace('-', '');
|
||||
const strippedValue = strValue.replace(/0+$/, '');
|
||||
|
||||
if (strippedValue.length > maxDigits) {
|
||||
return Number(Number(value).toFixed(decimalPlaces - 1));
|
||||
}
|
||||
|
||||
return rounded;
|
||||
} catch (error) {
|
||||
console.error('Invalid initial coordinates:', error);
|
||||
console.error('Coordinate normalization failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
validateCoordinates(lat, lng) {
|
||||
const normalizedLat = this.normalizeCoordinate(lat, 9, 6);
|
||||
const normalizedLng = this.normalizeCoordinate(lng, 10, 6);
|
||||
|
||||
// Handle map clicks - HTMX version
|
||||
map.on('click', function(e) {
|
||||
try {
|
||||
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
|
||||
|
||||
// Create a temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
lat: normalized.lat,
|
||||
lon: normalized.lng
|
||||
}));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
updateLocation(normalized.lat, normalized.lng, data);
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
} else {
|
||||
console.error('Geocoding request failed');
|
||||
alert('Failed to update location. Please try again.');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
if (normalizedLat === null || normalizedLng === null) {
|
||||
throw new Error('Invalid coordinate format');
|
||||
}
|
||||
|
||||
const parsedLat = parseFloat(normalizedLat);
|
||||
const parsedLng = parseFloat(normalizedLng);
|
||||
|
||||
if (parsedLat < -90 || parsedLat > 90) {
|
||||
throw new Error('Latitude must be between -90 and 90 degrees.');
|
||||
}
|
||||
if (parsedLng < -180 || parsedLng > 180) {
|
||||
throw new Error('Longitude must be between -180 and 180 degrees.');
|
||||
}
|
||||
|
||||
return { lat: normalizedLat, lng: normalizedLng };
|
||||
},
|
||||
|
||||
initMap() {
|
||||
this.map = L.map('locationMap').setView([0, 0], 2);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(this.map);
|
||||
|
||||
// Initialize with existing coordinates if available
|
||||
const initialLat = this.$el.querySelector('#latitude').value;
|
||||
const initialLng = this.$el.querySelector('#longitude').value;
|
||||
if (initialLat && initialLng) {
|
||||
try {
|
||||
const normalized = this.validateCoordinates(initialLat, initialLng);
|
||||
this.addMarker(normalized.lat, normalized.lng);
|
||||
} catch (error) {
|
||||
console.error('Invalid initial coordinates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle map clicks using AlpineJS approach
|
||||
this.map.on('click', (e) => {
|
||||
this.handleMapClick(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
const searchInput = this.$el.querySelector('#locationSearch');
|
||||
const form = this.$el.closest('form');
|
||||
|
||||
// Search input handler
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
this.handleSearchInput(e.target.value);
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => {
|
||||
this.handleFormSubmit(e);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
|
||||
}
|
||||
|
||||
// Click outside handler
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.$el.contains(e.target)) {
|
||||
this.showResults = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleMapClick(lat, lng) {
|
||||
try {
|
||||
const normalized = this.validateCoordinates(lat, lng);
|
||||
this.reverseGeocode(normalized.lat, normalized.lng);
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize map
|
||||
initMap();
|
||||
|
||||
// Handle location search - HTMX version
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
},
|
||||
|
||||
if (!query) {
|
||||
searchResults.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
handleSearchInput(query) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
|
||||
if (!query.trim()) {
|
||||
this.showResults = false;
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(function() {
|
||||
// Create a temporary form for HTMX request
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.searchLocation(query.trim());
|
||||
}, 300);
|
||||
},
|
||||
|
||||
handleFormSubmit(e) {
|
||||
const lat = this.$el.querySelector('#latitude').value;
|
||||
const lng = this.$el.querySelector('#longitude').value;
|
||||
|
||||
if (lat && lng) {
|
||||
try {
|
||||
this.validateCoordinates(lat, lng);
|
||||
} catch (error) {
|
||||
e.preventDefault();
|
||||
alert(error.message || 'Invalid coordinates. Please check the location.');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reverseGeocode(lat, lng) {
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
q: query
|
||||
}));
|
||||
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({ lat, lon: lng }));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.successful) {
|
||||
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.street,
|
||||
result.city || (result.address && (result.address.city || result.address.town || result.address.village)),
|
||||
result.state || (result.address && (result.address.state || result.address.region)),
|
||||
result.country || (result.address && result.address.country),
|
||||
result.postal_code || (result.address && result.address.postcode)
|
||||
].filter(Boolean).join(', ')}
|
||||
</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);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
this.updateLocation(lat, lng, data);
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
} else {
|
||||
console.error('Search request failed');
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
console.error('Geocoding request failed');
|
||||
alert('Failed to update location. Please try again.');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Hide search results when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchResults.contains(e.target) && e.target !== searchInput) {
|
||||
searchResults.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function addMarker(lat, lng) {
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
}
|
||||
marker = L.marker([lat, lng]).addTo(map);
|
||||
map.setView([lat, lng], 13);
|
||||
}
|
||||
|
||||
function updateLocation(lat, lng, data) {
|
||||
try {
|
||||
const normalized = validateCoordinates(lat, lng);
|
||||
|
||||
// Update coordinates
|
||||
document.getElementById('latitude').value = normalized.lat;
|
||||
document.getElementById('longitude').value = normalized.lng;
|
||||
|
||||
// Update marker
|
||||
addMarker(normalized.lat, normalized.lng);
|
||||
|
||||
// Update form fields with English names where available
|
||||
const address = data.address || {};
|
||||
document.getElementById('streetAddress').value =
|
||||
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
|
||||
document.getElementById('city').value =
|
||||
address.city || address.town || address.village || '';
|
||||
document.getElementById('state').value =
|
||||
address.state || address.region || '';
|
||||
document.getElementById('country').value = address.country || '';
|
||||
document.getElementById('postalCode').value = address.postcode || '';
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function selectLocation(result) {
|
||||
if (!result) return;
|
||||
},
|
||||
|
||||
try {
|
||||
const lat = parseFloat(result.lat);
|
||||
const lon = parseFloat(result.lon);
|
||||
searchLocation(query) {
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
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');
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
throw new Error('Invalid coordinates in search result');
|
||||
}
|
||||
|
||||
const normalized = validateCoordinates(lat, lon);
|
||||
|
||||
// Create a normalized address object
|
||||
const address = {
|
||||
name: result.display_name || result.name || '',
|
||||
address: {
|
||||
house_number: result.house_number || (result.address && result.address.house_number) || '',
|
||||
road: result.street || (result.address && (result.address.road || result.address.street)) || '',
|
||||
city: result.city || (result.address && (result.address.city || result.address.town || result.address.village)) || '',
|
||||
state: result.state || (result.address && (result.address.state || result.address.region)) || '',
|
||||
country: result.country || (result.address && result.address.country) || '',
|
||||
postcode: result.postal_code || (result.address && result.address.postcode) || ''
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
this.searchResults = data.results || [];
|
||||
this.showResults = true;
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
this.searchResults = [];
|
||||
this.showResults = false;
|
||||
}
|
||||
} else {
|
||||
console.error('Search request failed');
|
||||
this.searchResults = [];
|
||||
this.showResults = false;
|
||||
}
|
||||
};
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
updateLocation(normalized.lat, normalized.lng, address);
|
||||
searchResults.classList.add('hidden');
|
||||
searchInput.value = address.name;
|
||||
} catch (error) {
|
||||
console.error('Location selection failed:', error);
|
||||
alert(error.message || 'Failed to select location. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Add form submit handler
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const lat = document.getElementById('latitude').value;
|
||||
const lng = document.getElementById('longitude').value;
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
},
|
||||
|
||||
if (lat && lng) {
|
||||
selectLocation(result) {
|
||||
if (!result) return;
|
||||
|
||||
try {
|
||||
validateCoordinates(lat, lng);
|
||||
const lat = parseFloat(result.lat);
|
||||
const lon = parseFloat(result.lon);
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
throw new Error('Invalid coordinates in search result');
|
||||
}
|
||||
|
||||
const normalized = this.validateCoordinates(lat, lon);
|
||||
|
||||
const address = {
|
||||
name: result.display_name || result.name || '',
|
||||
address: {
|
||||
house_number: result.house_number || (result.address && result.address.house_number) || '',
|
||||
road: result.street || (result.address && (result.address.road || result.address.street)) || '',
|
||||
city: result.city || (result.address && (result.address.city || result.address.town || result.address.village)) || '',
|
||||
state: result.state || (result.address && (result.address.state || result.address.region)) || '',
|
||||
country: result.country || (result.address && result.address.country) || '',
|
||||
postcode: result.postal_code || (result.address && result.address.postcode) || ''
|
||||
}
|
||||
};
|
||||
|
||||
this.updateLocation(normalized.lat, normalized.lng, address);
|
||||
this.showResults = false;
|
||||
this.$el.querySelector('#locationSearch').value = address.name;
|
||||
} catch (error) {
|
||||
e.preventDefault();
|
||||
alert(error.message || 'Invalid coordinates. Please check the location.');
|
||||
console.error('Location selection failed:', error);
|
||||
alert(error.message || 'Failed to select location. Please try again.');
|
||||
}
|
||||
},
|
||||
|
||||
addMarker(lat, lng) {
|
||||
if (this.marker) {
|
||||
this.marker.remove();
|
||||
}
|
||||
this.marker = L.marker([lat, lng]).addTo(this.map);
|
||||
this.map.setView([lat, lng], 13);
|
||||
},
|
||||
|
||||
updateLocation(lat, lng, data) {
|
||||
try {
|
||||
const normalized = this.validateCoordinates(lat, lng);
|
||||
|
||||
// Update coordinates using AlpineJS $el
|
||||
this.$el.querySelector('#latitude').value = normalized.lat;
|
||||
this.$el.querySelector('#longitude').value = normalized.lng;
|
||||
|
||||
// Update marker
|
||||
this.addMarker(normalized.lat, normalized.lng);
|
||||
|
||||
// Update form fields
|
||||
const address = data.address || {};
|
||||
this.$el.querySelector('#streetAddress').value =
|
||||
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
|
||||
this.$el.querySelector('#city').value =
|
||||
address.city || address.town || address.village || '';
|
||||
this.$el.querySelector('#state').value =
|
||||
address.state || address.region || '';
|
||||
this.$el.querySelector('#country').value = address.country || '';
|
||||
this.$el.querySelector('#postalCode').value = address.postcode || '';
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user