mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
Refactor location widget and park search results templates to utilize Alpine.js for state management. Enhanced search functionality, improved data binding, and streamlined event handling for better user experience.
This commit is contained in:
@@ -19,30 +19,60 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
|
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
|
||||||
|
x-data="locationWidget({
|
||||||
|
submissionId: '{{ submission.id }}',
|
||||||
|
initialData: {
|
||||||
|
city: '{{ submission.changes.city|default:"" }}',
|
||||||
|
state: '{{ submission.changes.state|default:"" }}',
|
||||||
|
country: '{{ submission.changes.country|default:"" }}',
|
||||||
|
postal_code: '{{ submission.changes.postal_code|default:"" }}',
|
||||||
|
street_address: '{{ submission.changes.street_address|default:"" }}',
|
||||||
|
latitude: '{{ submission.changes.latitude|default:"" }}',
|
||||||
|
longitude: '{{ submission.changes.longitude|default:"" }}'
|
||||||
|
}
|
||||||
|
})"
|
||||||
|
x-init="init()">
|
||||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
|
||||||
|
|
||||||
<div class="location-widget" id="locationWidget-{{ submission.id }}">
|
<div class="location-widget">
|
||||||
{# Search Form #}
|
{# Search Form #}
|
||||||
<div class="relative mb-4">
|
<div class="relative mb-4">
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Search Location
|
Search Location
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="locationSearch-{{ submission.id }}"
|
x-model="searchQuery"
|
||||||
|
@input.debounce.300ms="handleSearch()"
|
||||||
|
@click.outside="showSearchResults = false"
|
||||||
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
||||||
placeholder="Search for a location..."
|
placeholder="Search for a location..."
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
style="z-index: 10;">
|
style="z-index: 10;">
|
||||||
<div id="searchResults-{{ submission.id }}"
|
<div x-show="showSearchResults"
|
||||||
|
x-transition
|
||||||
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
|
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">
|
||||||
|
<span x-text="result.address?.city ? result.address.city + ', ' : ''"></span>
|
||||||
|
<span x-text="result.address?.country || ''"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="searchResults.length === 0 && searchQuery.length > 0"
|
||||||
|
class="p-2 text-gray-500 dark:text-gray-400">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Map Container #}
|
{# Map Container #}
|
||||||
<div class="relative mb-4" style="z-index: 1;">
|
<div class="relative mb-4" style="z-index: 1;">
|
||||||
<div id="locationMap-{{ submission.id }}"
|
<div x-ref="mapContainer"
|
||||||
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,9 +84,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="street_address"
|
name="street_address"
|
||||||
id="streetAddress-{{ submission.id }}"
|
x-model="formData.street_address"
|
||||||
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
|
||||||
value="{{ submission.changes.street_address }}">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -64,9 +93,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="city"
|
name="city"
|
||||||
id="city-{{ submission.id }}"
|
x-model="formData.city"
|
||||||
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
|
||||||
value="{{ submission.changes.city }}">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -74,9 +102,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="state"
|
name="state"
|
||||||
id="state-{{ submission.id }}"
|
x-model="formData.state"
|
||||||
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
|
||||||
value="{{ submission.changes.state }}">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -84,9 +111,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="country"
|
name="country"
|
||||||
id="country-{{ submission.id }}"
|
x-model="formData.country"
|
||||||
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
|
||||||
value="{{ submission.changes.country }}">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -94,143 +120,140 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="postal_code"
|
name="postal_code"
|
||||||
id="postalCode-{{ submission.id }}"
|
x-model="formData.postal_code"
|
||||||
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
|
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
|
||||||
value="{{ submission.changes.postal_code }}">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Hidden Coordinate Fields #}
|
{# Hidden Coordinate Fields #}
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}">
|
<input type="hidden" name="latitude" x-model="formData.latitude">
|
||||||
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}">
|
<input type="hidden" name="longitude" x-model="formData.longitude">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('alpine:init', () => {
|
||||||
let maps = {};
|
Alpine.data('locationWidget', (config) => ({
|
||||||
let markers = {};
|
submissionId: config.submissionId,
|
||||||
const searchInput = document.getElementById('locationSearch-{{ submission.id }}');
|
formData: { ...config.initialData },
|
||||||
const searchResults = document.getElementById('searchResults-{{ submission.id }}');
|
searchQuery: '',
|
||||||
let searchTimeout;
|
searchResults: [],
|
||||||
|
showSearchResults: false,
|
||||||
|
map: null,
|
||||||
|
marker: null,
|
||||||
|
|
||||||
// Initialize form fields with existing values
|
init() {
|
||||||
const fields = {
|
// Set initial search query if location exists
|
||||||
city: '{{ submission.changes.city|default:"" }}',
|
if (this.formData.street_address || this.formData.city) {
|
||||||
state: '{{ submission.changes.state|default:"" }}',
|
const parts = [
|
||||||
country: '{{ submission.changes.country|default:"" }}',
|
this.formData.street_address,
|
||||||
postal_code: '{{ submission.changes.postal_code|default:"" }}',
|
this.formData.city,
|
||||||
street_address: '{{ submission.changes.street_address|default:"" }}',
|
this.formData.state,
|
||||||
latitude: '{{ submission.changes.latitude|default:"" }}',
|
this.formData.country
|
||||||
longitude: '{{ submission.changes.longitude|default:"" }}'
|
].filter(Boolean);
|
||||||
};
|
this.searchQuery = parts.join(', ');
|
||||||
|
|
||||||
Object.entries(fields).forEach(([field, value]) => {
|
|
||||||
const element = document.getElementById(`${field}-{{ submission.id }}`);
|
|
||||||
if (element) {
|
|
||||||
element.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set initial search input value if location exists
|
|
||||||
if (fields.street_address || fields.city) {
|
|
||||||
const parts = [
|
|
||||||
fields.street_address,
|
|
||||||
fields.city,
|
|
||||||
fields.state,
|
|
||||||
fields.country
|
|
||||||
].filter(Boolean);
|
|
||||||
searchInput.value = parts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
|
|
||||||
if (!value) return null;
|
|
||||||
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;
|
// Initialize map when component is ready
|
||||||
} catch (error) {
|
this.$nextTick(() => {
|
||||||
console.error('Coordinate normalization failed:', error);
|
this.initMap();
|
||||||
return null;
|
});
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
function validateCoordinates(lat, lng) {
|
normalizeCoordinate(value, maxDigits, decimalPlaces) {
|
||||||
const normalizedLat = normalizeCoordinate(lat, 9, 6);
|
if (!value) return null;
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMap() {
|
|
||||||
const submissionId = '{{ submission.id }}';
|
|
||||||
const mapId = `locationMap-${submissionId}`;
|
|
||||||
const mapContainer = document.getElementById(mapId);
|
|
||||||
|
|
||||||
if (!mapContainer) {
|
|
||||||
console.error(`Map container ${mapId} not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If map already exists, remove it
|
|
||||||
if (maps[submissionId]) {
|
|
||||||
maps[submissionId].remove();
|
|
||||||
delete maps[submissionId];
|
|
||||||
delete markers[submissionId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new map
|
|
||||||
maps[submissionId] = L.map(mapId);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(maps[submissionId]);
|
|
||||||
|
|
||||||
// Initialize with existing coordinates if available
|
|
||||||
const initialLat = fields.latitude;
|
|
||||||
const initialLng = fields.longitude;
|
|
||||||
|
|
||||||
if (initialLat && initialLng) {
|
|
||||||
try {
|
try {
|
||||||
const normalized = validateCoordinates(initialLat, initialLng);
|
const rounded = Number(value).toFixed(decimalPlaces);
|
||||||
maps[submissionId].setView([normalized.lat, normalized.lng], 13);
|
const strValue = rounded.replace('.', '').replace('-', '');
|
||||||
addMarker(normalized.lat, normalized.lng);
|
const strippedValue = strValue.replace(/0+$/, '');
|
||||||
|
|
||||||
|
if (strippedValue.length > maxDigits) {
|
||||||
|
return Number(Number(value).toFixed(decimalPlaces - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rounded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Invalid initial coordinates:', error);
|
console.error('Coordinate normalization failed:', error);
|
||||||
maps[submissionId].setView([0, 0], 2);
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
maps[submissionId].setView([0, 0], 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle map clicks - HTMX version
|
validateCoordinates(lat, lng) {
|
||||||
maps[submissionId].on('click', function(e) {
|
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() {
|
||||||
|
if (!this.$refs.mapContainer) {
|
||||||
|
console.error('Map container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If map already exists, remove it
|
||||||
|
if (this.map) {
|
||||||
|
this.map.remove();
|
||||||
|
this.map = null;
|
||||||
|
this.marker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new map
|
||||||
|
this.map = L.map(this.$refs.mapContainer);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(this.map);
|
||||||
|
|
||||||
|
// Initialize with existing coordinates if available
|
||||||
|
if (this.formData.latitude && this.formData.longitude) {
|
||||||
|
try {
|
||||||
|
const normalized = this.validateCoordinates(this.formData.latitude, this.formData.longitude);
|
||||||
|
this.map.setView([normalized.lat, normalized.lng], 13);
|
||||||
|
this.addMarker(normalized.lat, normalized.lng);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invalid initial coordinates:', error);
|
||||||
|
this.map.setView([0, 0], 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.map.setView([0, 0], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle map clicks
|
||||||
|
this.map.on('click', (e) => {
|
||||||
|
this.handleMapClick(e.latlng.lat, e.latlng.lng);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addMarker(lat, lng) {
|
||||||
|
if (this.marker) {
|
||||||
|
this.marker.remove();
|
||||||
|
}
|
||||||
|
this.marker = L.marker([lat, lng]).addTo(this.map);
|
||||||
|
this.map.setView([lat, lng], 13);
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleMapClick(lat, lng) {
|
||||||
try {
|
try {
|
||||||
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
|
const normalized = this.validateCoordinates(lat, lng);
|
||||||
|
|
||||||
// Create a temporary form for HTMX request
|
// Use HTMX for reverse geocoding
|
||||||
const tempForm = document.createElement('form');
|
const tempForm = document.createElement('form');
|
||||||
tempForm.style.display = 'none';
|
tempForm.style.display = 'none';
|
||||||
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
||||||
@@ -241,15 +264,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
tempForm.setAttribute('hx-trigger', 'submit');
|
tempForm.setAttribute('hx-trigger', 'submit');
|
||||||
tempForm.setAttribute('hx-swap', 'none');
|
tempForm.setAttribute('hx-swap', 'none');
|
||||||
|
|
||||||
// Add event listener for HTMX response
|
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
|
||||||
if (event.detail.successful) {
|
if (event.detail.successful) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.detail.xhr.responseText);
|
const data = JSON.parse(event.detail.xhr.responseText);
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
updateLocation(normalized.lat, normalized.lng, data);
|
this.updateLocation(normalized.lat, normalized.lng, data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Location update failed:', error);
|
console.error('Location update failed:', error);
|
||||||
alert(error.message || 'Failed to update location. Please try again.');
|
alert(error.message || 'Failed to update location. Please try again.');
|
||||||
@@ -258,7 +280,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
console.error('Geocoding request failed');
|
console.error('Geocoding request failed');
|
||||||
alert('Failed to update location. Please try again.');
|
alert('Failed to update location. Please try again.');
|
||||||
}
|
}
|
||||||
// Clean up temporary form
|
|
||||||
document.body.removeChild(tempForm);
|
document.body.removeChild(tempForm);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,102 +290,50 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
console.error('Location update failed:', error);
|
console.error('Location update failed:', error);
|
||||||
alert(error.message || 'Failed to update location. Please try again.');
|
alert(error.message || 'Failed to update location. Please try again.');
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
|
||||||
|
|
||||||
function addMarker(lat, lng) {
|
updateLocation(lat, lng, data) {
|
||||||
const submissionId = '{{ submission.id }}';
|
try {
|
||||||
if (markers[submissionId]) {
|
const normalized = this.validateCoordinates(lat, lng);
|
||||||
markers[submissionId].remove();
|
|
||||||
}
|
|
||||||
markers[submissionId] = L.marker([lat, lng]).addTo(maps[submissionId]);
|
|
||||||
maps[submissionId].setView([lat, lng], 13);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocation(lat, lng, data) {
|
// Update coordinates
|
||||||
try {
|
this.formData.latitude = normalized.lat;
|
||||||
const normalized = validateCoordinates(lat, lng);
|
this.formData.longitude = normalized.lng;
|
||||||
const submissionId = '{{ submission.id }}';
|
|
||||||
|
|
||||||
// Update coordinates
|
// Update marker
|
||||||
document.getElementById(`latitude-${submissionId}`).value = normalized.lat;
|
this.addMarker(normalized.lat, normalized.lng);
|
||||||
document.getElementById(`longitude-${submissionId}`).value = normalized.lng;
|
|
||||||
|
|
||||||
// Update marker
|
// Update form fields with English names where available
|
||||||
addMarker(normalized.lat, normalized.lng);
|
const address = data.address || {};
|
||||||
|
this.formData.street_address = `${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
|
||||||
|
this.formData.city = address.city || address.town || address.village || '';
|
||||||
|
this.formData.state = address.state || address.region || '';
|
||||||
|
this.formData.country = address.country || '';
|
||||||
|
this.formData.postal_code = address.postcode || '';
|
||||||
|
|
||||||
// Update form fields with English names where available
|
// Update search input
|
||||||
const address = data.address || {};
|
const locationParts = [
|
||||||
document.getElementById(`streetAddress-${submissionId}`).value =
|
this.formData.street_address,
|
||||||
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
|
this.formData.city,
|
||||||
document.getElementById(`city-${submissionId}`).value =
|
this.formData.state,
|
||||||
address.city || address.town || address.village || '';
|
this.formData.country
|
||||||
document.getElementById(`state-${submissionId}`).value =
|
].filter(Boolean);
|
||||||
address.state || address.region || '';
|
this.searchQuery = locationParts.join(', ');
|
||||||
document.getElementById(`country-${submissionId}`).value = address.country || '';
|
} catch (error) {
|
||||||
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || '';
|
console.error('Location update failed:', error);
|
||||||
|
alert(error.message || 'Failed to update location. Please try again.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Update search input
|
handleSearch() {
|
||||||
const locationString-3 = [
|
const query = this.searchQuery.trim();
|
||||||
document.getElementById(`streetAddress-${submissionId}`).value,
|
|
||||||
document.getElementById(`city-${submissionId}`).value,
|
|
||||||
document.getElementById(`state-${submissionId}`).value,
|
|
||||||
document.getElementById(`country-${submissionId}`).value
|
|
||||||
].filter(Boolean).join(', ');
|
|
||||||
searchInput.value = locationString;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Location update failed:', error);
|
|
||||||
alert(error.message || 'Failed to update location. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectLocation(result) {
|
if (!query) {
|
||||||
if (!result) return;
|
this.showSearchResults = false;
|
||||||
|
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 = validateCoordinates(lat, lon);
|
// Use HTMX for location search
|
||||||
|
|
||||||
// Create a normalized address object
|
|
||||||
const address = {
|
|
||||||
name: result.display_name || result.name || '',
|
|
||||||
address: {
|
|
||||||
house_number: result.address ? result.address.house_number : '',
|
|
||||||
road: result.address ? (result.address.road || result.address.street) : '',
|
|
||||||
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
|
|
||||||
state: result.address ? (result.address.state || result.address.region) : '',
|
|
||||||
country: result.address ? result.address.country : '',
|
|
||||||
postcode: result.address ? result.address.postcode : ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle location search - HTMX version
|
|
||||||
searchInput.addEventListener('input', function() {
|
|
||||||
clearTimeout(searchTimeout);
|
|
||||||
const query = this.value.trim();
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
searchResults.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTimeout = setTimeout(function() {
|
|
||||||
// Create a temporary form for HTMX request
|
|
||||||
const tempForm = document.createElement('form');
|
const tempForm = document.createElement('form');
|
||||||
tempForm.style.display = 'none';
|
tempForm.style.display = 'none';
|
||||||
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
||||||
@@ -374,88 +343,69 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
tempForm.setAttribute('hx-trigger', 'submit');
|
tempForm.setAttribute('hx-trigger', 'submit');
|
||||||
tempForm.setAttribute('hx-swap', 'none');
|
tempForm.setAttribute('hx-swap', 'none');
|
||||||
|
|
||||||
// Add event listener for HTMX response
|
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
|
||||||
if (event.detail.successful) {
|
if (event.detail.successful) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.detail.xhr.responseText);
|
const data = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
|
||||||
if (data.results && data.results.length > 0) {
|
if (data.results && data.results.length > 0) {
|
||||||
const resultsHtml = data.results.map((result, index) => `
|
this.searchResults = data.results;
|
||||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
this.showSearchResults = true;
|
||||||
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>
|
|
||||||
`).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 {
|
} else {
|
||||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
this.searchResults = [];
|
||||||
searchResults.classList.remove('hidden');
|
this.showSearchResults = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search failed:', 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>';
|
this.searchResults = [];
|
||||||
searchResults.classList.remove('hidden');
|
this.showSearchResults = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Search request failed');
|
console.error('Search request failed');
|
||||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
this.searchResults = [];
|
||||||
searchResults.classList.remove('hidden');
|
this.showSearchResults = false;
|
||||||
}
|
}
|
||||||
// Clean up temporary form
|
|
||||||
document.body.removeChild(tempForm);
|
document.body.removeChild(tempForm);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(tempForm);
|
document.body.appendChild(tempForm);
|
||||||
htmx.trigger(tempForm, 'submit');
|
htmx.trigger(tempForm, 'submit');
|
||||||
}, 300);
|
},
|
||||||
});
|
|
||||||
|
|
||||||
// Hide search results when clicking outside
|
selectLocation(result) {
|
||||||
document.addEventListener('click', function(e) {
|
if (!result) return;
|
||||||
if (!searchResults.contains(e.target) && e.target !== searchInput) {
|
|
||||||
searchResults.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize map when the element becomes visible
|
try {
|
||||||
const observer = new MutationObserver(function(mutations) {
|
const lat = parseFloat(result.lat);
|
||||||
mutations.forEach(function(mutation) {
|
const lon = parseFloat(result.lon);
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
|
||||||
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
|
if (isNaN(lat) || isNaN(lon)) {
|
||||||
if (mapContainer && window.getComputedStyle(mapContainer).display !== 'none') {
|
throw new Error('Invalid coordinates in search result');
|
||||||
initMap();
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalized = this.validateCoordinates(lat, lon);
|
||||||
|
|
||||||
|
// Create a normalized address object
|
||||||
|
const address = {
|
||||||
|
name: result.display_name || result.name || '',
|
||||||
|
address: {
|
||||||
|
house_number: result.address ? result.address.house_number : '',
|
||||||
|
road: result.address ? (result.address.road || result.address.street) : '',
|
||||||
|
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
|
||||||
|
state: result.address ? (result.address.state || result.address.region) : '',
|
||||||
|
country: result.address ? result.address.country : '',
|
||||||
|
postcode: result.address ? result.address.postcode : ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateLocation(normalized.lat, normalized.lng, address);
|
||||||
|
this.showSearchResults = false;
|
||||||
|
this.searchQuery = address.name;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Location selection failed:', error);
|
||||||
|
alert(error.message || 'Failed to select location. Please try again.');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
|
|
||||||
if (mapContainer) {
|
|
||||||
observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
|
|
||||||
|
|
||||||
// Also initialize immediately if the container is already visible
|
|
||||||
if (window.getComputedStyle(mapContainer).display !== 'none') {
|
|
||||||
initMap();
|
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
|
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
style="max-height: 240px; overflow-y: auto;"
|
||||||
|
x-data="manufacturerSearchResults('{{ submission_id }}')"
|
||||||
|
@click.outside="clearResults()">
|
||||||
{% if manufacturers %}
|
{% if manufacturers %}
|
||||||
{% for manufacturer in manufacturers %}
|
{% for manufacturer in manufacturers %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
|
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
|
||||||
onclick="selectManufacturerForSubmission('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}', '{{ submission_id }}')">
|
@click="selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}')">
|
||||||
{{ manufacturer.name }}
|
{{ manufacturer.name }}
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -19,49 +22,49 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function selectManufacturerForSubmission(id, name, submissionId) {
|
document.addEventListener('alpine:init', () => {
|
||||||
// Debug logging
|
Alpine.data('manufacturerSearchResults', (submissionId) => ({
|
||||||
console.log('Selecting manufacturer:', {id, name, submissionId});
|
submissionId: submissionId,
|
||||||
|
|
||||||
// Find elements
|
selectManufacturer(id, name) {
|
||||||
const manufacturerInput = document.querySelector(`#manufacturer-input-${submissionId}`);
|
// Debug logging
|
||||||
const searchInput = document.querySelector(`#manufacturer-search-${submissionId}`);
|
console.log('Selecting manufacturer:', {id, name, submissionId: this.submissionId});
|
||||||
const resultsDiv = document.querySelector(`#manufacturer-search-results-${submissionId}`);
|
|
||||||
|
|
||||||
// Debug logging
|
// Find elements using AlpineJS approach
|
||||||
console.log('Found elements:', {
|
const manufacturerInput = document.querySelector(`#manufacturer-input-${this.submissionId}`);
|
||||||
manufacturerInput: manufacturerInput?.id,
|
const searchInput = document.querySelector(`#manufacturer-search-${this.submissionId}`);
|
||||||
searchInput: searchInput?.id,
|
const resultsDiv = document.querySelector(`#manufacturer-search-results-${this.submissionId}`);
|
||||||
resultsDiv: resultsDiv?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update hidden input
|
// Debug logging
|
||||||
if (manufacturerInput) {
|
console.log('Found elements:', {
|
||||||
manufacturerInput.value = id;
|
manufacturerInput: manufacturerInput?.id,
|
||||||
console.log('Updated manufacturer input value:', manufacturerInput.value);
|
searchInput: searchInput?.id,
|
||||||
}
|
resultsDiv: resultsDiv?.id
|
||||||
|
});
|
||||||
|
|
||||||
// Update search input
|
// Update hidden input
|
||||||
if (searchInput) {
|
if (manufacturerInput) {
|
||||||
searchInput.value = name;
|
manufacturerInput.value = id;
|
||||||
console.log('Updated search input value:', searchInput.value);
|
console.log('Updated manufacturer input value:', manufacturerInput.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear results
|
// Update search input
|
||||||
if (resultsDiv) {
|
if (searchInput) {
|
||||||
resultsDiv.innerHTML = '';
|
searchInput.value = name;
|
||||||
console.log('Cleared results div');
|
console.log('Updated search input value:', searchInput.value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Close search results when clicking outside
|
// Clear results
|
||||||
document.addEventListener('click', function(e) {
|
this.clearResults();
|
||||||
const searchResults = document.querySelectorAll('[id^="manufacturer-search-results-"]');
|
},
|
||||||
searchResults.forEach(function(resultsDiv) {
|
|
||||||
const searchInput = document.querySelector(`#manufacturer-search-${resultsDiv.id.split('-').pop()}`);
|
clearResults() {
|
||||||
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
|
const resultsDiv = document.querySelector(`#manufacturer-search-results-${this.submissionId}`);
|
||||||
resultsDiv.innerHTML = '';
|
if (resultsDiv) {
|
||||||
|
resultsDiv.innerHTML = '';
|
||||||
|
console.log('Cleared results div');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
|
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
style="max-height: 240px; overflow-y: auto;"
|
||||||
|
x-data="parkSearchResults('{{ submission_id }}')"
|
||||||
|
@click.outside="clearResults()">
|
||||||
{% if parks %}
|
{% if parks %}
|
||||||
{% for park in parks %}
|
{% for park in parks %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
|
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
|
||||||
onclick="selectParkForSubmission('{{ park.id }}', '{{ park.name|escapejs }}', '{{ submission_id }}')">
|
@click="selectPark('{{ park.id }}', '{{ park.name|escapejs }}')">
|
||||||
{{ park.name }}
|
{{ park.name }}
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -19,55 +22,55 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function selectParkForSubmission(id, name, submissionId) {
|
document.addEventListener('alpine:init', () => {
|
||||||
// Debug logging
|
Alpine.data('parkSearchResults', (submissionId) => ({
|
||||||
console.log('Selecting park:', {id, name, submissionId});
|
submissionId: submissionId,
|
||||||
|
|
||||||
// Find elements
|
selectPark(id, name) {
|
||||||
const parkInput = document.querySelector(`#park-input-${submissionId}`);
|
// Debug logging
|
||||||
const searchInput = document.querySelector(`#park-search-${submissionId}`);
|
console.log('Selecting park:', {id, name, submissionId: this.submissionId});
|
||||||
const resultsDiv = document.querySelector(`#park-search-results-${submissionId}`);
|
|
||||||
|
|
||||||
// Debug logging
|
// Find elements using AlpineJS approach
|
||||||
console.log('Found elements:', {
|
const parkInput = document.querySelector(`#park-input-${this.submissionId}`);
|
||||||
parkInput: parkInput?.id,
|
const searchInput = document.querySelector(`#park-search-${this.submissionId}`);
|
||||||
searchInput: searchInput?.id,
|
const resultsDiv = document.querySelector(`#park-search-results-${this.submissionId}`);
|
||||||
resultsDiv: resultsDiv?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update hidden input
|
// Debug logging
|
||||||
if (parkInput) {
|
console.log('Found elements:', {
|
||||||
parkInput.value = id;
|
parkInput: parkInput?.id,
|
||||||
console.log('Updated park input value:', parkInput.value);
|
searchInput: searchInput?.id,
|
||||||
}
|
resultsDiv: resultsDiv?.id
|
||||||
|
});
|
||||||
|
|
||||||
// Update search input
|
// Update hidden input
|
||||||
if (searchInput) {
|
if (parkInput) {
|
||||||
searchInput.value = name;
|
parkInput.value = id;
|
||||||
console.log('Updated search input value:', searchInput.value);
|
console.log('Updated park input value:', parkInput.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear results
|
// Update search input
|
||||||
if (resultsDiv) {
|
if (searchInput) {
|
||||||
resultsDiv.innerHTML = '';
|
searchInput.value = name;
|
||||||
console.log('Cleared results div');
|
console.log('Updated search input value:', searchInput.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger park areas update
|
// Clear results
|
||||||
if (parkInput) {
|
this.clearResults();
|
||||||
htmx.trigger(parkInput, 'change');
|
|
||||||
console.log('Triggered change event');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close search results when clicking outside
|
// Trigger park areas update
|
||||||
document.addEventListener('click', function(e) {
|
if (parkInput) {
|
||||||
const searchResults = document.querySelectorAll('[id^="park-search-results-"]');
|
htmx.trigger(parkInput, 'change');
|
||||||
searchResults.forEach(function(resultsDiv) {
|
console.log('Triggered change event');
|
||||||
const searchInput = document.querySelector(`#park-search-${resultsDiv.id.split('-').pop()}`);
|
}
|
||||||
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
|
},
|
||||||
resultsDiv.innerHTML = '';
|
|
||||||
|
clearResults() {
|
||||||
|
const resultsDiv = document.querySelector(`#park-search-results-${this.submissionId}`);
|
||||||
|
if (resultsDiv) {
|
||||||
|
resultsDiv.innerHTML = '';
|
||||||
|
console.log('Cleared results div');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user