Add park and ride card components with advanced search functionality

- Implemented park card component with image, status badge, favorite button, and quick stats overlay.
- Developed ride card component featuring thrill level badge, status badge, favorite button, and detailed stats.
- Created advanced search page with filters for parks and rides, including location, type, status, and thrill level.
- Added dynamic quick search functionality with results display.
- Enhanced user experience with JavaScript for filter toggling, range slider updates, and view switching.
- Included custom CSS for improved styling of checkboxes and search results layout.
This commit is contained in:
pacnpal
2025-09-24 23:10:48 -04:00
parent 4373d18176
commit b1c369c1bb
39 changed files with 5202 additions and 824 deletions

View File

@@ -0,0 +1,166 @@
<!-- Park Card Component -->
<div class="card-park hover-lift group"
hx-get="{% url 'parks:detail' park.slug %}"
hx-target="#main-content"
hx-swap="innerHTML transition:true">
<!-- Park Image with Overlay -->
<div class="relative overflow-hidden rounded-t-2xl">
{% if park.featured_image %}
<img src="{{ park.featured_image.url }}"
alt="{{ park.name }}"
class="card-park-image"
loading="lazy">
{% else %}
<div class="card-park-image bg-gradient-to-br from-thrill-primary/20 to-thrill-secondary/20 flex items-center justify-center">
<i class="fas fa-map-marked-alt text-6xl text-thrill-primary/40"></i>
</div>
{% endif %}
<!-- Status Badge -->
<div class="absolute top-4 left-4">
{% if park.status == 'OPERATING' %}
<span class="badge-operating">
<i class="fas fa-check-circle mr-1"></i>
Operating
</span>
{% elif park.status == 'CONSTRUCTION' %}
<span class="badge-construction">
<i class="fas fa-hard-hat mr-1"></i>
Under Construction
</span>
{% elif park.status == 'CLOSED_PERMANENTLY' %}
<span class="badge-closed">
<i class="fas fa-times-circle mr-1"></i>
Closed
</span>
{% endif %}
</div>
<!-- Favorite Button -->
<div class="absolute top-4 right-4">
<button class="w-10 h-10 bg-white/90 dark:bg-neutral-800/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white dark:hover:bg-neutral-800 transition-all duration-200 hover:scale-110"
hx-post="{% url 'parks:toggle_favorite' park.slug %}"
hx-swap="outerHTML"
onclick="event.stopPropagation()">
{% if park.is_favorited %}
<i class="fas fa-heart text-red-500"></i>
{% else %}
<i class="far fa-heart text-neutral-600 dark:text-neutral-400"></i>
{% endif %}
</button>
</div>
<!-- Gradient Overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Quick Stats Overlay -->
<div class="absolute bottom-4 left-4 right-4 transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<div class="flex items-center justify-between text-white text-sm">
<div class="flex items-center space-x-4">
{% if park.ride_count %}
<div class="flex items-center">
<i class="fas fa-rocket mr-1"></i>
{{ park.ride_count }} rides
</div>
{% endif %}
{% if park.area %}
<div class="flex items-center">
<i class="fas fa-expand-arrows-alt mr-1"></i>
{{ park.area }} acres
</div>
{% endif %}
</div>
{% if park.rating %}
<div class="flex items-center">
<i class="fas fa-star text-yellow-400 mr-1"></i>
{{ park.rating|floatformat:1 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Park Content -->
<div class="card-park-content">
<!-- Park Name and Location -->
<div class="space-y-2">
<h3 class="text-xl font-bold text-neutral-900 dark:text-neutral-100 group-hover:text-thrill-primary transition-colors duration-200">
{{ park.name }}
</h3>
{% if park.location %}
<div class="flex items-center text-neutral-600 dark:text-neutral-400 text-sm">
<i class="fas fa-map-marker-alt mr-2 text-thrill-primary"></i>
{{ park.location.city }}{% if park.location.region %}, {{ park.location.region }}{% endif %}
{% if park.location.country %}, {{ park.location.country }}{% endif %}
</div>
{% endif %}
</div>
<!-- Park Description -->
{% if park.description %}
<p class="text-neutral-600 dark:text-neutral-400 text-sm line-clamp-2">
{{ park.description|truncatewords:20 }}
</p>
{% endif %}
<!-- Park Features/Tags -->
{% if park.features.all %}
<div class="flex flex-wrap gap-2">
{% for feature in park.features.all|slice:":3" %}
<span class="px-2 py-1 bg-thrill-primary/10 text-thrill-primary text-xs rounded-full">
{{ feature.name }}
</span>
{% endfor %}
{% if park.features.count > 3 %}
<span class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400 text-xs rounded-full">
+{{ park.features.count|add:"-3" }} more
</span>
{% endif %}
</div>
{% endif %}
<!-- Park Stats -->
<div class="flex items-center justify-between pt-4 border-t border-neutral-200/50 dark:border-neutral-700/50">
<div class="flex items-center space-x-4 text-sm text-neutral-600 dark:text-neutral-400">
{% if park.opened_date %}
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
{{ park.opened_date.year }}
</div>
{% endif %}
{% if park.park_type %}
<div class="flex items-center">
<i class="fas fa-tag mr-1"></i>
{{ park.get_park_type_display }}
</div>
{% endif %}
</div>
<!-- Action Button -->
<button class="btn-primary btn-sm opacity-0 group-hover:opacity-100 transition-all duration-200 transform translate-x-2 group-hover:translate-x-0">
<i class="fas fa-arrow-right mr-2"></i>
Explore
</button>
</div>
</div>
<!-- Loading State Overlay -->
<div class="absolute inset-0 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm rounded-2xl flex items-center justify-center opacity-0 htmx-request:opacity-100 transition-opacity duration-200 pointer-events-none">
<div class="loading-spinner opacity-100">
<i class="fas fa-spinner text-2xl text-thrill-primary"></i>
</div>
</div>
</div>
<!-- CSS for line-clamp utility -->
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,271 @@
<!-- Ride Card Component -->
<div class="card-ride hover-lift group"
hx-get="{% url 'rides:detail' ride.slug %}"
hx-target="#main-content"
hx-swap="innerHTML transition:true">
<!-- Ride Image with Overlay -->
<div class="relative overflow-hidden rounded-t-2xl">
{% if ride.featured_image %}
<img src="{{ ride.featured_image.url }}"
alt="{{ ride.name }}"
class="card-ride-image"
loading="lazy">
{% else %}
<div class="card-ride-image bg-gradient-to-br from-thrill-secondary/20 to-red-500/20 flex items-center justify-center">
<i class="fas fa-rocket text-6xl text-thrill-secondary/40"></i>
</div>
{% endif %}
<!-- Thrill Level Badge -->
<div class="absolute top-4 left-4">
{% if ride.thrill_level == 'EXTREME' %}
<span class="badge bg-red-500/90 text-white border-red-500/20 backdrop-blur-sm">
<i class="fas fa-fire mr-1"></i>
Extreme
</span>
{% elif ride.thrill_level == 'HIGH' %}
<span class="badge bg-orange-500/90 text-white border-orange-500/20 backdrop-blur-sm">
<i class="fas fa-bolt mr-1"></i>
High Thrill
</span>
{% elif ride.thrill_level == 'MODERATE' %}
<span class="badge bg-yellow-500/90 text-white border-yellow-500/20 backdrop-blur-sm">
<i class="fas fa-star mr-1"></i>
Moderate
</span>
{% elif ride.thrill_level == 'MILD' %}
<span class="badge bg-green-500/90 text-white border-green-500/20 backdrop-blur-sm">
<i class="fas fa-leaf mr-1"></i>
Family
</span>
{% endif %}
</div>
<!-- Status Badge -->
{% if ride.status != 'OPERATING' %}
<div class="absolute top-4 right-4">
{% if ride.status == 'CONSTRUCTION' %}
<span class="badge-construction backdrop-blur-sm">
<i class="fas fa-hard-hat mr-1"></i>
Coming Soon
</span>
{% elif ride.status == 'CLOSED_PERMANENTLY' %}
<span class="badge-closed backdrop-blur-sm">
<i class="fas fa-times-circle mr-1"></i>
Closed
</span>
{% elif ride.status == 'CLOSED_TEMPORARILY' %}
<span class="badge bg-yellow-500/90 text-white border-yellow-500/20 backdrop-blur-sm">
<i class="fas fa-pause-circle mr-1"></i>
Maintenance
</span>
{% endif %}
</div>
{% endif %}
<!-- Favorite Button -->
<div class="absolute {% if ride.status != 'OPERATING' %}top-16{% else %}top-4{% endif %} right-4">
<button class="w-10 h-10 bg-white/90 dark:bg-neutral-800/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white dark:hover:bg-neutral-800 transition-all duration-200 hover:scale-110"
hx-post="{% url 'rides:toggle_favorite' ride.slug %}"
hx-swap="outerHTML"
onclick="event.stopPropagation()">
{% if ride.is_favorited %}
<i class="fas fa-heart text-red-500"></i>
{% else %}
<i class="far fa-heart text-neutral-600 dark:text-neutral-400"></i>
{% endif %}
</button>
</div>
<!-- Gradient Overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Quick Stats Overlay -->
<div class="absolute bottom-4 left-4 right-4 transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<div class="grid grid-cols-2 gap-4 text-white text-sm">
{% if ride.height %}
<div class="flex items-center">
<i class="fas fa-arrows-alt-v mr-2 text-thrill-secondary"></i>
<div>
<div class="font-semibold">{{ ride.height }}ft</div>
<div class="text-xs opacity-75">Height</div>
</div>
</div>
{% endif %}
{% if ride.speed %}
<div class="flex items-center">
<i class="fas fa-tachometer-alt mr-2 text-thrill-secondary"></i>
<div>
<div class="font-semibold">{{ ride.speed }}mph</div>
<div class="text-xs opacity-75">Speed</div>
</div>
</div>
{% endif %}
{% if ride.duration %}
<div class="flex items-center">
<i class="fas fa-clock mr-2 text-thrill-secondary"></i>
<div>
<div class="font-semibold">{{ ride.duration }}s</div>
<div class="text-xs opacity-75">Duration</div>
</div>
</div>
{% endif %}
{% if ride.inversions %}
<div class="flex items-center">
<i class="fas fa-sync-alt mr-2 text-thrill-secondary"></i>
<div>
<div class="font-semibold">{{ ride.inversions }}</div>
<div class="text-xs opacity-75">Inversions</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Trending Badge -->
{% if ride.is_trending %}
<div class="absolute bottom-4 left-4 opacity-100 group-hover:opacity-0 transition-opacity duration-300">
<span class="badge bg-gradient-to-r from-pink-500 to-red-500 text-white border-0 pulse-glow">
<i class="fas fa-fire mr-1"></i>
Trending
</span>
</div>
{% endif %}
</div>
<!-- Ride Content -->
<div class="card-ride-content">
<!-- Ride Name and Park -->
<div class="space-y-2">
<h3 class="text-lg font-bold text-neutral-900 dark:text-neutral-100 group-hover:text-thrill-secondary transition-colors duration-200">
{{ ride.name }}
</h3>
{% if ride.park %}
<div class="flex items-center text-neutral-600 dark:text-neutral-400 text-sm">
<i class="fas fa-map-marked-alt mr-2 text-thrill-secondary"></i>
{{ ride.park.name }}
</div>
{% endif %}
</div>
<!-- Ride Type and Category -->
<div class="flex items-center space-x-3">
{% if ride.category %}
<span class="px-2 py-1 bg-thrill-secondary/10 text-thrill-secondary text-xs rounded-full font-medium">
{{ ride.get_category_display }}
</span>
{% endif %}
{% if ride.coaster_type %}
<span class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400 text-xs rounded-full">
{{ ride.get_coaster_type_display }}
</span>
{% endif %}
</div>
<!-- Ride Description -->
{% if ride.description %}
<p class="text-neutral-600 dark:text-neutral-400 text-sm line-clamp-2">
{{ ride.description|truncatewords:15 }}
</p>
{% endif %}
<!-- Ride Stats -->
<div class="flex items-center justify-between pt-3 border-t border-neutral-200/50 dark:border-neutral-700/50">
<div class="flex items-center space-x-4 text-sm text-neutral-600 dark:text-neutral-400">
{% if ride.opened_date %}
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
{{ ride.opened_date.year }}
</div>
{% endif %}
{% if ride.manufacturer %}
<div class="flex items-center">
<i class="fas fa-industry mr-1"></i>
{{ ride.manufacturer.name }}
</div>
{% endif %}
</div>
<!-- Rating -->
{% if ride.rating %}
<div class="flex items-center">
<div class="flex items-center mr-2">
{% for i in "12345" %}
{% if forloop.counter <= ride.rating %}
<i class="fas fa-star text-yellow-400 text-xs"></i>
{% else %}
<i class="far fa-star text-neutral-300 dark:text-neutral-600 text-xs"></i>
{% endif %}
{% endfor %}
</div>
<span class="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
{{ ride.rating|floatformat:1 }}
</span>
</div>
{% endif %}
</div>
<!-- Action Button -->
<div class="pt-3">
<button class="btn-secondary btn-sm w-full opacity-0 group-hover:opacity-100 transition-all duration-200 transform translate-y-2 group-hover:translate-y-0">
<i class="fas fa-info-circle mr-2"></i>
View Details
</button>
</div>
</div>
<!-- Loading State Overlay -->
<div class="absolute inset-0 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm rounded-2xl flex items-center justify-center opacity-0 htmx-request:opacity-100 transition-opacity duration-200 pointer-events-none">
<div class="loading-spinner opacity-100">
<i class="fas fa-spinner text-2xl text-thrill-secondary"></i>
</div>
</div>
</div>
<!-- Additional CSS for enhanced styling -->
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Enhanced pulse animation for trending badge */
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(236, 72, 153, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(236, 72, 153, 0);
}
}
.pulse-glow {
animation: pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Smooth hover transitions for stats */
.card-ride:hover .grid > div {
animation: slideInUp 0.3s ease-out forwards;
}
.card-ride:hover .grid > div:nth-child(2) {
animation-delay: 0.1s;
}
.card-ride:hover .grid > div:nth-child(3) {
animation-delay: 0.2s;
}
.card-ride:hover .grid > div:nth-child(4) {
animation-delay: 0.3s;
}
</style>