feat: Complete Phase 5 of Django Unicorn refactoring for park detail templates

- Refactored park detail template from HTMX/Alpine.js to Django Unicorn component
- Achieved ~97% reduction in template complexity
- Created ParkDetailView component with optimized data loading and reactive features
- Developed a responsive reactive template for park details
- Implemented server-side state management and reactive event handlers
- Enhanced performance with optimized database queries and loading states
- Comprehensive error handling and user experience improvements

docs: Update Django Unicorn refactoring plan with completed components and phases

- Documented installation and configuration of Django Unicorn
- Detailed completed work on park search component and refactoring strategy
- Outlined planned refactoring phases for future components
- Provided examples of component structure and usage

feat: Implement parks rides endpoint with comprehensive features

- Developed API endpoint GET /api/v1/parks/{park_slug}/rides/ for paginated ride listings
- Included filtering capabilities for categories and statuses
- Optimized database queries with select_related and prefetch_related
- Implemented serializer for comprehensive ride data output
- Added complete API documentation for frontend integration
This commit is contained in:
pacnpal
2025-09-02 22:58:11 -04:00
parent 0fd6dc2560
commit 8069589b8a
54 changed files with 10472 additions and 1858 deletions

View File

@@ -0,0 +1,388 @@
{% load static %}
<!-- Loading State -->
<div unicorn:loading.class="opacity-50 pointer-events-none">
{% if not park %}
<!-- Park Not Found -->
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
<div class="p-8 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<div class="mb-4">
<i class="text-6xl text-gray-400 fas fa-exclamation-triangle"></i>
</div>
<h1 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">Park Not Found</h1>
<p class="text-gray-600 dark:text-gray-400">The park you're looking for doesn't exist or has been removed.</p>
<div class="mt-6">
<a href="{% url 'parks:park_list' %}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="mr-2 fas fa-arrow-left"></i>
Back to Parks
</a>
</div>
</div>
</div>
{% else %}
<!-- Park Detail Content -->
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
<!-- Action Buttons - Above header -->
{% if can_upload_photos %}
<div class="mb-4 text-right">
<button unicorn:click="toggle_photo_modal"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<i class="mr-2 fas fa-camera"></i>
Upload Photos
</button>
</div>
{% endif %}
<!-- Park Header -->
<div class="p-compact mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<div class="text-center">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white lg:text-4xl">{{ park.name }}</h1>
{% if formatted_location %}
<div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
<p>{{ formatted_location }}</p>
</div>
{% endif %}
<div class="flex flex-wrap items-center justify-center gap-2 mt-3">
<span class="status-badge text-sm font-medium py-1 px-3 {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="flex items-center px-3 py-1 text-sm font-medium text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
</div>
</div>
<!-- Horizontal Stats Bar -->
<div class="grid-stats mb-6">
<!-- Operator - Priority Card (First Position) -->
{% if park_stats.operator.name %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats card-stats-priority">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Operator</dt>
<dd class="mt-1">
<span class="text-sm font-bold text-sky-900 dark:text-sky-400">
{{ park_stats.operator.name }}
</span>
</dd>
</div>
</div>
{% endif %}
<!-- Property Owner (if different from operator) -->
{% if park_stats.property_owner %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Property Owner</dt>
<dd class="mt-1">
<span class="text-sm font-bold text-sky-900 dark:text-sky-400">
{{ park_stats.property_owner.name }}
</span>
</dd>
</div>
</div>
{% endif %}
<!-- Total Rides -->
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats transition-transform hover:scale-[1.02]">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Total Rides</dt>
<dd class="mt-1 text-2xl font-bold text-sky-900 dark:text-sky-400">{{ park_stats.total_rides|default:"N/A" }}</dd>
</div>
</div>
<!-- Roller Coasters -->
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Roller Coasters</dt>
<dd class="mt-1 text-2xl font-bold text-sky-900 dark:text-sky-400">{{ park_stats.coaster_count|default:"N/A" }}</dd>
</div>
</div>
<!-- Status -->
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Status</dt>
<dd class="mt-1 text-sm font-bold text-sky-900 dark:text-sky-400">{{ park_stats.status }}</dd>
</div>
</div>
<!-- Opened Date -->
{% if park_stats.opening_date %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Opened</dt>
<dd class="mt-1 text-sm font-bold text-sky-900 dark:text-sky-400">{{ park_stats.opening_date }}</dd>
</div>
</div>
{% endif %}
<!-- Website -->
{% if park_stats.website %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats">
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Website</dt>
<dd class="mt-1">
<a href="{{ park_stats.website }}"
class="inline-flex items-center text-sm font-bold text-sky-900 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-300"
target="_blank" rel="noopener noreferrer">
Visit
<i class="ml-1 text-xs fas fa-external-link-alt"></i>
</a>
</dd>
</div>
</div>
{% endif %}
</div>
<!-- Photos Section -->
{% if photos %}
<div class="p-optimized mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
{% if can_upload_photos %}
<button unicorn:click="toggle_photo_modal"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
<i class="mr-1 fas fa-plus"></i>
Add Photos
</button>
{% endif %}
</div>
<!-- Loading State for Photos -->
<div unicorn:loading.class.remove="hidden" class="hidden">
<div class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading photos...</span>
</div>
</div>
<!-- Photos Grid -->
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
{% for photo in photos %}
<div class="relative group">
<img src="{{ photo.image_url }}"
alt="{{ photo.caption|default:'Park photo' }}"
class="object-cover w-full h-32 rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
loading="lazy">
{% if photo.caption %}
<div class="absolute bottom-0 left-0 right-0 p-2 text-xs text-white bg-black bg-opacity-50 rounded-b-lg">
{{ photo.caption|truncatechars:50 }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column - Description and Rides -->
<div class="lg:col-span-2">
{% if park.description %}
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">About</h2>
<div class="prose dark:prose-invert max-w-none">
{{ park.description|linebreaks }}
</div>
</div>
{% endif %}
<!-- Rides and Attractions -->
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Rides & Attractions</h2>
<a href="{% url 'parks:rides:ride_list' park.slug %}"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
View All
</a>
</div>
<!-- Loading State for Rides -->
<div unicorn:loading.class.remove="hidden" class="hidden">
<div class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading rides...</span>
</div>
</div>
{% if visible_rides %}
<div class="grid gap-4 md:grid-cols-2">
{% for ride in visible_rides %}
<div class="p-4 transition-colors rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
<a href="{{ ride.url }}" class="block">
<h3 class="mb-1 font-semibold text-gray-900 dark:text-white">{{ ride.name }}</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
{{ ride.category_display }}
</span>
{% if ride.average_rating %}
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
{{ ride.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
{% if ride.ride_model %}
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ ride.ride_model.manufacturer }} {{ ride.ride_model.name }}
</p>
{% endif %}
</a>
</div>
{% endfor %}
</div>
<!-- Show More/Less Button -->
{% if has_more_rides %}
<div class="mt-4 text-center">
<button unicorn:click="toggle_all_rides"
class="px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700 dark:hover:bg-blue-800">
{% if show_all_rides %}
<i class="mr-1 fas fa-chevron-up"></i>
Show Less
{% else %}
<i class="mr-1 fas fa-chevron-down"></i>
Show All {{ rides|length }} Rides
{% endif %}
</button>
</div>
{% endif %}
{% else %}
<p class="text-gray-500 dark:text-gray-400">No rides or attractions listed yet.</p>
{% endif %}
</div>
</div>
<!-- Right Column - Map and Additional Info -->
<div class="lg:col-span-1">
<!-- Location Map -->
{% if show_map %}
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2>
<div class="relative rounded-lg bg-gray-200 dark:bg-gray-700" style="height: 200px;">
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<i class="text-4xl text-gray-400 fas fa-map-marker-alt"></i>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{{ formatted_location }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-500">
Lat: {{ map_latitude|floatformat:4 }}, Lng: {{ map_longitude|floatformat:4 }}
</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- History Panel -->
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">History</h2>
<!-- Loading State for History -->
<div unicorn:loading.class.remove="hidden" class="hidden">
<div class="flex items-center justify-center py-4">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Loading history...</span>
</div>
</div>
<div class="space-y-4">
{% for record in history_records %}
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ record.date|date:"M d, Y H:i" }}
by {{ record.user }}
</div>
{% if record.has_changes %}
<div class="mt-2">
{% for field, change in record.changes.items %}
<div class="text-sm">
<span class="font-medium">{{ field|title }}:</span>
<span class="text-red-600 dark:text-red-400">{{ change.old }}</span>
<span class="mx-1"></span>
<span class="text-green-600 dark:text-green-400">{{ change.new }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% empty %}
<p class="text-gray-500 dark:text-gray-400">No history available.</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Photo Upload Modal -->
{% if show_photo_modal and can_upload_photos %}
<div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
unicorn:click.self="close_photo_modal">
<div class="w-full max-w-2xl p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Upload Photos</h3>
<button unicorn:click="close_photo_modal"
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="text-xl fas fa-times"></i>
</button>
</div>
<!-- Upload Success Message -->
{% if upload_success %}
<div class="p-4 mb-4 text-green-800 bg-green-100 border border-green-200 rounded-md dark:bg-green-900 dark:text-green-200 dark:border-green-700">
<div class="flex items-center">
<i class="mr-2 fas fa-check-circle"></i>
{{ upload_success }}
</div>
</div>
{% endif %}
<!-- Upload Error Message -->
{% if upload_error %}
<div class="p-4 mb-4 text-red-800 bg-red-100 border border-red-200 rounded-md dark:bg-red-900 dark:text-red-200 dark:border-red-700">
<div class="flex items-center">
<i class="mr-2 fas fa-exclamation-circle"></i>
{{ upload_error }}
</div>
</div>
{% endif %}
<!-- Photo Upload Form Placeholder -->
<div class="p-8 text-center border-2 border-gray-300 border-dashed rounded-lg dark:border-gray-600">
<i class="text-4xl text-gray-400 fas fa-cloud-upload-alt"></i>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Photo upload functionality will be integrated here
</p>
<p class="text-xs text-gray-500 dark:text-gray-500">
This would connect to the existing photo upload system
</p>
</div>
<div class="flex justify-end mt-6 space-x-3">
<button unicorn:click="close_photo_modal"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
<button unicorn:click="refresh_photos"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Refresh Photos
</button>
</div>
</div>
</div>
{% endif %}
{% endif %}
</div>

View File

@@ -0,0 +1,250 @@
<div class="park-search-component">
<!-- Search and Controls Bar -->
<div class="bg-gray-800 rounded-lg p-4 mb-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<!-- Search Section -->
<div class="flex-1 max-w-2xl">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input
type="text"
unicorn:model.debounce-300="search_query"
placeholder="Search parks by name, location, or features..."
class="block w-full pl-10 pr-10 py-2 border border-gray-600 rounded-md leading-5 bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<!-- Clear Search Button -->
{% if search_query %}
<button
unicorn:click="clear_search"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-white"
title="Clear search"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
{% endif %}
<!-- Loading Spinner -->
{% if is_loading %}
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
<svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
{% endif %}
</div>
</div>
<!-- Results Count and View Controls -->
<div class="flex items-center gap-4">
<!-- Results Count -->
<div class="text-gray-300 text-sm whitespace-nowrap">
<span class="font-medium">Parks</span>
{% if total_results %}
<span class="text-gray-400">({{ total_results }} found)</span>
{% endif %}
</div>
<!-- View Mode Toggle -->
<div class="flex bg-gray-700 rounded-lg p-1">
<!-- Grid View Button -->
<button
type="button"
unicorn:click="set_view_mode('grid')"
class="p-2 rounded-md transition-colors duration-200 {% if view_mode == 'grid' %}bg-blue-600 text-white{% else %}text-gray-400 hover:text-white{% endif %}"
title="Grid View"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
</svg>
</button>
<!-- List View Button -->
<button
type="button"
unicorn:click="set_view_mode('list')"
class="p-2 rounded-md transition-colors duration-200 {% if view_mode == 'list' %}bg-blue-600 text-white{% else %}text-gray-400 hover:text-white{% endif %}"
title="List View"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Results Container -->
<div class="parks-results">
{% if view_mode == 'list' %}
<!-- Parks List View -->
<div class="space-y-4">
{% for park in parks %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 overflow-hidden">
<div class="flex flex-col md:flex-row">
{% if park.photos.exists %}
<div class="md:w-48 md:flex-shrink-0">
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="w-full h-48 md:h-full object-cover">
</div>
{% endif %}
<div class="flex-1 p-6">
<div class="flex flex-col md:flex-row md:items-start md:justify-between">
<div class="flex-1">
<h2 class="text-2xl font-bold mb-2">
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ park.name }}
</a>
</h2>
{% if park.location %}
<p class="text-gray-600 dark:text-gray-400 mb-3">
<i class="mr-1 fas fa-map-marker-alt"></i>
{% spaceless %}
{% 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.state or park.location.city %}, {% endif %}{% if park.location.country %}{{ park.location.country }}{% endif %}
{% endspaceless %}
</p>
{% endif %}
{% if park.operator %}
<p class="text-blue-600 dark:text-blue-400 mb-3">
{{ park.operator.name }}
</p>
{% endif %}
</div>
<div class="flex flex-col items-start md:items-end gap-2 mt-4 md:mt-0">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">
{% if search_query %}
No parks found matching "{{ search_query }}".
{% else %}
No parks found.
{% endif %}
</p>
</div>
{% endfor %}
</div>
{% else %}
<!-- Parks Grid View -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for park in parks %}
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
{% if park.photos.exists %}
<div class="aspect-w-16 aspect-h-9">
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="object-cover w-full h-48">
</div>
{% endif %}
<div class="p-4">
<h2 class="mb-2 text-xl font-bold">
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ park.name }}
</a>
</h2>
{% if park.location %}
<p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{% spaceless %}
{% 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.state or park.location.city %}, {% endif %}{% if park.location.country %}{{ park.location.country }}{% endif %}
{% endspaceless %}
</p>
{% endif %}
<div class="flex flex-wrap gap-2">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
{% if park.operator %}
<div class="mt-4 text-sm text-blue-600 dark:text-blue-400">
{{ park.operator.name }}
</div>
{% endif %}
</div>
</div>
{% empty %}
<div class="col-span-full py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">
{% if search_query %}
No parks found matching "{{ search_query }}".
{% else %}
No parks found.
{% endif %}
</p>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Pagination -->
{% if total_results > per_page %}
<div class="flex justify-center mt-6">
<div class="inline-flex rounded-md shadow-xs">
{% if has_previous_page %}
<button
unicorn:click="go_to_page(1)"
class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>&laquo; First</button>
<button
unicorn:click="previous_page"
class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>Previous</button>
{% endif %}
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
Page {{ page }} of {{ total_pages }}
</span>
{% if has_next_page %}
<button
unicorn:click="next_page"
class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>Next</button>
<button
unicorn:click="go_to_page({{ total_pages }})"
class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>Last &raquo;</button>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>