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

@@ -176,19 +176,46 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// Handle map clicks
map.on('click', async function(e) {
// Handle map clicks - HTMX version
map.on('click', function(e) {
try {
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
if (!response.ok) {
throw new Error('Geocoding request failed');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateLocation(normalized.lat, normalized.lng, data);
// Create a temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.style.display = 'none';
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
tempForm.setAttribute('hx-vals', JSON.stringify({
lat: normalized.lat,
lon: normalized.lng
}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add event listener for HTMX response
tempForm.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.error) {
throw new Error(data.error);
}
updateLocation(normalized.lat, normalized.lng, data);
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
} else {
console.error('Geocoding request failed');
alert('Failed to update location. Please try again.');
}
// Clean up temporary form
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
@@ -199,7 +226,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize map
initMap();
// Handle location search
// Handle location search - HTMX version
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
@@ -209,54 +236,74 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Search request failed');
}
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${[
result.street,
result.city || (result.address && (result.address.city || result.address.town || result.address.village)),
result.state || (result.address && (result.address.state || result.address.region)),
result.country || (result.address && result.address.country),
result.postal_code || (result.address && result.address.postcode)
].filter(Boolean).join(', ')}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
searchTimeout = setTimeout(function() {
// Create a temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.style.display = 'none';
tempForm.setAttribute('hx-get', '/parks/search/location/');
tempForm.setAttribute('hx-vals', JSON.stringify({
q: query
}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
// Add event listener for HTMX response
tempForm.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${[
result.street,
result.city || (result.address && (result.address.city || result.address.town || result.address.village)),
result.state || (result.address && (result.address.state || result.address.region)),
result.country || (result.address && result.address.country),
result.postal_code || (result.address && result.address.postcode)
].filter(Boolean).join(', ')}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
console.error('Search request failed');
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
// Clean up temporary form
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}, 300);
});