import { API_URL, AUTO_REFRESH_MS, BLOODBROS_LOGO_URL, CREATE_CHECKOUT_URL } from './config.js'; import { fmtPrice, parseDescription, getStockState, getSalesHook, buildWhatsappLink, getFilteredProducts } from './catalog.js'; const els = { cards: document.getElementById('cards'), errorBox: document.getElementById('errorBox'), errorText: document.getElementById('errorText'), emptyBox: document.getElementById('emptyBox'), statVisible: document.getElementById('statVisible'), statTop: document.getElementById('statTop'), statUpdated: document.getElementById('statUpdated'), searchInput: document.getElementById('searchInput'), topFilter: document.getElementById('topFilter'), sortFilter: document.getElementById('sortFilter'), priceFilter: document.getElementById('priceFilter'), refreshBtn: document.getElementById('refreshBtn'), modal: document.getElementById('imageModal'), modalImage: document.getElementById('modalImage'), modalClose: document.getElementById('modalClose'), brandLogo: document.getElementById('brandLogo'), brandFallback: document.getElementById('brandFallback') }; let products = []; let lastFetchAt = null; let lastUpdatedAt = null; function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function fmtTime(value) { if (!value) return '--:--'; const d = new Date(value); if (Number.isNaN(d.getTime())) return '--:--'; return d.toLocaleTimeString('ca-ES', { hour: '2-digit', minute: '2-digit' }); } function renderDescription(product) { const items = parseDescription(product.description); if (items.length) { return `
Característiques
`; } return `
Característiques

Sense descripció disponible.

`; } function render() { const data = getFilteredProducts(products, { search: els.searchInput.value, top: els.topFilter.value, sort: els.sortFilter.value, price: els.priceFilter.value }); els.cards.innerHTML = ''; els.errorBox.style.display = 'none'; els.emptyBox.style.display = data.length ? 'none' : 'block'; const totalTop = data.filter(p => !!p.top_vendes).length; els.statVisible.textContent = data.length; els.statTop.textContent = totalTop; els.statUpdated.textContent = fmtTime(lastUpdatedAt || lastFetchAt); if (!data.length) return; const frag = document.createDocumentFragment(); data.forEach(product => { const card = document.createElement('article'); card.className = 'card'; const chips = []; if (product.colors) chips.push(product.colors); const imageHtml = product.image_url ? `${escapeHtml(product.product_code || product.model || 'Producte')}` : `
${escapeHtml(product.product_code || 'Sense codi')}
Sense imatge definida a IMAGE_URL.
`; const stockState = getStockState(product.stock); const salesHook = getSalesHook(product); card.innerHTML = ` ${product.top_vendes ? '
Top vendes
' : ''}
${imageHtml}
WhatsApp
${escapeHtml(product.product_code || '')}
Preu
${escapeHtml(fmtPrice(product))}
${escapeHtml(stockState.label)}
${salesHook ? `
${escapeHtml(salesHook.badge)}
` : ''} ${chips.length ? `
${chips.map(c => `${escapeHtml(c)}`).join('')}
` : ''} ${renderDescription(product)}
`; frag.appendChild(card); }); els.cards.appendChild(frag); document.querySelectorAll('.js-view, .media-stage').forEach(node => { node.addEventListener('click', () => { const src = node.getAttribute('data-image'); if (!src) return; els.modalImage.src = src; els.modal.classList.add('open'); els.modal.setAttribute('aria-hidden', 'false'); }); }); } async function loadData() { els.errorBox.style.display = 'none'; try { const response = await fetch(API_URL, { method: 'GET', cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const json = await response.json(); if (!json || json.ok !== true || !Array.isArray(json.products)) { throw new Error('Resposta JSON invàlida'); } products = json.products; lastFetchAt = new Date().toISOString(); lastUpdatedAt = json.updated_at || lastFetchAt; render(); } catch (err) { products = []; lastFetchAt = new Date().toISOString(); lastUpdatedAt = null; els.errorText.textContent = `No s'ha pogut llegir l'endpoint JSON. Detall: ${err.message}`; els.errorBox.style.display = 'block'; render(); } } const buyEls = { modal: document.getElementById('buyModal'), form: document.getElementById('buyForm'), summary: document.getElementById('buySummary'), error: document.getElementById('buyError'), cancel: document.getElementById('buyCancel'), }; function openBuyModal(product) { buyEls.form.product_code.value = product.code; buyEls.form.product_name.value = product.name; buyEls.form.price.value = product.price; buyEls.summary.innerHTML = `${escapeHtml(product.name)}
Preu: ${escapeHtml(String(product.price).replace('.', ',') + ' €')} · Stock visible: ${escapeHtml(product.stock)}`; buyEls.error.style.display = 'none'; buyEls.error.textContent = ''; buyEls.modal.classList.add('open'); buyEls.modal.setAttribute('aria-hidden', 'false'); } function closeBuyModal() { buyEls.modal.classList.remove('open'); buyEls.modal.setAttribute('aria-hidden', 'true'); } function initLogo() { if (!BLOODBROS_LOGO_URL) return; els.brandLogo.onload = () => { els.brandLogo.style.display = 'block'; els.brandFallback.style.display = 'none'; }; els.brandLogo.onerror = () => { els.brandLogo.style.display = 'none'; els.brandFallback.style.display = 'flex'; }; els.brandLogo.src = BLOODBROS_LOGO_URL; } [els.searchInput, els.topFilter, els.sortFilter, els.priceFilter].forEach(el => { el.addEventListener('input', render); el.addEventListener('change', render); }); els.refreshBtn.addEventListener('click', loadData); els.modalClose.addEventListener('click', () => { els.modal.classList.remove('open'); els.modal.setAttribute('aria-hidden', 'true'); els.modalImage.src = ''; }); els.modal.addEventListener('click', (e) => { if (e.target === els.modal) { els.modal.classList.remove('open'); els.modal.setAttribute('aria-hidden', 'true'); els.modalImage.src = ''; } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { els.modal.classList.remove('open'); els.modal.setAttribute('aria-hidden', 'true'); els.modalImage.src = ''; closeBuyModal(); } }); document.addEventListener('click', (e) => { const btn = e.target.closest('.js-buy'); if (!btn) return; openBuyModal({ code: btn.getAttribute('data-code') || '', name: btn.getAttribute('data-name') || '', price: btn.getAttribute('data-price') || '0', stock: btn.getAttribute('data-stock') || '0' }); }); buyEls.cancel.addEventListener('click', closeBuyModal); buyEls.modal.addEventListener('click', (e) => { if (e.target === buyEls.modal) closeBuyModal(); }); buyEls.form.addEventListener('submit', async (e) => { e.preventDefault(); buyEls.error.style.display = 'none'; const payload = Object.fromEntries(new FormData(buyEls.form).entries()); try { const response = await fetch(CREATE_CHECKOUT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await response.json(); if (!response.ok || !json.ok || !json.checkout_url) { throw new Error(json.error || 'No s\'ha pogut crear el checkout'); } window.location.href = json.checkout_url; } catch (err) { buyEls.error.textContent = err.message || 'Error inesperat'; buyEls.error.style.display = 'block'; } }); initLogo(); loadData(); setInterval(loadData, AUTO_REFRESH_MS);