Files
thrillwiki_django_no_react/templates/parks/partials/location_widget.html
pacnpal de8b6f67a3 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.
2025-09-26 15:25:12 -04:00

388 lines
16 KiB
HTML

{% load static %}
<style>
/* Ensure map container and its elements stay below other UI elements */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
z-index: 1 !important;
}
.leaflet-control {
z-index: 2 !important;
}
</style>
<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">
Search Location
</label>
<input type="text"
id="locationSearch"
class="relative w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div x-show="showResults && searchResults.length > 0"
x-transition
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
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>
{# Map Container #}
<div class="relative mb-4" style="z-index: 1;">
<div id="locationMap" class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.street_address.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.city.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.state.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.country.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.postal_code.value|default:'' }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude" value="{{ form.latitude.value|default:'' }}">
<input type="hidden" name="longitude" id="longitude" value="{{ form.longitude.value|default:'' }}">
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('locationWidget', () => ({
map: null,
marker: null,
searchTimeout: null,
searchResults: [],
showResults: false,
init() {
this.$nextTick(() => {
this.initMap();
this.setupEventListeners();
});
},
normalizeCoordinate(value, maxDigits, decimalPlaces) {
try {
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('Coordinate normalization failed:', error);
return null;
}
},
validateCoordinates(lat, lng) {
const normalizedLat = this.normalizeCoordinate(lat, 9, 6);
const normalizedLng = this.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 };
},
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);
});
}
// 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.');
}
},
handleSearchInput(query) {
clearTimeout(this.searchTimeout);
if (!query.trim()) {
this.showResults = false;
return;
}
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/reverse-geocode/');
tempForm.setAttribute('hx-vals', JSON.stringify({ lat, lon: lng }));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.successful) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.error) {
throw new Error(data.error);
}
this.updateLocation(lat, 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.');
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
},
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');
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);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
},
selectLocation(result) {
if (!result) return;
try {
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) {
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>