Tot funcionant al 100% i amb Looker
This commit is contained in:
@@ -2,7 +2,8 @@ import {
|
||||
API_URL,
|
||||
AUTO_REFRESH_MS,
|
||||
BLOODBROS_LOGO_URL,
|
||||
CREATE_CHECKOUT_URL
|
||||
CREATE_CHECKOUT_URL,
|
||||
TRACK_EVENT_URL
|
||||
} from './config.js';
|
||||
|
||||
import {
|
||||
@@ -13,297 +14,509 @@ import {
|
||||
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;
|
||||
const SHIPPING_PRICE = 7.99;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
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 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 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 renderDescription(product) {
|
||||
const items = parseDescription(product.description);
|
||||
if (items.length) {
|
||||
return `
|
||||
<div class="desc-box">
|
||||
<div class="desc-title">Característiques</div>
|
||||
<ul class="desc-list">${items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="desc-box">
|
||||
<div class="desc-title">Característiques</div>
|
||||
<p class="desc-plain">Sense descripció disponible.</p>
|
||||
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 `
|
||||
<div class="desc-box">
|
||||
<div class="desc-title">Característiques</div>
|
||||
<ul class="desc-list">${items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="desc-box">
|
||||
<div class="desc-title">Característiques</div>
|
||||
<p class="desc-plain">Sense descripció disponible.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<img src="${escapeHtml(product.image_url)}" alt="${escapeHtml(product.product_code || product.model || 'Producte')}" loading="lazy" referrerpolicy="no-referrer">`
|
||||
: `<div class="media-placeholder"><strong>${escapeHtml(product.product_code || 'Sense codi')}</strong><div>Sense imatge definida a IMAGE_URL.</div></div>`;
|
||||
|
||||
const stockState = getStockState(product.stock);
|
||||
const salesHook = getSalesHook(product);
|
||||
|
||||
card.innerHTML = `
|
||||
${product.top_vendes ? '<div class="top-badge">Top vendes</div>' : ''}
|
||||
<div class="card-media">
|
||||
<div class="media-stage" data-image="${escapeHtml(product.image_url || '')}">
|
||||
${imageHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<img src="${escapeHtml(product.image_url)}" alt="${escapeHtml(product.product_code || product.model || 'Producte')}" loading="lazy" referrerpolicy="no-referrer">`
|
||||
: `<div class="media-placeholder"><strong>${escapeHtml(product.product_code || 'Sense codi')}</strong><div>Sense imatge definida a IMAGE_URL.</div></div>`;
|
||||
|
||||
const stockState = getStockState(product.stock);
|
||||
const salesHook = getSalesHook(product);
|
||||
|
||||
card.innerHTML = `
|
||||
${product.top_vendes ? '<div class="top-badge">Top vendes</div>' : ''}
|
||||
<div class="card-media">
|
||||
<div class="media-stage" data-image="${escapeHtml(product.image_url || '')}">
|
||||
${imageHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="media-actions">
|
||||
<button class="action-btn action-view js-view" type="button" data-image="${escapeHtml(product.image_url || '')}">🔎 Veure gran</button>
|
||||
<button class="action-btn action-buy js-buy" type="button"
|
||||
data-code="${escapeHtml(product.product_code || '')}"
|
||||
data-name="${escapeHtml(product.product_code || '')}"
|
||||
data-price="${escapeHtml(product.europe_price_number || 0)}"
|
||||
data-stock="${escapeHtml(product.stock || 0)}"
|
||||
data-image="${escapeHtml(product.image_url || '')}">💳 Comprar ara</button>
|
||||
<a class="action-btn action-wa" href="${escapeHtml(buildWhatsappLink(product))}" target="_blank" rel="noopener noreferrer"><span class="wa-icon" aria-hidden="true"></span>WhatsApp</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title-row">
|
||||
<div class="title-stack">
|
||||
<div class="product-code">${escapeHtml(product.product_code || '')}</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="media-actions">
|
||||
<button class="action-btn action-view js-view" type="button" data-image="${escapeHtml(product.image_url || '')}">🔎 Veure gran</button>
|
||||
<button class="action-btn action-buy js-buy" type="button"
|
||||
data-code="${escapeHtml(product.product_code || '')}"
|
||||
data-name="${escapeHtml(product.product_code || '')}"
|
||||
data-price="${escapeHtml(product.europe_price_number || 0)}"
|
||||
data-stock="${escapeHtml(product.stock || 0)}">💳 Comprar ara</button>
|
||||
<a class="action-btn action-wa" href="${escapeHtml(buildWhatsappLink(product))}" target="_blank" rel="noopener noreferrer"><span class="wa-icon" aria-hidden="true"></span>WhatsApp</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-stock">
|
||||
<div class="price-box">
|
||||
<div class="price-label">Preu</div>
|
||||
<div class="price">${escapeHtml(fmtPrice(product))}</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title-row">
|
||||
<div class="title-stack">
|
||||
<div class="product-code">${escapeHtml(product.product_code || '')}</div>
|
||||
<div class="stock-pill ${stockState.cls}">${escapeHtml(stockState.label)}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
${salesHook ? `
|
||||
<div class="sales-hook">
|
||||
<span class="sales-hook-badge">${escapeHtml(salesHook.badge)}</span>
|
||||
</div>` : ''}
|
||||
|
||||
</div>
|
||||
${chips.length ? `<div class="chips">${chips.map(c => `<span class="chip">${escapeHtml(c)}</span>`).join('')}</div>` : ''}
|
||||
${renderDescription(product)}
|
||||
|
||||
<div class="price-stock">
|
||||
<div class="price-box">
|
||||
<div class="price-label">Preu</div>
|
||||
<div class="price">${escapeHtml(fmtPrice(product))}</div>
|
||||
</div>
|
||||
<div class="stock-pill ${stockState.cls}">${escapeHtml(stockState.label)}</div>
|
||||
</div>
|
||||
<div class="footer-row">
|
||||
<div class="footer-box"><strong>Família</strong><span>${escapeHtml(product.category || 'No indicada')}</span></div>
|
||||
<div class="footer-box"><strong>Vidre</strong><span>${escapeHtml(product.colors || 'No indicats')}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
${salesHook ? `
|
||||
<div class="sales-hook">
|
||||
<span class="sales-hook-badge">${escapeHtml(salesHook.badge)}</span>
|
||||
</div>` : ''}
|
||||
frag.appendChild(card);
|
||||
});
|
||||
|
||||
${chips.length ? `<div class="chips">${chips.map(c => `<span class="chip">${escapeHtml(c)}</span>`).join('')}</div>` : ''}
|
||||
${renderDescription(product)}
|
||||
els.cards.appendChild(frag);
|
||||
|
||||
<div class="footer-row">
|
||||
<div class="footer-box"><strong>Família</strong><span>${escapeHtml(product.category || 'No indicada')}</span></div>
|
||||
<div class="footer-box"><strong>Vidre</strong><span>${escapeHtml(product.colors || 'No indicats')}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
frag.appendChild(card);
|
||||
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.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 = `<strong>${escapeHtml(product.name)}</strong><br>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.modalImage.src = src;
|
||||
els.modal.classList.add('open');
|
||||
els.modal.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
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'
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
buyEls.cancel.addEventListener('click', closeBuyModal);
|
||||
buyEls.modal.addEventListener('click', (e) => {
|
||||
if (e.target === buyEls.modal) closeBuyModal();
|
||||
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'
|
||||
});
|
||||
|
||||
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';
|
||||
}
|
||||
const response = await fetch(CREATE_CHECKOUT_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
initLogo();
|
||||
loadData();
|
||||
setInterval(loadData, AUTO_REFRESH_MS);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user