import { API_URL, AUTO_REFRESH_MS, BLOODBROS_LOGO_URL, WHATSAPP_PHONE, CREATE_CHECKOUT_URL } from './config.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 fmtPrice(product) { if (product.europe_price_text) return product.europe_price_text; const n = Number(product.europe_price_number); if (Number.isFinite(n)) { return new Intl.NumberFormat('ca-ES', { style: 'currency', currency: 'EUR' }).format(n); } return '-'; } 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 normalizeText(v) { return String(v ?? '').toLowerCase().trim(); } function parseDescription(description) { const raw = String(description ?? '').trim(); if (!raw) return []; const lines = raw .split(/\n|\r|\||•|;/g) .map(s => s.trim()) .filter(Boolean); return lines; } function getStockState(stock) { const n = Number(stock || 0); if (n <= 1) { return { cls: 'stock-last', label: '🔥 Última unitat' }; } if (n <= 3) { return { cls: 'stock-low', label: `⚠️ Queden poques unitats · ${n} disponibles` }; } return { cls: 'stock-ok', label: `Stock disponible · ${n} unitats` }; } function getSalesHook(product) { if (product.top_vendes) { return null; } const stock = Number(product.stock || 0); if (stock <= 1) { return { badge: 'Última oportunitat', text: 'Aquest model està a punt d’esgotar-se.' }; } if (stock <= 3) { return { badge: 'Alta demanda', text: 'Model amb poques unitats disponibles.' }; } return { badge: 'Selecció premium', text: 'Una aposta segura per estil i rendiment.' }; } function buildWhatsappLink(product) { const text = [ 'Hola Blood Bros Sports,', 'voldria encomanar aquest model:', `${product.product_code || ''}`, product.model ? `Model: ${product.model}` : '', product.category ? `Família: ${product.category}` : '', product.colors ? `Colors: ${product.colors}` : '', Number(product.europe_price_number) ? `Preu: ${fmtPrice(product)}` : '', `Stock visible: ${product.stock ?? ''}` ].filter(Boolean).join('\n'); if (WHATSAPP_PHONE) { return `https://wa.me/${encodeURIComponent(WHATSAPP_PHONE)}?text=${encodeURIComponent(text)}`; } return `https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`; } function getFilteredProducts() { const q = normalizeText(els.searchInput.value); const top = els.topFilter.value; const sort = els.sortFilter.value; const price = els.priceFilter.value; let data = products.filter(p => Number(p.stock) >= 1); if (q) { data = data.filter(p => { const hay = [p.product_code, p.model, p.category, p.colors, p.description] .map(normalizeText).join(' '); return hay.includes(q); }); } if (top === 'top') data = data.filter(p => !!p.top_vendes); if (top === 'normal') data = data.filter(p => !p.top_vendes); if (price !== 'all') { data = data.filter(p => { const n = Number(p.europe_price_number); if (!Number.isFinite(n)) return false; if (price === '0-60') return n <= 60; if (price === '60-80') return n > 60 && n <= 80; if (price === '80+') return n > 80; return true; }); } data.sort((a, b) => { const codeA = String(a.product_code || ''); const codeB = String(b.product_code || ''); const priceA = Number.isFinite(Number(a.europe_price_number)) ? Number(a.europe_price_number) : -Infinity; const priceB = Number.isFinite(Number(b.europe_price_number)) ? Number(b.europe_price_number) : -Infinity; const stockA = Number(a.stock || 0); const stockB = Number(b.stock || 0); switch (sort) { case 'code-desc': return codeB.localeCompare(codeA, 'ca'); case 'price-asc': return priceA - priceB; case 'price-desc': return priceB - priceA; case 'stock-desc': return stockB - stockA; default: return codeA.localeCompare(codeB, 'ca'); } }); return data; } 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(); 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);