Files
thrillwiki_django_no_react/templates/cotton/enhanced_park_card.html
pacnpal 41fb41838c 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.
2025-09-23 20:35:44 -04:00

382 lines
28 KiB
HTML

{% comment %}
Enhanced Park Card Component - Django Cotton Version
A modern, responsive park card component with improved layouts for both grid and list views.
Features enhanced visual design, better mobile optimization, and rich interactive elements.
Usage Examples:
Grid View:
<c-enhanced_park_card
park=park
view_mode="grid"
/>
List View:
<c-enhanced_park_card
park=park
view_mode="list"
/>
Compact Grid View:
<c-enhanced_park_card
park=park
view_mode="grid"
size="compact"
/>
Parameters:
- park: Park object (required)
- view_mode: "list" or "grid" (default: "grid")
- size: "normal" or "compact" (default: "normal")
- show_stats: Whether to show ride/coaster stats (default: True)
- show_rating: Whether to show rating (default: True)
- class: Additional CSS classes (optional)
Features:
- Modern card design with enhanced visual hierarchy
- Responsive image handling with CloudFlare Images
- Interactive hover effects and animations
- Accessibility improvements with ARIA labels
- Status badges with improved styling
- Rating display with star visualization
- Optimized for both mobile and desktop
- Support for compact grid layout
- Enhanced typography and spacing
{% endcomment %}
<c-vars
park
view_mode="grid"
size="normal"
show_stats="true"
show_rating="true"
class=""
/>
{% if park %}
{% if view_mode == 'list' %}
{# Enhanced List View Layout #}
<article class="group bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm border border-gray-200/60 dark:border-gray-700/60 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 transform hover:scale-[1.01] overflow-hidden {{ class }}"
role="article"
aria-labelledby="park-title-{{ park.id }}"
aria-describedby="park-description-{{ park.id }}">
<div class="p-5 sm:p-7">
<div class="flex flex-col lg:flex-row gap-5 lg:gap-7">
{# Enhanced Image Section for List View #}
<div class="flex-shrink-0 w-full lg:w-64 xl:w-72">
<div class="relative aspect-[16/9] lg:aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-xl overflow-hidden group-hover:shadow-lg transition-shadow duration-300">
{% if park.card_image.image or park.photos.first.image %}
{% with image=park.card_image.image|default:park.photos.first.image %}
<picture class="w-full h-full">
<source media="(max-width: 1023px)"
srcset="{{ image.public_url }}/w=800,h=450,fit=cover,f=webp 1x, {{ image.public_url }}/w=1600,h=900,fit=cover,f=webp 2x"
type="image/webp">
<source media="(min-width: 1024px)"
srcset="{{ image.public_url }}/w=600,h=450,fit=cover,f=webp 1x, {{ image.public_url }}/w=1200,h=900,fit=cover,f=webp 2x"
type="image/webp">
<img src="{{ image.public_url }}/w=800,h=450,fit=cover"
alt="{{ park.name }} - {% if park.card_image.alt_text %}{{ park.card_image.alt_text }}{% elif park.photos.first.alt_text %}{{ park.photos.first.alt_text }}{% else %}Theme park exterior view{% endif %}"
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
loading="lazy"
decoding="async">
</picture>
{% endwith %}
{% else %}
<div class="flex items-center justify-center h-full text-white/70 bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600 dark:from-gray-600 dark:via-gray-700 dark:to-gray-800">
<div class="text-center p-6">
<svg class="w-12 h-12 mx-auto mb-3 opacity-60" 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"></path>
</svg>
<p class="text-sm font-medium opacity-80">No Image Available</p>
</div>
</div>
{% endif %}
{# Enhanced Status Badge #}
<div class="absolute top-3 right-3">
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-bold border-2 bg-white/95 backdrop-blur-sm shadow-lg
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-300 shadow-green-200/50
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' %}text-red-700 border-red-300 shadow-red-200/50
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}text-yellow-700 border-yellow-300 shadow-yellow-200/50
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-300 shadow-blue-200/50
{% else %}text-gray-700 border-gray-300 shadow-gray-200/50{% endif %}"
role="img"
aria-label="Park status: {{ park.get_status_display }}"
title="Park status: {{ park.get_status_display }}">
{{ park.get_status_display }}
</span>
</div>
{# Rating Badge (if enabled) #}
{% if show_rating == "true" and park.average_rating %}
<div class="absolute bottom-3 left-3">
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-black/70 text-white backdrop-blur-sm">
<svg class="w-3 h-3 mr-1 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
{{ park.average_rating|floatformat:1 }}
</div>
</div>
{% endif %}
</div>
</div>
{# Enhanced Content Section #}
<div class="flex-1 min-w-0 flex flex-col justify-between">
<div class="space-y-4">
{# Title and Operator #}
<div>
<h3 id="park-title-{{ park.id }}" class="text-xl sm:text-2xl lg:text-3xl font-bold line-clamp-2 leading-tight mb-2">
{% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
aria-label="View details for {{ park.name }}">
{{ park.name }}
</a>
{% else %}
<span class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
{{ park.name }}
</span>
{% endif %}
</h3>
{% if park.operator %}
<div class="flex items-center text-base font-medium text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4 mr-2 flex-shrink-0 opacity-60" 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>
<span class="truncate">{{ park.operator.name }}</span>
</div>
{% endif %}
</div>
{# Description #}
{% if park.description %}
<p id="park-description-{{ park.id }}" class="text-gray-600 dark:text-gray-400 line-clamp-3 leading-relaxed text-base">
{{ park.description|truncatewords:40 }}
</p>
{% endif %}
{# Location Info #}
{% if park.location %}
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>
{% if park.location.city %}{{ park.location.city }}{% endif %}{% if park.location.city and park.location.state %}, {% endif %}{% if park.location.state %}{{ park.location.state }}{% endif %}{% if park.location.country and park.location.country != "United States" %}, {{ park.location.country }}{% endif %}
</span>
</div>
{% endif %}
</div>
{# Stats Section #}
{% if show_stats == "true" and park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-5 border-t border-gray-200/60 dark:border-gray-600/60 mt-5">
<div class="flex items-center space-x-6">
{% if park.ride_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl border border-blue-200/50 dark:border-blue-800/50"
role="img"
aria-label="{{ park.ride_count }} ride{{ park.ride_count|pluralize }} available"
title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-bold text-blue-700 dark:text-blue-300 text-lg">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400 font-medium">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-xl border border-purple-200/50 dark:border-purple-800/50"
role="img"
aria-label="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }} available"
title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" 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>
<span class="font-bold text-purple-700 dark:text-purple-300 text-lg">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400 font-medium">coasters</span>
</div>
{% endif %}
</div>
{# View Details Arrow #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-300">
<svg class="w-6 h-6 transform group-hover:translate-x-2 transition-transform duration-300" 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>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</article>
{% else %}
{# Enhanced Grid View Layout #}
<article class="group bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm border border-gray-200/60 dark:border-gray-700/60 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}"
role="article"
aria-labelledby="park-title-grid-{{ park.id }}"
aria-describedby="park-description-grid-{{ park.id }}">
{# Enhanced Image Section for Grid View #}
<div class="relative {% if size == 'compact' %}aspect-[4/3]{% else %}aspect-[4/3]{% endif %} bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 overflow-hidden">
{% if park.card_image.image or park.photos.first.image %}
{% with image=park.card_image.image|default:park.photos.first.image %}
<picture class="w-full h-full">
{% if size == "compact" %}
<source media="(max-width: 767px)"
srcset="{{ image.public_url }}/w=400,h=300,fit=cover,f=webp 1x, {{ image.public_url }}/w=800,h=600,fit=cover,f=webp 2x"
type="image/webp">
<source media="(min-width: 768px)"
srcset="{{ image.public_url }}/w=300,h=225,fit=cover,f=webp 1x, {{ image.public_url }}/w=600,h=450,fit=cover,f=webp 2x"
type="image/webp">
{% else %}
<source media="(max-width: 767px)"
srcset="{{ image.public_url }}/w=600,h=450,fit=cover,f=webp 1x, {{ image.public_url }}/w=1200,h=900,fit=cover,f=webp 2x"
type="image/webp">
<source media="(min-width: 768px)"
srcset="{{ image.public_url }}/w=400,h=300,fit=cover,f=webp 1x, {{ image.public_url }}/w=800,h=600,fit=cover,f=webp 2x"
type="image/webp">
{% endif %}
<img src="{{ image.public_url }}/w=600,h=450,fit=cover"
alt="{{ park.name }} - {% if park.card_image.alt_text %}{{ park.card_image.alt_text }}{% elif park.photos.first.alt_text %}{{ park.photos.first.alt_text }}{% else %}Theme park exterior view{% endif %}"
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
loading="lazy"
decoding="async">
</picture>
{# Image Overlay Effects #}
<div class="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
{% endwith %}
{% else %}
<div class="flex flex-col items-center justify-center h-full text-white/70 bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600 dark:from-gray-600 dark:via-gray-700 dark:to-gray-800">
<div class="p-6 text-center">
<svg class="w-16 h-16 mx-auto mb-4 opacity-60" 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"></path>
</svg>
<p class="text-sm font-medium opacity-80">No Image Available</p>
<p class="text-xs opacity-60 mt-1">{{ park.name }}</p>
</div>
</div>
{% endif %}
{# Enhanced Status Badge #}
<div class="absolute top-3 right-3">
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold border-2 bg-white/95 backdrop-blur-sm shadow-lg
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-300 shadow-green-200/50
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' %}text-red-700 border-red-300 shadow-red-200/50
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}text-yellow-700 border-yellow-300 shadow-yellow-200/50
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-300 shadow-blue-200/50
{% else %}text-gray-700 border-gray-300 shadow-gray-200/50{% endif %}"
role="img"
aria-label="Park status: {{ park.get_status_display }}"
title="Park status: {{ park.get_status_display }}">
{{ park.get_status_display }}
</span>
</div>
{# Rating Badge (if enabled) #}
{% if show_rating == "true" and park.average_rating %}
<div class="absolute bottom-3 left-3">
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-black/70 text-white backdrop-blur-sm">
<svg class="w-3 h-3 mr-1 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
{{ park.average_rating|floatformat:1 }}
</div>
</div>
{% endif %}
</div>
{# Enhanced Content Area #}
<div class="p-5 {% if size == 'compact' %}sm:p-4{% else %}sm:p-6{% endif %}">
<div class="{% if size == 'compact' %}mb-3{% else %}mb-4{% endif %}">
{# Title #}
<h3 id="park-title-grid-{{ park.id }}" class="{% if size == 'compact' %}text-lg{% else %}text-xl{% endif %} font-bold line-clamp-2 mb-2 leading-tight">
{% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
aria-label="View details for {{ park.name }}">
{{ park.name }}
</a>
{% else %}
<span class="text-gray-900 dark:text-white">
{{ park.name }}
</span>
{% endif %}
</h3>
{# Operator #}
{% if park.operator %}
<div class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-3 truncate flex items-center">
<svg class="w-3 h-3 mr-1.5 flex-shrink-0 opacity-60" 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>
<span class="truncate">{{ park.operator.name }}</span>
</div>
{% endif %}
</div>
{# Description #}
{% if park.description and size != "compact" %}
<p id="park-description-grid-{{ park.id }}" class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed">
{{ park.description|truncatewords:20 }}
</p>
{% endif %}
{# Stats Footer #}
{% if show_stats == "true" and park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-4 border-t border-gray-200/60 dark:border-gray-600/60">
<div class="flex items-center space-x-4 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-1.5 text-blue-600 dark:text-blue-400"
role="img"
aria-label="{{ park.ride_count }} ride{{ park.ride_count|pluralize }} available"
title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-bold">{{ park.ride_count }}</span>
{% if size != "compact" %}<span class="text-xs opacity-75">rides</span>{% endif %}
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1.5 text-purple-600 dark:text-purple-400"
role="img"
aria-label="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }} available"
title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" 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>
<span class="font-bold">{{ park.coaster_count }}</span>
{% if size != "compact" %}<span class="text-xs opacity-75">coasters</span>{% endif %}
</div>
{% endif %}
</div>
{# View Details Arrow #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-300">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-300" 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>
</div>
</div>
{% else %}
{# Show arrow even when no stats for consistent layout #}
<div class="flex justify-end pt-4 border-t border-gray-200/60 dark:border-gray-600/60">
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-300">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-300" 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>
</div>
</div>
{% endif %}
</div>
</article>
{% endif %}
{% endif %}