mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:51:08 -05:00
- 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.
209 lines
7.6 KiB
JavaScript
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
|