/* ════════════════════════════════ INFINITY — APP.JS Carga datos desde el backend (/api/*) ════════════════════════════════ */ // ════════════════════════════════ // ESTADO // ════════════════════════════════ let categorias = []; let productos = []; function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } let catActiva = 'todos'; let busqueda = ''; let cargando = true; // ════════════════════════════════ // CARGA DE DATOS // ════════════════════════════════ async function cargarDatos() { try { const [resCats, resProds] = await Promise.all([ fetch('/api/categorias'), fetch('/api/productos') ]); if (!resCats.ok) throw new Error('No se pudieron cargar las categorías'); if (!resProds.ok) throw new Error('No se pudieron cargar los productos'); categorias = await resCats.json(); productos = await resProds.json(); cargando = false; renderTags(); renderProductos(); } catch (err) { console.error('Error cargando datos:', err); cargando = false; const grid = document.getElementById('productos'); const cont = document.getElementById('contador'); cont.textContent = ''; grid.innerHTML = `

😕 No pudimos cargar el catálogo

${err.message}

`; } } // ════════════════════════════════ // RENDER CATEGORÍAS // ════════════════════════════════ // RENDER TAGS FILTROS // ════════════════════════════════ function renderTags() { const cont = document.getElementById('filtrosTags'); // Filtrar la categoría del sistema "sin-categoria" de los tags const visibles = categorias.filter(c => c.id !== 'sin-categoria'); cont.innerHTML = `` + visibles.map(cat => ``).join(''); cont.querySelectorAll('.tag').forEach(tag => { tag.addEventListener('click', () => { catActiva = tag.dataset.cat; cont.querySelectorAll('.tag').forEach(t => t.classList.remove('active')); tag.classList.add('active'); renderProductos(); }); }); } function sincronizarTag(cat) { document.querySelectorAll('.tag').forEach(t => { t.classList.toggle('active', t.dataset.cat === cat); }); } // ════════════════════════════════ // RENDER PRODUCTOS // ════════════════════════════════ function renderProductos() { const grid = document.getElementById('productos'); const empty = document.getElementById('emptyState'); const contador = document.getElementById('contador'); // Filtrar productos sin categoría (sin-categoria) y productos AGOTADOS // (imagen movida a la carpeta "agotados/") — no se muestran en el catálogo público. let filtrados = productos.filter(p => p.cat && p.cat !== 'sin-categoria' && (!p.img || !p.img.startsWith('agotados/')) ); if (catActiva !== 'todos') { filtrados = filtrados.filter(p => p.cat === catActiva); } if (busqueda.trim()) { const q = busqueda.toLowerCase(); filtrados = filtrados.filter(p => (p.nombre || '').toLowerCase().includes(q) || (p.desc || '').toLowerCase().includes(q) || (p.cat || '').toLowerCase().includes(q) || (p.ref || '').toLowerCase().includes(q) ); } contador.textContent = filtrados.length === 1 ? 'Mostrando 1 producto' : `Mostrando ${filtrados.length} productos`; if (filtrados.length === 0) { grid.innerHTML = ''; empty.style.display = 'block'; return; } empty.style.display = 'none'; grid.innerHTML = filtrados.map((p, i) => { const cat = categorias.find(c => c.id === p.cat); // Fallbacks: si no hay nombre, usar la referencia; si no hay descripción, ocultar const nombre = p.nombre || p.ref || 'Sin nombre'; const desc = p.desc || ''; const precio = (p.precio !== undefined && p.precio !== null && p.precio !== '') ? `$${Number(p.precio).toLocaleString('es-CO')}` : '—'; // La ruta en el JSON es relativa a /Imagenes/ (ej: "bolsos/AF100.webp") const imgSrc = p.img && p.img.startsWith('/') ? p.img : `/Imagenes/${p.img}`; return `
${cat ? cat.nombre : p.cat} ${escapeHtml(nombre)} ${cat ? cat.icono : '📦'}

${escapeHtml(nombre)}

${desc ? `

${escapeHtml(desc)}

` : ''}
${precio}
`; }).join(''); } // ════════════════════════════════ // BUSCADOR // ════════════════════════════════ function setupSearch() { const input = document.getElementById('searchInput'); const clear = document.getElementById('searchClear'); input.addEventListener('input', e => { busqueda = e.target.value; clear.style.display = busqueda ? 'flex' : 'none'; renderProductos(); }); clear.addEventListener('click', () => { input.value = ''; busqueda = ''; clear.style.display = 'none'; renderProductos(); input.focus(); }); document.getElementById('btnLimpiar').addEventListener('click', () => { input.value = ''; busqueda = ''; catActiva = 'todos'; sincronizarTag('todos'); clear.style.display = 'none'; renderProductos(); }); } // ════════════════════════════════ // NAVBAR MOBILE // ════════════════════════════════ function setupNav() { const toggle = document.getElementById('navToggle'); const links = document.getElementById('navLinks'); toggle.addEventListener('click', () => { links.classList.toggle('open'); }); links.querySelectorAll('a').forEach(a => { a.addEventListener('click', () => links.classList.remove('open')); }); } // ════════════════════════════════ // INIT // ════════════════════════════════ document.addEventListener('DOMContentLoaded', () => { setupSearch(); setupNav(); cargarDatos(); });