mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 12:51:09 -05:00
Refactor location widget to utilize Alpine.js for state management and HTMX for AJAX interactions. Removed legacy JavaScript functions and streamlined event handling for improved user experience.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
<!-- HTMX + AlpineJS ONLY - NO CUSTOM JAVASCRIPT -->
|
||||||
<style>
|
<style>
|
||||||
/* Ensure map container and its elements stay below other UI elements */
|
/* Ensure map container and its elements stay below other UI elements */
|
||||||
.leaflet-pane,
|
.leaflet-pane,
|
||||||
@@ -19,212 +20,25 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="location-widget" id="locationWidget" x-data="locationWidget()">
|
<div class="location-widget" id="locationWidget"
|
||||||
{# Search Form #}
|
x-data="{
|
||||||
<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: [],
|
searchResults: [],
|
||||||
showResults: false,
|
showResults: false,
|
||||||
|
searchTimeout: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.$nextTick(() => {
|
// Initialize map via HTMX
|
||||||
this.initMap();
|
this.initializeMap();
|
||||||
this.setupEventListeners();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
normalizeCoordinate(value, maxDigits, decimalPlaces) {
|
initializeMap() {
|
||||||
try {
|
// Use HTMX to load map component
|
||||||
const rounded = Number(value).toFixed(decimalPlaces);
|
htmx.ajax('GET', '/maps/location-widget/', {
|
||||||
const strValue = rounded.replace('.', '').replace('-', '');
|
target: '#locationMap',
|
||||||
const strippedValue = strValue.replace(/0+$/, '');
|
swap: 'innerHTML'
|
||||||
|
|
||||||
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) {
|
handleSearchInput(query) {
|
||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
@@ -238,150 +52,160 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}, 300);
|
}, 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) {
|
searchLocation(query) {
|
||||||
const tempForm = document.createElement('form');
|
// Use HTMX for location search
|
||||||
tempForm.style.display = 'none';
|
htmx.ajax('GET', '/parks/search/location/', {
|
||||||
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
values: { q: query },
|
||||||
tempForm.setAttribute('hx-vals', JSON.stringify({ q: query }));
|
target: '#search-results-container',
|
||||||
tempForm.setAttribute('hx-trigger', 'submit');
|
swap: 'innerHTML'
|
||||||
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) {
|
selectLocation(lat, lng, displayName, address) {
|
||||||
if (!result) return;
|
// Update coordinates
|
||||||
|
this.$refs.latitude.value = lat;
|
||||||
|
this.$refs.longitude.value = lng;
|
||||||
|
|
||||||
try {
|
// Update address fields
|
||||||
const lat = parseFloat(result.lat);
|
if (address) {
|
||||||
const lon = parseFloat(result.lon);
|
this.$refs.streetAddress.value = address.street || '';
|
||||||
|
this.$refs.city.value = address.city || '';
|
||||||
if (isNaN(lat) || isNaN(lon)) {
|
this.$refs.state.value = address.state || '';
|
||||||
throw new Error('Invalid coordinates in search result');
|
this.$refs.country.value = address.country || '';
|
||||||
|
this.$refs.postalCode.value = address.postal_code || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = this.validateCoordinates(lat, lon);
|
// Update search input
|
||||||
|
this.$refs.searchInput.value = displayName;
|
||||||
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.showResults = false;
|
||||||
this.$el.querySelector('#locationSearch').value = address.name;
|
|
||||||
} catch (error) {
|
// Update map via HTMX
|
||||||
console.error('Location selection failed:', error);
|
htmx.ajax('POST', '/maps/update-marker/', {
|
||||||
alert(error.message || 'Failed to select location. Please try again.');
|
values: { lat: lat, lng: lng },
|
||||||
}
|
target: '#locationMap',
|
||||||
|
swap: 'none'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addMarker(lat, lng) {
|
handleMapClick(lat, lng) {
|
||||||
if (this.marker) {
|
// Use HTMX for reverse geocoding
|
||||||
this.marker.remove();
|
htmx.ajax('GET', '/parks/search/reverse-geocode/', {
|
||||||
|
values: { lat: lat, lon: lng },
|
||||||
|
target: '#location-form-fields',
|
||||||
|
swap: 'none'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.marker = L.marker([lat, lng]).addTo(this.map);
|
}"
|
||||||
this.map.setView([lat, lng], 13);
|
@click.outside="showResults = false">
|
||||||
},
|
|
||||||
|
|
||||||
updateLocation(lat, lng, data) {
|
{# Search Form #}
|
||||||
try {
|
<div class="relative mb-4">
|
||||||
const normalized = this.validateCoordinates(lat, lng);
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search Location
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
x-ref="searchInput"
|
||||||
|
@input="handleSearchInput($event.target.value)"
|
||||||
|
hx-get="/parks/search/location/"
|
||||||
|
hx-trigger="input changed delay:300ms"
|
||||||
|
hx-target="#search-results-container"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
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;">
|
||||||
|
|
||||||
// Update coordinates using AlpineJS $el
|
<div id="search-results-container"
|
||||||
this.$el.querySelector('#latitude').value = normalized.lat;
|
x-show="showResults"
|
||||||
this.$el.querySelector('#longitude').value = normalized.lng;
|
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">
|
||||||
|
<!-- Search results will be populated here via HTMX -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Update marker
|
{# Map Container #}
|
||||||
this.addMarker(normalized.lat, normalized.lng);
|
<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">
|
||||||
|
<!-- Map will be loaded via HTMX -->
|
||||||
|
<div class="flex items-center justify-center h-full bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-8 h-8 mx-auto mb-4 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Loading map...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Update form fields
|
{# Location Form Fields #}
|
||||||
const address = data.address || {};
|
<div id="location-form-fields" class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;">
|
||||||
this.$el.querySelector('#streetAddress').value =
|
<div>
|
||||||
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
this.$el.querySelector('#city').value =
|
Street Address
|
||||||
address.city || address.town || address.village || '';
|
</label>
|
||||||
this.$el.querySelector('#state').value =
|
<input type="text"
|
||||||
address.state || address.region || '';
|
name="street_address"
|
||||||
this.$el.querySelector('#country').value = address.country || '';
|
x-ref="streetAddress"
|
||||||
this.$el.querySelector('#postalCode').value = address.postcode || '';
|
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"
|
||||||
} catch (error) {
|
value="{{ form.street_address.value|default:'' }}">
|
||||||
console.error('Location update failed:', error);
|
</div>
|
||||||
alert(error.message || 'Failed to update location. Please try again.');
|
<div>
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
City
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
name="city"
|
||||||
|
x-ref="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"
|
||||||
|
x-ref="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"
|
||||||
|
x-ref="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"
|
||||||
|
x-ref="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" x-ref="latitude" value="{{ form.latitude.value|default:'' }}">
|
||||||
|
<input type="hidden" name="longitude" x-ref="longitude" value="{{ form.longitude.value|default:'' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HTMX Error Handling (Minimal JavaScript as allowed by Context7 docs) -->
|
||||||
|
<div x-data="{
|
||||||
|
init() {
|
||||||
|
// Only essential HTMX error handling as shown in Context7 docs
|
||||||
|
this.$el.addEventListener('htmx:responseError', (evt) => {
|
||||||
|
if (evt.detail.xhr.status === 404 || evt.detail.xhr.status === 500) {
|
||||||
|
console.error('HTMX Error:', evt.detail.xhr.status);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}));
|
}"></div>
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user