Files
thrillwiki_django_no_react/templates/cotton/park_card.html
pac7 d978217577 Enhance website's visual appeal and mobile responsiveness with style updates
Update CSS styles across various components to improve visual presentation and ensure better responsiveness on mobile devices, including adjustments to spacing, aspect ratios, and element sizing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c446bc9e-66df-438c-a86c-f53e6da13649
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
2025-09-23 22:11:05 +00:00

360 lines
26 KiB
HTML

{% comment %}
Park Card Component - Django Cotton Version
A reusable park card component that supports both list and grid view modes.
Includes status badges, operator information, description, and ride/coaster statistics.
Usage Examples:
List View:
<c-park_card
park=park
view_mode="list"
/>
Grid View:
<c-park_card
park=park
view_mode="grid"
/>
With custom CSS classes:
<c-park_card
park=park
view_mode="grid"
class="custom-class"
/>
Parameters:
- park: Park object (required)
- view_mode: "list" or "grid" (default: "grid")
- class: Additional CSS classes (optional)
Features:
- Responsive design with hover effects
- Status badge with proper color coding
- Operator information display
- Description with automatic truncation (30 words for list, 15 for grid)
- Ride and coaster count statistics with icons
- Gradient effects and modern styling
- Links to park detail pages
{% endcomment %}
<c-vars
park
view_mode="grid"
class=""
/>
{% if park %}
{% if view_mode == 'list' %}
{# Enhanced List View Item with CloudFlare Images #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] overflow-hidden {{ class }}">
<div class="p-4 sm:p-6">
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
{# Enhanced List View Image Section #}
<div class="flex-shrink-0 w-full sm:w-32 md:w-40 lg:w-48">
<div class="relative aspect-[16/9] sm:aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-lg overflow-hidden">
{% if park.card_image.image or park.photos.first.image %}
{% with image=park.card_image.image|default:park.photos.first.image %}
{# List View CloudFlare Images Optimization #}
<picture class="w-full h-full">
{# Mobile list view (full width, 16:9) #}
<source media="(max-width: 639px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Tablet/Desktop list view (smaller thumbnail) #}
<source media="(min-width: 640px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Fallback image #}
<img src="{{ image.public_url }}"
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-500 group-hover:scale-110"
loading="lazy"
decoding="async">
</picture>
{% endwith %}
{% else %}
{# Enhanced List View Fallback #}
<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">
<svg class="w-8 h-8 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>
</div>
{% endif %}
{# List View Status Badge Overlay #}
<div class="absolute top-1.5 right-1.5 sm:top-2 sm:right-2">
<span class="inline-flex items-center px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}text-red-700 border-red-200
{% elif park.status == 'seasonal' %}text-blue-700 border-blue-200
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}text-yellow-700 border-yellow-200
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200
{% else %}text-gray-700 border-gray-200{% endif %}">
<span class="hidden sm:inline">{{ park.get_status_display }}</span>
<span class="sm:hidden">{{ park.get_status_display|truncatechars:3 }}</span>
</span>
</div>
</div>
</div>
{# Enhanced Main Content Section with Better Mobile Layout #}
<div class="flex-1 min-w-0 flex flex-col justify-between">
<div class="space-y-2 sm:space-y-3">
{# Enhanced Title with Better Mobile Typography #}
<div class="flex items-start justify-between">
<h2 class="text-lg sm:text-xl lg:text-2xl font-bold line-clamp-2 leading-tight">
{% 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 %}
</h2>
{# View Details Arrow for Mobile #}
<div class="sm:hidden text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 ml-2 flex-shrink-0">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{# Enhanced Operator Display #}
{% if park.operator %}
<div class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-400 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 %}
{# Enhanced Description #}
{% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 leading-relaxed">
{{ park.description|truncatewords:30 }}
</p>
{% endif %}
</div>
{# Enhanced Stats Section with Better Mobile Layout #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3">
<div class="flex items-center space-x-3 sm:space-x-6 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50" title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm: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-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400 hidden sm:inline">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50" title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm: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-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400 hidden sm:inline">coasters</span>
</div>
{% endif %}
</div>
{# View Details Arrow for Desktop #}
<div class="hidden sm:block text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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="hidden sm:flex justify-end pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3">
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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 Item #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}">
{# Enhanced Park Image with CloudFlare Images Integration #}
<div class="relative aspect-[4/3] 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 %}
{# CloudFlare Images Responsive Picture Element #}
<picture class="w-full h-full">
{# Mobile optimization (320-767px) - thumbnail variant with mobile-specific transformations #}
<source media="(max-width: 767px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Tablet optimization (768-1023px) - medium variant #}
<source media="(min-width: 768px) and (max-width: 1023px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Desktop optimization (1024px+) - large variant #}
<source media="(min-width: 1024px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Fallback image with progressive enhancement #}
<img src="{{ image.public_url }}"
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-500 group-hover:scale-110"
loading="lazy"
decoding="async"
style="aspect-ratio: 4/3; object-position: center;">
</picture>
{# Image Overlay Effects #}
<div class="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{% endwith %}
{% else %}
{# Enhanced Fallback with Better UX #}
<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-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>
<p class="text-xs opacity-60 mt-1">{{ park.name }}</p>
</div>
</div>
{% endif %}
{# Enhanced Status Badge Overlay with Better Mobile Touch Targets #}
<div class="absolute top-2 right-2 sm:top-3 sm:right-3">
<span class="inline-flex items-center px-2 py-1 sm:px-2.5 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}text-red-700 border-red-200
{% elif park.status == 'seasonal' %}text-blue-700 border-blue-200
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}text-yellow-700 border-yellow-200
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200
{% else %}text-gray-700 border-gray-200{% endif %}">
{{ park.get_status_display }}
</span>
</div>
{# Loading Placeholder with Skeleton Effect #}
<div class="absolute inset-0 bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200 dark:from-gray-700 dark:via-gray-600 dark:to-gray-700 animate-pulse opacity-0 transition-opacity duration-300" data-loading-placeholder></div>
</div>
{# Enhanced Content Area with Better Mobile Optimization #}
<div class="p-4 sm:p-6">
<div class="mb-3 sm:mb-4">
{# Enhanced Title with Better Mobile Typography #}
<h2 class="text-lg sm:text-xl 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 %}
</h2>
</div>
{# Enhanced Operator Display with Better Mobile Layout #}
{% 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 %}
{# Enhanced Description with Better Mobile Readability #}
{% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed">
{{ park.description|truncatewords:15 }}
</p>
{% endif %}
{# Enhanced Stats Footer with Better Mobile Layout #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="flex items-center space-x-3 sm: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" 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-semibold">{{ park.ride_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1.5 text-purple-600 dark:text-purple-400" 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-semibold">{{ park.coaster_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">coasters</span>
</div>
{% endif %}
</div>
{# Enhanced View Details Arrow with Better Mobile Touch Target #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 p-1 -m-1">
<svg class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 p-1 -m-1">
<svg class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{% endif %}
</div>
</article>
{% endif %}
{% endif %}