mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
Add Road Trip Planner template with interactive map and trip management features
- Implemented a new HTML template for the Road Trip Planner. - Integrated Leaflet.js for interactive mapping and routing. - Added functionality for searching and selecting parks to include in a trip. - Enabled drag-and-drop reordering of selected parks. - Included trip optimization and route calculation features. - Created a summary display for trip statistics. - Added functionality to save trips and manage saved trips. - Enhanced UI with responsive design and dark mode support.
This commit is contained in:
432
templates/maps/partials/filter_panel.html
Normal file
432
templates/maps/partials/filter_panel.html
Normal file
@@ -0,0 +1,432 @@
|
||||
<!-- Reusable Filter Panel Component -->
|
||||
<div class="filter-panel {% if panel_classes %}{{ panel_classes }}{% endif %}">
|
||||
<form id="{{ form_id|default:'filters-form' }}"
|
||||
method="get"
|
||||
{% if hx_target %}hx-get="{{ hx_url }}" hx-trigger="{{ hx_trigger|default:'change, submit' }}" hx-target="{{ hx_target }}" hx-swap="{{ hx_swap|default:'none' }}" hx-push-url="false"{% endif %}
|
||||
class="space-y-4">
|
||||
|
||||
<!-- Search Input -->
|
||||
{% if show_search %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-search" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ search_label|default:"Search" }}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="q"
|
||||
id="{{ form_id }}-search"
|
||||
class="w-full pl-10 pr-4 py-2 border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="{{ search_placeholder|default:'Search by name, location, or keyword...' }}"
|
||||
value="{{ request.GET.q|default:'' }}"
|
||||
{% if search_hx_url %}
|
||||
hx-get="{{ search_hx_url }}"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-target="{{ search_hx_target|default:'#search-results' }}"
|
||||
hx-indicator="{{ search_hx_indicator|default:'#search-loading' }}"
|
||||
{% endif %}>
|
||||
<i class="absolute left-3 top-1/2 transform -translate-y-1/2 fas fa-search text-gray-400"></i>
|
||||
{% if search_hx_url %}
|
||||
<div id="{{ search_hx_indicator|default:'search-loading' }}" class="htmx-indicator absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<div class="w-4 h-4 border-2 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if search_hx_url %}
|
||||
<div id="{{ search_hx_target|default:'search-results' }}" class="mt-2"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Location Type Filters -->
|
||||
{% if show_location_types %}
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Location Types</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label class="filter-chip {% if 'park' in location_types %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="types" value="park" class="hidden"
|
||||
{% if 'park' in location_types %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-tree"></i>Parks
|
||||
</label>
|
||||
<label class="filter-chip {% if 'ride' in location_types %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="types" value="ride" class="hidden"
|
||||
{% if 'ride' in location_types %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-rocket"></i>Rides
|
||||
</label>
|
||||
<label class="filter-chip {% if 'company' in location_types %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="types" value="company" class="hidden"
|
||||
{% if 'company' in location_types %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-building"></i>Companies
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Park Status Filters -->
|
||||
{% if show_park_status and 'park' in location_types %}
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Park Status</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label class="filter-chip {% if 'OPERATING' in park_statuses %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="park_status" value="OPERATING" class="hidden"
|
||||
{% if 'OPERATING' in park_statuses %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-check-circle"></i>Operating
|
||||
</label>
|
||||
<label class="filter-chip {% if 'CLOSED_TEMP' in park_statuses %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="park_status" value="CLOSED_TEMP" class="hidden"
|
||||
{% if 'CLOSED_TEMP' in park_statuses %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-clock"></i>Temporarily Closed
|
||||
</label>
|
||||
<label class="filter-chip {% if 'CLOSED_PERM' in park_statuses %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="park_status" value="CLOSED_PERM" class="hidden"
|
||||
{% if 'CLOSED_PERM' in park_statuses %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-times-circle"></i>Permanently Closed
|
||||
</label>
|
||||
<label class="filter-chip {% if 'UNDER_CONSTRUCTION' in park_statuses %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="park_status" value="UNDER_CONSTRUCTION" class="hidden"
|
||||
{% if 'UNDER_CONSTRUCTION' in park_statuses %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-hard-hat"></i>Under Construction
|
||||
</label>
|
||||
<label class="filter-chip {% if 'DEMOLISHED' in park_statuses %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="park_status" value="DEMOLISHED" class="hidden"
|
||||
{% if 'DEMOLISHED' in park_statuses %}checked{% endif %}>
|
||||
<i class="mr-2 fas fa-ban"></i>Demolished
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Location Filters -->
|
||||
{% if show_location_filters %}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-{{ location_filter_columns|default:'3' }}">
|
||||
{% if show_country %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-country" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
|
||||
<input type="text"
|
||||
name="country"
|
||||
id="{{ form_id }}-country"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Filter by country..."
|
||||
value="{{ request.GET.country|default:'' }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_state %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-state" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">State/Region</label>
|
||||
<input type="text"
|
||||
name="state"
|
||||
id="{{ form_id }}-state"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Filter by state..."
|
||||
value="{{ request.GET.state|default:'' }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_city %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-city" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
|
||||
<input type="text"
|
||||
name="city"
|
||||
id="{{ form_id }}-city"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Filter by city..."
|
||||
value="{{ request.GET.city|default:'' }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Distance/Radius Filter -->
|
||||
{% if show_radius %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-radius" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ radius_label|default:"Search Radius" }} (miles)
|
||||
</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="range"
|
||||
name="radius"
|
||||
id="{{ form_id }}-radius"
|
||||
min="{{ radius_min|default:'1' }}"
|
||||
max="{{ radius_max|default:'500' }}"
|
||||
value="{{ request.GET.radius|default:'50' }}"
|
||||
class="flex-1"
|
||||
oninput="document.getElementById('{{ form_id }}-radius-value').textContent = this.value">
|
||||
<span id="{{ form_id }}-radius-value" class="text-sm font-medium text-gray-700 dark:text-gray-300 min-w-16">
|
||||
{{ request.GET.radius|default:'50' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sorting -->
|
||||
{% if show_sort %}
|
||||
<div>
|
||||
<label for="{{ form_id }}-sort" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Sort By</label>
|
||||
<select name="sort"
|
||||
id="{{ form_id }}-sort"
|
||||
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
{% for value, label in sort_options %}
|
||||
<option value="{{ value }}" {% if request.GET.sort == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% empty %}
|
||||
<option value="name" {% if request.GET.sort == 'name' %}selected{% endif %}>Name (A-Z)</option>
|
||||
<option value="-name" {% if request.GET.sort == '-name' %}selected{% endif %}>Name (Z-A)</option>
|
||||
<option value="location" {% if request.GET.sort == 'location' %}selected{% endif %}>Location</option>
|
||||
<option value="-created_at" {% if request.GET.sort == '-created_at' %}selected{% endif %}>Recently Added</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Map Options -->
|
||||
{% if show_map_options %}
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Map Options</label>
|
||||
<div class="space-y-2">
|
||||
{% if show_clustering_toggle %}
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="cluster"
|
||||
value="true"
|
||||
id="{{ form_id }}-cluster"
|
||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
{% if enable_clustering %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Group Nearby Locations</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
{% if show_satellite_toggle %}
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="satellite"
|
||||
value="true"
|
||||
id="{{ form_id }}-satellite"
|
||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
|
||||
<span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Satellite View</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom Filter Sections -->
|
||||
{% if custom_filters %}
|
||||
{% for filter_section in custom_filters %}
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">{{ filter_section.title }}</label>
|
||||
{% if filter_section.type == 'checkboxes' %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for option in filter_section.options %}
|
||||
<label class="filter-chip {% if option.value in filter_section.selected %}active{% else %}inactive{% endif %}">
|
||||
<input type="checkbox" name="{{ filter_section.name }}" value="{{ option.value }}" class="hidden"
|
||||
{% if option.value in filter_section.selected %}checked{% endif %}>
|
||||
{% if option.icon %}<i class="mr-2 {{ option.icon }}"></i>{% endif %}{{ option.label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif filter_section.type == 'select' %}
|
||||
<select name="{{ filter_section.name }}"
|
||||
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">{{ filter_section.placeholder|default:"All" }}</option>
|
||||
{% for option in filter_section.options %}
|
||||
<option value="{{ option.value }}" {% if option.value == filter_section.selected %}selected{% endif %}>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif filter_section.type == 'range' %}
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="range"
|
||||
name="{{ filter_section.name }}"
|
||||
min="{{ filter_section.min }}"
|
||||
max="{{ filter_section.max }}"
|
||||
value="{{ filter_section.value }}"
|
||||
class="flex-1"
|
||||
oninput="document.getElementById('{{ filter_section.name }}-value').textContent = this.value">
|
||||
<span id="{{ filter_section.name }}-value" class="text-sm font-medium text-gray-700 dark:text-gray-300 min-w-16">
|
||||
{{ filter_section.value }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 pt-2">
|
||||
{% if show_submit_button %}
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<i class="mr-2 fas fa-search"></i>{{ submit_text|default:"Apply Filters" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_clear_button %}
|
||||
<a href="{{ clear_url|default:request.path }}"
|
||||
class="px-4 py-2 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-times"></i>{{ clear_text|default:"Clear All" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom Action Buttons -->
|
||||
{% if custom_actions %}
|
||||
{% for action in custom_actions %}
|
||||
<a href="{{ action.url }}"
|
||||
class="px-4 py-2 text-sm font-medium {{ action.classes|default:'text-gray-700 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600' }} rounded-lg transition-colors">
|
||||
{% if action.icon %}<i class="mr-2 {{ action.icon }}"></i>{% endif %}{{ action.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter Panel Styles -->
|
||||
<style>
|
||||
.filter-panel {
|
||||
@apply bg-white dark:bg-gray-800 rounded-lg shadow p-4;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium cursor-pointer transition-all;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100;
|
||||
}
|
||||
|
||||
.filter-chip.inactive {
|
||||
@apply bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Custom range slider styling */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-track {
|
||||
background: #e5e7eb;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #3b82f6;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: #e5e7eb;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
background: #3b82f6;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark input[type="range"]::-webkit-slider-track {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.dark input[type="range"]::-moz-range-track {
|
||||
background: #4b5563;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Filter Panel JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const formId = '{{ form_id|default:"filters-form" }}';
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
if (!form) return;
|
||||
|
||||
// Handle filter chip toggles
|
||||
form.querySelectorAll('.filter-chip').forEach(chip => {
|
||||
const checkbox = chip.querySelector('input[type="checkbox"]');
|
||||
|
||||
chip.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
chip.classList.toggle('active', checkbox.checked);
|
||||
chip.classList.toggle('inactive', !checkbox.checked);
|
||||
|
||||
// Trigger form change for HTMX
|
||||
form.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-submit form on most changes (except search input)
|
||||
form.addEventListener('change', function(e) {
|
||||
if (e.target.name !== 'q' && !e.target.closest('.no-auto-submit')) {
|
||||
{% if hx_target %}
|
||||
// HTMX will handle the submission
|
||||
{% else %}
|
||||
this.submit();
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle search input separately with debouncing
|
||||
const searchInput = form.querySelector('input[name="q"]');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
{% if not search_hx_url %}
|
||||
form.dispatchEvent(new Event('change'));
|
||||
{% endif %}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Custom event for filter updates
|
||||
form.addEventListener('filtersUpdated', function(e) {
|
||||
// Custom logic when filters are updated
|
||||
console.log('Filters updated:', e.detail);
|
||||
});
|
||||
|
||||
// Emit initial filter state
|
||||
window.addEventListener('load', function() {
|
||||
const formData = new FormData(form);
|
||||
const filters = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (filters[key]) {
|
||||
if (Array.isArray(filters[key])) {
|
||||
filters[key].push(value);
|
||||
} else {
|
||||
filters[key] = [filters[key], value];
|
||||
}
|
||||
} else {
|
||||
filters[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const event = new CustomEvent('filtersInitialized', {
|
||||
detail: filters
|
||||
});
|
||||
form.dispatchEvent(event);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
346
templates/maps/partials/location_card.html
Normal file
346
templates/maps/partials/location_card.html
Normal file
@@ -0,0 +1,346 @@
|
||||
<!-- Reusable Location Card Component -->
|
||||
<div class="location-card {% if card_classes %}{{ card_classes }}{% endif %}"
|
||||
{% if location.id %}data-location-id="{{ location.id }}"{% endif %}
|
||||
{% if location.type %}data-location-type="{{ location.type }}"{% endif %}
|
||||
{% if location.latitude and location.longitude %}data-lat="{{ location.latitude }}" data-lng="{{ location.longitude }}"{% endif %}
|
||||
{% if clickable %}onclick="{{ onclick_action|default:'window.location.href=\''|add:location.get_absolute_url|add:'\'' }}"{% endif %}>
|
||||
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">
|
||||
{% if location.name %}
|
||||
{{ location.name }}
|
||||
{% else %}
|
||||
Unknown Location
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">
|
||||
{{ location.formatted_location|default:"Location not specified" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ml-3">
|
||||
<span class="location-type-badge location-type-{{ location.type|default:'unknown' }}">
|
||||
{% if location.type == 'park' %}
|
||||
<i class="mr-1 fas fa-tree"></i>Park
|
||||
{% elif location.type == 'ride' %}
|
||||
<i class="mr-1 fas fa-rocket"></i>Ride
|
||||
{% elif location.type == 'company' %}
|
||||
<i class="mr-1 fas fa-building"></i>Company
|
||||
{% else %}
|
||||
<i class="mr-1 fas fa-map-marker-alt"></i>Location
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Distance Badge (if applicable) -->
|
||||
{% if location.distance %}
|
||||
<div class="mb-3">
|
||||
<span class="distance-badge">
|
||||
<i class="mr-1 fas fa-route"></i>{{ location.distance|floatformat:1 }} miles away
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Type-specific Content -->
|
||||
{% if location.type == 'park' %}
|
||||
{% include 'maps/partials/park_card_content.html' with park=location %}
|
||||
{% elif location.type == 'ride' %}
|
||||
{% include 'maps/partials/ride_card_content.html' with ride=location %}
|
||||
{% elif location.type == 'company' %}
|
||||
{% include 'maps/partials/company_card_content.html' with company=location %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
{% if show_actions %}
|
||||
<div class="flex gap-2 mt-4">
|
||||
<a href="{{ location.get_absolute_url }}"
|
||||
class="flex-1 px-3 py-2 text-sm text-center text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
|
||||
{{ primary_action_text|default:"View Details" }}
|
||||
</a>
|
||||
|
||||
{% if location.latitude and location.longitude %}
|
||||
<a href="{% url 'maps:nearby_locations' %}?lat={{ location.latitude }}&lng={{ location.longitude }}&radius=25"
|
||||
class="px-3 py-2 text-sm text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors"
|
||||
title="Find nearby locations">
|
||||
<i class="fas fa-search-location"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if show_map_action %}
|
||||
<button onclick="showOnMap('{{ location.type }}', {{ location.id }})"
|
||||
class="px-3 py-2 text-sm text-green-600 border border-green-600 rounded-lg hover:bg-green-50 dark:hover:bg-green-900 transition-colors"
|
||||
title="Show on map">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_trip_action %}
|
||||
<button onclick="addToTrip({{ location|safe }})"
|
||||
class="px-3 py-2 text-sm text-purple-600 border border-purple-600 rounded-lg hover:bg-purple-50 dark:hover:bg-purple-900 transition-colors"
|
||||
title="Add to trip">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Card Content Partials -->
|
||||
|
||||
<!-- Park Card Content -->
|
||||
{% comment %}
|
||||
This would be in templates/maps/partials/park_card_content.html
|
||||
{% endcomment %}
|
||||
<script type="text/template" id="park-card-content-template">
|
||||
<div class="space-y-2">
|
||||
{% if park.status %}
|
||||
<div class="flex items-center">
|
||||
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction{% else %}status-demolished{% endif %}">
|
||||
{% if park.status == 'OPERATING' %}
|
||||
<i class="mr-1 fas fa-check-circle"></i>Operating
|
||||
{% elif park.status == 'CLOSED_TEMP' %}
|
||||
<i class="mr-1 fas fa-clock"></i>Temporarily Closed
|
||||
{% elif park.status == 'CLOSED_PERM' %}
|
||||
<i class="mr-1 fas fa-times-circle"></i>Permanently Closed
|
||||
{% elif park.status == 'UNDER_CONSTRUCTION' %}
|
||||
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
||||
{% elif park.status == 'DEMOLISHED' %}
|
||||
<i class="mr-1 fas fa-ban"></i>Demolished
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.operator %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-building"></i>
|
||||
<span>{{ park.operator }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.ride_count %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-rocket"></i>
|
||||
<span>{{ park.ride_count }} ride{{ park.ride_count|pluralize }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.average_rating %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-star text-yellow-500"></i>
|
||||
<span>{{ park.average_rating|floatformat:1 }}/10</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.opening_date %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-calendar"></i>
|
||||
<span>Opened {{ park.opening_date.year }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Ride Card Content -->
|
||||
<script type="text/template" id="ride-card-content-template">
|
||||
<div class="space-y-2">
|
||||
{% if ride.park_name %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-tree"></i>
|
||||
<span>{{ ride.park_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.manufacturer %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-industry"></i>
|
||||
<span>{{ ride.manufacturer }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.designer %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-drafting-compass"></i>
|
||||
<span>{{ ride.designer }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.opening_date %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-calendar"></i>
|
||||
<span>Opened {{ ride.opening_date.year }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.status %}
|
||||
<div class="flex items-center">
|
||||
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating{% elif ride.status == 'CLOSED' %}status-closed{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction{% else %}status-demolished{% endif %}">
|
||||
{% if ride.status == 'OPERATING' %}
|
||||
<i class="mr-1 fas fa-check-circle"></i>Operating
|
||||
{% elif ride.status == 'CLOSED' %}
|
||||
<i class="mr-1 fas fa-times-circle"></i>Closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}
|
||||
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}
|
||||
<i class="mr-1 fas fa-ban"></i>Demolished
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Company Card Content -->
|
||||
<script type="text/template" id="company-card-content-template">
|
||||
<div class="space-y-2">
|
||||
{% if company.company_type %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-tag"></i>
|
||||
<span>{{ company.get_company_type_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.founded_year %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-calendar"></i>
|
||||
<span>Founded {{ company.founded_year }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.website %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-globe"></i>
|
||||
<a href="{{ company.website }}" target="_blank" class="text-blue-600 hover:text-blue-700 dark:text-blue-400">
|
||||
Visit Website
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.parks_count %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-tree"></i>
|
||||
<span>{{ company.parks_count }} park{{ company.parks_count|pluralize }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.rides_count %}
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-rocket"></i>
|
||||
<span>{{ company.rides_count }} ride{{ company.rides_count|pluralize }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Location Card Styles -->
|
||||
<style>
|
||||
.location-card {
|
||||
@apply bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm hover:shadow-md transition-all border border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.location-card:hover {
|
||||
@apply border-blue-300 dark:border-blue-600 shadow-lg;
|
||||
}
|
||||
|
||||
.location-card.selected {
|
||||
@apply ring-2 ring-blue-500 border-blue-500;
|
||||
}
|
||||
|
||||
.location-card.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-type-badge {
|
||||
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.location-type-park {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100;
|
||||
}
|
||||
|
||||
.location-type-ride {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100;
|
||||
}
|
||||
|
||||
.location-type-company {
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100;
|
||||
}
|
||||
|
||||
.location-type-unknown {
|
||||
@apply bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.distance-badge {
|
||||
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.status-operating {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100;
|
||||
}
|
||||
|
||||
.status-construction {
|
||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100;
|
||||
}
|
||||
|
||||
.status-demolished {
|
||||
@apply bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Location Card JavaScript -->
|
||||
<script>
|
||||
// Global functions for location card actions
|
||||
window.showOnMap = function(type, id) {
|
||||
// Emit custom event for map integration
|
||||
const event = new CustomEvent('showLocationOnMap', {
|
||||
detail: { type, id }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
window.addToTrip = function(locationData) {
|
||||
// Emit custom event for trip integration
|
||||
const event = new CustomEvent('addLocationToTrip', {
|
||||
detail: locationData
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
// Handle location card selection
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('click', function(e) {
|
||||
const card = e.target.closest('.location-card');
|
||||
if (card && card.dataset.locationId) {
|
||||
// Remove previous selections
|
||||
document.querySelectorAll('.location-card.selected').forEach(c => {
|
||||
c.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to clicked card
|
||||
card.classList.add('selected');
|
||||
|
||||
// Emit selection event
|
||||
const event = new CustomEvent('locationCardSelected', {
|
||||
detail: {
|
||||
id: card.dataset.locationId,
|
||||
type: card.dataset.locationType,
|
||||
lat: card.dataset.lat,
|
||||
lng: card.dataset.lng,
|
||||
element: card
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
530
templates/maps/partials/location_popup.html
Normal file
530
templates/maps/partials/location_popup.html
Normal file
@@ -0,0 +1,530 @@
|
||||
<!-- Reusable Location Popup Component for Maps -->
|
||||
<div class="location-popup {% if popup_classes %}{{ popup_classes }}{% endif %}"
|
||||
data-location-id="{{ location.id }}"
|
||||
data-location-type="{{ location.type }}">
|
||||
|
||||
<!-- Popup Header -->
|
||||
<div class="popup-header">
|
||||
<h3 class="popup-title">{{ location.name|default:"Unknown Location" }}</h3>
|
||||
{% if location.type %}
|
||||
<span class="popup-type-badge popup-type-{{ location.type }}">
|
||||
{% if location.type == 'park' %}
|
||||
<i class="mr-1 fas fa-tree"></i>Park
|
||||
{% elif location.type == 'ride' %}
|
||||
<i class="mr-1 fas fa-rocket"></i>Ride
|
||||
{% elif location.type == 'company' %}
|
||||
<i class="mr-1 fas fa-building"></i>Company
|
||||
{% else %}
|
||||
<i class="mr-1 fas fa-map-marker-alt"></i>Location
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Location Information -->
|
||||
{% if location.formatted_location %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-map-marker-alt mr-1"></i>{{ location.formatted_location }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Distance (if applicable) -->
|
||||
{% if location.distance %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-route mr-1"></i>{{ location.distance|floatformat:1 }} miles away
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Type-specific Content -->
|
||||
{% if location.type == 'park' %}
|
||||
<!-- Park-specific popup content -->
|
||||
{% if location.status %}
|
||||
<div class="popup-meta">
|
||||
<span class="popup-status-badge popup-status-{% if location.status == 'OPERATING' %}operating{% elif location.status == 'CLOSED_TEMP' or location.status == 'CLOSED_PERM' %}closed{% elif location.status == 'UNDER_CONSTRUCTION' %}construction{% else %}demolished{% endif %}">
|
||||
{% if location.status == 'OPERATING' %}
|
||||
<i class="mr-1 fas fa-check-circle"></i>Operating
|
||||
{% elif location.status == 'CLOSED_TEMP' %}
|
||||
<i class="mr-1 fas fa-clock"></i>Temporarily Closed
|
||||
{% elif location.status == 'CLOSED_PERM' %}
|
||||
<i class="mr-1 fas fa-times-circle"></i>Permanently Closed
|
||||
{% elif location.status == 'UNDER_CONSTRUCTION' %}
|
||||
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
||||
{% elif location.status == 'DEMOLISHED' %}
|
||||
<i class="mr-1 fas fa-ban"></i>Demolished
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.operator %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-building mr-1"></i>{{ location.operator }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.ride_count %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-rocket mr-1"></i>{{ location.ride_count }} ride{{ location.ride_count|pluralize }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.average_rating %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-star mr-1 text-yellow-500"></i>{{ location.average_rating|floatformat:1 }}/10 rating
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.opening_date %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-calendar mr-1"></i>Opened {{ location.opening_date.year }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif location.type == 'ride' %}
|
||||
<!-- Ride-specific popup content -->
|
||||
{% if location.park_name %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-tree mr-1"></i>{{ location.park_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.manufacturer %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-industry mr-1"></i>{{ location.manufacturer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.designer %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-drafting-compass mr-1"></i>{{ location.designer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.opening_date %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-calendar mr-1"></i>Opened {{ location.opening_date.year }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.status %}
|
||||
<div class="popup-meta">
|
||||
<span class="popup-status-badge popup-status-{% if location.status == 'OPERATING' %}operating{% elif location.status == 'CLOSED' %}closed{% elif location.status == 'UNDER_CONSTRUCTION' %}construction{% else %}demolished{% endif %}">
|
||||
{% if location.status == 'OPERATING' %}
|
||||
<i class="mr-1 fas fa-check-circle"></i>Operating
|
||||
{% elif location.status == 'CLOSED' %}
|
||||
<i class="mr-1 fas fa-times-circle"></i>Closed
|
||||
{% elif location.status == 'UNDER_CONSTRUCTION' %}
|
||||
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
||||
{% elif location.status == 'DEMOLISHED' %}
|
||||
<i class="mr-1 fas fa-ban"></i>Demolished
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif location.type == 'company' %}
|
||||
<!-- Company-specific popup content -->
|
||||
{% if location.company_type %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-tag mr-1"></i>{{ location.get_company_type_display }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.founded_year %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-calendar mr-1"></i>Founded {{ location.founded_year }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.parks_count %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-tree mr-1"></i>{{ location.parks_count }} park{{ location.parks_count|pluralize }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if location.rides_count %}
|
||||
<div class="popup-meta">
|
||||
<i class="fas fa-rocket mr-1"></i>{{ location.rides_count }} ride{{ location.rides_count|pluralize }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom Content -->
|
||||
{% if custom_content %}
|
||||
<div class="popup-custom">
|
||||
{{ custom_content|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="popup-actions">
|
||||
{% if show_details_button %}
|
||||
<a href="{{ location.get_absolute_url }}"
|
||||
class="popup-btn popup-btn-primary">
|
||||
<i class="mr-1 fas fa-info-circle"></i>{{ details_button_text|default:"View Details" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if show_nearby_button and location.latitude and location.longitude %}
|
||||
<a href="{% url 'maps:nearby_locations' %}?lat={{ location.latitude }}&lng={{ location.longitude }}&radius=25"
|
||||
class="popup-btn popup-btn-secondary">
|
||||
<i class="mr-1 fas fa-search-location"></i>{{ nearby_button_text|default:"Find Nearby" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if show_directions_button and location.latitude and location.longitude %}
|
||||
<button onclick="getDirections({{ location.latitude }}, {{ location.longitude }})"
|
||||
class="popup-btn popup-btn-secondary">
|
||||
<i class="mr-1 fas fa-directions"></i>{{ directions_button_text|default:"Directions" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_trip_button %}
|
||||
<button onclick="addLocationToTrip({{ location|safe }})"
|
||||
class="popup-btn popup-btn-accent">
|
||||
<i class="mr-1 fas fa-plus"></i>{{ trip_button_text|default:"Add to Trip" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_share_button %}
|
||||
<button onclick="shareLocation('{{ location.type }}', {{ location.id }})"
|
||||
class="popup-btn popup-btn-secondary">
|
||||
<i class="mr-1 fas fa-share"></i>{{ share_button_text|default:"Share" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom Action Buttons -->
|
||||
{% if custom_actions %}
|
||||
{% for action in custom_actions %}
|
||||
<{{ action.tag|default:"button" }}
|
||||
{% if action.href %}href="{{ action.href }}"{% endif %}
|
||||
{% if action.onclick %}onclick="{{ action.onclick }}"{% endif %}
|
||||
class="popup-btn {{ action.classes|default:'popup-btn-secondary' }}">
|
||||
{% if action.icon %}<i class="mr-1 {{ action.icon }}"></i>{% endif %}{{ action.text }}
|
||||
</{{ action.tag|default:"button" }}>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Popup Styles -->
|
||||
<style>
|
||||
.location-popup {
|
||||
max-width: 350px;
|
||||
min-width: 250px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
line-height: 1.3;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.popup-type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-type-park {
|
||||
background-color: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.popup-type-ride {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.popup-type-company {
|
||||
background-color: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.popup-meta {
|
||||
margin: 0.375rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-meta i {
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-status-operating {
|
||||
background-color: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.popup-status-closed {
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.popup-status-construction {
|
||||
background-color: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.popup-status-demolished {
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.popup-custom {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.popup-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.popup-btn-primary {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup-btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup-btn-secondary {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.popup-btn-secondary:hover {
|
||||
background-color: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.popup-btn-accent {
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup-btn-accent:hover {
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.popup-title {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.popup-meta {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.popup-type-park {
|
||||
background-color: #166534;
|
||||
color: #dcfce7;
|
||||
}
|
||||
|
||||
.popup-type-ride {
|
||||
background-color: #1e40af;
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.popup-type-company {
|
||||
background-color: #7c3aed;
|
||||
color: #ede9fe;
|
||||
}
|
||||
|
||||
.popup-status-operating {
|
||||
background-color: #166534;
|
||||
color: #dcfce7;
|
||||
}
|
||||
|
||||
.popup-status-closed {
|
||||
background-color: #dc2626;
|
||||
color: #fee2e2;
|
||||
}
|
||||
|
||||
.popup-status-construction {
|
||||
background-color: #d97706;
|
||||
color: #fef3c7;
|
||||
}
|
||||
|
||||
.popup-status-demolished {
|
||||
background-color: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.popup-btn-secondary {
|
||||
background-color: #374151;
|
||||
color: #f3f4f6;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
.popup-btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.location-popup {
|
||||
max-width: 280px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Popup JavaScript Functions -->
|
||||
<script>
|
||||
// Global functions for popup actions
|
||||
window.getDirections = function(lat, lng) {
|
||||
// Open directions in user's preferred map app
|
||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
// Try to open in native maps app
|
||||
window.open(`geo:${lat},${lng}`, '_blank');
|
||||
} else {
|
||||
// Open in Google Maps
|
||||
window.open(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
window.addLocationToTrip = function(locationData) {
|
||||
// Emit custom event for trip integration
|
||||
const event = new CustomEvent('addLocationToTrip', {
|
||||
detail: locationData
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
// Show feedback
|
||||
showPopupFeedback('Added to trip!', 'success');
|
||||
};
|
||||
|
||||
window.shareLocation = function(type, id) {
|
||||
// Share location URL
|
||||
const url = window.location.origin + `/{{ type }}/${id}/`;
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'Check out this location on ThrillWiki',
|
||||
url: url
|
||||
});
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
showPopupFeedback('Link copied to clipboard!', 'success');
|
||||
}).catch(() => {
|
||||
showPopupFeedback('Could not copy link', 'error');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.showPopupFeedback = function(message, type = 'info') {
|
||||
// Create temporary feedback element
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = `popup-feedback popup-feedback-${type}`;
|
||||
feedback.textContent = message;
|
||||
feedback.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
feedback.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(feedback);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Add CSS animations
|
||||
if (!document.getElementById('popup-animations')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'popup-animations';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
</script>
|
||||
196
templates/maps/partials/map_container.html
Normal file
196
templates/maps/partials/map_container.html
Normal file
@@ -0,0 +1,196 @@
|
||||
<!-- Reusable Map Container Component -->
|
||||
<div class="relative">
|
||||
<div id="{{ map_id|default:'map-container' }}"
|
||||
class="map-container {% if map_classes %}{{ map_classes }}{% endif %}"
|
||||
style="{% if map_height %}height: {{ map_height }};{% endif %}">
|
||||
</div>
|
||||
|
||||
<!-- Map Loading Indicator -->
|
||||
<div id="{{ map_id|default:'map-container' }}-loading"
|
||||
class="htmx-indicator absolute inset-0 flex items-center justify-center 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_text|default:"Loading map data..." }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Controls Overlay -->
|
||||
{% if show_controls %}
|
||||
<div class="absolute top-4 right-4 z-10 space-y-2">
|
||||
{% if show_fullscreen %}
|
||||
<button id="{{ map_id|default:'map-container' }}-fullscreen"
|
||||
class="p-2 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
title="Toggle Fullscreen">
|
||||
<i class="fas fa-expand text-gray-600 dark:text-gray-400"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_layers %}
|
||||
<button id="{{ map_id|default:'map-container' }}-layers"
|
||||
class="p-2 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
title="Map Layers">
|
||||
<i class="fas fa-layer-group text-gray-600 dark:text-gray-400"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_locate %}
|
||||
<button id="{{ map_id|default:'map-container' }}-locate"
|
||||
class="p-2 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
title="Find My Location">
|
||||
<i class="fas fa-crosshairs text-gray-600 dark:text-gray-400"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Map Legend -->
|
||||
{% if show_legend %}
|
||||
<div class="absolute bottom-4 left-4 z-10">
|
||||
<div class="p-3 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Legend</h4>
|
||||
<div class="space-y-1 text-xs">
|
||||
{% if legend_items %}
|
||||
{% for item in legend_items %}
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 mr-2 rounded-full" style="background-color: {{ item.color }};"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ item.label }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-green-500"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">Operating Parks</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-blue-500"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">Rides</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-purple-500"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">Companies</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-red-500"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">Closed/Demolished</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Map Container Styles -->
|
||||
<style>
|
||||
.map-container {
|
||||
height: {{ map_height|default:'60vh' }};
|
||||
min-height: {{ min_height|default:'400px' }};
|
||||
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);
|
||||
}
|
||||
|
||||
.map-container.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
border-radius: 0;
|
||||
height: 100vh !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.map-container.fullscreen + .absolute {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.dark .map-container {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Map Container JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mapId = '{{ map_id|default:"map-container" }}';
|
||||
const mapContainer = document.getElementById(mapId);
|
||||
|
||||
{% if show_fullscreen %}
|
||||
// Fullscreen toggle
|
||||
const fullscreenBtn = document.getElementById(mapId + '-fullscreen');
|
||||
if (fullscreenBtn) {
|
||||
fullscreenBtn.addEventListener('click', function() {
|
||||
const icon = this.querySelector('i');
|
||||
|
||||
if (mapContainer.classList.contains('fullscreen')) {
|
||||
mapContainer.classList.remove('fullscreen');
|
||||
icon.className = 'fas fa-expand text-gray-600 dark:text-gray-400';
|
||||
this.title = 'Toggle Fullscreen';
|
||||
} else {
|
||||
mapContainer.classList.add('fullscreen');
|
||||
icon.className = 'fas fa-compress text-gray-600 dark:text-gray-400';
|
||||
this.title = 'Exit Fullscreen';
|
||||
}
|
||||
|
||||
// Trigger map resize if map instance exists
|
||||
if (window[mapId + 'Instance']) {
|
||||
setTimeout(() => {
|
||||
window[mapId + 'Instance'].invalidateSize();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if show_locate %}
|
||||
// Geolocation
|
||||
const locateBtn = document.getElementById(mapId + '-locate');
|
||||
if (locateBtn && navigator.geolocation) {
|
||||
locateBtn.addEventListener('click', function() {
|
||||
const icon = this.querySelector('i');
|
||||
icon.className = 'fas fa-spinner fa-spin text-gray-600 dark:text-gray-400';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
icon.className = 'fas fa-crosshairs text-gray-600 dark:text-gray-400';
|
||||
|
||||
// Trigger custom event with user location
|
||||
const event = new CustomEvent('userLocationFound', {
|
||||
detail: {
|
||||
lat: position.coords.latitude,
|
||||
lng: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy
|
||||
}
|
||||
});
|
||||
mapContainer.dispatchEvent(event);
|
||||
},
|
||||
function(error) {
|
||||
icon.className = 'fas fa-crosshairs text-red-500';
|
||||
console.error('Geolocation error:', error);
|
||||
|
||||
// Reset icon after delay
|
||||
setTimeout(() => {
|
||||
icon.className = 'fas fa-crosshairs text-gray-600 dark:text-gray-400';
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Escape key handler for fullscreen
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mapContainer.classList.contains('fullscreen')) {
|
||||
const fullscreenBtn = document.getElementById(mapId + '-fullscreen');
|
||||
if (fullscreenBtn) {
|
||||
fullscreenBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user