import { API_URL, AUTO_REFRESH_MS, BLOODBROS_LOGO_URL, CREATE_CHECKOUT_URL, TRACK_EVENT_URL } from './config.js'; import { fmtPrice, parseDescription, getStockState, getSalesHook, buildWhatsappLink, getFilteredProducts } from './catalog.js'; const SHIPPING_PRICE = 7.99; 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; let pageViewTracked = false; function getSessionId() { const key = 'kapvoe_session_id'; let sessionId = localStorage.getItem(key); if (!sessionId) { sessionId = `sess_${Math.random().toString(36).slice(2)}${Date.now()}`; localStorage.setItem(key, sessionId); } return sessionId; } function getPageType() { const path = window.location.pathname.toLowerCase(); if (path.includes('top-vendes')) return 'top-vendes'; if (path.includes('checkout')) return 'checkout'; return 'catalog'; } function getDeviceType() { const ua = navigator.userAgent || ''; if (/tablet|ipad/i.test(ua)) return 'tablet'; if (/mobile|android|iphone|ipod/i.test(ua)) return 'mobile'; return 'desktop'; } function getUtmParams() { const params = new URLSearchParams(window.location.search); return { utm_source: params.get('utm_source') || '', utm_medium: params.get('utm_medium') || '', utm_campaign: params.get('utm_campaign') || '' }; } function buildAnalyticsPayload(eventType, extra = {}) { const utm = getUtmParams(); return { event_type: eventType, session_id: getSessionId(), page_url: window.location.href, referrer: document.referrer || '', user_agent: navigator.userAgent || '', device_type: getDeviceType(), page_type: getPageType(), ...utm, ...extra }; } async function trackEvent(eventType, extra = {}) { try { await fetch(TRACK_EVENT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildAnalyticsPayload(eventType, extra)), keepalive: true }); } catch (_) { // Ignore tracking failures to avoid affecting the storefront UX. } } 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; const card = node.closest('.card'); const code = card?.querySelector('.product-code')?.textContent?.trim() || ''; trackEvent('view_large', { product_code: code }); els.modalImage.src = src; els.modal.classList.add('open'); els.modal.setAttribute('aria-hidden', 'false'); }); }); document.querySelectorAll('.action-wa').forEach(node => { node.addEventListener('click', () => { const card = node.closest('.card'); const code = card?.querySelector('.product-code')?.textContent?.trim() || ''; trackEvent('whatsapp_click', { product_code: code }); }); }); } 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(); if (!pageViewTracked) { pageViewTracked = true; trackEvent('page_view'); } } 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'), summaryImage: document.getElementById('buySummaryImage'), summaryCode: document.getElementById('buySummaryCode'), summaryMeta: document.getElementById('buySummaryMeta'), error: document.getElementById('buyError'), cancel: document.getElementById('buyCancel'), shippingToggle: document.getElementById('shippingToggle'), shippingFields: document.getElementById('shippingFields'), subtotal: document.getElementById('buySubtotal'), shippingCost: document.getElementById('buyShippingCost'), totalAmount: document.getElementById('buyTotalAmount') }; function formatEuro(value) { const n = Number(value || 0); return new Intl.NumberFormat('ca-ES', { style: 'currency', currency: 'EUR' }).format(n); } function updateBuyTotals() { const price = Number(buyEls.form.price.value || 0); const quantity = Number(buyEls.form.quantity.value || 1); const shippingRequested = !!buyEls.shippingToggle.checked; const subtotal = price * quantity; const shipping = shippingRequested ? SHIPPING_PRICE : 0; const total = subtotal + shipping; buyEls.form.shipping_method.value = shippingRequested ? 'shipping' : 'pickup'; buyEls.subtotal.textContent = formatEuro(subtotal); buyEls.shippingCost.textContent = formatEuro(shipping); buyEls.totalAmount.textContent = formatEuro(total); buyEls.shippingFields.style.display = shippingRequested ? 'grid' : 'none'; ['address', 'postal_code', 'city', 'province'].forEach(name => { const field = buyEls.form.elements[name]; if (!field) return; field.required = shippingRequested; }); } function openBuyModal(product) { trackEvent('checkout_open', { product_code: product.code || '', model: product.name || '', price: String(product.price || ''), stock: String(product.stock || ''), quantity: String(product.quantity || 1) }); buyEls.form.product_code.value = product.code; buyEls.form.product_name.value = product.name; buyEls.form.price.value = product.price; buyEls.form.quantity.value = product.quantity || 1; buyEls.summaryCode.textContent = product.name || product.code || '-'; buyEls.summaryMeta.innerHTML = `Preu: ${formatEuro(product.price)} · Stock visible: ${escapeHtml(product.stock)}`; if (product.image) { buyEls.summaryImage.src = product.image; buyEls.summaryImage.alt = product.name || product.code || 'Producte'; buyEls.summaryImage.style.display = 'block'; } else { buyEls.summaryImage.removeAttribute('src'); buyEls.summaryImage.alt = ''; buyEls.summaryImage.style.display = 'none'; } buyEls.shippingToggle.checked = false; buyEls.error.style.display = 'none'; buyEls.error.textContent = ''; updateBuyTotals(); 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.topFilter.addEventListener('change', () => { trackEvent('filter_use', { category: `${els.topFilter.value}|${els.sortFilter.value}|${els.priceFilter.value}|${els.searchInput.value || ''}` }); }); els.sortFilter.addEventListener('change', () => { trackEvent('filter_use', { category: `${els.topFilter.value}|${els.sortFilter.value}|${els.priceFilter.value}|${els.searchInput.value || ''}` }); }); els.priceFilter.addEventListener('change', () => { trackEvent('filter_use', { category: `${els.topFilter.value}|${els.sortFilter.value}|${els.priceFilter.value}|${els.searchInput.value || ''}` }); }); els.refreshBtn.addEventListener('click', () => { trackEvent('refresh_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; trackEvent('buy_click', { product_code: btn.getAttribute('data-code') || '', model: btn.getAttribute('data-name') || '', price: btn.getAttribute('data-price') || '', stock: btn.getAttribute('data-stock') || '', quantity: '1' }); openBuyModal({ code: btn.getAttribute('data-code') || '', name: btn.getAttribute('data-name') || '', price: Number(btn.getAttribute('data-price') || 0), stock: btn.getAttribute('data-stock') || '0', image: btn.getAttribute('data-image') || '', quantity: 1 }); }); buyEls.shippingToggle.addEventListener('change', updateBuyTotals); 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()); const analyticsPayload = buildAnalyticsPayload('checkout_submit', { product_code: payload.product_code || '', model: payload.product_name || '', price: payload.price || '', quantity: payload.quantity || '1', shipping_method: payload.shipping_method || 'pickup' }); payload.analytics_session_id = analyticsPayload.session_id; payload.analytics_page_url = analyticsPayload.page_url; payload.analytics_referrer = analyticsPayload.referrer; payload.analytics_user_agent = analyticsPayload.user_agent; payload.analytics_utm_source = analyticsPayload.utm_source; payload.analytics_utm_medium = analyticsPayload.utm_medium; payload.analytics_utm_campaign = analyticsPayload.utm_campaign; payload.analytics_device_type = analyticsPayload.device_type; payload.analytics_page_type = analyticsPayload.page_type; try { trackEvent('checkout_submit', { product_code: payload.product_code || '', model: payload.product_name || '', price: payload.price || '', quantity: payload.quantity || '1', shipping_method: payload.shipping_method || 'pickup' }); 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);