Refactor location results, universal map, and road trip planner templates to utilize Alpine.js for state management and event handling. Enhanced geolocation button functionality, improved map initialization, and streamlined trip management interactions.

This commit is contained in:
pacnpal
2025-09-26 13:55:06 -04:00
parent d4431acb39
commit 757ad1be89
3 changed files with 414 additions and 45 deletions

View File

@@ -67,9 +67,11 @@
{{ search_form.lng }} {{ search_form.lng }}
<div class="flex gap-2"> <div class="flex gap-2">
<button type="button" <button type="button"
id="use-my-location" x-data="geolocationButton"
class="flex-1 px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300"> @click="getLocation()"
📍 Use My Location :disabled="loading"
class="flex-1 px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 disabled:opacity-50">
<span x-text="buttonText">📍 Use My Location</span>
</button> </button>
</div> </div>
</div> </div>
@@ -290,43 +292,58 @@
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('alpine:init', () => {
// Geolocation support // Geolocation Button Component
const useLocationBtn = document.getElementById('use-my-location'); Alpine.data('geolocationButton', () => ({
const latInput = document.getElementById('lat-input'); loading: false,
const lngInput = document.getElementById('lng-input'); buttonText: '📍 Use My Location',
const locationInput = document.getElementById('location-input');
if (useLocationBtn && 'geolocation' in navigator) { init() {
useLocationBtn.addEventListener('click', function() { // Hide button if geolocation is not supported
this.textContent = '📍 Getting location...'; if (!('geolocation' in navigator)) {
this.disabled = true; this.$el.style.display = 'none';
}
},
getLocation() {
if (!('geolocation' in navigator)) return;
this.loading = true;
this.buttonText = '📍 Getting location...';
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
function(position) { (position) => {
latInput.value = position.coords.latitude; // Find form inputs
lngInput.value = position.coords.longitude; const form = this.$el.closest('form');
const latInput = form.querySelector('input[name="lat"]');
const lngInput = form.querySelector('input[name="lng"]');
const locationInput = form.querySelector('input[name="location"]');
if (latInput) latInput.value = position.coords.latitude;
if (lngInput) lngInput.value = position.coords.longitude;
if (locationInput) {
locationInput.value = `${position.coords.latitude}, ${position.coords.longitude}`; locationInput.value = `${position.coords.latitude}, ${position.coords.longitude}`;
useLocationBtn.textContent = '✅ Location set'; }
this.buttonText = '✅ Location set';
this.loading = false;
setTimeout(() => { setTimeout(() => {
useLocationBtn.textContent = '📍 Use My Location'; this.buttonText = '📍 Use My Location';
useLocationBtn.disabled = false;
}, 2000); }, 2000);
}, },
function(error) { (error) => {
useLocationBtn.textContent = '❌ Location failed';
console.error('Geolocation error:', error); console.error('Geolocation error:', error);
this.buttonText = '❌ Location failed';
this.loading = false;
setTimeout(() => { setTimeout(() => {
useLocationBtn.textContent = '📍 Use My Location'; this.buttonText = '📍 Use My Location';
useLocationBtn.disabled = false;
}, 2000); }, 2000);
} }
); );
});
} else if (useLocationBtn) {
useLocationBtn.style.display = 'none';
} }
}));
}); });
</script> </script>
<script src="{% static 'js/location-search.js' %}"></script>
{% endblock %} {% endblock %}

View File

@@ -197,10 +197,122 @@
<!-- Leaflet MarkerCluster JS --> <!-- Leaflet MarkerCluster JS -->
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script> <script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<!-- AlpineJS Map Component (HTMX + AlpineJS Only) --> <script>
<div x-data="universalMap" x-init="initMap()" style="display: none;"> document.addEventListener('alpine:init', () => {
<!-- Map functionality handled by AlpineJS + HTMX --> Alpine.data('universalMap', () => ({
</div> map: null,
markers: {},
markerCluster: null,
init() {
this.initMap();
this.setupFilters();
},
initMap() {
// Initialize Leaflet map
if (typeof L !== 'undefined') {
this.map = L.map('map-container').setView([39.8283, -98.5795], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
// Initialize marker cluster group
this.markerCluster = L.markerClusterGroup({
iconCreateFunction: (cluster) => {
const count = cluster.getChildCount();
return L.divIcon({
html: `<div class="cluster-marker-inner">${count}</div>`,
className: 'cluster-marker',
iconSize: [40, 40]
});
}
});
this.map.addLayer(this.markerCluster);
this.loadMapData();
}
},
setupFilters() {
// Handle filter pill clicks
document.querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', () => {
const checkbox = pill.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
pill.classList.toggle('active', checkbox.checked);
this.updateFilters();
});
// Set initial state
const checkbox = pill.querySelector('input[type="checkbox"]');
pill.classList.toggle('active', checkbox.checked);
});
},
loadMapData() {
// Load initial map data via HTMX
const form = document.getElementById('map-filters');
if (form) {
htmx.trigger(form, 'submit');
}
},
updateFilters() {
// Trigger HTMX filter update
const form = document.getElementById('map-filters');
if (form) {
htmx.trigger(form, 'change');
}
},
addMarker(data) {
const icon = this.getMarkerIcon(data.type);
const marker = L.marker([data.lat, data.lng], { icon })
.bindPopup(this.createPopupContent(data));
this.markerCluster.addLayer(marker);
this.markers[data.id] = marker;
},
getMarkerIcon(type) {
const icons = {
'park': '🎢',
'ride': '🎠',
'company': '🏢',
'default': '📍'
};
return L.divIcon({
html: `<div class="location-marker-inner">${icons[type] || icons.default}</div>`,
className: 'location-marker',
iconSize: [30, 30],
iconAnchor: [15, 15]
});
},
createPopupContent(data) {
return `
<div class="location-info-popup">
<h3>${data.name}</h3>
${data.description ? `<p>${data.description}</p>` : ''}
${data.location ? `<p><strong>Location:</strong> ${data.location}</p>` : ''}
${data.url ? `<p><a href="${data.url}" class="text-blue-600 hover:text-blue-800">View Details</a></p>` : ''}
</div>
`;
},
clearMarkers() {
this.markerCluster.clearLayers();
this.markers = {};
}
}));
});
</script>
<!-- Map Component Container -->
<div x-data="universalMap" x-init="init()" style="display: none;"></div>
<style> <style>
.cluster-marker { .cluster-marker {

View File

@@ -177,7 +177,7 @@
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Trip Itinerary</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Trip Itinerary</h3>
<button id="clear-trip" <button id="clear-trip"
class="px-3 py-1 text-sm text-red-600 hover:text-red-700 dark:text-red-400" class="px-3 py-1 text-sm text-red-600 hover:text-red-700 dark:text-red-400"
onclick="tripPlanner.clearTrip()"> @click="clearTrip()">
<i class="mr-1 fas fa-trash"></i>Clear All <i class="mr-1 fas fa-trash"></i>Clear All
</button> </button>
</div> </div>
@@ -193,12 +193,12 @@
<div class="mt-4 space-y-2"> <div class="mt-4 space-y-2">
<button id="optimize-route" <button id="optimize-route"
class="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" class="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onclick="tripPlanner.optimizeRoute()" disabled> @click="optimizeRoute()" :disabled="tripParks.length < 2">
<i class="mr-2 fas fa-route"></i>Optimize Route <i class="mr-2 fas fa-route"></i>Optimize Route
</button> </button>
<button id="calculate-route" <button id="calculate-route"
class="w-full px-4 py-2 text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" class="w-full px-4 py-2 text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onclick="tripPlanner.calculateRoute()" disabled> @click="calculateRoute()" :disabled="tripParks.length < 2">
<i class="mr-2 fas fa-map"></i>Calculate Route <i class="mr-2 fas fa-map"></i>Calculate Route
</button> </button>
</div> </div>
@@ -230,7 +230,7 @@
<div class="mt-4"> <div class="mt-4">
<button id="save-trip" <button id="save-trip"
class="w-full px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors" class="w-full px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors"
onclick="tripPlanner.saveTrip()"> @click="saveTrip()">
<i class="mr-2 fas fa-save"></i>Save Trip <i class="mr-2 fas fa-save"></i>Save Trip
</button> </button>
</div> </div>
@@ -245,13 +245,13 @@
<div class="flex gap-2"> <div class="flex gap-2">
<button id="fit-route" <button id="fit-route"
class="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" class="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
onclick="tripPlanner.fitRoute()"> @click="fitRoute()">
<i class="mr-1 fas fa-expand-arrows-alt"></i>Fit Route <i class="mr-1 fas fa-expand-arrows-alt"></i>Fit Route
</button> </button>
<button id="toggle-parks" <button id="toggle-parks"
class="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" class="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
onclick="tripPlanner.toggleAllParks()"> @click="toggleAllParks()">
<i class="mr-1 fas fa-eye"></i>Show All Parks <i class="mr-1 fas fa-eye"></i><span x-text="showAllParks ? 'Hide Parks' : 'Show All Parks'">Show All Parks</span>
</button> </button>
</div> </div>
</div> </div>
@@ -306,8 +306,248 @@
<!-- Sortable JS for drag & drop --> <!-- Sortable JS for drag & drop -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<!-- AlpineJS Trip Planner Component (HTMX + AlpineJS Only) --> <script>
<div x-data="tripPlanner" x-init="initMap()" style="display: none;"> document.addEventListener('alpine:init', () => {
<!-- Trip planner functionality handled by AlpineJS + HTMX --> Alpine.data('tripPlanner', () => ({
</div> map: null,
tripParks: [],
allParks: [],
showAllParks: false,
routeControl: null,
parkMarkers: {},
init() {
this.initMap();
this.setupSortable();
},
initMap() {
if (typeof L !== 'undefined') {
this.map = L.map('map-container').setView([39.8283, -98.5795], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
this.loadAllParks();
}
},
setupSortable() {
if (typeof Sortable !== 'undefined') {
const tripParksEl = document.getElementById('trip-parks');
if (tripParksEl) {
Sortable.create(tripParksEl, {
animation: 150,
ghostClass: 'drag-over',
onEnd: (evt) => {
this.reorderTrip(evt.oldIndex, evt.newIndex);
}
});
}
}
},
loadAllParks() {
// Load parks via HTMX or fetch
fetch('/api/parks/')
.then(response => response.json())
.then(data => {
this.allParks = data;
this.displayAllParks();
})
.catch(error => console.error('Error loading parks:', error));
},
displayAllParks() {
this.allParks.forEach(park => {
if (park.latitude && park.longitude) {
const marker = L.marker([park.latitude, park.longitude])
.bindPopup(`
<div class="p-2">
<h3 class="font-semibold">${park.name}</h3>
<p class="text-sm text-gray-600">${park.location || ''}</p>
<button onclick="Alpine.store('tripPlanner').addParkToTrip(${park.id})"
class="mt-2 px-2 py-1 bg-blue-500 text-white text-xs rounded">
Add to Trip
</button>
</div>
`);
this.parkMarkers[park.id] = marker;
if (this.showAllParks) {
marker.addTo(this.map);
}
}
});
},
addParkToTrip(parkId) {
const park = this.allParks.find(p => p.id === parkId);
if (park && !this.tripParks.find(p => p.id === parkId)) {
this.tripParks.push(park);
this.updateTripDisplay();
this.updateButtons();
}
},
removeParkFromTrip(parkId) {
this.tripParks = this.tripParks.filter(p => p.id !== parkId);
this.updateTripDisplay();
this.updateButtons();
this.clearRoute();
},
clearTrip() {
this.tripParks = [];
this.updateTripDisplay();
this.updateButtons();
this.clearRoute();
},
updateTripDisplay() {
const container = document.getElementById('trip-parks');
const emptyState = document.getElementById('empty-trip');
if (this.tripParks.length === 0) {
emptyState.style.display = 'block';
container.innerHTML = emptyState.outerHTML;
} else {
container.innerHTML = this.tripParks.map((park, index) => `
<div class="park-card draggable-item" data-park-id="${park.id}">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-bold mr-3">
${index + 1}
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">${park.name}</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">${park.location || ''}</p>
</div>
</div>
<button @click="removeParkFromTrip(${park.id})"
class="text-red-500 hover:text-red-700">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`).join('');
}
},
updateButtons() {
const hasParks = this.tripParks.length >= 2;
document.getElementById('optimize-route').disabled = !hasParks;
document.getElementById('calculate-route').disabled = !hasParks;
},
optimizeRoute() {
// Implement route optimization logic
console.log('Optimizing route for', this.tripParks.length, 'parks');
},
calculateRoute() {
if (this.tripParks.length < 2) return;
this.clearRoute();
const waypoints = this.tripParks.map(park =>
L.latLng(park.latitude, park.longitude)
);
if (typeof L.Routing !== 'undefined') {
this.routeControl = L.Routing.control({
waypoints: waypoints,
routeWhileDragging: false,
addWaypoints: false,
createMarker: (i, waypoint, n) => {
const park = this.tripParks[i];
return L.marker(waypoint.latLng, {
icon: L.divIcon({
html: `<div class="waypoint-marker-inner">${i + 1}</div>`,
className: `waypoint-marker ${i === 0 ? 'waypoint-start' : i === n - 1 ? 'waypoint-end' : 'waypoint-stop'}`,
iconSize: [30, 30]
})
}).bindPopup(park.name);
}
}).addTo(this.map);
this.routeControl.on('routesfound', (e) => {
const route = e.routes[0];
this.updateTripSummary(route);
});
}
},
clearRoute() {
if (this.routeControl) {
this.map.removeControl(this.routeControl);
this.routeControl = null;
}
this.hideTripSummary();
},
updateTripSummary(route) {
const summary = route.summary;
document.getElementById('total-distance').textContent = Math.round(summary.totalDistance / 1609.34); // Convert to miles
document.getElementById('total-time').textContent = Math.round(summary.totalTime / 3600) + 'h';
document.getElementById('total-parks').textContent = this.tripParks.length;
document.getElementById('total-rides').textContent = this.tripParks.reduce((sum, park) => sum + (park.ride_count || 0), 0);
document.getElementById('trip-summary').classList.remove('hidden');
},
hideTripSummary() {
document.getElementById('trip-summary').classList.add('hidden');
},
fitRoute() {
if (this.tripParks.length > 0) {
const group = new L.featureGroup(
this.tripParks.map(park => L.marker([park.latitude, park.longitude]))
);
this.map.fitBounds(group.getBounds().pad(0.1));
}
},
toggleAllParks() {
this.showAllParks = !this.showAllParks;
Object.values(this.parkMarkers).forEach(marker => {
if (this.showAllParks) {
marker.addTo(this.map);
} else {
this.map.removeLayer(marker);
}
});
},
reorderTrip(oldIndex, newIndex) {
const park = this.tripParks.splice(oldIndex, 1)[0];
this.tripParks.splice(newIndex, 0, park);
this.updateTripDisplay();
this.clearRoute();
},
saveTrip() {
if (this.tripParks.length === 0) return;
const tripData = {
name: `Trip ${new Date().toLocaleDateString()}`,
parks: this.tripParks.map(p => p.id)
};
// Save via HTMX
htmx.ajax('POST', '/trips/save/', {
values: tripData,
target: '#saved-trips',
swap: 'innerHTML'
});
}
}));
});
</script>
<!-- Trip Planner Component Container -->
<div x-data="tripPlanner" x-init="init()" style="display: none;"></div>
{% endblock %} {% endblock %}