Files
Autoparts-DB/dashboard/landing.js
consultoria-as e95f7cf684 feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts
Major features:
- Pixel-Perfect glassmorphism design (landing + POS + public catalog)
- OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types)
- Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications)
- Peer-to-peer inventory (multi-instance, LAN discovery)
- WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations
- Smart unified search (VIN/plate/part_number/keyword auto-detect)
- Shop Supplies tab (vehicle-independent parts)
- Chatbot AI fallback chain (5 models) + response cache
- CSV inventory import tool + setup_instance.sh installer
- Tablet-responsive CSS + sidebar toggle
- Filters, export CSV, employee edit, business data save
- Quotation system (WA→POS) with auto-print on confirmation
- Live stats on landing page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:35:53 +00:00

395 lines
16 KiB
JavaScript

/**
* landing.js — Pixel-Perfect inspired interactions for Nexus Autoparts
*
* Features:
* - Animated canvas grid background (grid lines, stars at intersections, glowing tiles)
* - Scroll-reveal via IntersectionObserver
* - Counter animation on stat cards
* - Typewriter effect
* - Infinite brand marquee loader
* - Theme toggle with icon swap
*/
(function () {
'use strict';
// ═══════════════════════════════════════════════════════════════════════════
// THEME TOGGLE
// ═══════════════════════════════════════════════════════════════════════════
var themeIcon = document.getElementById('themeIcon');
function updateThemeIcon() {
var theme = document.documentElement.getAttribute('data-theme');
if (themeIcon) themeIcon.innerHTML = theme === 'industrial' ? '&#9790;' : '&#9728;';
}
window.toggleTheme = function () {
var html = document.documentElement;
var current = html.getAttribute('data-theme');
var next = current === 'industrial' ? 'modern' : 'industrial';
html.setAttribute('data-theme', next);
localStorage.setItem('nexus-theme', next);
updateThemeIcon();
// Canvas will pick up new CSS vars on next frame
};
updateThemeIcon();
// ═══════════════════════════════════════════════════════════════════════════
// CANVAS ANIMATED GRID
// ═══════════════════════════════════════════════════════════════════════════
var canvas = document.getElementById('heroCanvas');
if (canvas) {
var ctx = canvas.getContext('2d');
var CELL = 80;
var MAX_GLOWS = 6;
var glows = [];
var frame = 0;
function resizeCanvas() {
var hero = canvas.parentElement;
canvas.width = hero.offsetWidth;
canvas.height = hero.offsetHeight;
}
function getColor(varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
}
function drawStar(x, y, outer, inner, color) {
ctx.beginPath();
for (var i = 0; i < 4; i++) {
var angle = (Math.PI / 2) * i;
ctx.lineTo(x + Math.cos(angle) * outer, y + Math.sin(angle) * outer);
ctx.lineTo(x + Math.cos(angle + Math.PI / 4) * inner, y + Math.sin(angle + Math.PI / 4) * inner);
}
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function spawnGlow() {
var cols = Math.floor(canvas.width / CELL);
var rows = Math.floor(canvas.height / CELL);
glows.push({
col: Math.floor(Math.random() * cols),
row: Math.floor(Math.random() * rows),
life: 0,
maxLife: 90 + Math.random() * 60, // 1.5-2.5s at 60fps
});
}
function animateGrid() {
frame++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
var gridColor = getColor('--canvas-grid-color') || 'rgba(255,255,255,0.06)';
var starColor = getColor('--canvas-star-color') || 'rgba(245,166,35,0.3)';
var glowColor = getColor('--canvas-glow-color') || 'rgba(245,166,35,0.08)';
var cols = Math.floor(canvas.width / CELL) + 1;
var rows = Math.floor(canvas.height / CELL) + 1;
// Draw grid lines
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
for (var c = 0; c <= cols; c++) {
var x = c * CELL;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (var r = 0; r <= rows; r++) {
var y = r * CELL;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// Draw stars at intersections (every other one for performance)
for (c = 0; c <= cols; c += 2) {
for (r = 0; r <= rows; r += 2) {
drawStar(c * CELL, r * CELL, 6, 3, starColor);
}
}
// Spawn glowing tiles
if (glows.length < MAX_GLOWS && Math.random() < 0.03) {
spawnGlow();
}
// Draw glowing tiles
for (var i = glows.length - 1; i >= 0; i--) {
var g = glows[i];
g.life++;
var progress = g.life / g.maxLife;
var opacity = progress < 0.5 ? progress * 2 : (1 - progress) * 2;
ctx.fillStyle = glowColor;
ctx.globalAlpha = opacity;
ctx.fillRect(g.col * CELL + 1, g.row * CELL + 1, CELL - 2, CELL - 2);
// Subtle shadow glow
ctx.shadowColor = glowColor;
ctx.shadowBlur = 20;
ctx.fillRect(g.col * CELL + 1, g.row * CELL + 1, CELL - 2, CELL - 2);
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
if (g.life >= g.maxLife) glows.splice(i, 1);
}
requestAnimationFrame(animateGrid);
}
resizeCanvas();
animateGrid();
var resizeTimeout;
window.addEventListener('resize', function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 150);
});
}
// ═══════════════════════════════════════════════════════════════════════════
// SCROLL REVEAL — IntersectionObserver
// ═══════════════════════════════════════════════════════════════════════════
var revealEls = document.querySelectorAll('.nx-reveal, .nx-reveal-scale');
if ('IntersectionObserver' in window) {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.15, rootMargin: '0px 0px -40px 0px' });
revealEls.forEach(function (el) {
observer.observe(el);
});
} else {
// Fallback: show all immediately
revealEls.forEach(function (el) { el.classList.add('is-visible'); });
}
// ═══════════════════════════════════════════════════════════════════════════
// COUNTER ANIMATION — Stat cards
// ═══════════════════════════════════════════════════════════════════════════
function animateCounter(el, target, format, suffix, duration) {
var start = 0;
var startTime = null;
function step(timestamp) {
if (!startTime) startTime = timestamp;
var progress = Math.min((timestamp - startTime) / duration, 1);
// Ease out cubic
var eased = 1 - Math.pow(1 - progress, 3);
var current = Math.floor(eased * target);
if (format.indexOf('M') !== -1) {
// e.g. "1.5M" or "15.8M"
var decimals = target >= 10e6 ? 1 : 1;
el.textContent = (current / 1e6).toFixed(decimals) + 'M' + suffix;
} else if (format.indexOf('K') !== -1) {
// e.g. "85K" or "304K"
el.textContent = Math.floor(current / 1e3) + 'K' + suffix;
} else {
el.textContent = current + suffix;
}
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// Trigger counters when stats become visible
var statCards = document.querySelectorAll('.stat-card');
if (statCards.length && 'IntersectionObserver' in window) {
var statsObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var numEl = entry.target.querySelector('.number');
if (numEl && !numEl._animated) {
numEl._animated = true;
var target = parseInt(numEl.getAttribute('data-target'), 10) || 0;
var suffix = numEl.getAttribute('data-suffix') || '';
var format = numEl.getAttribute('data-format') || 'num';
animateCounter(numEl, target, format, suffix, 2000);
}
statsObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
statCards.forEach(function (card) { statsObserver.observe(card); });
}
// ═══════════════════════════════════════════════════════════════════════════
// TYPEWRITER EFFECT
// ═══════════════════════════════════════════════════════════════════════════
var typewriterEl = document.getElementById('typewriterText');
if (typewriterEl) {
var phrases = [
'POS + Inventario + CFDI 4.0 + Contabilidad',
'Catalogo TecDoc: 1.5M+ partes, 304K aftermarket',
'15.8M cross-references OEM ↔ aftermarket',
'Chatbot IA con voz, foto y diagnostico',
'WhatsApp Business integrado',
'Busca por VIN, placas o numero de parte',
'Marketplace B2B: bodegas ↔ talleres',
'PWA + Android + modo kiosko + offline',
];
var phraseIdx = 0;
var charIdx = 0;
var isDeleting = false;
var typingSpeed = 50;
function typeStep() {
var current = phrases[phraseIdx];
if (!isDeleting) {
typewriterEl.textContent = current.substring(0, charIdx + 1);
charIdx++;
if (charIdx >= current.length) {
isDeleting = true;
setTimeout(typeStep, 2000); // Pause before deleting
return;
}
setTimeout(typeStep, typingSpeed);
} else {
typewriterEl.textContent = current.substring(0, charIdx);
charIdx--;
if (charIdx < 0) {
isDeleting = false;
charIdx = 0;
phraseIdx = (phraseIdx + 1) % phrases.length;
setTimeout(typeStep, 400);
return;
}
setTimeout(typeStep, 30);
}
}
// Start after hero reveals
setTimeout(typeStep, 1200);
}
// ═══════════════════════════════════════════════════════════════════════════
// BRAND MARQUEE — Load from API + duplicate for seamless loop
// ═══════════════════════════════════════════════════════════════════════════
var marqueeContainer = document.getElementById('brandsMarquee');
function buildMarquee(brands) {
var html = '';
brands.forEach(function (b) {
html += '<a href="/catalog?brand=' + b.id_brand + '" class="brand-tag">' + escHtml(b.name_brand) + '</a>';
});
// Duplicate for seamless loop
marqueeContainer.innerHTML = html + html;
}
function escHtml(s) {
var d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
// Fallback brands in case API fails
var fallbackBrands = [
'Toyota', 'Nissan', 'Ford', 'Volkswagen', 'Honda', 'Chevrolet',
'Hyundai', 'Kia', 'Mazda', 'BMW', 'Mercedes-Benz', 'Audi',
'Renault', 'Jeep', 'Dodge', 'Ram', 'Subaru', 'Mitsubishi',
'Suzuki', 'Peugeot', 'Volvo', 'Fiat', 'Chrysler', 'Acura',
'Infiniti', 'Lexus', 'Lincoln', 'Buick', 'GMC', 'Cadillac',
'Porsche', 'Mini', 'Seat', 'Alfa Romeo', 'Land Rover', 'Jaguar'
];
fetch('/api/catalog/brands')
.then(function (r) { return r.json(); })
.then(function (brands) {
if (brands && brands.length > 0) {
buildMarquee(brands);
} else {
buildMarquee(fallbackBrands.map(function (n, i) { return { id_brand: i, name_brand: n }; }));
}
})
.catch(function () {
buildMarquee(fallbackBrands.map(function (n, i) { return { id_brand: i, name_brand: n }; }));
});
// ── Live hero stats from API ──
// The landing has 4 stat cards with data-format tags that identify them.
// The counter animation runs on whatever data-target is set at observe
// time, so we update all 4 targets BEFORE the IntersectionObserver fires.
fetch('/api/catalog/stats')
.then(function (r) { return r.json(); })
.then(function (d) {
// Maps data-format (the card's identifier) → the JSON key to pull.
var statMap = [
{ format: '1.5M', key: 'parts' }, // OEM parts
{ format: '304K', key: 'aftermarket_parts' }, // Aftermarket
{ format: '15.8M', key: 'cross_references' }, // Cross-refs
{ format: 'num', key: 'brands' }, // Brand count
];
statMap.forEach(function (s) {
var el = document.querySelector('[data-format="' + s.format + '"]');
var value = d[s.key];
if (el && typeof value === 'number' && value > 0) {
el.setAttribute('data-target', value);
// If the animation already ran (cached, instant DOM), snap to the
// real value so users see the live number instead of the stale
// hardcoded default.
if (el._animated) {
if (s.format.indexOf('M') !== -1) {
el.textContent = (value / 1e6).toFixed(1) + 'M+';
} else if (s.format.indexOf('K') !== -1) {
el.textContent = Math.floor(value / 1e3) + 'K+';
} else {
el.textContent = String(value);
}
}
}
});
})
.catch(function () { /* fallback: hardcoded data-target values stay */ });
// ═══════════════════════════════════════════════════════════════════════════
// SMOOTH SCROLL for nav links
// ═══════════════════════════════════════════════════════════════════════════
document.querySelectorAll('.header-nav a[href^="#"]').forEach(function (a) {
a.addEventListener('click', function (e) {
e.preventDefault();
var target = document.querySelector(a.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// ═══════════════════════════════════════════════════════════════════════════
// HEADER — Solid on scroll
// ═══════════════════════════════════════════════════════════════════════════
var header = document.querySelector('.site-header');
if (header) {
window.addEventListener('scroll', function () {
if (window.scrollY > 80) {
header.style.background = 'var(--glass-bg-strong)';
} else {
header.style.background = 'var(--glass-bg)';
}
}, { passive: true });
}
})();