Add landing page and redesign catalog with dark theme

Landing page (customer-landing.html):
- Modern dark theme with orange accent (#ff6b35)
- Dynamic stats, categories, products from API
- Integrated search modal with FTS
- VIN decoder section
- Responsive design with mobile support

Catalog redesign (index.html):
- Unified design matching landing page
- Custom modal system (removed Bootstrap dependency)
- Simplified breadcrumb navigation
- Updated card styles with hover effects
- Consistent color scheme and typography

JavaScript updates (dashboard.js):
- Custom modal open/close methods
- Updated element IDs for new HTML
- Paginated response handling
- Search input event binding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 08:33:15 +00:00
parent d4d1c9b7ba
commit 834d7d2d96
3 changed files with 2658 additions and 1003 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -24,9 +24,21 @@ class VehicleDashboard {
await this.showBrands(); await this.showBrands();
this.bindFilterEvents(); this.bindFilterEvents();
this.bindKeyboardShortcuts(); // FASE 5: Keyboard shortcuts this.bindKeyboardShortcuts(); // FASE 5: Keyboard shortcuts
this.bindSearchEvents(); // Bind search input events
this.initDarkMode(); // FASE 5: Dark mode this.initDarkMode(); // FASE 5: Dark mode
} }
bindSearchEvents() {
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.searchPartNumber();
}
});
}
}
async loadStats() { async loadStats() {
try { try {
const [brandsRes, vehiclesRes, partsRes, categoriesRes] = await Promise.all([ const [brandsRes, vehiclesRes, partsRes, categoriesRes] = await Promise.all([
@@ -47,15 +59,18 @@ class VehicleDashboard {
this.stats.models = uniqueModels.size; this.stats.models = uniqueModels.size;
this.stats.vehicles = vehicles.length; this.stats.vehicles = vehicles.length;
document.getElementById('totalBrands').textContent = this.stats.brands; const brandsEl = document.getElementById('totalBrands');
document.getElementById('totalModels').textContent = this.stats.models; const modelsEl = document.getElementById('totalModels');
document.getElementById('totalVehicles').textContent = this.stats.vehicles; if (brandsEl) brandsEl.textContent = this.stats.brands;
if (modelsEl) modelsEl.textContent = this.stats.models > 1000 ? Math.floor(this.stats.models/1000) + 'K+' : this.stats.models;
} }
if (partsRes.ok) { if (partsRes.ok) {
const parts = await partsRes.json(); const partsData = await partsRes.json();
this.stats.parts = parts.length || 0; // Handle paginated response
document.getElementById('totalParts').textContent = this.stats.parts; this.stats.parts = partsData.pagination ? partsData.pagination.total : (partsData.data ? partsData.data.length : partsData.length || 0);
const partsEl = document.getElementById('totalParts');
if (partsEl) partsEl.textContent = this.stats.parts;
} }
if (categoriesRes.ok) { if (categoriesRes.ok) {
@@ -68,115 +83,55 @@ class VehicleDashboard {
updateBreadcrumb() { updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb'); const breadcrumb = document.getElementById('breadcrumb');
let html = ''; let items = [];
// FASE 5: Enhanced breadcrumb with full path: Brand -> Model -> Year -> Category -> Group
const yearDisplay = this.selectedYear ? `<span class="breadcrumb-year">${this.selectedYear}</span>` : '';
// Build breadcrumb items based on current view
if (this.currentView === 'brands') { if (this.currentView === 'brands') {
html = `<li class="breadcrumb-item active"><i class="fas fa-home"></i> Marcas</li>`; items.push({ label: '<i class="fas fa-home"></i> Marcas', active: true });
} else if (this.currentView === 'models') { } else if (this.currentView === 'models') {
html = ` items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
<li class="breadcrumb-item"> items.push({ label: this.selectedBrand, active: true });
<a href="#" onclick="dashboard.goToBrands(); return false;">
<i class="fas fa-home"></i> Marcas
</a>
</li>
<li class="breadcrumb-item active">${this.selectedBrand}</li>
`;
} else if (this.currentView === 'vehicles') { } else if (this.currentView === 'vehicles') {
html = ` items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
<li class="breadcrumb-item"> items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand}')` });
<a href="#" onclick="dashboard.goToBrands(); return false;"> items.push({ label: this.selectedModel, active: true });
<i class="fas fa-home"></i> Marcas
</a>
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToModels('${this.selectedBrand}'); return false;">
${this.selectedBrand}
</a>
</li>
<li class="breadcrumb-item active">${this.selectedModel}</li>
`;
} else if (this.currentView === 'categories') { } else if (this.currentView === 'categories') {
html = ` items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
<li class="breadcrumb-item"> items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand}')` });
<a href="#" onclick="dashboard.goToBrands(); return false;"> items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')` });
<i class="fas fa-home"></i> Marcas if (this.selectedYear) items.push({ label: this.selectedYear });
</a> items.push({ label: 'Categorías', active: true });
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToModels('${this.selectedBrand}'); return false;">
${this.selectedBrand}
</a>
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}'); return false;">
${this.selectedModel}
</a>
</li>
${this.selectedYear ? `<li class="breadcrumb-item">${yearDisplay}</li>` : ''}
<li class="breadcrumb-item active">Categorias</li>
`;
} else if (this.currentView === 'groups') { } else if (this.currentView === 'groups') {
html = ` items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
<li class="breadcrumb-item"> items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand}')` });
<a href="#" onclick="dashboard.goToBrands(); return false;"> items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')` });
<i class="fas fa-home"></i> Marcas if (this.selectedYear) items.push({ label: this.selectedYear });
</a> items.push({ label: 'Categorías', action: `dashboard.goToCategories(${this.selectedVehicleId})` });
</li> items.push({ label: this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Grupos', active: true });
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToModels('${this.selectedBrand}'); return false;">
${this.selectedBrand}
</a>
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}'); return false;">
${this.selectedModel}
</a>
</li>
${this.selectedYear ? `<li class="breadcrumb-item">${yearDisplay}</li>` : ''}
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToCategories(${this.selectedVehicleId}); return false;">
Categorias
</a>
</li>
<li class="breadcrumb-item active">${this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Grupos'}</li>
`;
} else if (this.currentView === 'parts') { } else if (this.currentView === 'parts') {
const groupName = this.selectedGroup ? (this.selectedGroup.name_es || this.selectedGroup.name) : 'Grupo'; const groupName = this.selectedGroup ? (this.selectedGroup.name_es || this.selectedGroup.name) : 'Grupo';
html = ` items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
<li class="breadcrumb-item"> items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand}')` });
<a href="#" onclick="dashboard.goToBrands(); return false;"> items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')` });
<i class="fas fa-home"></i> Marcas if (this.selectedYear) items.push({ label: this.selectedYear });
</a> items.push({ label: 'Categorías', action: `dashboard.goToCategories(${this.selectedVehicleId})` });
</li> items.push({ label: this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Categoría', action: `dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0})` });
<li class="breadcrumb-item"> items.push({ label: groupName, active: true });
<a href="#" onclick="dashboard.goToModels('${this.selectedBrand}'); return false;">
${this.selectedBrand}
</a>
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}'); return false;">
${this.selectedModel}
</a>
</li>
${this.selectedYear ? `<li class="breadcrumb-item">${yearDisplay}</li>` : ''}
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToCategories(${this.selectedVehicleId}); return false;">
Categorias
</a>
</li>
<li class="breadcrumb-item">
<a href="#" onclick="dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0}); return false;">
${this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Categoria'}
</a>
</li>
<li class="breadcrumb-item active">${groupName}</li>
`;
} }
breadcrumb.innerHTML = html; // Generate HTML
breadcrumb.innerHTML = items.map((item, i) => {
const isLast = i === items.length - 1;
const separator = !isLast ? '<span class="breadcrumb-separator">/</span>' : '';
if (item.action) {
return `<span class="breadcrumb-item"><a href="#" onclick="${item.action}; return false;">${item.label}</a></span>${separator}`;
} else if (item.active) {
return `<span class="breadcrumb-item active">${item.label}</span>`;
} else {
return `<span class="breadcrumb-item">${item.label}</span>${separator}`;
}
}).join('');
} }
// FASE 5: Keyboard shortcuts // FASE 5: Keyboard shortcuts
@@ -199,7 +154,7 @@ class VehicleDashboard {
// Focus search input with "/" or Ctrl+K // Focus search input with "/" or Ctrl+K
if (e.key === '/' || (e.ctrlKey && e.key === 'k')) { if (e.key === '/' || (e.ctrlKey && e.key === 'k')) {
e.preventDefault(); e.preventDefault();
const searchInput = document.getElementById('partNumberSearch'); const searchInput = document.getElementById('searchInput');
if (searchInput) { if (searchInput) {
searchInput.focus(); searchInput.focus();
} }
@@ -222,20 +177,39 @@ class VehicleDashboard {
}); });
} }
// FASE 5: Close all open modals // FASE 5: Close all open modals (custom modal system)
closeAllModals() { closeAllModals() {
const modals = ['partDetailModal', 'searchResultsModal', 'diagramModal', 'vinDecoderModal']; const modals = ['partDetailModal', 'searchResultsModal', 'diagramModal', 'vinDecoderModal'];
modals.forEach(modalId => { modals.forEach(modalId => {
const modalEl = document.getElementById(modalId); this.closeModal(modalId);
if (modalEl) {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
}
}); });
} }
// Custom modal open
openModal(modalId) {
this.lastFocusedElement = document.activeElement;
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
// Focus first focusable element
setTimeout(() => {
const focusable = modal.querySelector('input, button, [tabindex]:not([tabindex="-1"])');
if (focusable) focusable.focus();
}, 100);
}
}
// Custom modal close
closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
if (this.lastFocusedElement) {
this.lastFocusedElement.focus();
}
}
}
// FASE 5: Go back one level in navigation // FASE 5: Go back one level in navigation
goBack() { goBack() {
switch (this.currentView) { switch (this.currentView) {
@@ -264,46 +238,17 @@ class VehicleDashboard {
} }
} }
// FASE 5: Initialize dark mode from localStorage // Theme is now always dark, no toggle needed
initDarkMode() { initDarkMode() {
const savedTheme = localStorage.getItem('autopartes-theme'); // Dark theme is the default and only theme
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
this.updateDarkModeIcon(true);
} }
// Bind toggle button
const toggleBtn = document.getElementById('darkModeToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggleDarkMode());
}
}
// FASE 5: Toggle dark mode
toggleDarkMode() { toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-theme'); // No-op: single theme design
const isDark = currentTheme === 'dark';
if (isDark) {
document.documentElement.removeAttribute('data-theme');
localStorage.setItem('autopartes-theme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('autopartes-theme', 'dark');
} }
this.updateDarkModeIcon(!isDark); updateDarkModeIcon() {
} // No-op: no toggle button in new design
// FASE 5: Update dark mode toggle icon
updateDarkModeIcon(isDark) {
const toggleBtn = document.getElementById('darkModeToggle');
if (toggleBtn) {
const icon = toggleBtn.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
} }
// FASE 5: Make cards keyboard accessible // FASE 5: Make cards keyboard accessible
@@ -331,31 +276,10 @@ class VehicleDashboard {
}); });
} }
// FASE 5: Open modal with focus management // FASE 5: Open modal with focus management (custom modal system)
openModalWithFocus(modalId) { openModalWithFocus(modalId) {
this.lastFocusedElement = document.activeElement; this.openModal(modalId);
const modalEl = document.getElementById(modalId); return { hide: () => this.closeModal(modalId) };
if (!modalEl) return null;
const modal = new bootstrap.Modal(modalEl);
// Focus first focusable element when modal is shown
modalEl.addEventListener('shown.bs.modal', () => {
const focusable = modalEl.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable) {
focusable.focus();
}
}, { once: true });
// Return focus when modal is hidden
modalEl.addEventListener('hidden.bs.modal', () => {
if (this.lastFocusedElement && document.body.contains(this.lastFocusedElement)) {
this.lastFocusedElement.focus();
}
}, { once: true });
modal.show();
return modal;
} }
async showBrands() { async showBrands() {
@@ -733,14 +657,14 @@ class VehicleDashboard {
} }
showFilters() { showFilters() {
document.getElementById('filtersBar').style.display = 'block'; document.getElementById('filtersBar').classList.add('visible');
// Reset filters // Reset filters
document.getElementById('yearFilter').value = ''; document.getElementById('yearFilter').value = '';
document.getElementById('engineFilter').value = ''; document.getElementById('engineFilter').value = '';
} }
hideFilters() { hideFilters() {
document.getElementById('filtersBar').style.display = 'none'; document.getElementById('filtersBar').classList.remove('visible');
} }
async goToCategories(myeId) { async goToCategories(myeId) {
@@ -956,7 +880,9 @@ class VehicleDashboard {
throw new Error('Error al cargar partes'); throw new Error('Error al cargar partes');
} }
this.allParts = await response.json(); const partsData = await response.json();
// Handle paginated response
this.allParts = partsData.data || partsData;
this.displayParts(); this.displayParts();
} catch (error) { } catch (error) {
@@ -1183,8 +1109,8 @@ class VehicleDashboard {
// FASE 2/4: Search by part number or general search // FASE 2/4: Search by part number or general search
async searchPartNumber() { async searchPartNumber() {
const searchInput = document.getElementById('partNumberSearch'); const searchInputEl = document.getElementById('searchInput');
const searchTerm = searchInput.value.trim(); const searchTerm = searchInputEl.value.trim();
if (!searchTerm) { if (!searchTerm) {
return; return;

File diff suppressed because it is too large Load Diff