mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-26 09:11:14 -05:00
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
This commit is contained in:
@@ -104,7 +104,7 @@ Features:
|
||||
name="search"
|
||||
x-model="search"
|
||||
placeholder="{{ placeholder }}"
|
||||
class="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg leading-5 bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 transition-colors"
|
||||
class="block w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-base sm:text-sm min-h-[44px] sm:min-h-0"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-trigger="keyup changed delay:{{ debounce_delay }}ms"
|
||||
hx-target="#park-results"
|
||||
|
||||
@@ -31,9 +31,9 @@ Features:
|
||||
<div class="flex flex-wrap gap-2 {{ class }}">
|
||||
{% for filter_name, filter_value in filters.items %}
|
||||
{% if filter_value and filter_name != 'page' and filter_name != 'view_mode' %}
|
||||
<div class="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-blue-700 bg-blue-50 rounded-full border border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700/50">
|
||||
<span class="capitalize">{{ filter_name|title }}:</span>
|
||||
<span class="font-semibold">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 sm:py-1 text-sm font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 rounded-full border border-blue-200 dark:border-blue-700/50">
|
||||
<span class="capitalize text-xs sm:text-sm">{{ filter_name|title }}:</span>
|
||||
<span class="font-semibold text-xs sm:text-sm">
|
||||
{% if filter_value == 'True' %}
|
||||
Yes
|
||||
{% elif filter_value == 'False' %}
|
||||
@@ -44,7 +44,7 @@ Features:
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 p-0.5 text-blue-600 hover:text-blue-800 hover:bg-blue-100 rounded-full dark:text-blue-300 dark:hover:text-blue-200 dark:hover:bg-blue-800/50 transition-colors"
|
||||
class="ml-1 p-1 sm:p-0.5 text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-200 hover:bg-blue-100 dark:hover:bg-blue-800/50 rounded-full transition-all duration-200 min-w-[24px] min-h-[24px] sm:min-w-0 sm:min-h-0 flex items-center justify-center"
|
||||
hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}?{% for name, value in request.GET.items %}{% if name != filter_name and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
|
||||
@@ -48,103 +48,219 @@ Features:
|
||||
|
||||
{% if park %}
|
||||
{% if view_mode == 'list' %}
|
||||
{# Enhanced List View Item #}
|
||||
{# 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-6">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
||||
{# Main Content Section #}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<h2 class="text-xl lg:text-2xl font-bold">
|
||||
{% 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">
|
||||
{{ 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>
|
||||
<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 %}
|
||||
|
||||
{# Status Badge #}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border
|
||||
{% if park.status == 'operating' or park.status == 'OPERATING' %}bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800
|
||||
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
|
||||
{% elif park.status == 'seasonal' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
|
||||
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800
|
||||
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
|
||||
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600
|
||||
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800
|
||||
{% else %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600{% endif %}">
|
||||
{{ park.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if park.operator %}
|
||||
<div class="text-base font-medium text-gray-600 dark:text-gray-400 mb-3">
|
||||
{{ park.operator.name }}
|
||||
{# 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>
|
||||
{% endif %}
|
||||
|
||||
{% if park.description %}
|
||||
<p class="text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
|
||||
{{ park.description|truncatewords:30 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Stats Section #}
|
||||
{% if park.ride_count or park.coaster_count %}
|
||||
<div class="flex items-center space-x-6 text-sm">
|
||||
{% 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-lg border border-blue-200/50 dark:border-blue-800/50">
|
||||
<svg class="w-5 h-5 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 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"/>
|
||||
{# 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>
|
||||
<span class="font-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span>
|
||||
<span class="text-blue-600 dark:text-blue-400">rides</span>
|
||||
</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 %}
|
||||
{% 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-lg border border-purple-200/50 dark:border-purple-800/50">
|
||||
<svg class="w-5 h-5 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>
|
||||
<span class="font-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span>
|
||||
<span class="text-purple-600 dark:text-purple-400">coasters</span>
|
||||
</div>
|
||||
|
||||
{# 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>
|
||||
{% endif %}
|
||||
|
||||
{# 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 }}">
|
||||
{# Park Image #}
|
||||
<div class="relative h-48 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500">
|
||||
{% if park.card_image %}
|
||||
<img src="{{ park.card_image.image.url }}"
|
||||
alt="{{ park.name }}"
|
||||
class="w-full h-full object-cover">
|
||||
{% elif park.photos.first %}
|
||||
<img src="{{ park.photos.first.image.url }}"
|
||||
alt="{{ park.name }}"
|
||||
class="w-full h-full object-cover">
|
||||
{# 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 %}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<svg class="w-16 h-16 text-white opacity-50" 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"></path>
|
||||
</svg>
|
||||
{# 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 %}
|
||||
|
||||
{# Status Badge Overlay #}
|
||||
<div class="absolute top-3 right-3">
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/90 backdrop-blur-sm
|
||||
{# 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
|
||||
@@ -156,14 +272,20 @@ Features:
|
||||
{{ 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>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-bold line-clamp-2 mb-2">
|
||||
{# 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">
|
||||
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 %}
|
||||
@@ -174,43 +296,59 @@ Features:
|
||||
</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">
|
||||
{{ park.operator.name }}
|
||||
<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">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed">
|
||||
{{ park.description|truncatewords:15 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# Stats Footer #}
|
||||
{# Enhanced Stats Footer with Better Mobile Layout #}
|
||||
{% if park.ride_count or park.coaster_count %}
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
|
||||
<div class="flex items-center space-x-4 text-sm">
|
||||
<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 text-blue-600 dark:text-blue-400">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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 text-purple-600 dark:text-purple-400">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
|
||||
{# View Details Arrow #}
|
||||
<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">
|
||||
{# 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>
|
||||
|
||||
@@ -37,7 +37,7 @@ Features:
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="inline-flex items-center justify-center w-full px-3 sm:px-4 py-2.5 sm: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 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 min-h-[44px] sm:min-h-0"
|
||||
@click="open = !open"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
|
||||
@@ -33,7 +33,7 @@ Features:
|
||||
<div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 {{ class }}" role="group" aria-label="View toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-l-lg transition-colors {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700{% endif %}"
|
||||
class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-l-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'view_mode' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}view_mode=grid"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
@@ -50,7 +50,7 @@ Features:
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-r-lg transition-colors {% if current_view == 'list' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700{% endif %}"
|
||||
class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-r-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'list' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'view_mode' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}view_mode=list"
|
||||
hx-target="#park-results"
|
||||
hx-push-url="true"
|
||||
|
||||
Reference in New Issue
Block a user