mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-25 10:11:08 -05:00
feat: Implement enhanced park list template with improved layout and accessibility features
- Created a new enhanced park list template with a responsive design. - Added skip navigation links for better accessibility. - Introduced an enhanced header section with park statistics overview. - Developed a sidebar for advanced filters and a search section. - Implemented loading overlay and error handling for HTMX requests. - Enhanced park results display with animations and improved empty states. - Added pagination controls with improved UX for navigating park listings.
This commit is contained in:
495
templates/parks/enhanced_park_list.html
Normal file
495
templates/parks/enhanced_park_list.html
Normal file
@@ -0,0 +1,495 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
{% load cotton %}
|
||||
|
||||
{% block title %}Parks - Enhanced Experience{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Skip Navigation Links for Accessibility #}
|
||||
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
|
||||
Skip to main content
|
||||
</a>
|
||||
<a href="#search-form" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-32 z-50 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
|
||||
Skip to search
|
||||
</a>
|
||||
|
||||
{# Enhanced Container with Better Layout #}
|
||||
<div class="min-h-screen bg-transparent"
|
||||
x-data="enhancedParkListState()"
|
||||
x-init="init()">
|
||||
|
||||
<div class="container mx-auto px-3 sm:px-4 lg:px-6 py-6 sm:py-8">
|
||||
|
||||
{# Enhanced Header Section #}
|
||||
<header class="mb-8 sm:mb-12" aria-labelledby="page-title">
|
||||
<div class="text-center mb-8">
|
||||
<h1 id="page-title" class="text-3xl sm:text-4xl lg:text-5xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent leading-tight mb-4">
|
||||
Discover Amazing Theme Parks
|
||||
</h1>
|
||||
<p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto leading-relaxed" id="page-description">
|
||||
Explore the world's most thrilling theme parks with comprehensive information, reviews, and insider details
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Enhanced Statistics Cards #}
|
||||
<section aria-labelledby="park-statistics" class="grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
|
||||
<h2 id="park-statistics" class="sr-only">Park Statistics Overview</h2>
|
||||
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-4 sm:p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||
role="img"
|
||||
aria-labelledby="total-parks-stat"
|
||||
tabindex="0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div id="total-parks-stat" class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"
|
||||
aria-label="{{ filter_counts.total_parks|default:0 }} total parks in database">
|
||||
{{ filter_counts.total_parks|default:0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-medium">Total Parks</div>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-4 sm:p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||
role="img"
|
||||
aria-labelledby="operating-parks-stat"
|
||||
tabindex="0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div id="operating-parks-stat" class="text-2xl sm:text-3xl font-bold text-green-600 dark:text-green-400"
|
||||
aria-label="{{ filter_counts.operating_parks|default:0 }} currently operating parks">
|
||||
{{ filter_counts.operating_parks|default:0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-medium">Operating</div>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-4 sm:p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||
role="img"
|
||||
aria-labelledby="coaster-parks-stat"
|
||||
tabindex="0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div id="coaster-parks-stat" class="text-2xl sm:text-3xl font-bold text-purple-600 dark:text-purple-400"
|
||||
aria-label="{{ filter_counts.parks_with_coasters|default:0 }} parks with roller coasters">
|
||||
{{ filter_counts.parks_with_coasters|default:0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-medium">With Coasters</div>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-4 sm:p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||
role="img"
|
||||
aria-labelledby="countries-stat"
|
||||
tabindex="0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div id="countries-stat" class="text-2xl sm:text-3xl font-bold text-orange-600 dark:text-orange-400"
|
||||
aria-label="{{ filter_counts.countries_count|default:0 }} countries represented">
|
||||
{{ filter_counts.countries_count|default:0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-medium">Countries</div>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-100 dark:bg-orange-900/30 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
{# Main Content Layout #}
|
||||
<div class="flex flex-col xl:flex-row gap-8">
|
||||
|
||||
{# Sidebar with Advanced Filters #}
|
||||
<aside class="xl:w-80 flex-shrink-0" aria-label="Park filters">
|
||||
<div class="sticky top-6">
|
||||
{# Enhanced Search Section #}
|
||||
<section class="mb-6" aria-labelledby="search-section" role="search">
|
||||
<h2 id="search-section" class="sr-only">Search Parks</h2>
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Search Parks</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Find parks by name, location, or features</p>
|
||||
</div>
|
||||
<div id="search-form">
|
||||
<c-enhanced_search
|
||||
placeholder="Search parks by name, location, or features..."
|
||||
current_value="{{ search_query }}"
|
||||
autocomplete_url="{% url 'parks:park_autocomplete' %}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Advanced Filters Component #}
|
||||
<c-advanced_filters
|
||||
filter_counts=filter_counts
|
||||
current_filters=request.GET
|
||||
show_advanced="false"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{# Main Content Area #}
|
||||
<main class="flex-1 min-w-0" id="main-content" aria-label="Park listings">
|
||||
|
||||
{# Controls Bar #}
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-4 sm:p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg mb-6">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
|
||||
{# Results Stats #}
|
||||
<div class="flex-1">
|
||||
<c-result_stats
|
||||
total_results="{{ total_results }}"
|
||||
page_obj="{{ page_obj }}"
|
||||
search_query="{{ search_query }}"
|
||||
is_search="{{ is_search }}"
|
||||
filter_count="{{ filter_count }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{# View Controls #}
|
||||
<div class="flex items-center gap-4">
|
||||
{# Sort Controls #}
|
||||
<c-sort_controls
|
||||
current_sort="{{ current_ordering }}"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
|
||||
{# View Toggle #}
|
||||
<c-view_toggle
|
||||
current_view="{{ view_mode }}"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Active Filter Chips #}
|
||||
{% if active_filters %}
|
||||
<div class="border-t border-gray-200/50 dark:border-gray-700/50 pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Active Filters ({{ filter_count }})
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
@click="clearAllFilters()"
|
||||
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium focus:outline-none focus:underline transition-colors duration-200"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<c-filter_chips
|
||||
filters=active_filters
|
||||
base_url="{% url 'parks:park_list' %}"
|
||||
class="flex-wrap gap-2"
|
||||
/>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Loading Overlay #}
|
||||
<div id="loading-overlay" class="htmx-indicator fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-2xl max-w-sm w-full mx-4">
|
||||
<div class="flex flex-col items-center space-y-4 text-center">
|
||||
<div class="relative">
|
||||
<div class="w-16 h-16 border-4 border-blue-200 dark:border-blue-800 rounded-full animate-spin border-t-blue-600 dark:border-t-blue-400"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">Loading Parks...</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">Please wait while we fetch the latest data</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Park Results Container #}
|
||||
<div id="park-results"
|
||||
hx-indicator="#loading-overlay"
|
||||
class="min-h-[400px]">
|
||||
{% include "parks/partials/enhanced_park_list.html" %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced AlpineJS State Management -->
|
||||
<script>
|
||||
function enhancedParkListState() {
|
||||
return {
|
||||
viewMode: '{{ view_mode }}',
|
||||
searchQuery: '{{ search_query }}',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
filterPanelOpen: false,
|
||||
|
||||
init() {
|
||||
// Handle responsive behavior
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.debounce(() => this.handleResize(), 250));
|
||||
|
||||
// Enhanced HTMX event handling
|
||||
document.addEventListener('htmx:beforeRequest', (event) => {
|
||||
this.setLoading(true);
|
||||
this.error = null;
|
||||
|
||||
// Add loading class to target element
|
||||
if (event.detail.target) {
|
||||
event.detail.target.classList.add('opacity-75', 'pointer-events-none');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', (event) => {
|
||||
this.setLoading(false);
|
||||
|
||||
// Remove loading class from target element
|
||||
if (event.detail.target) {
|
||||
event.detail.target.classList.remove('opacity-75', 'pointer-events-none');
|
||||
}
|
||||
|
||||
// Scroll to top of results on mobile after filter changes
|
||||
if (window.innerWidth < 768 && event.detail.target?.id === 'park-results') {
|
||||
this.scrollToResults();
|
||||
}
|
||||
|
||||
// Update URL state
|
||||
if (event.detail.xhr.responseURL) {
|
||||
const url = new URL(event.detail.xhr.responseURL);
|
||||
history.replaceState(null, '', url.pathname + url.search);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', (event) => {
|
||||
this.setLoading(false);
|
||||
this.showError('Failed to load results. Please check your connection and try again.');
|
||||
|
||||
// Remove loading class from target element
|
||||
if (event.detail.target) {
|
||||
event.detail.target.classList.remove('opacity-75', 'pointer-events-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mobile viewport changes
|
||||
this.handleMobileViewport();
|
||||
|
||||
// Initialize intersection observer for lazy loading
|
||||
this.initIntersectionObserver();
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
// Auto-close filter panel on mobile when resizing to desktop
|
||||
if (window.innerWidth >= 1280) {
|
||||
this.filterPanelOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleMobileViewport() {
|
||||
// Handle mobile viewport changes for better UX
|
||||
if ('visualViewport' in window) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--viewport-height',
|
||||
`${window.visualViewport.height}px`
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initIntersectionObserver() {
|
||||
// Lazy load images and animations
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-fade-in');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.1,
|
||||
rootMargin: '50px'
|
||||
});
|
||||
|
||||
// Observe park cards
|
||||
document.querySelectorAll('[role="article"]').forEach(card => {
|
||||
observer.observe(card);
|
||||
});
|
||||
},
|
||||
|
||||
scrollToResults() {
|
||||
const resultsElement = document.getElementById('park-results');
|
||||
if (resultsElement) {
|
||||
resultsElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setLoading(loading) {
|
||||
this.isLoading = loading;
|
||||
|
||||
// Update document title
|
||||
if (loading) {
|
||||
document.title = 'Loading Parks... - ThrillWiki';
|
||||
} else {
|
||||
document.title = 'Parks - ThrillWiki';
|
||||
}
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.error = message;
|
||||
|
||||
// Show toast notification
|
||||
this.showToast(message, 'error');
|
||||
|
||||
// Auto-clear error after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.error = null;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Create and show toast notification
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transform transition-all duration-300 translate-x-full ${
|
||||
type === 'error' ? 'bg-red-500 text-white' : 'bg-blue-500 text-white'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 300);
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
this.setLoading(true);
|
||||
window.location.href = '{% url "parks:park_list" %}';
|
||||
},
|
||||
|
||||
toggleFilterPanel() {
|
||||
this.filterPanelOpen = !this.filterPanelOpen;
|
||||
},
|
||||
|
||||
// Utility function for better performance
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom CSS for animations -->
|
||||
<style>
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Smooth transitions for HTMX updates */
|
||||
.htmx-settling {
|
||||
transition: all 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.htmx-swapping {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.htmx-request .htmx-indicator {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
280
templates/parks/partials/enhanced_park_list.html
Normal file
280
templates/parks/partials/enhanced_park_list.html
Normal file
@@ -0,0 +1,280 @@
|
||||
{% load cotton %}
|
||||
|
||||
{# Enhanced Park List Partial - Used for HTMX updates #}
|
||||
|
||||
{% if view_mode == 'list' %}
|
||||
{# Enhanced List View #}
|
||||
<div class="space-y-6" role="list" aria-label="Parks in list view">
|
||||
{% for park in parks %}
|
||||
<div role="listitem">
|
||||
<c-enhanced_park_card
|
||||
park=park
|
||||
view_mode="list"
|
||||
show_stats="true"
|
||||
show_rating="true"
|
||||
/>
|
||||
</div>
|
||||
{% empty %}
|
||||
{# Enhanced Empty State for List View #}
|
||||
<div class="text-center py-16">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="w-24 h-24 mx-auto mb-6 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-gray-400 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{% if is_search %}
|
||||
No parks found for "{{ search_query }}"
|
||||
{% else %}
|
||||
No parks found
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||
{% if is_search %}
|
||||
Try adjusting your search terms or removing some filters to see more results.
|
||||
{% else %}
|
||||
Try adjusting your filters to see more parks.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if active_filters %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Clear All Filters
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# Enhanced Grid View with Responsive Columns #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 sm:gap-8" role="list" aria-label="Parks in grid view">
|
||||
{% for park in parks %}
|
||||
<div role="listitem" class="animate-fade-in" style="animation-delay: {{ forloop.counter0|floatformat:0|add:"00" }}ms;">
|
||||
<c-enhanced_park_card
|
||||
park=park
|
||||
view_mode="grid"
|
||||
size="normal"
|
||||
show_stats="true"
|
||||
show_rating="true"
|
||||
/>
|
||||
</div>
|
||||
{% empty %}
|
||||
{# Enhanced Empty State for Grid View #}
|
||||
<div class="col-span-full text-center py-16">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="w-24 h-24 mx-auto mb-6 bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900/30 dark:to-purple-900/30 rounded-full flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{% if is_search %}
|
||||
No parks found for "{{ search_query }}"
|
||||
{% else %}
|
||||
No parks available
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
|
||||
{% if is_search %}
|
||||
We couldn't find any parks matching your search. Try different keywords or remove some filters.
|
||||
{% else %}
|
||||
No parks match your current filter criteria. Try adjusting your filters to see more results.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{# Helpful suggestions #}
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6 text-left">
|
||||
<h4 class="font-medium text-blue-900 dark:text-blue-100 mb-2">Try these suggestions:</h4>
|
||||
<ul class="text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<li>• Check your spelling</li>
|
||||
<li>• Use more general search terms</li>
|
||||
<li>• Remove some filters to broaden your search</li>
|
||||
{% if is_search %}
|
||||
<li>• Search for park operators like "Disney" or "Universal"</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
{% if active_filters %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Clear All Filters
|
||||
</button>
|
||||
{% endif %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
hx-get="{% url 'parks:park_list' %}?has_coasters=true"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
Show Parks with Coasters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Enhanced Pagination #}
|
||||
{% if is_paginated %}
|
||||
<nav class="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm px-4 py-3 sm:px-6 rounded-2xl mt-8" aria-label="Pagination Navigation">
|
||||
<div class="flex flex-1 justify-between sm:hidden">
|
||||
{# Mobile Pagination #}
|
||||
{% if page_obj.has_previous %}
|
||||
<button
|
||||
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
hx-get="?page={{ page_obj.previous_page_number }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Previous
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 dark:text-gray-600 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg cursor-not-allowed">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Previous
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<button
|
||||
class="relative ml-3 inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
hx-get="?page={{ page_obj.next_page_number }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay">
|
||||
Next
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="relative ml-3 inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 dark:text-gray-600 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg cursor-not-allowed">
|
||||
Next
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Showing
|
||||
<span class="font-medium">{{ page_obj.start_index }}</span>
|
||||
to
|
||||
<span class="font-medium">{{ page_obj.end_index }}</span>
|
||||
of
|
||||
<span class="font-medium">{{ page_obj.paginator.count }}</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="isolate inline-flex -space-x-px rounded-lg shadow-sm" aria-label="Pagination">
|
||||
{# First Page #}
|
||||
{% if page_obj.has_previous %}
|
||||
<button
|
||||
class="relative inline-flex items-center rounded-l-lg px-2 py-2 text-gray-400 dark:text-gray-500 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-20 focus:outline-offset-0 transition-colors duration-200"
|
||||
hx-get="?page=1&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay"
|
||||
aria-label="Go to first page">
|
||||
<span class="sr-only">First</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="relative inline-flex items-center px-2 py-2 text-gray-400 dark:text-gray-500 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-20 focus:outline-offset-0 transition-colors duration-200"
|
||||
hx-get="?page={{ page_obj.previous_page_number }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay"
|
||||
aria-label="Go to previous page">
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{# Page Numbers #}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<span aria-current="page" class="relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">{{ num }}</span>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<button
|
||||
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-20 focus:outline-offset-0 transition-colors duration-200"
|
||||
hx-get="?page={{ num }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay"
|
||||
aria-label="Go to page {{ num }}">{{ num }}</button>
|
||||
{% elif num == page_obj.number|add:'-4' or num == page_obj.number|add:'4' %}
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:outline-offset-0">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Next and Last Page #}
|
||||
{% if page_obj.has_next %}
|
||||
<button
|
||||
class="relative inline-flex items-center px-2 py-2 text-gray-400 dark:text-gray-500 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-20 focus:outline-offset-0 transition-colors duration-200"
|
||||
hx-get="?page={{ page_obj.next_page_number }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay"
|
||||
aria-label="Go to next page">
|
||||
<span class="sr-only">Next</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="relative inline-flex items-center rounded-r-lg px-2 py-2 text-gray-400 dark:text-gray-500 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-20 focus:outline-offset-0 transition-colors duration-200"
|
||||
hx-get="?page={{ page_obj.paginator.num_pages }}&{{ request.GET.urlencode }}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-overlay"
|
||||
aria-label="Go to last page">
|
||||
<span class="sr-only">Last</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02zm6 0a.75.75 0 01.02-1.06L14.168 10l-3.938-3.71a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user