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

@@ -177,7 +177,7 @@
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Trip Itinerary</h3>
<button id="clear-trip"
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
</button>
</div>
@@ -193,12 +193,12 @@
<div class="mt-4 space-y-2">
<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"
onclick="tripPlanner.optimizeRoute()" disabled>
@click="optimizeRoute()" :disabled="tripParks.length < 2">
<i class="mr-2 fas fa-route"></i>Optimize Route
</button>
<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"
onclick="tripPlanner.calculateRoute()" disabled>
@click="calculateRoute()" :disabled="tripParks.length < 2">
<i class="mr-2 fas fa-map"></i>Calculate Route
</button>
</div>
@@ -230,7 +230,7 @@
<div class="mt-4">
<button id="save-trip"
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
</button>
</div>
@@ -245,13 +245,13 @@
<div class="flex gap-2">
<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"
onclick="tripPlanner.fitRoute()">
@click="fitRoute()">
<i class="mr-1 fas fa-expand-arrows-alt"></i>Fit Route
</button>
<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"
onclick="tripPlanner.toggleAllParks()">
<i class="mr-1 fas fa-eye"></i>Show All Parks
@click="toggleAllParks()">
<i class="mr-1 fas fa-eye"></i><span x-text="showAllParks ? 'Hide Parks' : 'Show All Parks'">Show All Parks</span>
</button>
</div>
</div>
@@ -306,8 +306,248 @@
<!-- Sortable JS for drag & drop -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<!-- AlpineJS Trip Planner Component (HTMX + AlpineJS Only) -->
<div x-data="tripPlanner" x-init="initMap()" style="display: none;">
<!-- Trip planner functionality handled by AlpineJS + HTMX -->
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('tripPlanner', () => ({
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 %}