/* ════════════════════════════════ 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 subActiva = []; // [nivel0, nivel1, ...] — null = "Todos" en ese nivel 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}

`; } } // ════════════════════════════════ // HELPERS DE FILTRADO POR SUBRAMA // ════════════════════════════════ // Devuelve los productos que matchean la rama actual (cat + sub[niveles anteriores]) // y cuyo sub[i] === id. Sirve para contar/mostrar subcats con productos. function productosEnNivel(catId, nivelesPrevios, idNivel) { return productos.filter(p => { if (!p || p.cat !== catId) return false; if (p.img && p.img.startsWith('agotados/')) return false; // El sub del producto debe tener al menos la longitud de nivelesPrevios + 1 const sub = Array.isArray(p.sub) ? p.sub : []; if (sub.length < nivelesPrevios.length + 1) return false; // Coincidir con los niveles previos for (let i = 0; i < nivelesPrevios.length; i++) { if (sub[i] !== nivelesPrevios[i]) return false; } // El nivel actual debe ser este id return sub[nivelesPrevios.length] === idNivel; }); } // Devuelve el nodo del árbol (categoría o subcat) según el path function getNodo(catId, niveles) { const cat = categorias.find(c => c.id === catId); if (!cat) return null; let nodo = cat; for (const id of niveles) { if (!Array.isArray(nodo.subcategorias)) return null; const sig = nodo.subcategorias.find(s => s.id === id); if (!sig) return null; nodo = sig; } return nodo; } // ════════════════════════════════ // RENDER TAGS FILTROS (multinivel) // ════════════════════════════════ function renderTags() { const cont = document.getElementById('filtrosTags'); cont.innerHTML = ''; // ——— Nivel 0: Categorías ——— const catsVisibles = categorias.filter(c => c.id !== 'sin-categoria'); const nivel0 = document.createElement('div'); nivel0.className = 'filtros-tags filtros-nivel'; nivel0.innerHTML = `` + catsVisibles.map(cat => { const tieneSubcats = Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0; // Solo mostrar la categoría como tag si tiene productos directos O tiene subcats con productos if (tieneSubcats) { // No renderizar la categoría como "hoja" — sus subcats la cubren // PERO si alguna subcat es hoja con productos, sí tiene sentido mostrarla igual // porque al hacer click se entra al nivel 1 } return ``; }).join(''); cont.appendChild(nivel0); // ——— Niveles 1+ : subcategorías (solo si catActiva está elegida) ——— if (catActiva !== 'todos') { const cat = categorias.find(c => c.id === catActiva); if (cat && Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0) { renderNivelSub(cont, cat, [], 1); } } // Wire click handlers cont.querySelectorAll('.tag').forEach(tag => { tag.addEventListener('click', () => onTagClick(parseInt(tag.dataset.nivel, 10), tag.dataset.id)); }); } // Renderiza un nivel de subcategorías. `previos` = niveles ya elegidos arriba. function renderNivelSub(cont, nodo, previos, nivel) { if (!Array.isArray(nodo.subcategorias) || nodo.subcategorias.length === 0) return; // Subcats que tienen productos en este nivel o cuyas sub-subs tienen productos const subsVisibles = nodo.subcategorias.filter(s => { const tieneSubSub = Array.isArray(s.subcategorias) && s.subcategorias.length > 0; if (tieneSubSub) { // Esta subcat NO recibe productos directamente. Mostrar solo si ALGUNA // de sus sub-subs tiene productos. return s.subcategorias.some(ss => productosEnNivel(catActiva, [...previos, s.id], ss.id).length > 0 ); } else { // Hoja: recibe productos. Mostrar si tiene al menos uno. return productosEnNivel(catActiva, [...previos], s.id).length > 0; } }); if (subsVisibles.length === 0) return; const nivelEl = document.createElement('div'); nivelEl.className = 'filtros-tags filtros-nivel'; const idTodos = `__all_${nivel}`; nivelEl.innerHTML = `${escapeHtml(nodo.nombre || '')}` + `` + subsVisibles.map(s => { const esActivo = subActiva[nivel - 1] === s.id; return ``; }).join(''); cont.appendChild(nivelEl); // Si la subcat activa tiene sub-subs, renderizar el siguiente nivel if (subActiva[nivel - 1]) { const subActivaNodo = nodo.subcategorias.find(s => s.id === subActiva[nivel - 1]); if (subActivaNodo && Array.isArray(subActivaNodo.subcategorias) && subActivaNodo.subcategorias.length > 0) { renderNivelSub(cont, subActivaNodo, [...previos, subActiva[nivel - 1]], nivel + 1); } } } function onTagClick(nivel, id) { if (nivel === 0) { if (id === 'todos') { catActiva = 'todos'; } else { catActiva = id; } subActiva = []; // reset sub al cambiar categoría } else { if (id.startsWith('__all_')) { subActiva[nivel - 1] = null; } else { subActiva[nivel - 1] = id; } // Resetear niveles más profundos subActiva = subActiva.slice(0, nivel); // Rellenar con null hasta el nivel actual while (subActiva.length < nivel) subActiva.push(null); } renderTags(); renderProductos(); } function sincronizarTag(cat) { // Legacy — mantenido por compatibilidad, pero ya no se usa 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'); let filtrados = productos.filter(p => p.cat && p.cat !== 'sin-categoria' && (!p.img || !p.img.startsWith('agotados/')) ); if (catActiva !== 'todos') { filtrados = filtrados.filter(p => { if (p.cat !== catActiva) return false; // Validar que la cadena de sub coincida const sub = Array.isArray(p.sub) ? p.sub : []; for (let i = 0; i < subActiva.length; i++) { if (subActiva[i] != null && sub[i] !== subActiva[i]) return false; } return true; }); } 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); 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')}` : '—'; 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(); });