Compare commits

...

7 Commits

Author SHA1 Message Date
pacnpal
5b7b203619 Refactor add ride modal to utilize Alpine.js for state management. Improved modal open/close functionality and enhanced event handling for better user experience. 2025-09-26 14:32:10 -04:00
pacnpal
47c435d2f5 Refactor ride model search results template to utilize Alpine.js for state management. Enhanced selection handling and improved event dispatching for better user experience. 2025-09-26 14:31:15 -04:00
pacnpal
ce382a4361 Refactor designer search results template to utilize Alpine.js for state management. Enhanced designer selection handling and improved event dispatching for better user experience. 2025-09-26 14:30:22 -04:00
pacnpal
07ab9f28f2 Refactor manufacturer search results template to utilize Alpine.js for state management. Enhanced manufacturer selection handling and improved event dispatching for better user experience. 2025-09-26 14:29:37 -04:00
pacnpal
40e5cf3162 Refactor ride form template to utilize Alpine.js for state management. Enhanced form submission handling and improved search result clearing functionality for better user experience. 2025-09-26 14:27:47 -04:00
pacnpal
b9377ead37 Refactor designer and ride model search results templates to utilize Alpine.js for state management. Enhanced selection functionality and improved event handling for better user experience. 2025-09-26 14:23:03 -04:00
pacnpal
851709058f Refactor location widget and park search results templates to utilize Alpine.js for state management. Enhanced search functionality, improved data binding, and streamlined event handling for better user experience. 2025-09-26 14:21:28 -04:00
11 changed files with 700 additions and 622 deletions

View File

@@ -199,24 +199,31 @@
</div> </div>
<script> <script>
document.body.addEventListener('htmx:afterRequest', function(evt) { document.addEventListener('alpine:init', () => {
Alpine.data('moderationDashboard', () => ({
init() {
// Handle HTMX events using AlpineJS approach
document.body.addEventListener('htmx:afterRequest', (evt) => {
if (evt.detail.successful) { if (evt.detail.successful) {
const path = evt.detail.requestConfig.path; const path = evt.detail.requestConfig.path;
let event; let eventName;
if (path.includes('approve')) { if (path.includes('approve')) {
event = new CustomEvent('submission-approved'); eventName = 'submission-approved';
} else if (path.includes('reject')) { } else if (path.includes('reject')) {
event = new CustomEvent('submission-rejected'); eventName = 'submission-rejected';
} else if (path.includes('escalate')) { } else if (path.includes('escalate')) {
event = new CustomEvent('submission-escalated'); eventName = 'submission-escalated';
} else if (path.includes('edit')) { } else if (path.includes('edit')) {
event = new CustomEvent('submission-updated'); eventName = 'submission-updated';
} }
if (event) { if (eventName) {
window.dispatchEvent(event); this.$dispatch(eventName);
} }
} }
}); });
}
}));
});
</script> </script>

View File

@@ -1,9 +1,12 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;"
x-data="designerSearchResults('{{ submission_id }}')"
@click.outside="clearResults()">
{% if designers %} {% if designers %}
{% for designer in designers %} {% for designer in designers %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectDesignerForSubmission('{{ designer.id }}', '{{ designer.name|escapejs }}', '{{ submission_id }}')"> @click="selectDesigner('{{ designer.id }}', '{{ designer.name|escapejs }}')">
{{ designer.name }} {{ designer.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -19,14 +22,18 @@
</div> </div>
<script> <script>
function selectDesignerForSubmission(id, name, submissionId) { document.addEventListener('alpine:init', () => {
// Debug logging Alpine.data('designerSearchResults', (submissionId) => ({
console.log('Selecting designer:', {id, name, submissionId}); submissionId: submissionId,
// Find elements selectDesigner(id, name) {
const designerInput = document.querySelector(`#designer-input-${submissionId}`); // Debug logging
const searchInput = document.querySelector(`#designer-search-${submissionId}`); console.log('Selecting designer:', {id, name, submissionId: this.submissionId});
const resultsDiv = document.querySelector(`#designer-search-results-${submissionId}`);
// Find elements using AlpineJS approach
const designerInput = document.querySelector(`#designer-input-${this.submissionId}`);
const searchInput = document.querySelector(`#designer-search-${this.submissionId}`);
const resultsDiv = document.querySelector(`#designer-search-results-${this.submissionId}`);
// Debug logging // Debug logging
console.log('Found elements:', { console.log('Found elements:', {
@@ -48,20 +55,16 @@ function selectDesignerForSubmission(id, name, submissionId) {
} }
// Clear results // Clear results
this.clearResults();
},
clearResults() {
const resultsDiv = document.querySelector(`#designer-search-results-${this.submissionId}`);
if (resultsDiv) { if (resultsDiv) {
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = '';
console.log('Cleared results div'); console.log('Cleared results div');
} }
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="designer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#designer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
} }
}); }));
}); });
</script> </script>

View File

@@ -19,30 +19,60 @@
} }
</style> </style>
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"> <div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
x-data="locationWidget({
submissionId: '{{ submission.id }}',
initialData: {
city: '{{ submission.changes.city|default:"" }}',
state: '{{ submission.changes.state|default:"" }}',
country: '{{ submission.changes.country|default:"" }}',
postal_code: '{{ submission.changes.postal_code|default:"" }}',
street_address: '{{ submission.changes.street_address|default:"" }}',
latitude: '{{ submission.changes.latitude|default:"" }}',
longitude: '{{ submission.changes.longitude|default:"" }}'
}
})"
x-init="init()">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3> <h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<div class="location-widget" id="locationWidget-{{ submission.id }}"> <div class="location-widget">
{# Search Form #} {# Search Form #}
<div class="relative mb-4"> <div class="relative mb-4">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location Search Location
</label> </label>
<input type="text" <input type="text"
id="locationSearch-{{ submission.id }}" x-model="searchQuery"
@input.debounce.300ms="handleSearch()"
@click.outside="showSearchResults = false"
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
placeholder="Search for a location..." placeholder="Search for a location..."
autocomplete="off" autocomplete="off"
style="z-index: 10;"> style="z-index: 10;">
<div id="searchResults-{{ submission.id }}" <div x-show="showSearchResults"
x-transition
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;" style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600"> class="w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
<template x-for="(result, index) in searchResults" :key="index">
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
@click="selectLocation(result)">
<div class="font-medium text-gray-900 dark:text-white" x-text="result.display_name || result.name || ''"></div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<span x-text="result.address?.city ? result.address.city + ', ' : ''"></span>
<span x-text="result.address?.country || ''"></span>
</div>
</div>
</template>
<div x-show="searchResults.length === 0 && searchQuery.length > 0"
class="p-2 text-gray-500 dark:text-gray-400">
No results found
</div>
</div> </div>
</div> </div>
{# Map Container #} {# Map Container #}
<div class="relative mb-4" style="z-index: 1;"> <div class="relative mb-4" style="z-index: 1;">
<div id="locationMap-{{ submission.id }}" <div x-ref="mapContainer"
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div> class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div> </div>
@@ -54,9 +84,8 @@
</label> </label>
<input type="text" <input type="text"
name="street_address" name="street_address"
id="streetAddress-{{ submission.id }}" x-model="formData.street_address"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
value="{{ submission.changes.street_address }}">
</div> </div>
<div> <div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -64,9 +93,8 @@
</label> </label>
<input type="text" <input type="text"
name="city" name="city"
id="city-{{ submission.id }}" x-model="formData.city"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
value="{{ submission.changes.city }}">
</div> </div>
<div> <div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -74,9 +102,8 @@
</label> </label>
<input type="text" <input type="text"
name="state" name="state"
id="state-{{ submission.id }}" x-model="formData.state"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
value="{{ submission.changes.state }}">
</div> </div>
<div> <div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -84,9 +111,8 @@
</label> </label>
<input type="text" <input type="text"
name="country" name="country"
id="country-{{ submission.id }}" x-model="formData.country"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
value="{{ submission.changes.country }}">
</div> </div>
<div> <div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -94,58 +120,49 @@
</label> </label>
<input type="text" <input type="text"
name="postal_code" name="postal_code"
id="postalCode-{{ submission.id }}" x-model="formData.postal_code"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input" class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input">
value="{{ submission.changes.postal_code }}">
</div> </div>
</div> </div>
{# Hidden Coordinate Fields #} {# Hidden Coordinate Fields #}
<div class="hidden"> <div class="hidden">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}"> <input type="hidden" name="latitude" x-model="formData.latitude">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}"> <input type="hidden" name="longitude" x-model="formData.longitude">
</div> </div>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('alpine:init', () => {
let maps = {}; Alpine.data('locationWidget', (config) => ({
let markers = {}; submissionId: config.submissionId,
const searchInput = document.getElementById('locationSearch-{{ submission.id }}'); formData: { ...config.initialData },
const searchResults = document.getElementById('searchResults-{{ submission.id }}'); searchQuery: '',
let searchTimeout; searchResults: [],
showSearchResults: false,
map: null,
marker: null,
// Initialize form fields with existing values init() {
const fields = { // Set initial search query if location exists
city: '{{ submission.changes.city|default:"" }}', if (this.formData.street_address || this.formData.city) {
state: '{{ submission.changes.state|default:"" }}',
country: '{{ submission.changes.country|default:"" }}',
postal_code: '{{ submission.changes.postal_code|default:"" }}',
street_address: '{{ submission.changes.street_address|default:"" }}',
latitude: '{{ submission.changes.latitude|default:"" }}',
longitude: '{{ submission.changes.longitude|default:"" }}'
};
Object.entries(fields).forEach(([field, value]) => {
const element = document.getElementById(`${field}-{{ submission.id }}`);
if (element) {
element.value = value;
}
});
// Set initial search input value if location exists
if (fields.street_address || fields.city) {
const parts = [ const parts = [
fields.street_address, this.formData.street_address,
fields.city, this.formData.city,
fields.state, this.formData.state,
fields.country this.formData.country
].filter(Boolean); ].filter(Boolean);
searchInput.value = parts.join(', '); this.searchQuery = parts.join(', ');
} }
function normalizeCoordinate(value, maxDigits, decimalPlaces) { // Initialize map when component is ready
this.$nextTick(() => {
this.initMap();
});
},
normalizeCoordinate(value, maxDigits, decimalPlaces) {
if (!value) return null; if (!value) return null;
try { try {
const rounded = Number(value).toFixed(decimalPlaces); const rounded = Number(value).toFixed(decimalPlaces);
@@ -161,11 +178,11 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Coordinate normalization failed:', error); console.error('Coordinate normalization failed:', error);
return null; return null;
} }
} },
function validateCoordinates(lat, lng) { validateCoordinates(lat, lng) {
const normalizedLat = normalizeCoordinate(lat, 9, 6); const normalizedLat = this.normalizeCoordinate(lat, 9, 6);
const normalizedLng = normalizeCoordinate(lng, 10, 6); const normalizedLng = this.normalizeCoordinate(lng, 10, 6);
if (normalizedLat === null || normalizedLng === null) { if (normalizedLat === null || normalizedLng === null) {
throw new Error('Invalid coordinate format'); throw new Error('Invalid coordinate format');
@@ -182,55 +199,61 @@ document.addEventListener('DOMContentLoaded', function() {
} }
return { lat: normalizedLat, lng: normalizedLng }; return { lat: normalizedLat, lng: normalizedLng };
} },
function initMap() { initMap() {
const submissionId = '{{ submission.id }}'; if (!this.$refs.mapContainer) {
const mapId = `locationMap-${submissionId}`; console.error('Map container not found');
const mapContainer = document.getElementById(mapId);
if (!mapContainer) {
console.error(`Map container ${mapId} not found`);
return; return;
} }
// If map already exists, remove it // If map already exists, remove it
if (maps[submissionId]) { if (this.map) {
maps[submissionId].remove(); this.map.remove();
delete maps[submissionId]; this.map = null;
delete markers[submissionId]; this.marker = null;
} }
// Create new map // Create new map
maps[submissionId] = L.map(mapId); this.map = L.map(this.$refs.mapContainer);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors' attribution: '© OpenStreetMap contributors'
}).addTo(maps[submissionId]); }).addTo(this.map);
// Initialize with existing coordinates if available // Initialize with existing coordinates if available
const initialLat = fields.latitude; if (this.formData.latitude && this.formData.longitude) {
const initialLng = fields.longitude;
if (initialLat && initialLng) {
try { try {
const normalized = validateCoordinates(initialLat, initialLng); const normalized = this.validateCoordinates(this.formData.latitude, this.formData.longitude);
maps[submissionId].setView([normalized.lat, normalized.lng], 13); this.map.setView([normalized.lat, normalized.lng], 13);
addMarker(normalized.lat, normalized.lng); this.addMarker(normalized.lat, normalized.lng);
} catch (error) { } catch (error) {
console.error('Invalid initial coordinates:', error); console.error('Invalid initial coordinates:', error);
maps[submissionId].setView([0, 0], 2); this.map.setView([0, 0], 2);
} }
} else { } else {
maps[submissionId].setView([0, 0], 2); this.map.setView([0, 0], 2);
} }
// Handle map clicks - HTMX version // Handle map clicks
maps[submissionId].on('click', function(e) { this.map.on('click', (e) => {
try { this.handleMapClick(e.latlng.lat, e.latlng.lng);
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng); });
},
// Create a temporary form for HTMX request addMarker(lat, lng) {
if (this.marker) {
this.marker.remove();
}
this.marker = L.marker([lat, lng]).addTo(this.map);
this.map.setView([lat, lng], 13);
},
async handleMapClick(lat, lng) {
try {
const normalized = this.validateCoordinates(lat, lng);
// Use HTMX for reverse geocoding
const tempForm = document.createElement('form'); const tempForm = document.createElement('form');
tempForm.style.display = 'none'; tempForm.style.display = 'none';
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/'); tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
@@ -241,15 +264,14 @@ document.addEventListener('DOMContentLoaded', function() {
tempForm.setAttribute('hx-trigger', 'submit'); tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none'); tempForm.setAttribute('hx-swap', 'none');
// Add event listener for HTMX response tempForm.addEventListener('htmx:afterRequest', (event) => {
tempForm.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful) { if (event.detail.successful) {
try { try {
const data = JSON.parse(event.detail.xhr.responseText); const data = JSON.parse(event.detail.xhr.responseText);
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error);
} }
updateLocation(normalized.lat, normalized.lng, data); this.updateLocation(normalized.lat, normalized.lng, data);
} catch (error) { } catch (error) {
console.error('Location update failed:', error); console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.'); alert(error.message || 'Failed to update location. Please try again.');
@@ -258,7 +280,6 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Geocoding request failed'); console.error('Geocoding request failed');
alert('Failed to update location. Please try again.'); alert('Failed to update location. Please try again.');
} }
// Clean up temporary form
document.body.removeChild(tempForm); document.body.removeChild(tempForm);
}); });
@@ -269,56 +290,89 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Location update failed:', error); console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.'); alert(error.message || 'Failed to update location. Please try again.');
} }
}); },
}
function addMarker(lat, lng) { updateLocation(lat, lng, data) {
const submissionId = '{{ submission.id }}';
if (markers[submissionId]) {
markers[submissionId].remove();
}
markers[submissionId] = L.marker([lat, lng]).addTo(maps[submissionId]);
maps[submissionId].setView([lat, lng], 13);
}
function updateLocation(lat, lng, data) {
try { try {
const normalized = validateCoordinates(lat, lng); const normalized = this.validateCoordinates(lat, lng);
const submissionId = '{{ submission.id }}';
// Update coordinates // Update coordinates
document.getElementById(`latitude-${submissionId}`).value = normalized.lat; this.formData.latitude = normalized.lat;
document.getElementById(`longitude-${submissionId}`).value = normalized.lng; this.formData.longitude = normalized.lng;
// Update marker // Update marker
addMarker(normalized.lat, normalized.lng); this.addMarker(normalized.lat, normalized.lng);
// Update form fields with English names where available // Update form fields with English names where available
const address = data.address || {}; const address = data.address || {};
document.getElementById(`streetAddress-${submissionId}`).value = this.formData.street_address = `${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || ''; this.formData.city = address.city || address.town || address.village || '';
document.getElementById(`city-${submissionId}`).value = this.formData.state = address.state || address.region || '';
address.city || address.town || address.village || ''; this.formData.country = address.country || '';
document.getElementById(`state-${submissionId}`).value = this.formData.postal_code = address.postcode || '';
address.state || address.region || '';
document.getElementById(`country-${submissionId}`).value = address.country || '';
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || '';
// Update search input // Update search input
const locationString-3 = [ const locationParts = [
document.getElementById(`streetAddress-${submissionId}`).value, this.formData.street_address,
document.getElementById(`city-${submissionId}`).value, this.formData.city,
document.getElementById(`state-${submissionId}`).value, this.formData.state,
document.getElementById(`country-${submissionId}`).value this.formData.country
].filter(Boolean).join(', '); ].filter(Boolean);
searchInput.value = locationString; this.searchQuery = locationParts.join(', ');
} catch (error) { } catch (error) {
console.error('Location update failed:', error); console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.'); alert(error.message || 'Failed to update location. Please try again.');
} }
},
handleSearch() {
const query = this.searchQuery.trim();
if (!query) {
this.showSearchResults = false;
return;
} }
function selectLocation(result) { // Use HTMX for location search
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');
tempForm.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.successful) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
if (data.results && data.results.length > 0) {
this.searchResults = data.results;
this.showSearchResults = true;
} else {
this.searchResults = [];
this.showSearchResults = true;
}
} catch (error) {
console.error('Search failed:', error);
this.searchResults = [];
this.showSearchResults = false;
}
} else {
console.error('Search request failed');
this.searchResults = [];
this.showSearchResults = false;
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
},
selectLocation(result) {
if (!result) return; if (!result) return;
try { try {
@@ -329,7 +383,7 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error('Invalid coordinates in search result'); throw new Error('Invalid coordinates in search result');
} }
const normalized = validateCoordinates(lat, lon); const normalized = this.validateCoordinates(lat, lon);
// Create a normalized address object // Create a normalized address object
const address = { const address = {
@@ -344,118 +398,14 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}; };
updateLocation(normalized.lat, normalized.lng, address); this.updateLocation(normalized.lat, normalized.lng, address);
searchResults.classList.add('hidden'); this.showSearchResults = false;
searchInput.value = address.name; this.searchQuery = address.name;
} catch (error) { } catch (error) {
console.error('Location selection failed:', error); console.error('Location selection failed:', error);
alert(error.message || 'Failed to select location. Please try again.'); alert(error.message || 'Failed to select location. Please try again.');
} }
} }
// Handle location search - HTMX version
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (!query) {
searchResults.classList.add('hidden');
return;
}
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.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</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 {
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');
}
// Clean up temporary form
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
}, 300);
});
// Hide search results when clicking outside
document.addEventListener('click', function(e) {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
// Initialize map when the element becomes visible
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer && window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
observer.disconnect();
}
}
});
});
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer) {
observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
// Also initialize immediately if the container is already visible
if (window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
}
}
}); });
</script> </script>

View File

@@ -1,9 +1,12 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;"
x-data="manufacturerSearchResults('{{ submission_id }}')"
@click.outside="clearResults()">
{% if manufacturers %} {% if manufacturers %}
{% for manufacturer in manufacturers %} {% for manufacturer in manufacturers %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectManufacturerForSubmission('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}', '{{ submission_id }}')"> @click="selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}')">
{{ manufacturer.name }} {{ manufacturer.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -19,14 +22,18 @@
</div> </div>
<script> <script>
function selectManufacturerForSubmission(id, name, submissionId) { document.addEventListener('alpine:init', () => {
// Debug logging Alpine.data('manufacturerSearchResults', (submissionId) => ({
console.log('Selecting manufacturer:', {id, name, submissionId}); submissionId: submissionId,
// Find elements selectManufacturer(id, name) {
const manufacturerInput = document.querySelector(`#manufacturer-input-${submissionId}`); // Debug logging
const searchInput = document.querySelector(`#manufacturer-search-${submissionId}`); console.log('Selecting manufacturer:', {id, name, submissionId: this.submissionId});
const resultsDiv = document.querySelector(`#manufacturer-search-results-${submissionId}`);
// Find elements using AlpineJS approach
const manufacturerInput = document.querySelector(`#manufacturer-input-${this.submissionId}`);
const searchInput = document.querySelector(`#manufacturer-search-${this.submissionId}`);
const resultsDiv = document.querySelector(`#manufacturer-search-results-${this.submissionId}`);
// Debug logging // Debug logging
console.log('Found elements:', { console.log('Found elements:', {
@@ -48,20 +55,16 @@ function selectManufacturerForSubmission(id, name, submissionId) {
} }
// Clear results // Clear results
this.clearResults();
},
clearResults() {
const resultsDiv = document.querySelector(`#manufacturer-search-results-${this.submissionId}`);
if (resultsDiv) { if (resultsDiv) {
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = '';
console.log('Cleared results div'); console.log('Cleared results div');
} }
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="manufacturer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#manufacturer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
} }
}); }));
}); });
</script> </script>

View File

@@ -1,9 +1,12 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;"
x-data="parkSearchResults('{{ submission_id }}')"
@click.outside="clearResults()">
{% if parks %} {% if parks %}
{% for park in parks %} {% for park in parks %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectParkForSubmission('{{ park.id }}', '{{ park.name|escapejs }}', '{{ submission_id }}')"> @click="selectPark('{{ park.id }}', '{{ park.name|escapejs }}')">
{{ park.name }} {{ park.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -19,14 +22,18 @@
</div> </div>
<script> <script>
function selectParkForSubmission(id, name, submissionId) { document.addEventListener('alpine:init', () => {
// Debug logging Alpine.data('parkSearchResults', (submissionId) => ({
console.log('Selecting park:', {id, name, submissionId}); submissionId: submissionId,
// Find elements selectPark(id, name) {
const parkInput = document.querySelector(`#park-input-${submissionId}`); // Debug logging
const searchInput = document.querySelector(`#park-search-${submissionId}`); console.log('Selecting park:', {id, name, submissionId: this.submissionId});
const resultsDiv = document.querySelector(`#park-search-results-${submissionId}`);
// Find elements using AlpineJS approach
const parkInput = document.querySelector(`#park-input-${this.submissionId}`);
const searchInput = document.querySelector(`#park-search-${this.submissionId}`);
const resultsDiv = document.querySelector(`#park-search-results-${this.submissionId}`);
// Debug logging // Debug logging
console.log('Found elements:', { console.log('Found elements:', {
@@ -48,26 +55,22 @@ function selectParkForSubmission(id, name, submissionId) {
} }
// Clear results // Clear results
if (resultsDiv) { this.clearResults();
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
// Trigger park areas update // Trigger park areas update
if (parkInput) { if (parkInput) {
htmx.trigger(parkInput, 'change'); htmx.trigger(parkInput, 'change');
console.log('Triggered change event'); console.log('Triggered change event');
} }
} },
// Close search results when clicking outside clearResults() {
document.addEventListener('click', function(e) { const resultsDiv = document.querySelector(`#park-search-results-${this.submissionId}`);
const searchResults = document.querySelectorAll('[id^="park-search-results-"]'); if (resultsDiv) {
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#park-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = '';
console.log('Cleared results div');
} }
}); }
}));
}); });
</script> </script>

View File

@@ -1,9 +1,12 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;"
x-data="rideModelSearchResults('{{ submission_id }}')"
@click.outside="clearResults()">
{% if ride_models %} {% if ride_models %}
{% for model in ride_models %} {% for model in ride_models %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectRideModelForSubmission('{{ model.id }}', '{{ model.name|escapejs }}', '{{ submission_id }}')"> @click="selectRideModel('{{ model.id }}', '{{ model.name|escapejs }}')">
{{ model.name }} {{ model.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -19,14 +22,18 @@
</div> </div>
<script> <script>
function selectRideModelForSubmission(id, name, submissionId) { document.addEventListener('alpine:init', () => {
// Debug logging Alpine.data('rideModelSearchResults', (submissionId) => ({
console.log('Selecting ride model:', {id, name, submissionId}); submissionId: submissionId,
// Find elements selectRideModel(id, name) {
const modelInput = document.querySelector(`#ride-model-input-${submissionId}`); // Debug logging
const searchInput = document.querySelector(`#ride-model-search-${submissionId}`); console.log('Selecting ride model:', {id, name, submissionId: this.submissionId});
const resultsDiv = document.querySelector(`#ride-model-search-results-${submissionId}`);
// Find elements using AlpineJS approach
const modelInput = document.querySelector(`#ride-model-input-${this.submissionId}`);
const searchInput = document.querySelector(`#ride-model-search-${this.submissionId}`);
const resultsDiv = document.querySelector(`#ride-model-search-results-${this.submissionId}`);
// Debug logging // Debug logging
console.log('Found elements:', { console.log('Found elements:', {
@@ -48,20 +55,16 @@ function selectRideModelForSubmission(id, name, submissionId) {
} }
// Clear results // Clear results
this.clearResults();
},
clearResults() {
const resultsDiv = document.querySelector(`#ride-model-search-results-${this.submissionId}`);
if (resultsDiv) { if (resultsDiv) {
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = '';
console.log('Cleared results div'); console.log('Cleared results div');
} }
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="ride-model-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#ride-model-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
} }
}); }));
}); });
</script> </script>

View File

@@ -1,15 +1,63 @@
<!-- Add Ride Modal --> <script>
<div id="add-ride-modal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> document.addEventListener('alpine:init', () => {
Alpine.data('addRideModal', () => ({
isOpen: false,
openModal() {
this.isOpen = true;
},
closeModal() {
this.isOpen = false;
},
handleBackdropClick(event) {
if (event.target === event.currentTarget) {
this.closeModal();
}
}
}));
});
</script>
<div x-data="addRideModal()">
<!-- Add Ride Modal -->
<div x-show="isOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="handleBackdropClick($event)"
@keydown.escape.window="closeModal()"
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
style="display: none;">
<div class="flex items-center justify-center min-h-screen p-4"> <div class="flex items-center justify-center min-h-screen p-4">
<!-- Background overlay --> <!-- Background overlay -->
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" aria-hidden="true"></div> <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" aria-hidden="true"></div>
<!-- Modal panel --> <!-- Modal panel -->
<div class="relative w-full max-w-3xl p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800"> <div class="relative w-full max-w-3xl p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800"
x-transition:enter="transition ease-out duration-300 transform"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="transition ease-in duration-200 transform"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white"> <h2 class="text-2xl font-bold text-gray-900 dark:text-white">
Add Ride at {{ park.name }} Add Ride at {{ park.name }}
</h2> </h2>
<button @click="closeModal()"
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div> </div>
<div id="modal-content"> <div id="modal-content">
@@ -17,31 +65,15 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal Toggle Button --> <!-- Modal Toggle Button -->
<button type="button" <button type="button"
onclick="openModal('add-ride-modal')" @click="openModal()"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"> class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg> </svg>
Add Ride Add Ride
</button> </button>
</div>
<script>
function openModal(modalId) {
document.getElementById(modalId).classList.remove('hidden');
}
function closeModal() {
document.getElementById('add-ride-modal').classList.add('hidden');
}
// Close modal when clicking outside
document.getElementById('add-ride-modal').addEventListener('click', function(event) {
if (event.target === this) {
closeModal();
}
});
</script>

View File

@@ -1,9 +1,32 @@
<div class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <script>
document.addEventListener('alpine:init', () => {
Alpine.data('designerSearchResults', () => ({
selectDesigner(id, name) {
// Update designer fields using AlpineJS reactive approach
const designerInput = this.$el.closest('form').querySelector('#id_designer');
const searchInput = this.$el.closest('form').querySelector('#id_designer_search');
const resultsDiv = this.$el.closest('form').querySelector('#designer-search-results');
if (designerInput) designerInput.value = id;
if (searchInput) searchInput.value = name;
if (resultsDiv) resultsDiv.innerHTML = '';
// Dispatch custom event for parent component
this.$dispatch('designer-selected', { id, name });
}
}));
});
</script>
<div x-data="designerSearchResults()"
@click.outside="$el.innerHTML = ''"
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;">
{% if designers %} {% if designers %}
{% for designer in designers %} {% for designer in designers %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectDesigner('{{ designer.id }}', '{{ designer.name|escapejs }}')"> @click="selectDesigner('{{ designer.id }}', '{{ designer.name|escapejs }}')">
{{ designer.name }} {{ designer.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -17,11 +40,3 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<script>
function selectDesigner(id, name) {
document.getElementById('id_designer').value = id;
document.getElementById('id_designer_search').value = name;
document.getElementById('designer-search-results').innerHTML = '';
}
</script>

View File

@@ -1,9 +1,38 @@
<div class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <script>
document.addEventListener('alpine:init', () => {
Alpine.data('manufacturerSearchResults', () => ({
selectManufacturer(id, name) {
// Update manufacturer fields using AlpineJS reactive approach
const manufacturerInput = this.$el.closest('form').querySelector('#id_manufacturer');
const searchInput = this.$el.closest('form').querySelector('#id_manufacturer_search');
const resultsDiv = this.$el.closest('form').querySelector('#manufacturer-search-results');
if (manufacturerInput) manufacturerInput.value = id;
if (searchInput) searchInput.value = name;
if (resultsDiv) resultsDiv.innerHTML = '';
// Update ride model search to include manufacturer using HTMX
const rideModelSearch = this.$el.closest('form').querySelector('#id_ride_model_search');
if (rideModelSearch) {
rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]');
}
// Dispatch custom event for parent component
this.$dispatch('manufacturer-selected', { id, name });
}
}));
});
</script>
<div x-data="manufacturerSearchResults()"
@click.outside="$el.innerHTML = ''"
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;">
{% if manufacturers %} {% if manufacturers %}
{% for manufacturer in manufacturers %} {% for manufacturer in manufacturers %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}')"> @click="selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}')">
{{ manufacturer.name }} {{ manufacturer.name }}
</button> </button>
{% endfor %} {% endfor %}
@@ -17,17 +46,3 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<script>
function selectManufacturer(id, name) {
document.getElementById('id_manufacturer').value = id;
document.getElementById('id_manufacturer_search').value = name;
document.getElementById('manufacturer-search-results').innerHTML = '';
// Update ride model search to include manufacturer
const rideModelSearch = document.getElementById('id_ride_model_search');
if (rideModelSearch) {
rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]');
}
}
</script>

View File

@@ -1,59 +1,85 @@
{% load static %} {% load static %}
<script> <script>
function selectManufacturer(id, name) { document.addEventListener('alpine:init', () => {
document.getElementById('id_manufacturer').value = id; Alpine.data('rideForm', () => ({
document.getElementById('id_manufacturer_search').value = name; init() {
document.getElementById('manufacturer-search-results').innerHTML = ''; // Handle form submission cleanup
this.$el.addEventListener('submit', () => {
this.clearAllSearchResults();
});
},
selectManufacturer(id, name) {
const manufacturerInput = document.getElementById('id_manufacturer');
const manufacturerSearch = document.getElementById('id_manufacturer_search');
const manufacturerResults = document.getElementById('manufacturer-search-results');
if (manufacturerInput) manufacturerInput.value = id;
if (manufacturerSearch) manufacturerSearch.value = name;
if (manufacturerResults) manufacturerResults.innerHTML = '';
// Update ride model search to include manufacturer // Update ride model search to include manufacturer
const rideModelSearch = document.getElementById('id_ride_model_search'); const rideModelSearch = document.getElementById('id_ride_model_search');
if (rideModelSearch) { if (rideModelSearch) {
rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]'); rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]');
} }
} },
function selectDesigner(id, name) { selectDesigner(id, name) {
document.getElementById('id_designer').value = id; const designerInput = document.getElementById('id_designer');
document.getElementById('id_designer_search').value = name; const designerSearch = document.getElementById('id_designer_search');
document.getElementById('designer-search-results').innerHTML = ''; const designerResults = document.getElementById('designer-search-results');
}
function selectRideModel(id, name) { if (designerInput) designerInput.value = id;
document.getElementById('id_ride_model').value = id; if (designerSearch) designerSearch.value = name;
document.getElementById('id_ride_model_search').value = name; if (designerResults) designerResults.innerHTML = '';
document.getElementById('ride-model-search-results').innerHTML = ''; },
}
// Handle form submission selectRideModel(id, name) {
document.addEventListener('submit', function(e) { const rideModelInput = document.getElementById('id_ride_model');
if (e.target.id === 'ride-form') { const rideModelSearch = document.getElementById('id_ride_model_search');
// Clear search results const rideModelResults = document.getElementById('ride-model-search-results');
document.getElementById('manufacturer-search-results').innerHTML = '';
document.getElementById('designer-search-results').innerHTML = '';
document.getElementById('ride-model-search-results').innerHTML = '';
}
});
// Handle clicks outside search results if (rideModelInput) rideModelInput.value = id;
document.addEventListener('click', function(e) { if (rideModelSearch) rideModelSearch.value = name;
if (rideModelResults) rideModelResults.innerHTML = '';
},
clearAllSearchResults() {
const manufacturerResults = document.getElementById('manufacturer-search-results'); const manufacturerResults = document.getElementById('manufacturer-search-results');
const designerResults = document.getElementById('designer-search-results'); const designerResults = document.getElementById('designer-search-results');
const rideModelResults = document.getElementById('ride-model-search-results'); const rideModelResults = document.getElementById('ride-model-search-results');
if (!e.target.closest('#manufacturer-search-container')) { if (manufacturerResults) manufacturerResults.innerHTML = '';
manufacturerResults.innerHTML = ''; if (designerResults) designerResults.innerHTML = '';
} if (rideModelResults) rideModelResults.innerHTML = '';
if (!e.target.closest('#designer-search-container')) { },
designerResults.innerHTML = '';
} clearManufacturerResults() {
if (!e.target.closest('#ride-model-search-container')) { const manufacturerResults = document.getElementById('manufacturer-search-results');
rideModelResults.innerHTML = ''; if (manufacturerResults) manufacturerResults.innerHTML = '';
},
clearDesignerResults() {
const designerResults = document.getElementById('designer-search-results');
if (designerResults) designerResults.innerHTML = '';
},
clearRideModelResults() {
const rideModelResults = document.getElementById('ride-model-search-results');
if (rideModelResults) rideModelResults.innerHTML = '';
} }
}));
}); });
</script> </script>
<form method="post" id="ride-form" class="space-y-6" enctype="multipart/form-data"> <form method="post"
id="ride-form"
class="space-y-6"
enctype="multipart/form-data"
x-data="rideForm"
x-init="init()">
{% csrf_token %} {% csrf_token %}
<!-- Park Area --> <!-- Park Area -->
@@ -86,7 +112,9 @@ document.addEventListener('click', function(e) {
<!-- Manufacturer --> <!-- Manufacturer -->
<div class="space-y-2"> <div class="space-y-2">
<div id="manufacturer-search-container" class="relative"> <div id="manufacturer-search-container"
class="relative"
@click.outside="clearManufacturerResults()">
<label for="{{ form.manufacturer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="{{ form.manufacturer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Manufacturer Manufacturer
</label> </label>
@@ -103,7 +131,9 @@ document.addEventListener('click', function(e) {
<!-- Designer --> <!-- Designer -->
<div class="space-y-2"> <div class="space-y-2">
<div id="designer-search-container" class="relative"> <div id="designer-search-container"
class="relative"
@click.outside="clearDesignerResults()">
<label for="{{ form.designer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="{{ form.designer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Designer Designer
</label> </label>
@@ -120,7 +150,9 @@ document.addEventListener('click', function(e) {
<!-- Ride Model --> <!-- Ride Model -->
<div class="space-y-2"> <div class="space-y-2">
<div id="ride-model-search-container" class="relative"> <div id="ride-model-search-container"
class="relative"
@click.outside="clearRideModelResults()">
<label for="{{ form.ride_model_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="{{ form.ride_model_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Ride Model Ride Model
</label> </label>

View File

@@ -1,4 +1,27 @@
<div class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;"> <script>
document.addEventListener('alpine:init', () => {
Alpine.data('rideModelSearchResults', () => ({
selectRideModel(id, name) {
// Update ride model fields using AlpineJS reactive approach
const rideModelInput = this.$el.closest('form').querySelector('#id_ride_model');
const searchInput = this.$el.closest('form').querySelector('#id_ride_model_search');
const resultsDiv = this.$el.closest('form').querySelector('#ride-model-search-results');
if (rideModelInput) rideModelInput.value = id;
if (searchInput) searchInput.value = name;
if (resultsDiv) resultsDiv.innerHTML = '';
// Dispatch custom event for parent component
this.$dispatch('ride-model-selected', { id, name });
}
}));
});
</script>
<div x-data="rideModelSearchResults()"
@click.outside="$el.innerHTML = ''"
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
style="max-height: 240px; overflow-y: auto;">
{% if not manufacturer_id %} {% if not manufacturer_id %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300"> <div class="px-4 py-2 text-gray-700 dark:text-gray-300">
Please select a manufacturer first Please select a manufacturer first
@@ -8,7 +31,7 @@
{% for ride_model in ride_models %} {% for ride_model in ride_models %}
<button type="button" <button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600" class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectRideModel('{{ ride_model.id }}', '{{ ride_model.name|escapejs }}')"> @click="selectRideModel('{{ ride_model.id }}', '{{ ride_model.name|escapejs }}')">
{{ ride_model.name }} {{ ride_model.name }}
{% if ride_model.manufacturer %} {% if ride_model.manufacturer %}
<div class="text-sm text-gray-700 dark:text-gray-300"> <div class="text-sm text-gray-700 dark:text-gray-300">
@@ -28,11 +51,3 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
<script>
function selectRideModel(id, name) {
document.getElementById('id_ride_model').value = id;
document.getElementById('id_ride_model_search').value = name;
document.getElementById('ride-model-search-results').innerHTML = '';
}
</script>