/** * Enhanced Search Component * Features: Autocomplete, filters, recent searches, keyboard navigation, highlighting */ const enhancedSearch = { // Configuration config: { minChars: 2, debounceMs: 300, maxResults: 8, maxRecent: 5, storageKey: 'nexus_recent_searches' }, // State state: { query: '', results: { parts: [], vehicles: [] }, highlightedIndex: -1, isOpen: false, isLoading: false, filtersVisible: false, debounceTimer: null }, // DOM elements cache elements: {}, // Initialize init() { this.cacheElements(); this.loadCategories(); this.renderRecentSearches(); this.setupClickOutside(); }, cacheElements() { this.elements = { input: document.getElementById('searchInput'), dropdown: document.getElementById('searchDropdown'), loading: document.getElementById('searchLoading'), filters: document.getElementById('searchFilters'), recent: document.getElementById('searchRecent'), recentItems: document.getElementById('searchRecentItems'), resultsContainer: document.getElementById('searchResultsContainer'), partsResults: document.getElementById('partsResults'), partsResultsList: document.getElementById('partsResultsList'), vehiclesResults: document.getElementById('vehiclesResults'), vehiclesResultsList: document.getElementById('vehiclesResultsList'), noResults: document.getElementById('searchNoResults'), footer: document.getElementById('searchFooter'), categoryFilter: document.getElementById('searchCategoryFilter'), typeFilter: document.getElementById('searchTypeFilter') }; }, // Load categories for filter async loadCategories() { try { const response = await fetch('/api/categories'); const categories = await response.json(); if (this.elements.categoryFilter) { this.elements.categoryFilter.innerHTML = '' + this.flattenCategories(categories).map(cat => `` ).join(''); } } catch (e) { console.error('Error loading categories:', e); } }, flattenCategories(categories, result = []) { categories.forEach(cat => { result.push(cat); if (cat.children && cat.children.length) { this.flattenCategories(cat.children, result); } }); return result; }, // Event handlers onInput(value) { this.state.query = value.trim(); // Clear existing timer if (this.state.debounceTimer) { clearTimeout(this.state.debounceTimer); } if (this.state.query.length < this.config.minChars) { this.showRecent(); return; } // Debounce search this.state.debounceTimer = setTimeout(() => { this.performSearch(); }, this.config.debounceMs); }, onKeydown(event) { const allItems = this.getAllResultItems(); switch (event.key) { case 'ArrowDown': event.preventDefault(); this.highlightNext(allItems); break; case 'ArrowUp': event.preventDefault(); this.highlightPrevious(allItems); break; case 'Enter': event.preventDefault(); if (this.state.highlightedIndex >= 0 && allItems[this.state.highlightedIndex]) { allItems[this.state.highlightedIndex].click(); } else if (this.state.query.length >= this.config.minChars) { this.viewAllResults(); } break; case 'Escape': this.close(); this.elements.input.blur(); break; case 'Tab': // Autocomplete with highlighted item if (this.state.isOpen && this.state.highlightedIndex >= 0 && allItems[this.state.highlightedIndex]) { event.preventDefault(); const item = allItems[this.state.highlightedIndex]; const autocompleteText = item.dataset.autocomplete; if (autocompleteText && this.elements.input) { this.elements.input.value = autocompleteText; this.state.query = autocompleteText; this.performSearch(); } } break; } }, onFocus() { if (this.state.query.length >= this.config.minChars) { this.open(); } else { this.showRecent(); } }, // Navigation helpers getAllResultItems() { return Array.from(this.elements.dropdown.querySelectorAll('.search-result-item')); }, highlightNext(items) { if (items.length === 0) return; this.state.highlightedIndex++; if (this.state.highlightedIndex >= items.length) { this.state.highlightedIndex = 0; } this.updateHighlight(items); }, highlightPrevious(items) { if (items.length === 0) return; this.state.highlightedIndex--; if (this.state.highlightedIndex < 0) { this.state.highlightedIndex = items.length - 1; } this.updateHighlight(items); }, updateHighlight(items) { items.forEach((item, index) => { item.classList.toggle('highlighted', index === this.state.highlightedIndex); if (index === this.state.highlightedIndex) { item.scrollIntoView({ block: 'nearest' }); } }); }, // Search async performSearch() { const query = this.state.query; const categoryId = this.elements.categoryFilter?.value; const searchType = this.elements.typeFilter?.value || 'all'; this.showLoading(true); this.open(); try { let url = `/api/search?q=${encodeURIComponent(query)}&limit=${this.config.maxResults}`; if (categoryId) url += `&category_id=${categoryId}`; if (searchType !== 'all') url += `&type=${searchType}`; const response = await fetch(url); const data = await response.json(); this.state.results = { parts: data.parts || [], vehicles: data.vehicles || [], vehicleParts: data.vehicle_parts || [], matchedVehicle: data.matched_vehicle || null }; this.renderResults(); } catch (e) { console.error('Search error:', e); this.showNoResults(); } finally { this.showLoading(false); } }, applyFilters() { if (this.state.query.length >= this.config.minChars) { this.performSearch(); } }, // Rendering renderResults() { const { parts, vehicles, vehicleParts, matchedVehicle } = this.state.results; const hasVehicleParts = vehicleParts && vehicleParts.length > 0; const hasResults = parts.length > 0 || vehicles.length > 0 || hasVehicleParts; // Hide recent searches when showing results if (this.elements.recent) this.elements.recent.style.display = 'none'; // If we have matched vehicle + parts (combined search result) if (hasVehicleParts && matchedVehicle && this.elements.partsResults && this.elements.partsResultsList) { this.elements.partsResults.style.display = 'block'; // Show vehicle header const vehicleHeader = `
${matchedVehicle.brand} ${matchedVehicle.model} ${matchedVehicle.year} ${matchedVehicle.engine}
`; this.elements.partsResultsList.innerHTML = vehicleHeader + vehicleParts.map((part, index) => this.renderVehiclePartItem(part, matchedVehicle, index) ).join(''); // Hide regular vehicles section if (this.elements.vehiclesResults) { this.elements.vehiclesResults.style.display = 'none'; } } else { // Regular parts if (parts.length > 0 && this.elements.partsResults && this.elements.partsResultsList) { this.elements.partsResults.style.display = 'block'; this.elements.partsResultsList.innerHTML = parts.map((part, index) => this.renderPartItem(part, index) ).join(''); } else if (this.elements.partsResults) { this.elements.partsResults.style.display = 'none'; } // Vehicles if (vehicles.length > 0 && this.elements.vehiclesResults && this.elements.vehiclesResultsList) { this.elements.vehiclesResults.style.display = 'block'; this.elements.vehiclesResultsList.innerHTML = vehicles.map((vehicle, index) => this.renderVehicleItem(vehicle, parts.length + index) ).join(''); } else if (this.elements.vehiclesResults) { this.elements.vehiclesResults.style.display = 'none'; } } // No results if (this.elements.noResults) { this.elements.noResults.style.display = hasResults ? 'none' : 'block'; } // Footer if (this.elements.footer) { this.elements.footer.style.display = hasResults ? 'flex' : 'none'; } // Auto-highlight first result for quick Enter selection if (hasResults) { this.state.highlightedIndex = 0; const firstItem = this.elements.dropdown.querySelector('.search-result-item'); if (firstItem) { firstItem.classList.add('highlighted'); } } else { this.state.highlightedIndex = -1; } }, renderPartItem(part, index) { const title = this.highlightText(part.name, this.state.query); const partNumber = part.matched_number || part.oem_part_number; const subtitle = this.highlightText(partNumber, this.state.query); const categoryBadge = part.category_name ? `${part.category_name}` : ''; // Match type badge with better labels const matchLabels = { 'aftermarket': 'Aftermarket', 'cross_reference': 'Cross-Ref' }; const matchBadge = part.match_type && part.match_type !== 'oem' ? `${matchLabels[part.match_type] || part.match_type}` : ''; // Text for Tab autocomplete const autocompleteText = part.oem_part_number || part.name; return `
${part.image_url ? `` : ''}
${title}
${subtitle} ${categoryBadge}
${matchBadge}
`; }, // Render a part that's linked to a specific vehicle (combined search) renderVehiclePartItem(part, vehicle, index) { const title = this.highlightText(part.name_es || part.name, this.state.query); const subtitle = this.highlightText(part.oem_part_number, this.state.query); const groupBadge = part.group_name ? `${part.group_name}` : ''; // Escape data for onclick const vehicleData = JSON.stringify({ id: vehicle.id, brand: vehicle.brand, model: vehicle.model, year: vehicle.year }).replace(/'/g, "\\'").replace(/"/g, '"'); const autocompleteText = `${vehicle.brand} ${vehicle.model} ${vehicle.year} ${part.name}`; return `
${part.image_url ? `` : ''}
${title}
${subtitle} ${groupBadge}
Compatible
`; }, renderVehicleItem(vehicle, index) { const title = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`; const subtitle = vehicle.engine || ''; // Escape vehicle data for onclick const vehicleData = JSON.stringify({ id: vehicle.id, brand: vehicle.brand, model: vehicle.model, year: vehicle.year }).replace(/'/g, "\\'").replace(/"/g, '"'); // Quick category buttons (IDs match database) const categories = [ { id: 6, icon: 'fa-cog', name: 'Motor' }, { id: 2, icon: 'fa-compact-disc', name: 'Frenos' }, { id: 5, icon: 'fa-bolt', name: 'Eléctrico' }, { id: 11, icon: 'fa-truck-monster', name: 'Suspensión' }, { id: 8, icon: 'fa-gas-pump', name: 'Combustible' }, { id: 12, icon: 'fa-gears', name: 'Transmisión' } ]; const categoryButtons = categories.map(cat => `` ).join(''); // Text for Tab autocomplete (vehicle name for adding part search) const autocompleteText = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`; return `
${this.highlightText(title, this.state.query)}
${this.highlightText(subtitle, this.state.query)}
${categoryButtons}
`; }, highlightText(text, query) { if (!query || !text) return text; const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi'); return text.replace(regex, '$1'); }, escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, escapeHtml(str) { return str.replace(/'/g, "\\'").replace(/"/g, '\\"'); }, // Selection handlers selectPart(partId, name) { this.saveRecentSearch(name); this.close(); // Open part detail modal via dashboard if (typeof dashboard !== 'undefined' && typeof dashboard.showPartDetail === 'function') { dashboard.showPartDetail(partId); } else { // Fallback: search in dashboard window.location.href = `/?search=${encodeURIComponent(name)}`; } }, // Select a part that's specific to a vehicle (from combined search) selectVehiclePart(vehicleDataStr, partId, categoryId, groupId) { try { const vehicle = JSON.parse(vehicleDataStr.replace(/"/g, '"')); const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`; this.saveRecentSearch(this.state.query); this.close(); // Navigate to vehicle's category/group and show part detail if (typeof dashboard !== 'undefined') { // Set up vehicle context first if (typeof dashboard.navigateToVehicleCategory === 'function') { dashboard.navigateToVehicleCategory(vehicle.id, vehicle.brand, vehicle.model, vehicle.year, categoryId); // Then show part detail after a short delay setTimeout(() => { if (typeof dashboard.showPartDetail === 'function') { dashboard.showPartDetail(partId); } }, 500); } } } catch (e) { console.error('Error selecting vehicle part:', e); } }, selectVehicle(vehicleDataStr) { try { // Parse vehicle data from JSON string const vehicle = JSON.parse(vehicleDataStr.replace(/"/g, '"')); const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`; this.saveRecentSearch(displayName); this.close(); // Navigate directly to the vehicle's categories if (typeof dashboard !== 'undefined' && typeof dashboard.navigateToVehicle === 'function') { dashboard.navigateToVehicle(vehicle.id, vehicle.brand, vehicle.model, vehicle.year); } else { console.log('Navigating to vehicle:', vehicle); } } catch (e) { console.error('Error parsing vehicle data:', e); } }, selectVehicleCategory(vehicleDataStr, categoryId) { try { const vehicle = JSON.parse(vehicleDataStr.replace(/"/g, '"')); const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`; this.saveRecentSearch(displayName); this.close(); // Navigate to vehicle and then to specific category if (typeof dashboard !== 'undefined' && typeof dashboard.navigateToVehicleCategory === 'function') { dashboard.navigateToVehicleCategory(vehicle.id, vehicle.brand, vehicle.model, vehicle.year, categoryId); } else { console.log('Navigating to vehicle category:', vehicle, categoryId); } } catch (e) { console.error('Error parsing vehicle data:', e); } }, viewAllResults() { if (this.state.query) { this.saveRecentSearch(this.state.query); this.close(); // Open search modal with full results if (typeof dashboard !== 'undefined' && typeof dashboard.searchPartNumber === 'function') { dashboard.searchPartNumber(); } else { // Fallback: reload page with search parameter console.log('Dashboard not available, search query:', this.state.query); } } }, // Recent searches getRecentSearches() { try { return JSON.parse(localStorage.getItem(this.config.storageKey)) || []; } catch { return []; } }, saveRecentSearch(query) { if (!query || query.length < 2) return; let recent = this.getRecentSearches(); // Remove if exists recent = recent.filter(r => r.toLowerCase() !== query.toLowerCase()); // Add to front recent.unshift(query); // Limit recent = recent.slice(0, this.config.maxRecent); localStorage.setItem(this.config.storageKey, JSON.stringify(recent)); this.renderRecentSearches(); }, clearRecent() { localStorage.removeItem(this.config.storageKey); this.renderRecentSearches(); }, renderRecentSearches() { const recent = this.getRecentSearches(); if (recent.length === 0 || !this.elements.recent) { if (this.elements.recent) this.elements.recent.style.display = 'none'; return; } if (this.elements.recentItems) { this.elements.recentItems.innerHTML = recent.map(term => `${term}` ).join(''); } }, searchRecent(term) { if (this.elements.input) { this.elements.input.value = term; } this.state.query = term; this.performSearch(); }, showRecent() { const recent = this.getRecentSearches(); if (recent.length > 0 && this.elements.recent) { this.elements.recent.style.display = 'block'; if (this.elements.partsResults) this.elements.partsResults.style.display = 'none'; if (this.elements.vehiclesResults) this.elements.vehiclesResults.style.display = 'none'; if (this.elements.noResults) this.elements.noResults.style.display = 'none'; if (this.elements.footer) this.elements.footer.style.display = 'none'; this.open(); } else { this.close(); } }, // Filters toggle toggleFilters() { this.state.filtersVisible = !this.state.filtersVisible; if (this.elements.filters) { this.elements.filters.style.display = this.state.filtersVisible ? 'flex' : 'none'; } const toggle = document.querySelector('.search-filters-toggle'); if (toggle) { toggle.classList.toggle('active', this.state.filtersVisible); } }, // UI state open() { this.state.isOpen = true; if (this.elements.dropdown) this.elements.dropdown.classList.add('active'); }, close() { this.state.isOpen = false; if (this.elements.dropdown) this.elements.dropdown.classList.remove('active'); this.state.highlightedIndex = -1; }, showLoading(show) { this.state.isLoading = show; if (this.elements.loading) this.elements.loading.style.display = show ? 'block' : 'none'; }, showNoResults() { if (this.elements.partsResults) this.elements.partsResults.style.display = 'none'; if (this.elements.vehiclesResults) this.elements.vehiclesResults.style.display = 'none'; if (this.elements.noResults) this.elements.noResults.style.display = 'block'; if (this.elements.footer) this.elements.footer.style.display = 'none'; if (this.elements.recent) this.elements.recent.style.display = 'none'; }, setupClickOutside() { document.addEventListener('click', (e) => { const searchBox = document.querySelector('.search-box-enhanced'); if (searchBox && !searchBox.contains(e.target)) { this.close(); } }); } }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { enhancedSearch.init(); }); // Global keyboard shortcut for search document.addEventListener('keydown', (e) => { // "/" to focus search (when not in an input) if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) { e.preventDefault(); const input = document.getElementById('searchInput'); if (input) { input.focus(); input.select(); } } // Ctrl+K or Cmd+K to focus search if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); const input = document.getElementById('searchInput'); if (input) { input.focus(); input.select(); } } });