- Migrate from SQLite to PostgreSQL with normalized schema - Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission, materials, position_part, manufacture_type, quality_tier, countries, reference_type, shapes) - Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries - Rewrite console/db.py (27 methods) using SQLAlchemy ORM - Add models.py with 27 SQLAlchemy model definitions - Add config.py for centralized DB_URL configuration - Add migrate_to_postgres.py migration script - Add docs/METABASE_GUIDE.md with complete data entry guide - Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS" - Fill vehicle data gaps via NHTSA API + heuristics: engines (cylinders, power, torque), brands (country, founded_year), models (body_type, production years), MYE (drivetrain, transmission, trim) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
678 lines
25 KiB
JavaScript
678 lines
25 KiB
JavaScript
/**
|
|
* 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 = '<option value="">Todas las categorías</option>' +
|
|
this.flattenCategories(categories).map(cat =>
|
|
`<option value="${cat.id}">${cat.name}</option>`
|
|
).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 = `
|
|
<div class="vehicle-parts-header">
|
|
<i class="fas fa-car"></i>
|
|
<span>${matchedVehicle.brand} ${matchedVehicle.model} ${matchedVehicle.year}</span>
|
|
<small>${matchedVehicle.engine}</small>
|
|
</div>
|
|
`;
|
|
|
|
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 ? `<span class="search-category-badge">${part.category_name}</span>` : '';
|
|
|
|
// Match type badge with better labels
|
|
const matchLabels = {
|
|
'aftermarket': 'Aftermarket',
|
|
'cross_reference': 'Cross-Ref'
|
|
};
|
|
const matchBadge = part.match_type && part.match_type !== 'oem'
|
|
? `<span class="search-result-badge ${part.match_type}">${matchLabels[part.match_type] || part.match_type}</span>`
|
|
: '';
|
|
|
|
// Text for Tab autocomplete
|
|
const autocompleteText = part.oem_part_number || part.name;
|
|
|
|
return `
|
|
<div class="search-result-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)}" onclick="enhancedSearch.selectPart(${part.id}, '${this.escapeHtml(part.name)}')">
|
|
<div class="search-result-icon">
|
|
${part.image_url ? `<img src="${part.image_url}" alt="">` : '<i class="fas fa-cog"></i>'}
|
|
</div>
|
|
<div class="search-result-info">
|
|
<div class="search-result-title">${title}</div>
|
|
<div class="search-result-subtitle">
|
|
<span class="part-number">${subtitle}</span>
|
|
${categoryBadge}
|
|
</div>
|
|
</div>
|
|
${matchBadge}
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
// 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 ? `<span class="search-category-badge">${part.group_name}</span>` : '';
|
|
|
|
// 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 `
|
|
<div class="search-result-item vehicle-part-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)}"
|
|
onclick="enhancedSearch.selectVehiclePart('${vehicleData}', ${part.id}, ${part.category_id}, ${part.group_id})">
|
|
<div class="search-result-icon">
|
|
${part.image_url ? `<img src="${part.image_url}" alt="">` : '<i class="fas fa-cog"></i>'}
|
|
</div>
|
|
<div class="search-result-info">
|
|
<div class="search-result-title">${title}</div>
|
|
<div class="search-result-subtitle">
|
|
<span class="part-number">${subtitle}</span>
|
|
${groupBadge}
|
|
</div>
|
|
</div>
|
|
<span class="search-result-badge vehicle-part-badge">
|
|
<i class="fas fa-check-circle"></i> Compatible
|
|
</span>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
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 =>
|
|
`<button class="vehicle-category-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicleCategory('${vehicleData}', ${cat.id})" title="${cat.name}">
|
|
<i class="fas ${cat.icon}"></i>
|
|
</button>`
|
|
).join('');
|
|
|
|
// Text for Tab autocomplete (vehicle name for adding part search)
|
|
const autocompleteText = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
|
|
|
|
return `
|
|
<div class="search-result-item vehicle-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)} ">
|
|
<div class="search-result-icon"><i class="fas fa-car"></i></div>
|
|
<div class="search-result-info">
|
|
<div class="search-result-title">${this.highlightText(title, this.state.query)}</div>
|
|
<div class="search-result-subtitle">${this.highlightText(subtitle, this.state.query)}</div>
|
|
<div class="vehicle-categories-row">
|
|
${categoryButtons}
|
|
<button class="vehicle-category-btn vehicle-all-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicle('${vehicleData}')" title="Ver todas las categorías">
|
|
<i class="fas fa-th"></i> Todo
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
highlightText(text, query) {
|
|
if (!query || !text) return text;
|
|
|
|
const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi');
|
|
return text.replace(regex, '<span class="highlight">$1</span>');
|
|
},
|
|
|
|
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 =>
|
|
`<span class="search-recent-item" onclick="enhancedSearch.searchRecent('${this.escapeHtml(term)}')">${term}</span>`
|
|
).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();
|
|
}
|
|
}
|
|
});
|