pestana descargas y subcategorias funcionando

This commit is contained in:
2026-06-06 01:15:43 -05:00
parent 21889a3761
commit 3439e598b3
38 changed files with 1609 additions and 95 deletions
+149 -22
View File
@@ -18,6 +18,7 @@ function escapeHtml(str) {
.replace(/'/g, ''');
}
let catActiva = 'todos';
let subActiva = []; // [nivel0, nivel1, ...] — null = "Todos" en ese nivel
let busqueda = '';
let cargando = true;
@@ -56,29 +57,151 @@ async function cargarDatos() {
}
// ════════════════════════════════
// RENDER CATEGORÍAS
// HELPERS DE FILTRADO POR SUBRAMA
// ════════════════════════════════
// 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 =
`<button class="tag active" data-cat="todos">Todos</button>` +
visibles.map(cat => `<button class="tag" data-cat="${cat.id}"><span class="tag-icon">${cat.icono || '📦'}</span> ${cat.nombre}</button>`).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();
});
// 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 =
`<button class="tag ${catActiva === 'todos' ? 'active' : ''}" data-nivel="0" data-id="todos">Todos</button>` +
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 `<button class="tag ${catActiva === cat.id ? 'active' : ''}" data-nivel="0" data-id="${cat.id}">
<span class="tag-icon">${cat.icono || '📦'}</span> ${cat.nombre}
</button>`;
}).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 =
`<span class="filtros-nivel-label">${escapeHtml(nodo.nombre || '')}</span>` +
`<button class="tag ${subActiva[nivel - 1] == null ? 'active' : ''}" data-nivel="${nivel}" data-id="${idTodos}">Todos</button>` +
subsVisibles.map(s => {
const esActivo = subActiva[nivel - 1] === s.id;
return `<button class="tag ${esActivo ? 'active' : ''}" data-nivel="${nivel}" data-id="${s.id}">
<span class="tag-icon">${s.icono || '📂'}</span> ${escapeHtml(s.nombre)}
</button>`;
}).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);
});
@@ -92,8 +215,6 @@ function renderProductos() {
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' &&
@@ -101,7 +222,15 @@ function renderProductos() {
);
if (catActiva !== 'todos') {
filtrados = filtrados.filter(p => p.cat === catActiva);
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()) {
@@ -128,13 +257,11 @@ function renderProductos() {
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 `
<article class="producto-card" style="animation-delay: ${i * 0.04}s">