Solo items y usuarios
This commit is contained in:
+211
@@ -0,0 +1,211 @@
|
||||
/* ════════════════════════════════
|
||||
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, '"')
|
||||
.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 = `
|
||||
<div class="error-state">
|
||||
<p>😕 No pudimos cargar el catálogo</p>
|
||||
<p class="error-detalle">${err.message}</p>
|
||||
<button class="btn btn-primary" onclick="location.reload()">Reintentar</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
// 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 =
|
||||
`<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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 `
|
||||
<article class="producto-card" style="animation-delay: ${i * 0.04}s">
|
||||
<div class="producto-img">
|
||||
<span class="producto-cat">${cat ? cat.nombre : p.cat}</span>
|
||||
<img src="${escapeHtml(imgSrc)}" alt="${escapeHtml(nombre)}" loading="lazy"
|
||||
onerror="this.remove(); this.parentElement.classList.add('img-fallback');">
|
||||
<span class="producto-fallback">${cat ? cat.icono : '📦'}</span>
|
||||
</div>
|
||||
<div class="producto-info">
|
||||
<h3 class="producto-nombre">${escapeHtml(nombre)}</h3>
|
||||
${desc ? `<p class="producto-desc">${escapeHtml(desc)}</p>` : ''}
|
||||
<div class="producto-precio">${precio}</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).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();
|
||||
});
|
||||
Reference in New Issue
Block a user