mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:31:08 -05:00
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:
@@ -67,9 +67,11 @@
|
||||
{{ search_form.lng }}
|
||||
<div class="flex gap-2">
|
||||
<button type="button"
|
||||
id="use-my-location"
|
||||
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">
|
||||
📍 Use My Location
|
||||
x-data="geolocationButton"
|
||||
@click="getLocation()"
|
||||
: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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,43 +292,58 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Geolocation support
|
||||
const useLocationBtn = document.getElementById('use-my-location');
|
||||
const latInput = document.getElementById('lat-input');
|
||||
const lngInput = document.getElementById('lng-input');
|
||||
const locationInput = document.getElementById('location-input');
|
||||
|
||||
if (useLocationBtn && 'geolocation' in navigator) {
|
||||
useLocationBtn.addEventListener('click', function() {
|
||||
this.textContent = '📍 Getting location...';
|
||||
this.disabled = true;
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Geolocation Button Component
|
||||
Alpine.data('geolocationButton', () => ({
|
||||
loading: false,
|
||||
buttonText: '📍 Use My Location',
|
||||
|
||||
init() {
|
||||
// Hide button if geolocation is not supported
|
||||
if (!('geolocation' in navigator)) {
|
||||
this.$el.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
getLocation() {
|
||||
if (!('geolocation' in navigator)) return;
|
||||
|
||||
this.loading = true;
|
||||
this.buttonText = '📍 Getting location...';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
latInput.value = position.coords.latitude;
|
||||
lngInput.value = position.coords.longitude;
|
||||
locationInput.value = `${position.coords.latitude}, ${position.coords.longitude}`;
|
||||
useLocationBtn.textContent = '✅ Location set';
|
||||
(position) => {
|
||||
// Find form inputs
|
||||
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}`;
|
||||
}
|
||||
|
||||
this.buttonText = '✅ Location set';
|
||||
this.loading = false;
|
||||
|
||||
setTimeout(() => {
|
||||
useLocationBtn.textContent = '📍 Use My Location';
|
||||
useLocationBtn.disabled = false;
|
||||
this.buttonText = '📍 Use My Location';
|
||||
}, 2000);
|
||||
},
|
||||
function(error) {
|
||||
useLocationBtn.textContent = '❌ Location failed';
|
||||
(error) => {
|
||||
console.error('Geolocation error:', error);
|
||||
this.buttonText = '❌ Location failed';
|
||||
this.loading = false;
|
||||
|
||||
setTimeout(() => {
|
||||
useLocationBtn.textContent = '📍 Use My Location';
|
||||
useLocationBtn.disabled = false;
|
||||
this.buttonText = '📍 Use My Location';
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
});
|
||||
} else if (useLocationBtn) {
|
||||
useLocationBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/location-search.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -197,10 +197,122 @@
|
||||
<!-- Leaflet MarkerCluster JS -->
|
||||
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
|
||||
|
||||
<!-- AlpineJS Map Component (HTMX + AlpineJS Only) -->
|
||||
<div x-data="universalMap" x-init="initMap()" style="display: none;">
|
||||
<!-- Map functionality handled by AlpineJS + HTMX -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('universalMap', () => ({
|
||||
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>
|
||||
.cluster-marker {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user