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:
pac7
2025-09-23 22:11:05 +00:00
parent 4c954fff6f
commit d978217577
8 changed files with 897 additions and 231 deletions

View File

@@ -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);
};
}
}
}