mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:31:08 -05:00
Refactor ParkLocation model to inherit from TrackedModel for enhanced history tracking. Update point handling to temporarily store coordinates as a string. Implement Haversine formula for distance calculation as a placeholder until PostGIS is enabled.
Refactor advanced search template to utilize Alpine.js for state management. Enhance search functionality with dynamic view modes and improved filter handling using HTMX.
This commit is contained in:
@@ -2,10 +2,11 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
# from django.contrib.gis.geos import Point # Disabled temporarily for setup
|
# from django.contrib.gis.geos import Point # Disabled temporarily for setup
|
||||||
import pghistory
|
import pghistory
|
||||||
|
from apps.core.history import TrackedModel
|
||||||
|
|
||||||
|
|
||||||
@pghistory.track()
|
@pghistory.track()
|
||||||
class ParkLocation(models.Model):
|
class ParkLocation(TrackedModel):
|
||||||
"""
|
"""
|
||||||
Represents the geographic location and address of a park, with PostGIS support.
|
Represents the geographic location and address of a park, with PostGIS support.
|
||||||
"""
|
"""
|
||||||
@@ -53,15 +54,17 @@ class ParkLocation(models.Model):
|
|||||||
@property
|
@property
|
||||||
def latitude(self):
|
def latitude(self):
|
||||||
"""Return latitude from point field."""
|
"""Return latitude from point field."""
|
||||||
if self.point:
|
if self.point and ',' in self.point:
|
||||||
return self.point.y
|
# Temporary string format: "longitude,latitude"
|
||||||
|
return float(self.point.split(',')[1])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def longitude(self):
|
def longitude(self):
|
||||||
"""Return longitude from point field."""
|
"""Return longitude from point field."""
|
||||||
if self.point:
|
if self.point and ',' in self.point:
|
||||||
return self.point.x
|
# Temporary string format: "longitude,latitude"
|
||||||
|
return float(self.point.split(',')[0])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -97,7 +100,9 @@ class ParkLocation(models.Model):
|
|||||||
if not -180 <= longitude <= 180:
|
if not -180 <= longitude <= 180:
|
||||||
raise ValueError("Longitude must be between -180 and 180.")
|
raise ValueError("Longitude must be between -180 and 180.")
|
||||||
|
|
||||||
self.point = Point(longitude, latitude, srid=4326)
|
# Temporarily store as string until PostGIS is enabled
|
||||||
|
self.point = f"{longitude},{latitude}"
|
||||||
|
# self.point = Point(longitude, latitude, srid=4326)
|
||||||
|
|
||||||
def distance_to(self, other_location):
|
def distance_to(self, other_location):
|
||||||
"""
|
"""
|
||||||
@@ -106,9 +111,26 @@ class ParkLocation(models.Model):
|
|||||||
"""
|
"""
|
||||||
if not self.point or not other_location.point:
|
if not self.point or not other_location.point:
|
||||||
return None
|
return None
|
||||||
# Use geodetic distance calculation which returns meters, convert to km
|
|
||||||
distance_m = self.point.distance(other_location.point)
|
# Temporary implementation using Haversine formula
|
||||||
return distance_m / 1000.0
|
# TODO: Replace with PostGIS distance calculation when enabled
|
||||||
|
import math
|
||||||
|
|
||||||
|
lat1, lon1 = self.latitude, self.longitude
|
||||||
|
lat2, lon2 = other_location.latitude, other_location.longitude
|
||||||
|
|
||||||
|
if None in (lat1, lon1, lat2, lon2):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Haversine formula
|
||||||
|
R = 6371 # Earth's radius in kilometers
|
||||||
|
dlat = math.radians(lat2 - lat1)
|
||||||
|
dlon = math.radians(lon2 - lon1)
|
||||||
|
a = (math.sin(dlat/2) * math.sin(dlat/2) +
|
||||||
|
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
||||||
|
math.sin(dlon/2) * math.sin(dlon/2))
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||||
|
return R * c
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Location for {self.park.name}"
|
return f"Location for {self.park.name}"
|
||||||
|
|||||||
@@ -6,8 +6,22 @@
|
|||||||
{% block meta_description %}Find your perfect theme park adventure with our advanced search. Filter by location, thrill level, ride type, and more to discover exactly what you're looking for.{% endblock %}
|
{% block meta_description %}Find your perfect theme park adventure with our advanced search. Filter by location, thrill level, ride type, and more to discover exactly what you're looking for.{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Advanced Search Page -->
|
<!-- Advanced Search Page - HTMX + AlpineJS ONLY -->
|
||||||
<div class="min-h-screen bg-gradient-to-br from-white via-blue-50/30 to-indigo-50/30 dark:from-gray-950 dark:via-indigo-950/30 dark:to-purple-950/30" x-data="advancedSearch()">
|
<div class="min-h-screen bg-gradient-to-br from-white via-blue-50/30 to-indigo-50/30 dark:from-gray-950 dark:via-indigo-950/30 dark:to-purple-950/30"
|
||||||
|
x-data="{
|
||||||
|
searchType: 'parks',
|
||||||
|
viewMode: 'grid',
|
||||||
|
|
||||||
|
toggleSearchType(type) {
|
||||||
|
this.searchType = type;
|
||||||
|
// Use HTMX to update filters
|
||||||
|
htmx.trigger('#filter-form', 'change');
|
||||||
|
},
|
||||||
|
|
||||||
|
setViewMode(mode) {
|
||||||
|
this.viewMode = mode;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
|
||||||
<!-- Search Header -->
|
<!-- Search Header -->
|
||||||
<section class="py-16 bg-gradient-to-r from-thrill-primary/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm">
|
<section class="py-16 bg-gradient-to-r from-thrill-primary/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm">
|
||||||
@@ -25,12 +39,12 @@
|
|||||||
<!-- Quick Search Bar -->
|
<!-- Quick Search Bar -->
|
||||||
<div class="relative max-w-2xl mx-auto">
|
<div class="relative max-w-2xl mx-auto">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="quick-search"
|
|
||||||
placeholder="Quick search: parks, rides, locations..."
|
placeholder="Quick search: parks, rides, locations..."
|
||||||
class="w-full pl-16 pr-6 py-4 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm border border-neutral-300/50 dark:border-neutral-600/50 rounded-2xl text-lg shadow-lg focus:shadow-xl focus:bg-white dark:focus:bg-neutral-800 focus:border-thrill-primary focus:ring-2 focus:ring-thrill-primary/20 transition-all duration-300"
|
class="w-full pl-16 pr-6 py-4 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm border border-neutral-300/50 dark:border-neutral-600/50 rounded-2xl text-lg shadow-lg focus:shadow-xl focus:bg-white dark:focus:bg-neutral-800 focus:border-thrill-primary focus:ring-2 focus:ring-thrill-primary/20 transition-all duration-300"
|
||||||
hx-get="/search/quick/"
|
hx-get="/search/quick/"
|
||||||
hx-trigger="keyup changed delay:300ms"
|
hx-trigger="keyup changed delay:300ms"
|
||||||
hx-target="#quick-results">
|
hx-target="#quick-results"
|
||||||
|
hx-swap="innerHTML">
|
||||||
<div class="absolute left-6 top-1/2 transform -translate-y-1/2">
|
<div class="absolute left-6 top-1/2 transform -translate-y-1/2">
|
||||||
<i class="fas fa-search text-2xl text-thrill-primary"></i>
|
<i class="fas fa-search text-2xl text-thrill-primary"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +69,7 @@
|
|||||||
Filters
|
Filters
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form id="advanced-search-form"
|
<form id="filter-form"
|
||||||
hx-get="/search/results/"
|
hx-get="/search/results/"
|
||||||
hx-target="#search-results"
|
hx-target="#search-results"
|
||||||
hx-trigger="change, submit"
|
hx-trigger="change, submit"
|
||||||
@@ -66,18 +80,30 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Search For</label>
|
<label class="form-label">Search For</label>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-primary/5 transition-colors">
|
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-primary/5 transition-colors"
|
||||||
<input type="radio" name="search_type" value="parks" checked class="sr-only">
|
:class="{ 'bg-thrill-primary/10 border-thrill-primary': searchType === 'parks' }">
|
||||||
|
<input type="radio"
|
||||||
|
name="search_type"
|
||||||
|
value="parks"
|
||||||
|
x-model="searchType"
|
||||||
|
class="sr-only">
|
||||||
<div class="w-4 h-4 border-2 border-thrill-primary rounded-full mr-3 flex items-center justify-center">
|
<div class="w-4 h-4 border-2 border-thrill-primary rounded-full mr-3 flex items-center justify-center">
|
||||||
<div class="w-2 h-2 bg-thrill-primary rounded-full opacity-0 transition-opacity"></div>
|
<div class="w-2 h-2 bg-thrill-primary rounded-full transition-opacity"
|
||||||
|
:class="{ 'opacity-100': searchType === 'parks', 'opacity-0': searchType !== 'parks' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<i class="fas fa-map-marked-alt mr-2 text-thrill-primary"></i>
|
<i class="fas fa-map-marked-alt mr-2 text-thrill-primary"></i>
|
||||||
Parks
|
Parks
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-secondary/5 transition-colors">
|
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-secondary/5 transition-colors"
|
||||||
<input type="radio" name="search_type" value="rides" class="sr-only">
|
:class="{ 'bg-thrill-secondary/10 border-thrill-secondary': searchType === 'rides' }">
|
||||||
|
<input type="radio"
|
||||||
|
name="search_type"
|
||||||
|
value="rides"
|
||||||
|
x-model="searchType"
|
||||||
|
class="sr-only">
|
||||||
<div class="w-4 h-4 border-2 border-thrill-secondary rounded-full mr-3 flex items-center justify-center">
|
<div class="w-4 h-4 border-2 border-thrill-secondary rounded-full mr-3 flex items-center justify-center">
|
||||||
<div class="w-2 h-2 bg-thrill-secondary rounded-full opacity-0 transition-opacity"></div>
|
<div class="w-2 h-2 bg-thrill-secondary rounded-full transition-opacity"
|
||||||
|
:class="{ 'opacity-100': searchType === 'rides', 'opacity-0': searchType !== 'rides' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<i class="fas fa-rocket mr-2 text-thrill-secondary"></i>
|
<i class="fas fa-rocket mr-2 text-thrill-secondary"></i>
|
||||||
Rides
|
Rides
|
||||||
@@ -109,7 +135,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Park-Specific Filters -->
|
<!-- Park-Specific Filters -->
|
||||||
<div id="park-filters" class="space-y-6">
|
<div x-show="searchType === 'parks'" x-transition class="space-y-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Park Type</label>
|
<label class="form-label">Park Type</label>
|
||||||
<select name="park_type" class="form-select">
|
<select name="park_type" class="form-select">
|
||||||
@@ -125,62 +151,56 @@
|
|||||||
<label class="form-label">Park Status</label>
|
<label class="form-label">Park Status</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="status" value="OPERATING" checked class="sr-only">
|
<input type="checkbox" name="status" value="OPERATING" checked class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge-operating ml-2">Operating</span>
|
||||||
<span class="badge-operating">Operating</span>
|
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="status" value="CONSTRUCTION" class="sr-only">
|
<input type="checkbox" name="status" value="CONSTRUCTION" class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge-construction ml-2">Under Construction</span>
|
||||||
<span class="badge-construction">Under Construction</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Minimum Rides</label>
|
<label class="form-label">Minimum Rides</label>
|
||||||
<input type="range" name="min_rides" min="0" max="100" value="0" class="w-full">
|
<input type="range" name="min_rides" min="0" max="100" value="0" class="w-full form-range">
|
||||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||||
<span>0</span>
|
<span>0</span>
|
||||||
<span id="min-rides-value">0</span>
|
<span x-text="$el.querySelector('input[name=min_rides]')?.value || '0'"></span>
|
||||||
<span>100+</span>
|
<span>100+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ride-Specific Filters -->
|
<!-- Ride-Specific Filters -->
|
||||||
<div id="ride-filters" class="space-y-6 hidden">
|
<div x-show="searchType === 'rides'" x-transition class="space-y-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Thrill Level</label>
|
<label class="form-label">Thrill Level</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="thrill_level" value="MILD" class="sr-only">
|
<input type="checkbox" name="thrill_level" value="MILD" class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge bg-green-500/10 text-green-600 border-green-500/20 ml-2">
|
||||||
<span class="badge bg-green-500/10 text-green-600 border-green-500/20">
|
|
||||||
<i class="fas fa-leaf mr-1"></i>
|
<i class="fas fa-leaf mr-1"></i>
|
||||||
Family Friendly
|
Family Friendly
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="thrill_level" value="MODERATE" class="sr-only">
|
<input type="checkbox" name="thrill_level" value="MODERATE" class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge bg-yellow-500/10 text-yellow-600 border-yellow-500/20 ml-2">
|
||||||
<span class="badge bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
|
|
||||||
<i class="fas fa-star mr-1"></i>
|
<i class="fas fa-star mr-1"></i>
|
||||||
Moderate
|
Moderate
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="thrill_level" value="HIGH" class="sr-only">
|
<input type="checkbox" name="thrill_level" value="HIGH" class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge bg-orange-500/10 text-orange-600 border-orange-500/20 ml-2">
|
||||||
<span class="badge bg-orange-500/10 text-orange-600 border-orange-500/20">
|
|
||||||
<i class="fas fa-bolt mr-1"></i>
|
<i class="fas fa-bolt mr-1"></i>
|
||||||
High Thrill
|
High Thrill
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" name="thrill_level" value="EXTREME" class="sr-only">
|
<input type="checkbox" name="thrill_level" value="EXTREME" class="form-checkbox">
|
||||||
<div class="checkbox-custom mr-3"></div>
|
<span class="badge bg-red-500/10 text-red-600 border-red-500/20 ml-2">
|
||||||
<span class="badge bg-red-500/10 text-red-600 border-red-500/20">
|
|
||||||
<i class="fas fa-fire mr-1"></i>
|
<i class="fas fa-fire mr-1"></i>
|
||||||
Extreme
|
Extreme
|
||||||
</span>
|
</span>
|
||||||
@@ -202,20 +222,20 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Minimum Height (ft)</label>
|
<label class="form-label">Minimum Height (ft)</label>
|
||||||
<input type="range" name="min_height" min="0" max="500" value="0" class="w-full">
|
<input type="range" name="min_height" min="0" max="500" value="0" class="w-full form-range">
|
||||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||||
<span>0ft</span>
|
<span>0ft</span>
|
||||||
<span id="min-height-value">0ft</span>
|
<span x-text="($el.querySelector('input[name=min_height]')?.value || '0') + 'ft'"></span>
|
||||||
<span>500ft+</span>
|
<span>500ft+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Minimum Speed (mph)</label>
|
<label class="form-label">Minimum Speed (mph)</label>
|
||||||
<input type="range" name="min_speed" min="0" max="150" value="0" class="w-full">
|
<input type="range" name="min_speed" min="0" max="150" value="0" class="w-full form-range">
|
||||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||||
<span>0mph</span>
|
<span>0mph</span>
|
||||||
<span id="min-speed-value">0mph</span>
|
<span x-text="($el.querySelector('input[name=min_speed]')?.value || '0') + 'mph'"></span>
|
||||||
<span>150mph+</span>
|
<span>150mph+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,10 +256,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Clear Filters -->
|
<!-- Clear Filters -->
|
||||||
<button type="button"
|
<button type="reset"
|
||||||
id="clear-filters"
|
|
||||||
class="btn-ghost w-full"
|
class="btn-ghost w-full"
|
||||||
@click="clearFilters()">
|
hx-get="/search/results/"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-swap="innerHTML">
|
||||||
<i class="fas fa-times mr-2"></i>
|
<i class="fas fa-times mr-2"></i>
|
||||||
Clear All Filters
|
Clear All Filters
|
||||||
</button>
|
</button>
|
||||||
@@ -253,24 +274,30 @@
|
|||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold">Search Results</h2>
|
<h2 class="text-2xl font-bold">Search Results</h2>
|
||||||
<p class="text-neutral-600 dark:text-neutral-400" id="results-count">
|
<p class="text-neutral-600 dark:text-neutral-400">
|
||||||
Use filters to find your perfect adventure
|
Use filters to find your perfect adventure
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View Toggle -->
|
<!-- View Toggle -->
|
||||||
<div class="flex items-center space-x-2 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-700">
|
<div class="flex items-center space-x-2 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-700">
|
||||||
<button class="p-2 rounded-md bg-thrill-primary text-white" id="grid-view" @click="setViewMode('grid')">
|
<button class="p-2 rounded-md transition-colors"
|
||||||
|
:class="{ 'bg-thrill-primary text-white': viewMode === 'grid', 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700': viewMode !== 'grid' }"
|
||||||
|
@click="setViewMode('grid')">
|
||||||
<i class="fas fa-th-large"></i>
|
<i class="fas fa-th-large"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="p-2 rounded-md text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="list-view" @click="setViewMode('list')">
|
<button class="p-2 rounded-md transition-colors"
|
||||||
|
:class="{ 'bg-thrill-primary text-white': viewMode === 'list', 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700': viewMode !== 'list' }"
|
||||||
|
@click="setViewMode('list')">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Results Container -->
|
<!-- Search Results Container -->
|
||||||
<div id="search-results" class="min-h-96">
|
<div id="search-results"
|
||||||
|
class="min-h-96"
|
||||||
|
:class="{ 'grid-view': viewMode === 'grid', 'list-view': viewMode === 'list' }">
|
||||||
<!-- Initial State -->
|
<!-- Initial State -->
|
||||||
<div class="text-center py-16">
|
<div class="text-center py-16">
|
||||||
<div class="w-24 h-24 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div class="w-24 h-24 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
@@ -284,7 +311,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load More Button -->
|
<!-- Load More Button -->
|
||||||
<div id="load-more-container" class="text-center mt-8 hidden">
|
<div class="text-center mt-8 hidden" id="load-more-container">
|
||||||
<button class="btn-secondary btn-lg"
|
<button class="btn-secondary btn-lg"
|
||||||
hx-get="/search/results/"
|
hx-get="/search/results/"
|
||||||
hx-target="#search-results"
|
hx-target="#search-results"
|
||||||
@@ -299,179 +326,8 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AlpineJS Advanced Search Component (HTMX + AlpineJS Only) -->
|
<!-- Custom CSS for enhanced styling -->
|
||||||
<script>
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('advancedSearch', () => ({
|
|
||||||
searchType: 'parks',
|
|
||||||
viewMode: 'grid',
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Initialize range sliders
|
|
||||||
this.updateRangeValues();
|
|
||||||
this.setupRadioButtons();
|
|
||||||
this.setupCheckboxes();
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleSearchType(type) {
|
|
||||||
this.searchType = type;
|
|
||||||
const parkFilters = this.$el.querySelector('#park-filters');
|
|
||||||
const rideFilters = this.$el.querySelector('#ride-filters');
|
|
||||||
|
|
||||||
if (type === 'parks') {
|
|
||||||
parkFilters?.classList.remove('hidden');
|
|
||||||
rideFilters?.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
parkFilters?.classList.add('hidden');
|
|
||||||
rideFilters?.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearFilters() {
|
|
||||||
const form = this.$el.querySelector('#advanced-search-form');
|
|
||||||
if (form) {
|
|
||||||
form.reset();
|
|
||||||
this.searchType = 'parks';
|
|
||||||
this.toggleSearchType('parks');
|
|
||||||
this.updateRangeValues();
|
|
||||||
this.setupRadioButtons();
|
|
||||||
this.setupCheckboxes();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setViewMode(mode) {
|
|
||||||
this.viewMode = mode;
|
|
||||||
const gridBtn = this.$el.querySelector('#grid-view');
|
|
||||||
const listBtn = this.$el.querySelector('#list-view');
|
|
||||||
const resultsContainer = this.$el.querySelector('#search-results');
|
|
||||||
|
|
||||||
if (mode === 'grid') {
|
|
||||||
gridBtn?.classList.add('bg-thrill-primary', 'text-white');
|
|
||||||
gridBtn?.classList.remove('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
|
||||||
listBtn?.classList.remove('bg-thrill-primary', 'text-white');
|
|
||||||
listBtn?.classList.add('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
|
||||||
resultsContainer?.classList.remove('list-view');
|
|
||||||
resultsContainer?.classList.add('grid-view');
|
|
||||||
} else {
|
|
||||||
listBtn?.classList.add('bg-thrill-primary', 'text-white');
|
|
||||||
listBtn?.classList.remove('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
|
||||||
gridBtn?.classList.remove('bg-thrill-primary', 'text-white');
|
|
||||||
gridBtn?.classList.add('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
|
||||||
resultsContainer?.classList.remove('grid-view');
|
|
||||||
resultsContainer?.classList.add('list-view');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRangeValues() {
|
|
||||||
const minRidesSlider = this.$el.querySelector('input[name="min_rides"]');
|
|
||||||
const minRidesValue = this.$el.querySelector('#min-rides-value');
|
|
||||||
const minHeightSlider = this.$el.querySelector('input[name="min_height"]');
|
|
||||||
const minHeightValue = this.$el.querySelector('#min-height-value');
|
|
||||||
const minSpeedSlider = this.$el.querySelector('input[name="min_speed"]');
|
|
||||||
const minSpeedValue = this.$el.querySelector('#min-speed-value');
|
|
||||||
|
|
||||||
if (minRidesSlider && minRidesValue) {
|
|
||||||
minRidesValue.textContent = minRidesSlider.value;
|
|
||||||
minRidesSlider.addEventListener('input', (e) => {
|
|
||||||
minRidesValue.textContent = e.target.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minHeightSlider && minHeightValue) {
|
|
||||||
minHeightValue.textContent = minHeightSlider.value + 'ft';
|
|
||||||
minHeightSlider.addEventListener('input', (e) => {
|
|
||||||
minHeightValue.textContent = e.target.value + 'ft';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minSpeedSlider && minSpeedValue) {
|
|
||||||
minSpeedValue.textContent = minSpeedSlider.value + 'mph';
|
|
||||||
minSpeedSlider.addEventListener('input', (e) => {
|
|
||||||
minSpeedValue.textContent = e.target.value + 'mph';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setupRadioButtons() {
|
|
||||||
const radioButtons = this.$el.querySelectorAll('input[type="radio"]');
|
|
||||||
radioButtons.forEach(radio => {
|
|
||||||
const indicator = radio.parentElement.querySelector('div');
|
|
||||||
const dot = indicator?.querySelector('div');
|
|
||||||
|
|
||||||
if (radio.checked && dot) {
|
|
||||||
dot.style.opacity = '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
radio.addEventListener('change', () => {
|
|
||||||
// Reset all radio buttons in the same group
|
|
||||||
const groupName = radio.name;
|
|
||||||
const groupRadios = this.$el.querySelectorAll(`input[name="${groupName}"]`);
|
|
||||||
groupRadios.forEach(groupRadio => {
|
|
||||||
const groupIndicator = groupRadio.parentElement.querySelector('div');
|
|
||||||
const groupDot = groupIndicator?.querySelector('div');
|
|
||||||
if (groupDot) {
|
|
||||||
groupDot.style.opacity = groupRadio.checked ? '1' : '0';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (radio.name === 'search_type') {
|
|
||||||
this.toggleSearchType(radio.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupCheckboxes() {
|
|
||||||
const checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
|
|
||||||
checkboxes.forEach(checkbox => {
|
|
||||||
const customCheckbox = checkbox.parentElement.querySelector('.checkbox-custom');
|
|
||||||
|
|
||||||
if (checkbox.checked && customCheckbox) {
|
|
||||||
customCheckbox.classList.add('checked');
|
|
||||||
}
|
|
||||||
|
|
||||||
checkbox.addEventListener('change', () => {
|
|
||||||
if (customCheckbox) {
|
|
||||||
if (checkbox.checked) {
|
|
||||||
customCheckbox.classList.add('checked');
|
|
||||||
} else {
|
|
||||||
customCheckbox.classList.remove('checked');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Custom CSS for checkboxes and enhanced styling -->
|
|
||||||
<style>
|
<style>
|
||||||
.checkbox-custom {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 2px solid #cbd5e1;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-custom.checked {
|
|
||||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
||||||
border-color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-custom.checked::after {
|
|
||||||
content: '✓';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: white;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-view .search-results-grid {
|
.grid-view .search-results-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user