mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:51:08 -05:00
- Enhanced filter sidebar with AlpineJS for collapsible sections and localStorage persistence. - Removed custom JavaScript in favor of AlpineJS for managing filter states and interactions. - Updated ride form to utilize AlpineJS for handling manufacturer, designer, and ride model selections. - Simplified search script to leverage AlpineJS for managing search input and suggestions. - Improved error handling for HTMX requests with minimal JavaScript. - Refactored ride form data handling to encapsulate logic within an AlpineJS component.
417 lines
16 KiB
HTML
417 lines
16 KiB
HTML
{% extends 'base/base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Road Trip Planner - ThrillWiki{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<!-- Leaflet Routing Machine CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.css" />
|
|
|
|
<style>
|
|
.map-container {
|
|
height: 70vh;
|
|
min-height: 500px;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.park-selection-card {
|
|
@apply bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm hover:shadow-md transition-all cursor-pointer border-2 border-transparent;
|
|
}
|
|
|
|
.park-selection-card:hover {
|
|
@apply border-blue-200 dark:border-blue-700;
|
|
}
|
|
|
|
.park-selection-card.selected {
|
|
@apply border-blue-500 bg-blue-50 dark:bg-blue-900 dark:bg-opacity-30;
|
|
}
|
|
|
|
.park-card {
|
|
@apply bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm;
|
|
}
|
|
|
|
.trip-summary-card {
|
|
@apply bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900 dark:to-indigo-900 rounded-lg p-4 shadow-sm;
|
|
}
|
|
|
|
.waypoint-marker {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
.waypoint-marker-inner {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
border: 3px solid white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.waypoint-start .waypoint-marker-inner {
|
|
background: #10b981;
|
|
}
|
|
|
|
.waypoint-end .waypoint-marker-inner {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.waypoint-stop .waypoint-marker-inner {
|
|
background: #3b82f6;
|
|
}
|
|
|
|
.route-line {
|
|
color: #3b82f6;
|
|
weight: 4;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.dark .route-line {
|
|
color: #60a5fa;
|
|
}
|
|
|
|
.trip-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.trip-stat {
|
|
@apply text-center;
|
|
}
|
|
|
|
.trip-stat-value {
|
|
@apply text-2xl font-bold text-blue-600 dark:text-blue-400;
|
|
}
|
|
|
|
.trip-stat-label {
|
|
@apply text-sm text-gray-600 dark:text-gray-400 mt-1;
|
|
}
|
|
|
|
.draggable-item {
|
|
cursor: grab;
|
|
}
|
|
|
|
.draggable-item:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.drag-over {
|
|
@apply border-dashed border-2 border-blue-400 bg-blue-50 dark:bg-blue-900 dark:bg-opacity-30;
|
|
}
|
|
|
|
.park-search-result {
|
|
@apply p-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700;
|
|
}
|
|
|
|
.park-search-result:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.park-search-result:hover {
|
|
@apply bg-gray-50 dark:bg-gray-700;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- HTMX + AlpineJS ONLY - NO CUSTOM JAVASCRIPT -->
|
|
<div x-data="{
|
|
tripParks: [],
|
|
showAllParks: false,
|
|
mapInitialized: false,
|
|
|
|
init() {
|
|
// Initialize map via HTMX
|
|
this.initializeMap();
|
|
},
|
|
|
|
initializeMap() {
|
|
// Use HTMX to load map component
|
|
htmx.ajax('GET', '/maps/roadtrip-map/', {
|
|
target: '#map-container',
|
|
swap: 'innerHTML'
|
|
});
|
|
this.mapInitialized = true;
|
|
},
|
|
|
|
addParkToTrip(parkId, parkName, parkLocation) {
|
|
// Check if park already exists
|
|
if (!this.tripParks.find(p => p.id === parkId)) {
|
|
this.tripParks.push({
|
|
id: parkId,
|
|
name: parkName,
|
|
location: parkLocation
|
|
});
|
|
}
|
|
},
|
|
|
|
removeParkFromTrip(parkId) {
|
|
this.tripParks = this.tripParks.filter(p => p.id !== parkId);
|
|
},
|
|
|
|
clearTrip() {
|
|
this.tripParks = [];
|
|
},
|
|
|
|
optimizeRoute() {
|
|
if (this.tripParks.length >= 2) {
|
|
// Use HTMX to optimize route
|
|
htmx.ajax('POST', '/trips/optimize/', {
|
|
values: { parks: this.tripParks.map(p => p.id) },
|
|
target: '#trip-summary',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
},
|
|
|
|
calculateRoute() {
|
|
if (this.tripParks.length >= 2) {
|
|
// Use HTMX to calculate route
|
|
htmx.ajax('POST', '/trips/calculate/', {
|
|
values: { parks: this.tripParks.map(p => p.id) },
|
|
target: '#trip-summary',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
},
|
|
|
|
saveTrip() {
|
|
if (this.tripParks.length > 0) {
|
|
// Use HTMX to save trip
|
|
htmx.ajax('POST', '/trips/save/', {
|
|
values: {
|
|
name: 'Trip ' + new Date().toLocaleDateString(),
|
|
parks: this.tripParks.map(p => p.id)
|
|
},
|
|
target: '#saved-trips',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
}
|
|
}" class="container px-4 mx-auto">
|
|
<!-- Header -->
|
|
<div class="flex flex-col items-start justify-between gap-4 mb-6 md:flex-row md:items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Road Trip Planner</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Plan the perfect theme park adventure across multiple destinations
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<a href="{% url 'maps:universal_map' %}"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors">
|
|
<i class="mr-2 fas fa-globe"></i>View Map
|
|
</a>
|
|
<a href="{% url 'parks:park_list' %}"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors">
|
|
<i class="mr-2 fas fa-list"></i>Browse Parks
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
<!-- Left Panel - Trip Planning -->
|
|
<div class="lg:col-span-1 space-y-6">
|
|
<!-- Park Search -->
|
|
<div class="p-4 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Add Parks to Trip</h3>
|
|
|
|
<div class="relative">
|
|
<input type="text" id="park-search"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Search parks by name or location..."
|
|
hx-get="{% url 'parks:htmx_search_parks' %}"
|
|
hx-trigger="input changed delay:300ms"
|
|
hx-target="#park-search-results"
|
|
hx-indicator="#search-loading">
|
|
|
|
<div id="search-loading" class="htmx-indicator absolute right-3 top-3">
|
|
<div class="w-4 h-4 border-2 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="park-search-results" class="mt-3 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 hidden">
|
|
<!-- Search results will be populated here via HTMX -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trip Itinerary -->
|
|
<div class="p-4 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Trip Itinerary</h3>
|
|
<button class="px-3 py-1 text-sm text-red-600 hover:text-red-700 dark:text-red-400"
|
|
@click="clearTrip()">
|
|
<i class="mr-1 fas fa-trash"></i>Clear All
|
|
</button>
|
|
</div>
|
|
|
|
<div id="trip-parks" class="space-y-2 min-h-20">
|
|
<template x-if="tripParks.length === 0">
|
|
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<i class="fas fa-route text-3xl mb-3"></i>
|
|
<p>Add parks to start planning your trip</p>
|
|
<p class="text-sm mt-1">Search above or click parks on the map</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-for="(park, index) in tripParks" :key="park.id">
|
|
<div class="park-card">
|
|
<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"
|
|
x-text="index + 1"></div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900 dark:text-white" x-text="park.name"></h4>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="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>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="mt-4 space-y-2">
|
|
<button 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"
|
|
@click="optimizeRoute()"
|
|
:disabled="tripParks.length < 2">
|
|
<i class="mr-2 fas fa-route"></i>Optimize Route
|
|
</button>
|
|
<button 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"
|
|
@click="calculateRoute()"
|
|
:disabled="tripParks.length < 2">
|
|
<i class="mr-2 fas fa-map"></i>Calculate Route
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trip Summary -->
|
|
<div id="trip-summary" class="trip-summary-card" x-show="tripParks.length >= 2" x-transition>
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Trip Summary</h3>
|
|
|
|
<div class="trip-stats">
|
|
<div class="trip-stat">
|
|
<div class="trip-stat-value">-</div>
|
|
<div class="trip-stat-label">Total Miles</div>
|
|
</div>
|
|
<div class="trip-stat">
|
|
<div class="trip-stat-value">-</div>
|
|
<div class="trip-stat-label">Drive Time</div>
|
|
</div>
|
|
<div class="trip-stat">
|
|
<div class="trip-stat-value" x-text="tripParks.length">-</div>
|
|
<div class="trip-stat-label">Parks</div>
|
|
</div>
|
|
<div class="trip-stat">
|
|
<div class="trip-stat-value">-</div>
|
|
<div class="trip-stat-label">Total Rides</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button class="w-full px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors"
|
|
@click="saveTrip()">
|
|
<i class="mr-2 fas fa-save"></i>Save Trip
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel - Map -->
|
|
<div class="lg:col-span-2">
|
|
<div class="p-4 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Route Map</h3>
|
|
<div class="flex gap-2">
|
|
<button 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"
|
|
hx-post="/maps/fit-route/"
|
|
hx-vals='{"parks": "{{ tripParks|join:"," }}"}'
|
|
hx-target="#map-container"
|
|
hx-swap="none">
|
|
<i class="mr-1 fas fa-expand-arrows-alt"></i>Fit Route
|
|
</button>
|
|
<button 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"
|
|
@click="showAllParks = !showAllParks"
|
|
hx-post="/maps/toggle-parks/"
|
|
hx-vals='{"show": "{{ showAllParks }}"}'
|
|
hx-target="#map-container"
|
|
hx-swap="none">
|
|
<i class="mr-1 fas fa-eye"></i>
|
|
<span x-text="showAllParks ? 'Hide Parks' : 'Show All Parks'">Show All Parks</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="map-container" class="map-container">
|
|
<!-- Map will be loaded via HTMX -->
|
|
<div class="flex items-center justify-center h-full bg-gray-100 dark:bg-gray-800 rounded-lg">
|
|
<div class="text-center">
|
|
<div class="w-8 h-8 mx-auto mb-4 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Loading map...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Saved Trips Section -->
|
|
<div class="mt-8">
|
|
<div class="p-4 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">My Saved Trips</h3>
|
|
<button class="px-3 py-1 text-sm text-blue-600 hover:text-blue-700"
|
|
hx-get="{% url 'parks:htmx_saved_trips' %}"
|
|
hx-target="#saved-trips"
|
|
hx-trigger="click">
|
|
<i class="mr-1 fas fa-sync"></i>Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div id="saved-trips"
|
|
hx-get="{% url 'parks:htmx_saved_trips' %}"
|
|
hx-trigger="load"
|
|
hx-indicator="#trips-loading">
|
|
<!-- Saved trips will be loaded here via HTMX -->
|
|
</div>
|
|
|
|
<div id="trips-loading" class="htmx-indicator text-center py-4">
|
|
<div class="w-6 h-6 mx-auto border-2 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2">Loading saved trips...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<!-- External libraries for map functionality only -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
|
|
|
|
<!-- HTMX Error Handling (Minimal JavaScript as allowed by Context7 docs) -->
|
|
<div x-data="{
|
|
init() {
|
|
// Only essential HTMX error handling as shown in Context7 docs
|
|
this.$el.addEventListener('htmx:responseError', (evt) => {
|
|
if (evt.detail.xhr.status === 404 || evt.detail.xhr.status === 500) {
|
|
console.error('HTMX Error:', evt.detail.xhr.status);
|
|
}
|
|
});
|
|
}
|
|
}"></div>
|
|
{% endblock %}
|