mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 03: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.contrib.gis.geos import Point # Disabled temporarily for setup
|
||||
import pghistory
|
||||
from apps.core.history import TrackedModel
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class ParkLocation(models.Model):
|
||||
class ParkLocation(TrackedModel):
|
||||
"""
|
||||
Represents the geographic location and address of a park, with PostGIS support.
|
||||
"""
|
||||
@@ -53,15 +54,17 @@ class ParkLocation(models.Model):
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude from point field."""
|
||||
if self.point:
|
||||
return self.point.y
|
||||
if self.point and ',' in self.point:
|
||||
# Temporary string format: "longitude,latitude"
|
||||
return float(self.point.split(',')[1])
|
||||
return None
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude from point field."""
|
||||
if self.point:
|
||||
return self.point.x
|
||||
if self.point and ',' in self.point:
|
||||
# Temporary string format: "longitude,latitude"
|
||||
return float(self.point.split(',')[0])
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -97,7 +100,9 @@ class ParkLocation(models.Model):
|
||||
if not -180 <= longitude <= 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):
|
||||
"""
|
||||
@@ -106,9 +111,26 @@ class ParkLocation(models.Model):
|
||||
"""
|
||||
if not self.point or not other_location.point:
|
||||
return None
|
||||
# Use geodetic distance calculation which returns meters, convert to km
|
||||
distance_m = self.point.distance(other_location.point)
|
||||
return distance_m / 1000.0
|
||||
|
||||
# Temporary implementation using Haversine formula
|
||||
# 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):
|
||||
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 content %}
|
||||
<!-- Advanced Search Page -->
|
||||
<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()">
|
||||
<!-- 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="{
|
||||
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 -->
|
||||
<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 -->
|
||||
<div class="relative max-w-2xl mx-auto">
|
||||
<input type="text"
|
||||
id="quick-search"
|
||||
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"
|
||||
hx-get="/search/quick/"
|
||||
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">
|
||||
<i class="fas fa-search text-2xl text-thrill-primary"></i>
|
||||
</div>
|
||||
@@ -55,7 +69,7 @@
|
||||
Filters
|
||||
</h2>
|
||||
|
||||
<form id="advanced-search-form"
|
||||
<form id="filter-form"
|
||||
hx-get="/search/results/"
|
||||
hx-target="#search-results"
|
||||
hx-trigger="change, submit"
|
||||
@@ -66,18 +80,30 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search For</label>
|
||||
<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">
|
||||
<input type="radio" name="search_type" value="parks" checked class="sr-only">
|
||||
<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"
|
||||
: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-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>
|
||||
<i class="fas fa-map-marked-alt mr-2 text-thrill-primary"></i>
|
||||
Parks
|
||||
</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">
|
||||
<input type="radio" name="search_type" value="rides" class="sr-only">
|
||||
<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"
|
||||
: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-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>
|
||||
<i class="fas fa-rocket mr-2 text-thrill-secondary"></i>
|
||||
Rides
|
||||
@@ -109,7 +135,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<label class="form-label">Park Type</label>
|
||||
<select name="park_type" class="form-select">
|
||||
@@ -125,62 +151,56 @@
|
||||
<label class="form-label">Park Status</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="status" value="OPERATING" checked class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge-operating">Operating</span>
|
||||
<input type="checkbox" name="status" value="OPERATING" checked class="form-checkbox">
|
||||
<span class="badge-operating ml-2">Operating</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="status" value="CONSTRUCTION" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge-construction">Under Construction</span>
|
||||
<input type="checkbox" name="status" value="CONSTRUCTION" class="form-checkbox">
|
||||
<span class="badge-construction ml-2">Under Construction</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<label class="form-label">Thrill Level</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="MILD" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-green-500/10 text-green-600 border-green-500/20">
|
||||
<input type="checkbox" name="thrill_level" value="MILD" class="form-checkbox">
|
||||
<span class="badge bg-green-500/10 text-green-600 border-green-500/20 ml-2">
|
||||
<i class="fas fa-leaf mr-1"></i>
|
||||
Family Friendly
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="MODERATE" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
|
||||
<input type="checkbox" name="thrill_level" value="MODERATE" class="form-checkbox">
|
||||
<span class="badge bg-yellow-500/10 text-yellow-600 border-yellow-500/20 ml-2">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Moderate
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="HIGH" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-orange-500/10 text-orange-600 border-orange-500/20">
|
||||
<input type="checkbox" name="thrill_level" value="HIGH" class="form-checkbox">
|
||||
<span class="badge bg-orange-500/10 text-orange-600 border-orange-500/20 ml-2">
|
||||
<i class="fas fa-bolt mr-1"></i>
|
||||
High Thrill
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="EXTREME" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-red-500/10 text-red-600 border-red-500/20">
|
||||
<input type="checkbox" name="thrill_level" value="EXTREME" class="form-checkbox">
|
||||
<span class="badge bg-red-500/10 text-red-600 border-red-500/20 ml-2">
|
||||
<i class="fas fa-fire mr-1"></i>
|
||||
Extreme
|
||||
</span>
|
||||
@@ -202,20 +222,20 @@
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,10 +256,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters -->
|
||||
<button type="button"
|
||||
id="clear-filters"
|
||||
<button type="reset"
|
||||
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>
|
||||
Clear All Filters
|
||||
</button>
|
||||
@@ -253,24 +274,30 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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">
|
||||
@@ -284,7 +311,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
hx-get="/search/results/"
|
||||
hx-target="#search-results"
|
||||
@@ -299,179 +326,8 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- AlpineJS Advanced Search Component (HTMX + AlpineJS Only) -->
|
||||
<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 -->
|
||||
<!-- Custom CSS for enhanced styling -->
|
||||
<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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user