mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
feat: Implement comprehensive ride filtering system with API integration
- Added `useRideFiltering` composable for managing ride filters and fetching rides from the API. - Created `useParkRideFiltering` for park-specific ride filtering. - Developed `useTheme` composable for theme management with localStorage support. - Established `rideFiltering` Pinia store for centralized state management of ride filters and UI state. - Defined enhanced filter types in `filters.ts` for better type safety and clarity. - Built `RideFilteringPage.vue` to provide a user interface for filtering rides with responsive design. - Integrated filter sidebar and ride list display components for a cohesive user experience. - Added support for filter presets and search suggestions. - Implemented computed properties for active filters, average ratings, and operating counts.
This commit is contained in:
465
backend/templates/rides/partials/filter_sidebar.html
Normal file
465
backend/templates/rides/partials/filter_sidebar.html
Normal file
@@ -0,0 +1,465 @@
|
||||
{% load static %}
|
||||
|
||||
<!-- Advanced Ride Filters Sidebar -->
|
||||
<div class="filter-sidebar bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 h-full overflow-y-auto">
|
||||
<!-- Filter Header -->
|
||||
<div class="sticky top-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 p-4 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<i class="fas fa-filter mr-2 text-blue-600 dark:text-blue-400"></i>
|
||||
Filters
|
||||
</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Filter Count Badge -->
|
||||
{% if has_filters %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{{ active_filters|length }} active
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Clear All Filters -->
|
||||
{% if has_filters %}
|
||||
<button type="button"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
class="text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 font-medium">
|
||||
Clear All
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Filters Summary -->
|
||||
{% if has_filters %}
|
||||
<div class="mt-3 space-y-1">
|
||||
{% for category, filters in active_filters.items %}
|
||||
{% if filters %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for filter_name, filter_value in filters.items %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
||||
{{ filter_name }}: {{ filter_value }}
|
||||
<button type="button"
|
||||
class="ml-1 h-3 w-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
onclick="removeFilter('{{ category }}', '{{ filter_name }}')">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<form id="filter-form"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="change, input delay:500ms"
|
||||
hx-push-url="true"
|
||||
class="space-y-1">
|
||||
|
||||
<!-- Search Text Filter -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="search-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-search mr-2 text-gray-500"></i>
|
||||
Search
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="search-section" class="filter-content p-4 space-y-3">
|
||||
{{ filter_form.search_text.label_tag }}
|
||||
{{ filter_form.search_text }}
|
||||
|
||||
<div class="mt-2">
|
||||
<label class="flex items-center text-sm">
|
||||
{{ filter_form.search_exact }}
|
||||
<span class="ml-2 text-gray-600 dark:text-gray-400">Exact match</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Info Filter -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="basic-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2 text-gray-500"></i>
|
||||
Basic Info
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="basic-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Categories -->
|
||||
<div>
|
||||
{{ filter_form.categories.label_tag }}
|
||||
{{ filter_form.categories }}
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div>
|
||||
{{ filter_form.status.label_tag }}
|
||||
{{ filter_form.status }}
|
||||
</div>
|
||||
|
||||
<!-- Parks -->
|
||||
<div>
|
||||
{{ filter_form.parks.label_tag }}
|
||||
{{ filter_form.parks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Filters -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="date-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-calendar mr-2 text-gray-500"></i>
|
||||
Date Ranges
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="date-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Opening Date Range -->
|
||||
<div>
|
||||
{{ filter_form.opening_date_range.label_tag }}
|
||||
{{ filter_form.opening_date_range }}
|
||||
</div>
|
||||
|
||||
<!-- Closing Date Range -->
|
||||
<div>
|
||||
{{ filter_form.closing_date_range.label_tag }}
|
||||
{{ filter_form.closing_date_range }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Height & Safety -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="height-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-ruler-vertical mr-2 text-gray-500"></i>
|
||||
Height & Safety
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="height-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Height Requirements -->
|
||||
<div>
|
||||
{{ filter_form.height_requirements.label_tag }}
|
||||
{{ filter_form.height_requirements }}
|
||||
</div>
|
||||
|
||||
<!-- Height Range -->
|
||||
<div>
|
||||
{{ filter_form.height_range.label_tag }}
|
||||
{{ filter_form.height_range }}
|
||||
</div>
|
||||
|
||||
<!-- Accessibility -->
|
||||
<div>
|
||||
{{ filter_form.accessibility_features.label_tag }}
|
||||
{{ filter_form.accessibility_features }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="performance-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-tachometer-alt mr-2 text-gray-500"></i>
|
||||
Performance
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="performance-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Speed Range -->
|
||||
<div>
|
||||
{{ filter_form.speed_range.label_tag }}
|
||||
{{ filter_form.speed_range }}
|
||||
</div>
|
||||
|
||||
<!-- Duration Range -->
|
||||
<div>
|
||||
{{ filter_form.duration_range.label_tag }}
|
||||
{{ filter_form.duration_range }}
|
||||
</div>
|
||||
|
||||
<!-- Capacity Range -->
|
||||
<div>
|
||||
{{ filter_form.capacity_range.label_tag }}
|
||||
{{ filter_form.capacity_range }}
|
||||
</div>
|
||||
|
||||
<!-- Intensity -->
|
||||
<div>
|
||||
{{ filter_form.intensity_levels.label_tag }}
|
||||
{{ filter_form.intensity_levels }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relationships -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="relationships-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-sitemap mr-2 text-gray-500"></i>
|
||||
Companies & Models
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="relationships-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Manufacturers -->
|
||||
<div>
|
||||
{{ filter_form.manufacturers.label_tag }}
|
||||
{{ filter_form.manufacturers }}
|
||||
</div>
|
||||
|
||||
<!-- Designers -->
|
||||
<div>
|
||||
{{ filter_form.designers.label_tag }}
|
||||
{{ filter_form.designers }}
|
||||
</div>
|
||||
|
||||
<!-- Ride Models -->
|
||||
<div>
|
||||
{{ filter_form.ride_models.label_tag }}
|
||||
{{ filter_form.ride_models }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roller Coaster Specific -->
|
||||
<div class="filter-section border-b border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="coaster-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-mountain mr-2 text-gray-500"></i>
|
||||
Roller Coaster Details
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="coaster-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Track Type -->
|
||||
<div>
|
||||
{{ filter_form.track_types.label_tag }}
|
||||
{{ filter_form.track_types }}
|
||||
</div>
|
||||
|
||||
<!-- Launch Types -->
|
||||
<div>
|
||||
{{ filter_form.launch_types.label_tag }}
|
||||
{{ filter_form.launch_types }}
|
||||
</div>
|
||||
|
||||
<!-- Inversions Range -->
|
||||
<div>
|
||||
{{ filter_form.inversions_range.label_tag }}
|
||||
{{ filter_form.inversions_range }}
|
||||
</div>
|
||||
|
||||
<!-- Drop Range -->
|
||||
<div>
|
||||
{{ filter_form.drop_range.label_tag }}
|
||||
{{ filter_form.drop_range }}
|
||||
</div>
|
||||
|
||||
<!-- Length Range -->
|
||||
<div>
|
||||
{{ filter_form.length_range.label_tag }}
|
||||
{{ filter_form.length_range }}
|
||||
</div>
|
||||
|
||||
<!-- Has Features -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Features</label>
|
||||
<div class="space-y-1">
|
||||
<label class="flex items-center text-sm">
|
||||
{{ filter_form.has_inversions }}
|
||||
<span class="ml-2 text-gray-600 dark:text-gray-400">Has Inversions</span>
|
||||
</label>
|
||||
<label class="flex items-center text-sm">
|
||||
{{ filter_form.has_launch }}
|
||||
<span class="ml-2 text-gray-600 dark:text-gray-400">Has Launch System</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sorting -->
|
||||
<div class="filter-section">
|
||||
<button type="button"
|
||||
class="filter-toggle w-full px-4 py-3 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none"
|
||||
data-target="sorting-section">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-sort mr-2 text-gray-500"></i>
|
||||
Sorting
|
||||
</span>
|
||||
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div id="sorting-section" class="filter-content p-4 space-y-4">
|
||||
<!-- Sort By -->
|
||||
<div>
|
||||
{{ filter_form.sort_by.label_tag }}
|
||||
{{ filter_form.sort_by }}
|
||||
</div>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<div>
|
||||
{{ filter_form.sort_order.label_tag }}
|
||||
{{ filter_form.sort_order }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize collapsible sections
|
||||
initializeFilterSections();
|
||||
|
||||
// Initialize filter form handlers
|
||||
initializeFilterForm();
|
||||
});
|
||||
|
||||
function initializeFilterSections() {
|
||||
const toggles = document.querySelectorAll('.filter-toggle');
|
||||
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-target');
|
||||
const content = document.getElementById(targetId);
|
||||
const chevron = this.querySelector('.fa-chevron-down');
|
||||
|
||||
if (content.style.display === 'none' || content.style.display === '') {
|
||||
content.style.display = 'block';
|
||||
chevron.style.transform = 'rotate(180deg)';
|
||||
localStorage.setItem(`filter-${targetId}`, 'open');
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
chevron.style.transform = 'rotate(0deg)';
|
||||
localStorage.setItem(`filter-${targetId}`, 'closed');
|
||||
}
|
||||
});
|
||||
|
||||
// Restore section state from localStorage
|
||||
const targetId = toggle.getAttribute('data-target');
|
||||
const content = document.getElementById(targetId);
|
||||
const chevron = toggle.querySelector('.fa-chevron-down');
|
||||
const state = localStorage.getItem(`filter-${targetId}`);
|
||||
|
||||
if (state === 'closed') {
|
||||
content.style.display = 'none';
|
||||
chevron.style.transform = 'rotate(0deg)';
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
chevron.style.transform = 'rotate(180deg)';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializeFilterForm() {
|
||||
const form = document.getElementById('filter-form');
|
||||
if (!form) return;
|
||||
|
||||
// Handle multi-select changes
|
||||
const selects = form.querySelectorAll('select[multiple]');
|
||||
selects.forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
// Trigger HTMX update
|
||||
htmx.trigger(form, 'change');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle range inputs
|
||||
const rangeInputs = form.querySelectorAll('input[type="range"], input[type="number"]');
|
||||
rangeInputs.forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
// Debounced update
|
||||
clearTimeout(this.updateTimeout);
|
||||
this.updateTimeout = setTimeout(() => {
|
||||
htmx.trigger(form, 'input');
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeFilter(category, filterName) {
|
||||
const form = document.getElementById('filter-form');
|
||||
const input = form.querySelector(`[name*="${filterName}"]`);
|
||||
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = false;
|
||||
} else if (input.tagName === 'SELECT') {
|
||||
if (input.multiple) {
|
||||
Array.from(input.options).forEach(option => option.selected = false);
|
||||
} else {
|
||||
input.value = '';
|
||||
}
|
||||
} else {
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
// Trigger form update
|
||||
htmx.trigger(form, 'change');
|
||||
}
|
||||
}
|
||||
|
||||
// Update filter counts
|
||||
function updateFilterCounts() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const formData = new FormData(form);
|
||||
let activeCount = 0;
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const badge = document.querySelector('.filter-count-badge');
|
||||
if (badge) {
|
||||
badge.textContent = activeCount;
|
||||
badge.style.display = activeCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,105 +1,326 @@
|
||||
{% load ride_tags %}
|
||||
<!-- Active filters display (mobile and desktop) -->
|
||||
{% if has_filters %}
|
||||
<div class="mb-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
<i class="fas fa-filter mr-2"></i>
|
||||
Active Filters ({{ active_filters|length }})
|
||||
</h3>
|
||||
<button hx-get="{% url 'rides:ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 transition-colors">
|
||||
<i class="fas fa-times mr-1"></i>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for filter_key, filter_info in active_filters.items %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100">
|
||||
{{ filter_info.label }}: {{ filter_info.value }}
|
||||
<button hx-get="{{ filter_info.remove_url }}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="ml-1 text-blue-600 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-100">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Rides Grid -->
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for ride in rides %}
|
||||
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
|
||||
<div class="aspect-w-16 aspect-h-9">
|
||||
<a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}"
|
||||
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||
{{ ride.name }}
|
||||
{% if ride.photos.exists %}
|
||||
<img src="{{ ride.photos.first.image.url }}"
|
||||
alt="{{ ride.name }}"
|
||||
class="object-cover w-full">
|
||||
{% else %}
|
||||
<img src="{% get_ride_placeholder_image ride.category %}"
|
||||
alt="{{ ride.name }}"
|
||||
class="object-cover w-full">
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h2 class="mb-2 text-xl font-bold">
|
||||
<a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}"
|
||||
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||
{{ ride.name }}
|
||||
</a>
|
||||
</h2>
|
||||
{% if not park %}
|
||||
<p class="mb-3 text-gray-600 dark:text-gray-400">
|
||||
at <a href="{% url 'parks:park_detail' ride.park.slug %}"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ ride.park.name }}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-400/30 dark:text-blue-200 dark:ring-1 dark:ring-blue-400/30">
|
||||
{{ ride.get_category_display }}
|
||||
</span>
|
||||
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating
|
||||
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}status-demolished
|
||||
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
</span>
|
||||
{% if ride.average_rating %}
|
||||
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
|
||||
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
|
||||
{{ ride.average_rating|floatformat:1 }}/10
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if ride.coaster_stats %}
|
||||
<div class="grid grid-cols-2 gap-2 mt-4">
|
||||
{% if ride.coaster_stats.height_ft %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Height: {{ ride.coaster_stats.height_ft }}ft
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ride.coaster_stats.speed_mph %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Speed: {{ ride.coaster_stats.speed_mph }}mph
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Results header with count and sorting -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
||||
<div class="mb-4 sm:mb-0">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{% if filtered_count == total_rides %}
|
||||
All {{ total_rides }} ride{{ total_rides|pluralize }}
|
||||
{% else %}
|
||||
{{ filtered_count }} of {{ total_rides }} ride{{ total_rides|pluralize }}
|
||||
{% endif %}
|
||||
{% if park %}
|
||||
at {{ park.name }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if query %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<i class="fas fa-search mr-1"></i>
|
||||
Searching for: "{{ query }}"
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Sort options -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="sort-select" class="text-sm font-medium text-gray-700 dark:text-gray-300">Sort by:</label>
|
||||
<select id="sort-select"
|
||||
name="sort"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-include="[name='q'], [name='category'], [name='manufacturer'], [name='designer'], [name='min_height'], [name='max_height'], [name='min_speed'], [name='max_speed'], [name='min_capacity'], [name='max_capacity'], [name='min_duration'], [name='max_duration'], [name='opened_after'], [name='opened_before'], [name='closed_after'], [name='closed_before'], [name='operating_status'], [name='has_inversions'], [name='has_launches'], [name='track_type'], [name='min_inversions'], [name='max_inversions'], [name='min_launches'], [name='max_launches'], [name='min_top_speed'], [name='max_top_speed'], [name='min_max_height'], [name='max_max_height']{% if park %}, [name='park']{% endif %}"
|
||||
hx-push-url="true"
|
||||
class="form-select text-sm">
|
||||
<option value="name" {% if current_sort == 'name' %}selected{% endif %}>Name (A-Z)</option>
|
||||
<option value="-name" {% if current_sort == '-name' %}selected{% endif %}>Name (Z-A)</option>
|
||||
<option value="-opened_date" {% if current_sort == '-opened_date' %}selected{% endif %}>Newest First</option>
|
||||
<option value="opened_date" {% if current_sort == 'opened_date' %}selected{% endif %}>Oldest First</option>
|
||||
<option value="-height" {% if current_sort == '-height' %}selected{% endif %}>Tallest First</option>
|
||||
<option value="height" {% if current_sort == 'height' %}selected{% endif %}>Shortest First</option>
|
||||
<option value="-max_speed" {% if current_sort == '-max_speed' %}selected{% endif %}>Fastest First</option>
|
||||
<option value="max_speed" {% if current_sort == 'max_speed' %}selected{% endif %}>Slowest First</option>
|
||||
<option value="-capacity_per_hour" {% if current_sort == '-capacity_per_hour' %}selected{% endif %}>Highest Capacity</option>
|
||||
<option value="capacity_per_hour" {% if current_sort == 'capacity_per_hour' %}selected{% endif %}>Lowest Capacity</option>
|
||||
<option value="relevance" {% if current_sort == 'relevance' %}selected{% endif %}>Most Relevant</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results grid -->
|
||||
{% if rides %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{% for ride in rides %}
|
||||
<div class="ride-card bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-lg transition-all duration-300">
|
||||
<!-- Ride image -->
|
||||
<div class="relative h-48 bg-gradient-to-br from-blue-500 to-purple-600">
|
||||
{% if ride.image %}
|
||||
<img src="{{ ride.image.url }}"
|
||||
alt="{{ ride.name }}"
|
||||
class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<i class="fas fa-rocket text-4xl text-white opacity-50"></i>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-3 py-8 text-center">
|
||||
<p class="text-gray-500 dark:text-gray-400">No rides found matching your criteria.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status badge -->
|
||||
<div class="absolute top-3 right-3">
|
||||
{% if ride.operating_status == 'operating' %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
<i class="fas fa-play-circle mr-1"></i>
|
||||
Operating
|
||||
</span>
|
||||
{% elif ride.operating_status == 'closed_temporarily' %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
|
||||
<i class="fas fa-pause-circle mr-1"></i>
|
||||
Temporarily Closed
|
||||
</span>
|
||||
{% elif ride.operating_status == 'closed_permanently' %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
<i class="fas fa-stop-circle mr-1"></i>
|
||||
Permanently Closed
|
||||
</span>
|
||||
{% elif ride.operating_status == 'under_construction' %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
<i class="fas fa-hard-hat mr-1"></i>
|
||||
Under Construction
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ride details -->
|
||||
<div class="p-5">
|
||||
<!-- Name and category -->
|
||||
<div class="mb-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
||||
<a href="{% url 'rides:ride_detail' ride.id %}"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
{{ ride.name }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 mr-2">
|
||||
{{ ride.category|default:"Ride" }}
|
||||
</span>
|
||||
{% if ride.park %}
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-map-marker-alt mr-1"></i>
|
||||
{{ ride.park.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Key stats -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
{% if ride.height %}
|
||||
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.height }}ft</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">Height</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.rollercoaster_stats.max_speed %}
|
||||
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.rollercoaster_stats.max_speed }}mph</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">Top Speed</div>
|
||||
</div>
|
||||
{% elif ride.max_speed %}
|
||||
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.max_speed }}mph</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">Max Speed</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.capacity_per_hour %}
|
||||
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.capacity_per_hour }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">Capacity/Hr</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.duration %}
|
||||
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.duration }}s</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">Duration</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Special features -->
|
||||
{% if ride.has_inversions or ride.has_launches or ride.rollercoaster_stats %}
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
{% if ride.has_inversions %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||||
<i class="fas fa-sync-alt mr-1"></i>
|
||||
{% if ride.rollercoaster_stats.number_of_inversions %}
|
||||
{{ ride.rollercoaster_stats.number_of_inversions }} Inversion{{ ride.rollercoaster_stats.number_of_inversions|pluralize }}
|
||||
{% else %}
|
||||
Inversions
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.has_launches %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
<i class="fas fa-rocket mr-1"></i>
|
||||
{% if ride.rollercoaster_stats.number_of_launches %}
|
||||
{{ ride.rollercoaster_stats.number_of_launches }} Launch{{ ride.rollercoaster_stats.number_of_launches|pluralize }}
|
||||
{% else %}
|
||||
Launched
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.rollercoaster_stats.track_type %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{{ ride.rollercoaster_stats.track_type|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Opening date -->
|
||||
{% if ride.opened_date %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
Opened {{ ride.opened_date|date:"F j, Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Manufacturer -->
|
||||
{% if ride.manufacturer %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="fas fa-industry mr-1"></i>
|
||||
{{ ride.manufacturer.name }}
|
||||
{% if ride.designer and ride.designer != ride.manufacturer %}
|
||||
• Designed by {{ ride.designer.name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-center mt-6" hx-target="#ride-list-results" hx-push-url="false">
|
||||
<div class="inline-flex rounded-md shadow-xs">
|
||||
{% if page_obj.has_previous %}
|
||||
<a hx-get="?page=1{{ request.GET.urlencode }}"
|
||||
hx-target="#ride-list-results"
|
||||
class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">« First</a>
|
||||
<a hx-get="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}"
|
||||
hx-target="#ride-list-results"
|
||||
class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a hx-get="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}"
|
||||
hx-target="#ride-list-results"
|
||||
class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Next</a>
|
||||
<a hx-get="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}"
|
||||
hx-target="#ride-list-results"
|
||||
class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Last »</a>
|
||||
<div class="mt-8 flex items-center justify-between">
|
||||
<div class="flex items-center text-sm text-gray-700 dark:text-gray-300">
|
||||
<p>
|
||||
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ filtered_count }} results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}"
|
||||
hx-get="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
<i class="fas fa-chevron-left mr-1"></i>
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Page numbers -->
|
||||
<div class="hidden sm:flex items-center space-x-1">
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<span class="inline-flex items-center px-3 py-2 border border-blue-500 bg-blue-50 text-blue-600 rounded-md text-sm font-medium dark:bg-blue-900 dark:text-blue-200 dark:border-blue-700">
|
||||
{{ num }}
|
||||
</span>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}"
|
||||
hx-get="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
{{ num }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}"
|
||||
hx-get="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
Next
|
||||
<i class="fas fa-chevron-right ml-1"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- No results state -->
|
||||
<div class="text-center py-12">
|
||||
<div class="mx-auto h-24 w-24 text-gray-400 dark:text-gray-600 mb-4">
|
||||
<i class="fas fa-search text-6xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No rides found</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||
{% if has_filters %}
|
||||
Try adjusting your filters to see more results.
|
||||
{% else %}
|
||||
No rides match your current search criteria.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if has_filters %}
|
||||
<button hx-get="{% url 'rides:ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear All Filters
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- HTMX loading indicator -->
|
||||
<div class="htmx-indicator fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 z-50">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Loading results...</span>
|
||||
</div>
|
||||
</div>
|
||||
258
backend/templates/rides/ride_list.html
Normal file
258
backend/templates/rides/ride_list.html
Normal file
@@ -0,0 +1,258 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% if park %}
|
||||
{{ park.name }} - Rides
|
||||
{% else %}
|
||||
All Rides
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Custom styles for the ride filtering interface */
|
||||
.filter-sidebar {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
max-height: calc(100vh - 4rem);
|
||||
position: sticky;
|
||||
top: 4rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.filter-sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-sidebar {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
position: relative;
|
||||
top: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.filter-content.open {
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
/* Custom form styling */
|
||||
.form-input, .form-select, .form-textarea {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
dark:bg-gray-700 dark:border-gray-600 dark:text-white
|
||||
dark:focus:ring-blue-400 dark:focus:border-blue-400;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
@apply w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
|
||||
focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800
|
||||
dark:bg-gray-700 dark:border-gray-600;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
||||
}
|
||||
|
||||
/* Ride card animations */
|
||||
.ride-card {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.ride-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mobile filter overlay */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-filter-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.mobile-filter-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
background: white;
|
||||
z-index: 50;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.mobile-filter-panel.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Mobile filter overlay -->
|
||||
<div id="mobile-filter-overlay" class="mobile-filter-overlay hidden lg:hidden"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-full px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Title and breadcrumb -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{% if park %}
|
||||
<i class="fas fa-map-marker-alt mr-2 text-blue-600 dark:text-blue-400"></i>
|
||||
{{ park.name }} - Rides
|
||||
{% else %}
|
||||
<i class="fas fa-rocket mr-2 text-blue-600 dark:text-blue-400"></i>
|
||||
All Rides
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<!-- Mobile filter toggle -->
|
||||
<button id="mobile-filter-toggle"
|
||||
class="lg:hidden inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
<i class="fas fa-filter mr-2"></i>
|
||||
Filters
|
||||
{% if has_filters %}
|
||||
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{{ active_filters|length }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Results summary -->
|
||||
<div class="hidden sm:flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>
|
||||
<i class="fas fa-list mr-1"></i>
|
||||
{{ filtered_count }} of {{ total_rides }} rides
|
||||
</span>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div class="htmx-indicator">
|
||||
<i class="fas fa-spinner fa-spin text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex">
|
||||
<!-- Desktop filter sidebar -->
|
||||
<div class="hidden lg:block">
|
||||
{% include "rides/partials/filter_sidebar.html" %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile filter panel -->
|
||||
<div id="mobile-filter-panel" class="mobile-filter-panel lg:hidden dark:bg-gray-900">
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Filters</h3>
|
||||
<button id="mobile-filter-close" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% include "rides/partials/filter_sidebar.html" %}
|
||||
</div>
|
||||
|
||||
<!-- Results area -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div id="filter-results" class="p-4 lg:p-6">
|
||||
{% include "rides/partials/ride_list_results.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile filter JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileToggle = document.getElementById('mobile-filter-toggle');
|
||||
const mobilePanel = document.getElementById('mobile-filter-panel');
|
||||
const mobileOverlay = document.getElementById('mobile-filter-overlay');
|
||||
const mobileClose = document.getElementById('mobile-filter-close');
|
||||
|
||||
function openMobileFilter() {
|
||||
mobilePanel.classList.add('open');
|
||||
mobileOverlay.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeMobileFilter() {
|
||||
mobilePanel.classList.remove('open');
|
||||
mobileOverlay.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (mobileToggle) {
|
||||
mobileToggle.addEventListener('click', openMobileFilter);
|
||||
}
|
||||
|
||||
if (mobileClose) {
|
||||
mobileClose.addEventListener('click', closeMobileFilter);
|
||||
}
|
||||
|
||||
if (mobileOverlay) {
|
||||
mobileOverlay.addEventListener('click', closeMobileFilter);
|
||||
}
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobilePanel.classList.contains('open')) {
|
||||
closeMobileFilter();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Dark mode toggle (if not already implemented globally)
|
||||
function toggleDarkMode() {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
|
||||
}
|
||||
|
||||
// Initialize dark mode from localStorage
|
||||
if (localStorage.getItem('darkMode') === 'true' ||
|
||||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user