Files
thrillwiki_django_no_react/backend/static/js/roadtrip.js
pacnpal b9063ff4f8 feat: Add detailed park and ride pages with HTMX integration
- Implemented park detail page with dynamic content loading for rides and weather.
- Created park list page with filters and search functionality.
- Developed ride detail page showcasing ride stats, reviews, and similar rides.
- Added ride list page with filtering options and dynamic loading.
- Introduced search results page with tabs for parks, rides, and users.
- Added HTMX tests for global search functionality.
2025-12-19 19:53:20 -05:00

209 lines
7.6 KiB
JavaScript

/* Minimal Roadtrip JS helpers for HTMX-driven planner
- Initializes map helpers when Leaflet is available
- Exposes `RoadtripMap` global with basic marker helpers
- Heavy client-side trip logic is intentionally moved to HTMX endpoints
*/
class RoadtripMap {
constructor() {
this.map = null;
this.markers = {};
}
init(containerId, opts = {}) {
if (typeof L === 'undefined') return;
try {
this.map = L.map(containerId).setView([51.505, -0.09], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
} catch (e) {
console.error('Failed to initialize map', e);
}
}
addMarker(park) {
if (!this.map || !park || !park.latitude || !park.longitude) return;
const id = park.id;
if (this.markers[id]) return;
const m = L.marker([park.latitude, park.longitude]).addTo(this.map).bindPopup(park.name);
this.markers[id] = m;
}
removeMarker(parkId) {
const m = this.markers[parkId];
if (m && this.map) {
this.map.removeLayer(m);
delete this.markers[parkId];
}
}
fitToMarkers() {
const keys = Object.keys(this.markers);
if (!this.map || keys.length === 0) return;
const group = new L.featureGroup(keys.map(k => this.markers[k]));
this.map.fitBounds(group.getBounds().pad(0.2));
}
showRoute(orderedParks = []) {
if (!this.map || typeof L.Routing === 'undefined') return;
// remove existing control if present
if (this._routingControl) {
try {
this.map.removeControl(this._routingControl);
} catch (e) {}
this._routingControl = null;
}
const waypoints = orderedParks
.filter(p => p.latitude && p.longitude)
.map(p => L.latLng(p.latitude, p.longitude));
if (waypoints.length < 2) return;
try {
this._routingControl = L.Routing.control({
waypoints: waypoints,
draggableWaypoints: false,
addWaypoints: false,
showAlternatives: false,
routeWhileDragging: false,
fitSelectedRoute: true,
createMarker: function(i, wp) {
const cls = i === 0 ? 'waypoint-start' : (i === waypoints.length - 1 ? 'waypoint-end' : 'waypoint-stop');
return L.marker(wp.latLng, { className: 'waypoint-marker ' + cls }).bindPopup(`Stop ${i+1}`);
}
}).addTo(this.map);
} catch (e) {
console.error('Routing error', e);
}
}
}
// Expose simple global for templates to call
globalThis.RoadtripMap = new RoadtripMap();
// Backwards-compatible lightweight planner shim used by other scripts
class RoadTripPlannerShim {
constructor(containerId) {
this.containerId = containerId;
}
async addPark(parkId) {
// POST to HTMX add endpoint and insert returned fragment
try {
const csrftoken = (document.cookie.match(/(^|;)\s*csrftoken=([^;]+)/) || [])[2];
const resp = await fetch(`/parks/roadtrip/htmx/add-park/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrftoken || ''
},
body: `park_id=${encodeURIComponent(parkId)}`,
credentials: 'same-origin'
});
const html = await resp.text();
const container = document.getElementById('trip-parks');
if (container) container.insertAdjacentHTML('afterbegin', html);
if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.addMarker === 'function') {
try {
const parkResp = await fetch(`/api/parks/${parkId}/`);
const parkJson = await parkResp.json();
if (parkJson && parkJson.data) globalThis.RoadtripMap.addMarker(parkJson.data);
} catch (e) {
// ignore
}
}
} catch (e) {
console.error('Failed to add park via HTMX shim', e);
}
}
removePark(parkId) {
const el = document.querySelector(`[data-park-id="${parkId}"]`);
if (el) el.remove();
if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.removeMarker === 'function') {
globalThis.RoadtripMap.removeMarker(parkId);
}
}
fitRoute() {
if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.fitToMarkers === 'function') {
globalThis.RoadtripMap.fitToMarkers();
}
}
toggleAllParks() {
// No-op in shim; map integration can implement this separately
console.debug('toggleAllParks called (shim)');
}
}
// Expose compatibility globals
globalThis.RoadTripPlanner = RoadTripPlannerShim;
document.addEventListener('DOMContentLoaded', function() {
try {
globalThis.roadTripPlanner = new RoadTripPlannerShim('roadtrip-planner');
} catch (e) {
// ignore
}
});
// Initialize Sortable for #trip-parks and POST new order to server
document.addEventListener('DOMContentLoaded', function () {
try {
if (typeof Sortable === 'undefined') return;
const el = document.getElementById('trip-parks');
if (!el) return;
// avoid double-init
if (el._sortableInit) return;
el._sortableInit = true;
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
if (match) return decodeURIComponent(match[2]);
return null;
}
new Sortable(el, {
animation: 150,
ghostClass: 'drag-over',
handle: '.draggable-item',
onEnd: function (evt) {
// gather order from container children
const order = Array.from(el.children).map(function (c) { return c.dataset.parkId; }).filter(Boolean);
const csrftoken = getCookie('csrftoken');
fetch('/parks/roadtrip/htmx/reorder/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken || ''
},
credentials: 'same-origin',
body: JSON.stringify({ order: order })
}).then(function (r) { return r.text(); }).then(function (html) {
// replace inner HTML with server-rendered partial
el.innerHTML = html;
// notify other listeners (map, summary)
document.dispatchEvent(new CustomEvent('tripReordered', { detail: { order: order } }));
}).catch(function (err) {
console.error('Failed to post reorder', err);
});
}
});
} catch (e) {
console.error('Sortable init error', e);
}
});
// Listen for HTMX trigger event and show route when available
document.addEventListener('tripOptimized', function (ev) {
try {
const payload = ev && ev.detail ? ev.detail : {};
const parks = (payload && payload.parks) || [];
if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.showRoute === 'function') {
globalThis.RoadtripMap.showRoute(parks);
}
} catch (e) {
// ignore
}
});
// End of roadtrip helpers