mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-26 19:07:06 -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:
@@ -5,43 +5,45 @@
|
||||
{% block title %}Parks{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-6" x-data="parkListState()">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white">
|
||||
{# Enhanced Mobile-First Container with Better Spacing #}
|
||||
<div class="container mx-auto px-3 sm:px-4 lg:px-6 py-4 sm:py-6" x-data="parkListState()">
|
||||
{# Enhanced Mobile-First Header Section #}
|
||||
<div class="mb-6 sm:mb-8">
|
||||
<div class="flex flex-col gap-4 sm:gap-6">
|
||||
{# Enhanced Mobile-First Title Section #}
|
||||
<div class="text-center sm:text-left">
|
||||
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white leading-tight">
|
||||
Theme Parks
|
||||
</h1>
|
||||
<p class="mt-2 text-lg text-gray-600 dark:text-gray-400">
|
||||
<p class="mt-1 sm:mt-2 text-base sm:text-lg text-gray-600 dark:text-gray-400">
|
||||
Discover amazing theme parks around the world
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="text-center">
|
||||
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.total_parks|default:0 }}</div>
|
||||
<div>Total Parks</div>
|
||||
{# Enhanced Mobile-First Quick Stats with Better Touch Targets #}
|
||||
<div class="grid grid-cols-3 gap-3 sm:gap-4 lg:gap-6">
|
||||
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.total_parks|default:0 }}</div>
|
||||
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Total Parks</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.operating_parks|default:0 }}</div>
|
||||
<div>Operating</div>
|
||||
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.operating_parks|default:0 }}</div>
|
||||
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Operating</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.parks_with_coasters|default:0 }}</div>
|
||||
<div>With Coasters</div>
|
||||
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.parks_with_coasters|default:0 }}</div>
|
||||
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">With Coasters</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Search and Filter Bar -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 mb-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Main Search Row -->
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<!-- Enhanced Search Input -->
|
||||
{# Enhanced Mobile-First Search and Filter Bar #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6 sm:mb-8">
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
{# Enhanced Mobile-First Main Search Row #}
|
||||
<div class="space-y-3 sm:space-y-0 sm:flex sm:flex-col lg:flex-row gap-4">
|
||||
{# Enhanced Search Input with Better Mobile UX #}
|
||||
<div class="flex-1">
|
||||
<c-enhanced_search
|
||||
placeholder="Search parks by name, location, or features..."
|
||||
@@ -51,53 +53,62 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Sort Controls -->
|
||||
<c-sort_controls
|
||||
current_sort="{{ current_ordering }}"
|
||||
class="min-w-0"
|
||||
/>
|
||||
{# Enhanced Mobile-First Controls Row with Better Touch Targets #}
|
||||
<div class="flex items-center justify-between sm:justify-start gap-2 sm:gap-3">
|
||||
{# Sort Controls with Mobile Optimization #}
|
||||
<div class="flex-1 sm:flex-none min-w-0">
|
||||
<c-sort_controls
|
||||
current_sort="{{ current_ordering }}"
|
||||
class="w-full sm:w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<c-view_toggle
|
||||
current_view="{{ view_mode }}"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
{# View Toggle with Better Mobile Touch Target #}
|
||||
<div class="flex-shrink-0">
|
||||
<c-view_toggle
|
||||
current_view="{{ view_mode }}"
|
||||
class=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button (Mobile) -->
|
||||
{# Enhanced Mobile Filter Toggle Button with Better Design #}
|
||||
<button
|
||||
type="button"
|
||||
class="lg:hidden inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
class="lg:hidden inline-flex items-center px-3 py-2.5 sm:px-4 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm bg-white dark:bg-gray-700 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 min-w-[44px] min-h-[44px] justify-center"
|
||||
@click="showFilters = !showFilters"
|
||||
:aria-expanded="showFilters"
|
||||
aria-label="Toggle filters"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300': showFilters }"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 sm:w-5 sm:h-5 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': showFilters }"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
<span class="ml-1">Filters</span>
|
||||
<span class="ml-1 sm:ml-2 hidden sm:inline">Filters</span>
|
||||
<span class="sr-only sm:hidden" x-text="showFilters ? 'Hide filters' : 'Show filters'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Filters Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
{# Enhanced Mobile-First Advanced Filters with Better Touch Interaction #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4"
|
||||
x-show="showFilters"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95">
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform scale-95 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 transform scale-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform scale-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform scale-95 -translate-y-2">
|
||||
|
||||
<!-- Status Filter -->
|
||||
{# Enhanced Mobile-First Status Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
|
||||
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='operator'], [name='park_type']"
|
||||
@@ -105,21 +116,21 @@
|
||||
hx-indicator="#search-spinner"
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="OPERATING" {% if request.GET.status == 'OPERATING' %}selected{% endif %}>Operating</option>
|
||||
<option value="CLOSED_TEMP" {% if request.GET.status == 'CLOSED_TEMP' %}selected{% endif %}>Temporarily Closed</option>
|
||||
<option value="CLOSED_PERM" {% if request.GET.status == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
|
||||
<option value="UNDER_CONSTRUCTION" {% if request.GET.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>Under Construction</option>
|
||||
<option value="OPERATING" {% if request.GET.status == 'OPERATING' %}selected{% endif %}>🟢 Operating</option>
|
||||
<option value="CLOSED_TEMP" {% if request.GET.status == 'CLOSED_TEMP' %}selected{% endif %}>🟡 Temporarily Closed</option>
|
||||
<option value="CLOSED_PERM" {% if request.GET.status == 'CLOSED_PERM' %}selected{% endif %}>🔴 Permanently Closed</option>
|
||||
<option value="UNDER_CONSTRUCTION" {% if request.GET.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>🚧 Under Construction</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Operator Filter -->
|
||||
{# Enhanced Mobile-First Operator Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Operator
|
||||
</label>
|
||||
<select
|
||||
name="operator"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
|
||||
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='park_type']"
|
||||
@@ -136,14 +147,14 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Park Type Filter -->
|
||||
{# Enhanced Mobile-First Park Type Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Park Type
|
||||
</label>
|
||||
<select
|
||||
name="park_type"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
|
||||
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator']"
|
||||
@@ -151,70 +162,86 @@
|
||||
hx-indicator="#search-spinner"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="disney" {% if request.GET.park_type == 'disney' %}selected{% endif %}>Disney Parks</option>
|
||||
<option value="universal" {% if request.GET.park_type == 'universal' %}selected{% endif %}>Universal Parks</option>
|
||||
<option value="six_flags" {% if request.GET.park_type == 'six_flags' %}selected{% endif %}>Six Flags</option>
|
||||
<option value="cedar_fair" {% if request.GET.park_type == 'cedar_fair' %}selected{% endif %}>Cedar Fair</option>
|
||||
<option value="independent" {% if request.GET.park_type == 'independent' %}selected{% endif %}>Independent</option>
|
||||
<option value="disney" {% if request.GET.park_type == 'disney' %}selected{% endif %}>🏰 Disney Parks</option>
|
||||
<option value="universal" {% if request.GET.park_type == 'universal' %}selected{% endif %}>🎬 Universal Parks</option>
|
||||
<option value="six_flags" {% if request.GET.park_type == 'six_flags' %}selected{% endif %}>🎢 Six Flags</option>
|
||||
<option value="cedar_fair" {% if request.GET.park_type == 'cedar_fair' %}selected{% endif %}>🌲 Cedar Fair</option>
|
||||
<option value="independent" {% if request.GET.park_type == 'independent' %}selected{% endif %}>⭐ Independent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Quick Filters -->
|
||||
{# Enhanced Mobile-First Quick Filters with Better Touch Targets #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Quick Filters
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center p-2 -m-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="has_coasters"
|
||||
value="true"
|
||||
{% if request.GET.has_coasters %}checked{% endif %}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-800 transition-colors duration-200"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator'], [name='park_type']"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#search-spinner"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Has Roller Coasters</span>
|
||||
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300 select-none">
|
||||
🎢 Has Roller Coasters
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<label class="flex items-center p-2 -m-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="big_parks_only"
|
||||
value="true"
|
||||
{% if request.GET.big_parks_only %}checked{% endif %}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-800 transition-colors duration-200"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#park-results"
|
||||
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator'], [name='park_type']"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#search-spinner"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Major Parks (10+ rides)</span>
|
||||
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300 select-none">
|
||||
🏢 Major Parks (10+ rides)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Filter Chips -->
|
||||
{# Enhanced Mobile-First Active Filter Chips #}
|
||||
{% if active_filters %}
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 sm:pt-4">
|
||||
<div class="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Active Filters
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
@click="clearAllFilters()"
|
||||
class="text-xs sm:text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium focus:outline-none focus:underline transition-colors duration-200 min-h-[44px] px-2 py-1 sm:min-h-auto sm:px-0 sm:py-0"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<c-filter_chips
|
||||
filters=active_filters
|
||||
base_url="{% url 'parks:park_list' %}"
|
||||
class="flex-wrap"
|
||||
class="flex-wrap gap-2"
|
||||
/>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div class="space-y-6">
|
||||
<!-- Results Statistics -->
|
||||
{# Enhanced Mobile-First Results Section #}
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
{# Enhanced Mobile-First Results Statistics #}
|
||||
<c-result_stats
|
||||
total_results="{{ total_results }}"
|
||||
page_obj="{{ page_obj }}"
|
||||
@@ -223,25 +250,28 @@
|
||||
filter_count="{{ filter_count }}"
|
||||
/>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
{# Enhanced Mobile-First Loading Overlay #}
|
||||
<div id="loading-overlay" class="htmx-indicator">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-xl">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="animate-spin h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 sm:p-6 shadow-xl max-w-sm w-full">
|
||||
<div class="flex flex-col items-center space-y-3 text-center">
|
||||
<svg class="animate-spin h-8 w-8 sm:h-10 sm:w-10 text-blue-600 dark:text-blue-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>
|
||||
<span class="text-lg font-medium text-gray-900 dark:text-white">Loading parks...</span>
|
||||
<div>
|
||||
<div class="text-base sm:text-lg font-medium text-gray-900 dark:text-white">Loading parks...</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">Please wait a moment</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Park Results Container -->
|
||||
{# Enhanced Mobile-First Park Results Container #}
|
||||
<div id="park-results"
|
||||
hx-indicator="#loading-overlay"
|
||||
class="min-h-[400px]">
|
||||
class="min-h-[300px] sm:min-h-[400px]">
|
||||
{% include "parks/partials/park_list.html" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,51 +279,110 @@
|
||||
|
||||
<!-- AlpineJS State Management -->
|
||||
<script>
|
||||
{# Enhanced Mobile-First AlpineJS State Management #}
|
||||
function parkListState() {
|
||||
return {
|
||||
showFilters: window.innerWidth >= 1024, // Show on desktop by default
|
||||
viewMode: '{{ view_mode }}',
|
||||
searchQuery: '{{ search_query }}',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
init() {
|
||||
// Handle responsive filter visibility
|
||||
// Handle responsive filter visibility with better mobile UX
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', () => this.handleResize());
|
||||
window.addEventListener('resize', this.debounce(() => this.handleResize(), 250));
|
||||
|
||||
// Handle HTMX events
|
||||
// Enhanced HTMX events with better mobile feedback
|
||||
document.addEventListener('htmx:beforeRequest', () => {
|
||||
this.setLoading(true);
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', () => {
|
||||
document.addEventListener('htmx:afterRequest', (event) => {
|
||||
this.setLoading(false);
|
||||
// Scroll to top of results on mobile after filter changes
|
||||
if (window.innerWidth < 768 && event.detail.target?.id === 'park-results') {
|
||||
this.scrollToResults();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', () => {
|
||||
this.setLoading(false);
|
||||
this.showError('Failed to load results. Please try again.');
|
||||
this.showError('Failed to load results. Please check your connection and try again.');
|
||||
});
|
||||
|
||||
// Handle mobile viewport changes (orientation, virtual keyboard)
|
||||
this.handleMobileViewport();
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if (window.innerWidth >= 1024) {
|
||||
this.showFilters = true;
|
||||
} else {
|
||||
// Keep current state on mobile
|
||||
}
|
||||
// Auto-hide filters on mobile after interaction for better UX
|
||||
// Keep current state but could add auto-hide logic here
|
||||
},
|
||||
|
||||
handleMobileViewport() {
|
||||
// Handle mobile viewport changes for better UX
|
||||
if ('visualViewport' in window) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
// Handle virtual keyboard appearance/disappearance
|
||||
document.documentElement.style.setProperty(
|
||||
'--viewport-height',
|
||||
`${window.visualViewport.height}px`
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
scrollToResults() {
|
||||
// Smooth scroll to results on mobile for better UX
|
||||
const resultsElement = document.getElementById('park-results');
|
||||
if (resultsElement) {
|
||||
resultsElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setLoading(loading) {
|
||||
// Additional loading state management if needed
|
||||
this.isLoading = loading;
|
||||
// Disable form interactions while loading for better UX
|
||||
const formElements = document.querySelectorAll('select, input, button');
|
||||
formElements.forEach(el => {
|
||||
el.disabled = loading;
|
||||
});
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
// Show error notification
|
||||
this.error = message;
|
||||
// Auto-clear error after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.error = null;
|
||||
}, 5000);
|
||||
console.error(message);
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
// Add loading state for better UX
|
||||
this.setLoading(true);
|
||||
window.location.href = '{% url "parks:park_list" %}';
|
||||
},
|
||||
|
||||
// Utility function for better performance
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user