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:
pacnpal
2025-09-23 20:35:44 -04:00
parent fd42ee1161
commit 41fb41838c
14 changed files with 1716 additions and 44 deletions

View 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 %}