"use strict"; // Contants: const _HASHBNDW = "gadnic_produccion_eds9l"; const _COMPANY = "gadnic_produccion"; const _PURLBNDW = window.location.protocol + "//s2.braindw.com"; const _NSURLBNDW = window.location.protocol + "//s2.braindw.com"; const _GRAPHQL_URL = "https://u2.vrein.ai/graphql"; const _TENANT_ID = "gadnic"; const _GUID = ''; const _BDW_GETGUID_SECRET = "9DIIDJ7DHDA8SDUA9SUOKDS2309.DJDJC.99DD8U3"; let _resolvedGuid = _GUID; let _resolvedGuidPromise = null; const bdwResolveGuid = async () => { // Un token sin reemplazar es truthy y viajaria como GUID basura. if (_resolvedGuid && _resolvedGuid.indexOf("{") !== -1) _resolvedGuid = ""; if (_resolvedGuid) return _resolvedGuid; if (_resolvedGuidPromise) return _resolvedGuidPromise; const storedGuid = localStorage.getItem("bdw_guid"); if (storedGuid) { _resolvedGuid = storedGuid; return _resolvedGuid; } // Cookie first-party escrita por el pixel del store. Cubre el caso en que el // servidor no pudo inyectar el GUID por tener bloqueada su cookie de tercero // (Safari/iOS con ITP, webviews), y evita pedir uno nuevo al pedo. const cookieMatch = /(^|;)\s*bdw_session=([^;]*)/.exec(document.cookie); if (cookieMatch) { _resolvedGuid = decodeURIComponent(cookieMatch[2]); if (_resolvedGuid) return _resolvedGuid; } const email = getEmail() || ""; const getGuidUrl = "https://s2.braindw.com/tracking/GetGuid" + "?HASH=" + encodeURIComponent(_HASHBNDW) + "¤tguid=" + "&url=" + encodeURIComponent(window.location.href + "/") + "&email=" + encodeURIComponent(email) + "×tamp=" + Date.now(); _resolvedGuidPromise = (async function () { try { const response = await fetch(getGuidUrl, { headers: { "Cache-Control": "no-store, no-cache, max-age=0", Pragma: "no-cache", "bdw-secretcode": _BDW_GETGUID_SECRET, bdw_secretcode: _BDW_GETGUID_SECRET, }, }); if (!response.ok) return ""; const text = await response.text(); const normalized = typeof text === "string" ? text.trim() : ""; // Asignar ANTES de guardar: al reves se persistia el valor viejo (vacio, // porque con valor se hubiera retornado antes), asi que el cache nunca // servia y cada carga volvia a pedir un GUID nuevo. _resolvedGuid = normalized || ""; if (_resolvedGuid) localStorage.setItem("bdw_guid", _resolvedGuid); return _resolvedGuid; } catch (e) { return ""; } finally { _resolvedGuidPromise = null; } })(); return _resolvedGuidPromise; }; // ===================== DATA LAYER HELPERS ===================== /** * Finds and returns the first object in window.dataLayer that contains a page_type field. * Returns null if dataLayer is not available or no page_type entry exists. */ const getDataLayerPageInfo = () => { if (!Array.isArray(window.dataLayer)) return null; return window.dataLayer.find((entry) => entry && typeof entry.page_type === "string") || null; }; /** * Maps window.dataLayer page_type to a BDW section name. * Returns null when the section cannot be determined from dataLayer. * * page_type mappings: * "home" → "Home" * "product" → "PDP" * "categoria" → "PLP" * "ofertas" → "PLP" * "busqueda" → "Search" | "SearchNoResult" (resolved by DOM result count) * "checkout" → "Checkout" * "carrito" → "Checkout" */ const getSectionFromDataLayer = () => { const pageInfo = getDataLayerPageInfo(); if (!pageInfo) return null; switch (pageInfo.page_type.toLowerCase()) { case "home": return "Home"; case "product": return "PDP"; case "category": case "ofertas": return "PLP"; case "busqueda": { const idSkus = pageInfo["id-skus"]; const ecomProdid = pageInfo["ecom_prodid"]; const hasResults = (Array.isArray(idSkus) && idSkus.length > 0) || (Array.isArray(ecomProdid) && ecomProdid.length > 0); return hasResults ? "Search" : "SearchNoResult"; } case "checkout": case "carrito": return "Checkout"; default: return null; } }; /** * Extracts and normalises product data from window.dataLayer for PDP pages. * Returns null if not on a product page or required fields are missing. */ const getDataLayerProduct = () => { const pageInfo = getDataLayerPageInfo(); if (!pageInfo || pageInfo.page_type !== "product" || !pageInfo.id) return null; return { id: String(pageInfo.id), sku: String(pageInfo.id), title: pageInfo.name || "", url: pageInfo.url || window.location.href, image: pageInfo.image || "", brand: pageInfo.brand || "", category: typeof producto != 'undefined' ? producto?.categoria_principal_producto || "" : "", // pageInfo.category || "", this commented until get clarity on category vs categorycode in dataLayer categoryName: pageInfo.category || ((typeof producto != 'undefined') ? producto?.categoria_principal_name : "")|| "", // here must be the name of the category, we use the id for now price: pageInfo.price != null ? String(pageInfo.price) : "", }; }; // ===================== END DATA LAYER HELPERS ===================== const SetBrainObject = () => { let data = JSON.parse(localStorage.getItem("vrein-info") || "{}"); if (!data || Object.keys(data).length === 0) { data = { filterSectionId: [], history: { productViewed: [], productAdded: [], searchedTerm: [], categoryViewed: [], collectionViewed: [], }, current: { product: {}, category: {}, collection: {}, searchTerm: "", cartOpen: [], cartClose: [], }, session: { name: "", email: getEmail() || "", branchOffice: "", sellerId: "", guid: "", country: "", state: "", city: "", ip: "", }, enable: 0, }; localStorage.setItem("vrein-info", JSON.stringify(data)); } return data; }; // page carrito const onRemoveProductFromPage = () => { const removeButtons = document.querySelectorAll("#resumen-cart-fijo a.delete-item-cart"); if (removeButtons?.length) { removeButtons.forEach((button) => { button.addEventListener("click", (e) => { scheduleCartSync(); }); }); } }; const updateCartOpenCheckoutObj = () => { if (typeof productos !== 'undefined' && productos.forEach) { let brainObject = SetBrainObject(); brainObject.current.cartOpen = []; productos.forEach(prod => { const sku = prod?.productSKU; const idxo = brainObject.history.productAdded.findIndex((i) => i.sku == sku); if (idxo == -1) { brainObject.history.productAdded.unshift({ id: prod?.productId + "", sku: sku, name_sku: prod?.productDescription, sellerId: "", }); } brainObject.current.cartOpen.push({ id : prod?.productId + "", sku : prod?.productSKU, name_sku : prod?.productDescription, price : prod?.productPrice, qt : prod?.productQuantity, sellerId : "", total : ( parseFloat(prod?.productPrice.replace(/\$/g, "").replace(/\./g, "")) * prod?.productQuantity) .toLocaleString("es-AR", { style: "currency", currency: "ARS", minimumFractionDigits: 0, }).replace(/\s+/g, ""), category :prod?.productCategory, brand : prod?.productMarca, url_image : prod?.productPhoto }); }); localStorage.setItem("vrein-info", JSON.stringify(brainObject)); } }; const updateCartOpenObj = () => { if (typeof carrito !== 'undefined' && carrito.forEach) { let brainObject = SetBrainObject(); brainObject.current.cartOpen = []; carrito.forEach(prod => { brainObject.current.cartOpen.push({ id : prod?.id + "", sku : prod?.sku, name_sku : prod?.name, price : prod?.price, qt : prod?.quantity, sellerId : "", total : (parseFloat(prod?.price) * prod?.quantity) .toLocaleString("es-AR", { style: "currency", currency: "ARS", minimumFractionDigits: 0, }).replace(/\s+/g, ""), category :prod?.category, brand : prod?.brand, url_image : prod?.photo }); }); localStorage.setItem("vrein-info", JSON.stringify(brainObject)); } }; const onRemoveProductFromCart = () => { const removeButtons = document.querySelectorAll( "div.contenedor-items-carrito > section .delete-item-carrito"); if (removeButtons?.length) { removeButtons.forEach((button) => { button.addEventListener("click", (e) => { scheduleCartSync(); }); }); } }; const getCartItemsFromStorage = () => { let brainObject = SetBrainObject(); let cart_items = []; brainObject.current.cartOpen.forEach(prod => { cart_items.push({ ProductCode: prod?.sku, ProductUrlImage: prod?.url_image, ProductTitle: prod?.name_sku, ProductQuantity: prod?.qt, ProductQuantityDetails: "", ProductPrice: prod?.price, PriceTotal: prod?.total, Event: "Load", }); }); return cart_items; }; // get the cart open event // this function will be called when the cart is opened const getOpenCart = () => { const url = new URL(window.location.href); updateCartOpenObj(); let brainObject = SetBrainObject(); let cart_items = getCartItemsFromStorage(); const e = { CartNumber: "", BranchOffice: "", Items: cart_items, }; let s = {}, t = { SalesChannel: "" }; // braindw_getSessionDate(); t && (s.sessionDate = t); s.cartOpened = e; if (url.searchParams.get("s")) { const params = new URLSearchParams(window.location.search); if (params.has("s")) { const searchValue = params.get("s"); s.search = s.search || {}; s.search.searchterm = searchValue; s.search.displayResult = document.querySelectorAll("#swiper-products.swiper-products > div") .length > 0 ? 1 : 0; brainObject.current.searchTerm = searchValue; brainObject.history.searchedTerm.unshift(searchValue); localStorage.setItem("vrein-info", JSON.stringify(brainObject)); } } // An empty cart is a valid state transition: the backend needs the event to // know the cart was cleared. Bailing out here made "cart emptied" invisible. BrainPersistanceEvent(s); sendCartEvent(); }; const captureOpenCartFromPage = () => { updateCartOpenCheckoutObj(); const cartItems = getCartItemsFromStorage(); const e = { CartNumber: "", BranchOffice: "", Items: cartItems, }; let s = {}, t = { SalesChannel: "" }; // braindw_getSessionDate(); t && (s.sessionDate = t); s.cartOpened = e; BrainPersistanceEvent(s); sendCartEvent(); }; // ===================== CART STATE OBSERVER ===================== // The cart is a dynamically rendered modal and the store re-renders it on every // mutation, so static click listeners bound at load time never survive. We watch // the cart STATE instead of the clicks that produce it: a signature of the site's // own cart globals is compared on every tick, and any change emits cartOpened. const _BDW_CART_POLL_MS = 1000; const _BDW_CART_DEBOUNCE_MS = 300; // Store globals that mutate the cart. Wrapping them only shortens the latency of // the happy path; the poll below is what actually guarantees the event. const _BDW_CART_MUTATORS = [ "addToCartClick", "actualizar_cantidad_carrito", "eliminar_del_carrito", "deleteCart", "getCart", "actualizarProductoEnvoltorioCarritoPorSku", ]; let _bdwCartSignature = null; let _bdwCartDebounceId = null; let _bdwCartPollId = null; const isCheckoutSection = () => getSectionFromDataLayer() === "Checkout"; // Returns null when the page exposes no cart source at all (e.g. the new PDP // stack), which is different from an empty cart and must not emit an event. const getCartSourceItems = () => { if (isCheckoutSection() && typeof productos !== "undefined" && Array.isArray(productos)) { return productos.map((p) => ({ sku: p?.productSKU, qt: p?.productQuantity, price: p?.productPrice, })); } if (typeof carrito !== "undefined" && Array.isArray(carrito)) { return carrito.map((p) => ({ sku: p?.sku, qt: p?.quantity, price: p?.price })); } return null; }; const getCartSignature = () => { try { const items = getCartSourceItems(); if (items === null) return null; return items .map((i) => `${i.sku}:${i.qt}:${i.price}`) .sort() .join("|"); } catch { return null; } }; // Emits cartOpened only when the cart actually changed. Every cart-driven call // site funnels through here, so the multiple triggers can never duplicate events. const syncCartState = (options) => { const force = !!(options && options.force); const signature = getCartSignature(); if (signature === null) return; if (!force && signature === _bdwCartSignature) return; _bdwCartSignature = signature; try { if (isCheckoutSection()) { captureOpenCartFromPage(); } else { getOpenCart(); } } catch (error) { console.error("BDW: error syncing cart state", error); } }; const scheduleCartSync = (delay) => { clearTimeout(_bdwCartDebounceId); _bdwCartDebounceId = setTimeout( () => syncCartState(), delay == null ? _BDW_CART_DEBOUNCE_MS : delay, ); }; const wrapCartMutators = () => { _BDW_CART_MUTATORS.forEach((name) => { const original = window[name]; if (typeof original !== "function" || original.__bdwWrapped) return; const wrapped = function (...args) { const result = original.apply(this, args); // The cart globals are refreshed after the store's own ajax call, so we // schedule the read instead of doing it inline. if (result && typeof result.then === "function") { result.then( () => scheduleCartSync(), () => scheduleCartSync(), ); } else { scheduleCartSync(); } return result; }; wrapped.__bdwWrapped = true; try { window[name] = wrapped; } catch {} }); }; const startCartPolling = () => { if (_bdwCartPollId) return; _bdwCartPollId = setInterval(() => syncCartState(), _BDW_CART_POLL_MS); }; const initCartObserver = () => { wrapCartMutators(); const signature = getCartSignature(); _bdwCartSignature = signature; // On load we only report a cart that already has items: an empty cart on // arrival is not an update. Emptying it later IS, and the signature catches it. if (signature) syncCartState({ force: true }); startCartPolling(); }; // ===================== END CART STATE OBSERVER ===================== // capture url location const getUrlLocation = () => { let s = {}, t = { SalesChannel: "" }; // braindw_getSessionDate(); t && (s.sessionDate = t); BrainPersistanceEvent(s); }; const getSearchTermFromLocation = () => { const params = new URLSearchParams(window.location.search); return ( params.get("q") || params.get("s") || params.get("ft") || params.get("search") || "" ).trim(); }; const normalizePersistenceEvent = (event) => { const normalizedEvent = event && typeof event === "object" ? { ...event } : {}; const section = getSectionFromDataLayer(); if (!normalizedEvent.sessionDate || typeof normalizedEvent.sessionDate !== "object") { normalizedEvent.sessionDate = {}; } if (!normalizedEvent.sessionDate.SalesChannel) { normalizedEvent.sessionDate.SalesChannel = ""; } if (section === "Search" || section === "SearchNoResult") { const searchterm = getSearchTermFromLocation(); normalizedEvent.search = { ...(normalizedEvent.search || {}), searchterm: normalizedEvent.search?.searchterm || searchterm, }; if (section === "SearchNoResult" && normalizedEvent.search.displayResult == null) { normalizedEvent.search.displayResult = 0; } } return normalizedEvent; }; // braindw persistence capture const BrainPersistanceEvent = async (event) => { const normalizedEvent = normalizePersistenceEvent(event); const email = getEmail(); const guid = _resolvedGuid || (await bdwResolveGuid()); // El backend rechaza el payload sin GUID: no hay a quien atribuir la // navegacion. Cortar aca evita el request y el error del lado del servidor. if (!guid) { console.warn("BDW: sin GUID, se omite la persistencia"); return; } const urlQuery = window.location.search || ""; const isSearchSection = ["Search", "SearchNoResult"].includes(getSectionFromDataLayer()); const n = { emailguest: email != undefined ? email : "", dataset: encodeURIComponent(JSON.stringify(normalizedEvent)), page: "", guid: guid, url: isSearchSection ? window.location.href : window.location.href.split("?")[0], urlb: _NSURLBNDW + "/Script/braindw/" + _HASHBNDW + "?hs=" + Date.now(), BranchOffice: "", }; fetch("https://p2.vrein.ai/api/data/capture?VScript=1.0", { method: "POST", body: JSON.stringify(n), credentials: "include", keepalive: true, headers: { Accept: "application/json", "Content-Type": "application/json", }, }) .then((resp) => resp.json()) .then(function (data) {}) .catch(function (ex) { console.log(ex); }); }; // track email for braindw const trackEmail = (email) => { if (email) { const script = document.createElement("script"); script.src = `${_NSURLBNDW}/tracking/SetCookiesNdway?email=${email}&company=${_COMPANY}`; script.async = true; document.head.appendChild(script); } }; let _abTestEnabledCache = null; let _abTestEnabledCacheAt = 0; const _ABTEST_CACHE_TTL_MS = 5 * 60 * 1000; const parseAbtestEnabled = (responseData) => { if (typeof responseData === "boolean") { return responseData; } if (typeof responseData === "string") { const normalized = responseData.trim().toLowerCase(); return normalized === "true" || normalized === "1"; } if (!responseData || typeof responseData !== "object") { return false; } if (typeof responseData.abtestenabled === "boolean") { return responseData.abtestenabled; } if (typeof responseData.abTestenabled === "boolean") { return responseData.abTestenabled; } if (typeof responseData.enabled === "boolean") { return responseData.enabled; } if (typeof responseData.flagstatus === "boolean") { return responseData.flagstatus; } if (typeof responseData.isenabled === "boolean") { return responseData.isenabled; } if (responseData.data !== undefined) { return parseAbtestEnabled(responseData.data); } if (responseData.value !== undefined) { return parseAbtestEnabled(responseData.value); } return false; }; const getAbtestEnabled = async () => { const now = Date.now(); if ( _abTestEnabledCache !== null && now - _abTestEnabledCacheAt < _ABTEST_CACHE_TTL_MS ) { return _abTestEnabledCache; } try { const response = await fetch( `https://abtest.braindw.com/api/abtest/GetFlagStatus?hashclient=${_HASHBNDW}`, { method: "GET", headers: { Accept: "application/json", }, }, ); if (!response.ok) { throw new Error(`Abtest status request failed: ${response.status}`); } const data = await response.json(); const enabled = parseAbtestEnabled(data); _abTestEnabledCache = enabled; _abTestEnabledCacheAt = now; return enabled; } catch (error) { console.warn("Error getting abtest flag status", error); _abTestEnabledCache = false; _abTestEnabledCacheAt = now; return false; } }; // get or set the slider caller map in localStorage // tracks which products were added from BDW sliders const getBdwSliderCallerMap = () => { try { return JSON.parse(localStorage.getItem("bdw_slider_callers") || "{}"); } catch { return {}; } }; const setBdwSliderCaller = (productId, callerSection) => { const map = getBdwSliderCallerMap(); map[productId.toString()] = callerSection; localStorage.setItem("bdw_slider_callers", JSON.stringify(map)); }; // send cart event to abtest API const sendCartEvent = async () => { /* TO-DO abTest */ return; try { const guid = _resolvedGuid || (await bdwResolveGuid()); // getCookie("guid"); if (!guid) { console.warn("No guid cookie found, skipping cart event"); return; } const email = getEmail() || ""; const sessionId = guid; /* TO-DO abTest */ const abtestenabled = await getAbtestEnabled(); const callerMap = getBdwSliderCallerMap(); const products = []; let totalAmount = 0; let brainObject = SetBrainObject(); brainObject.current.cartOpen.forEach(prod => { const caller = callerMap[prod?.sku.toString()] || null; products.push({ productid: prod?.sku + "", productname: prod?.name_sku, quantity: prod?.qt, caller: caller, price: prod?.price, }); totalAmount += parseFloat(prod?.price) * prod?.qt; }); const payload = { eventtype: "cart", abtestenabled: abtestenabled, bdwsession: true, sessionid: sessionId, timestamp: new Date().toISOString(), email: email, guidcartnumber: "", products: products, totalamount: totalAmount, tenant: _HASHBNDW, eventcontextkey: sessionId, }; const response = await fetch( `https://abtest.braindw.com/api/events/Capture?hashclient=${_HASHBNDW}`, { method: "POST", body: JSON.stringify(payload), headers: { Accept: "application/json", "Content-Type": "application/json", }, }, ); const data = await response.json(); console.log("Cart event sent successfully", data); } catch (error) { console.error("Error in sendCartEvent", error); } }; // get email from localstorage const getEmail = () => { const oldEmail = localStorage.getItem("email_bdw"); const email = localStorage.getItem("bdw_email"); if (oldEmail) { localStorage.setItem("bdw_email", oldEmail); localStorage.removeItem("email_bdw"); return oldEmail; } if (email) { return email; } else { return null; } }; // check if email is valid // this is a regex to check if the email is valid const checkIfEmailIsValid = (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; // get email from the page const getEmailFromPage = () => { // get brain object const brainObject = SetBrainObject(); // check if the email comes from the newsletter form by name const formNewsletter = document.querySelector("form#formSuscripciones"); if (formNewsletter) { formNewsletter.addEventListener("submit", (e) => { try { const emailInput = formNewsletter.querySelector("#email")?.value; if (emailInput && checkIfEmailIsValid(emailInput)) { // set email in brain object brainObject.session.email = emailInput; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); // set email in localstorage localStorage.setItem("bdw_email", emailInput); trackEmail(emailInput); // track email for braindw getUrlLocation(); // capture url location for all sections and send to persistence getOpenCart(); // capture cart open event for all sections } } catch {} }); } // check if the email comes from login const formLogin = document.querySelector("form[action*='estado-compra']"); if (formLogin) { formLogin.addEventListener("submit", (e) => { try { const emailInput = formLogin.querySelector("#email")?.value; if (emailInput && checkIfEmailIsValid(emailInput)) { // set email in brain object brainObject.session.email = emailInput; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); // set email in localstorage localStorage.setItem("bdw_email", emailInput); trackEmail(emailInput); // track email for braindw getUrlLocation(); // capture url location for all sections and send to persistence getOpenCart(); // capture cart open event for all sections } } catch { } }); } // check if the email comes from checkout const formCheckout = document.querySelector("form#frm_step0"); if (formCheckout) { // formCheckout.addEventListener("submit", (e) => { const emailInput = formCheckout .querySelector("#email") .addEventListener("focusout", (event) => { try { if (event?.target?.value && checkIfEmailIsValid(event.target.value)) { // set email in brain object brainObject.session.email = event?.target?.value; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); // set email in localstorage localStorage.setItem("bdw_email", event?.target?.value); trackEmail(event?.target?.value); // track email for braindw getUrlLocation(); // capture url location for all sections and send to persistence getOpenCart(); // capture cart open event for all sections } } catch {} }); // }); } }; // capture product for persistence const captureProductPersistence = (product) => { let e = null; if (product) { var s = {}, t = ""; t && (s.sessionDate = t); e = { ProductCode: product.id, Title: product.title, PriceOnline: product.price, Brand: product?.Brand, Category: product.categoryName, // product.categories.split(",")[0], // here must be the name of the category, whe use the id for now CategoryCode: product?.categorycode.toString(), UrlImage: product.image, UrlProduct: product.url, BranchOffice: "", }; s.product = e; BrainPersistanceEvent(s); } }; // capture product for s2 const captureProductS2 = (product, isCart = 0) => { const productCaptureS2 = `og:title=${product.title.replace( "#", "%23", )}&og:image=${product.image}&og:url=${product.url}&og:categoriescodes=${ product.categoriescodes },${product.Brand.toLowerCase() ?? ""}&og:codeproduct=${product.id}&og:brand=${ product.Brand.toLowerCase() ?? "" }&og:categorycode=${product.categorycode}&og:price=${ product.price }&og:priceold=${ product.oldPrice == null || product.oldPrice == product.price ? "" : product.oldPrice }&og:pricesale=${product.price}&iscart=${isCart}&info=${product.info}`; const script = document.createElement("script"); script.src = `${_PURLBNDW}/tracking/capture?HASH=${_HASHBNDW}&${encodeURI( productCaptureS2, )}`; script.async = true; document.head.appendChild(script); }; const captureProductFromAllSections = async () => { try { const brainObject = SetBrainObject(); switch (getSectionFromDataLayer()) { case "PLP": const categoryTitle = dataLayer.find((entry) => entry && entry.page_type === "category")?.category[0] || data.length > 0 ? data[0].categoria_principal_name : ""; const categoryCode = data.length ? data[0].categoria_principal_producto : ""; if (categoryTitle && categoryCode) { if (brainObject.history.categoryViewed.length == 15) { brainObject.history.categoryViewed.pop(); } // check if category is already in array if (brainObject.history.categoryViewed.findIndex( (idx) => idx.id == categoryCode, ) == -1 ) { brainObject.history.categoryViewed.unshift({ id: categoryCode, name: categoryTitle }); } brainObject.current.category = { id: categoryCode, name: categoryTitle }; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); // get category plp from braindw captureCategoryPersistence(categoryCode, categoryTitle); captureCategoryS2(categoryCode); } break; case "PDP": { // Primary: read product data from window.dataLayer let product = null; const dlProduct = getDataLayerProduct(); if (dlProduct) { product = { id: dlProduct.id, title: dlProduct.title, url: dlProduct.url, image: dlProduct.image, skuProduct: dlProduct.sku, categories: dlProduct.category, categoryName: dlProduct.categoryName, // here must be the name of the category, we use the id for now categoriescodes: dlProduct.category, categorycode: dlProduct.category, price: dlProduct.price, oldPrice: null, priceInternet: dlProduct.price, info: "", Brand: dlProduct.brand, }; } else if (typeof producto !== "undefined" && producto?.CodigoAguila) { // Fallback: read from global `producto` variable product = { id: String(producto?.id_producto), title: producto?.title, url: window.location.href, image: producto?.foto, skuProduct: producto?.CodigoAguila, categories: producto?.categoria_principal_name, categoryName: dlProduct.categoryName, // here must be the name of the category, we use the id for now categoriescodes: producto?.categoria_principal_producto, categorycode: producto?.categoria_principal_producto, price: producto?.precio, oldPrice: producto?.precio_tachado, priceInternet: producto?.precio, info: "", Brand: producto?.marca, }; } if (!product) break; if (brainObject.history.productViewed.length === 15) { brainObject.history.productViewed.pop(); } if (!brainObject.history.productViewed.find( (i) => i.id === product.id && i.sku === product.skuProduct) ) { brainObject.history.productViewed.unshift({ id: product.id, sku: product.skuProduct, title: product.title, sellerId: "", }); } brainObject.current.product = { id: product.id, sku: product.skuProduct, title: product.title, sellerId: "", }; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); captureProductS2(product, 0); captureProductPersistence(product); addProductToCartFromPDP(product); break; } case "Home": scheduleCartSync(1500); default: break; } } catch (error) { console.error("Error Capturing product", error); } }; // capture category for persistence * category params is the code for now * function captureCategoryPersistence(category, categoryTitle) { let e = null; try { if (category) { e = { Title: categoryTitle, CategoryCode: category.toString(), }; } if (e) { var s = {}, t = ""; // braindw_getSessionDate(); t && (s.sessionDate = t); s.category = e; BrainPersistanceEvent(s); } } catch (e) { console.log("braindw_takeCategoryerror", e); } //return result; } // capture category for s2 const captureCategoryS2 = (category) => { const categoryCaptureS2 = `&minify=1&iscategory=true&og:categorycode=${category}`; const script = document.createElement("script"); script.src = `${_PURLBNDW}/tracking/capture?HASH=${_HASHBNDW}${categoryCaptureS2}`; script.async = true; document.head.appendChild(script); }; // add product to cart from PDP const addProductToCartFromPDP = (product) => { const addToCartButton = document.querySelectorAll("#descripcion_boton > a#AddToCart, #comprar_ahora > #buy_now"); // const buyNowButton = document.querySelector("button#wd-add-to-cart"); if (addToCartButton?.length) { addToCartButton.forEach((item) => { item.addEventListener("click", (e) => { try { const brainObject = SetBrainObject(); const sku = product?.skuProduct; const idxo = brainObject.history.productAdded.findIndex( (i) => i.id == product.id && i.sku == sku, ); if (idxo == -1) { brainObject.history.productAdded.unshift({ id: product.id, sku: sku, name_sku: product.title, sellerId: "", }); } localStorage.setItem("vrein-info", JSON.stringify(brainObject)); captureProductS2(product, 1); scheduleCartSync(); } catch {} }); }); } }; // ===================== BDW SLIDER / CAROUSEL ===================== // Mock products data (replace with API response later) const _BDW_MOCK_PRODUCTS = [ ]; const _bdwCarouselSourceBySection = {}; const _bdwCarouselProductBySection = {}; const bdwNormalizeSectionKey = (sectionId) => (sectionId || "").toString().replace(/^[#.]+/, "").toLowerCase(); // Render star rating HTML. Returns empty string when rating is 0/falsy. // Half star: >= x.5 rounds up to half, < x.5 stays at full integer stars. const bdwRenderRating = (rating) => { if (!rating || rating <= 0) return ""; const val = Math.min(5, Math.max(0, parseFloat(rating))); const full = Math.floor(val); const half = (val - full) >= 0.5 ? 1 : 0; const empty = 5 - full - half; const blue = "#00C492"; const grey = "#cccccc"; let stars = ""; for (let i = 0; i < full; i++) { stars += `★`; } if (half) { // Overlay a blue half-width star on top of a grey star stars += `★★`; } for (let i = 0; i < empty; i++) { stars += `☆`; } const displayVal = val % 1 === 0 ? val.toFixed(1) : String(val); return `
${displayVal}
${stars}
`; }; // Build a single product card HTML for the blaze slider const buildBdwProductCard = (product, index, callerSection) => { const slugUrl = product.url.startsWith("http://") || product.url.startsWith("https://") ? product.url : product.url.startsWith("/") ? product.url : "/" + product.url; const categorySlug = product.category || ""; const productSlug = slugUrl.split("/").pop(); return `
 
${/*product.cucarda ? `
cucarda
` : */""} ${product.title}
`; }; // Track click on a BDW slider product (called from onclick) const _bdwSelectItemFired = new Set(); const bdwSliderClickTrack = (sku, callerSection, category, productSlug, position) => { try { setBdwSliderCaller(sku, callerSection); // trackCarouselClick(callerSection, sku); // GA4 select_item — debounce: ignore if the same sku was fired within 500ms const debounceKey = callerSection + "|" + String(sku); if (_bdwSelectItemFired.has(debounceKey)) return; _bdwSelectItemFired.add(debounceKey); setTimeout(() => _bdwSelectItemFired.delete(debounceKey), 500); if (Array.isArray(window.dataLayer)) { const sectionKey = bdwNormalizeSectionKey(callerSection); const productMeta = (_bdwCarouselProductBySection[sectionKey] || {})[String(sku)] || {}; const sourceMeta = _bdwCarouselSourceBySection[sectionKey] || {}; window.dataLayer.push({ event: "ga4.trackEvent", eventName: "select_item", eventParams: { items: [{ item_name: productMeta.productname || "", item_id: productMeta.productid || String(sku), item_list_name: sourceMeta.title || callerSection || "", item_list_id: callerSection || "", price: productMeta.rawPrice || 0, quantity: 1, }], }, }); } } catch (e) { console.warn("bdwSliderClickTrack error", e); } }; // Build and render a blaze slider carousel into a container // containerSelector: CSS selector string (id or class) e.g. "#bdw-home-carrusel-1" or ".bdw-slider-1" // products: array of product objects // callerSection: identifier for tracking which slider originated the click // options: { slidesToShow, slideGap, title } const buildBdwSlider = (containerSelector, products, callerSection, options = {}) => { const container = document.querySelector(containerSelector); if (!container) { console.warn("BDW Slider: container not found for selector", containerSelector); return null; } container.classList.add("app", "container"); if (!products || products.length === 0) { console.warn("BDW Slider: no products to render for", containerSelector); return null; } const slidesToShow = options.slidesToShow || 5; const slideGap = options.slideGap || 20; const title = options.title || ""; const sectionKey = bdwNormalizeSectionKey(callerSection); _bdwCarouselSourceBySection[sectionKey] = { type: options.sourceType || "bdw", caller: options.sourceCaller || callerSection || "", title: options.sourceTitle || title || "", endpoint: options.sourceEndpoint || "", }; const sectionProductMap = _bdwCarouselProductBySection[sectionKey] || {}; products.forEach((p) => { const productKey = String(p.sku || p.id || ""); if (!productKey) return; sectionProductMap[productKey] = { productid: String(p.id || p.sku || ""), productname: p.title || "", rawPrice: p.rawPrice || 0, }; }); _bdwCarouselProductBySection[sectionKey] = sectionProductMap; const cardsHtml = products .map((p, i) => buildBdwProductCard(p, i, callerSection)) .join(""); // remember inject .app class to the container const sliderHtml = ` ${title ? `

${title}

` : ""}
${cardsHtml}
`; container.innerHTML = sliderHtml; // GA4 view_item_list — fire once when the carousel enters the viewport if (typeof IntersectionObserver !== "undefined" && Array.isArray(window.dataLayer)) { const currency = (() => { const entry = Array.isArray(window.dataLayer) ? window.dataLayer.find((e) => e && e.currency) : null; return (entry && entry.currency) || "ARS"; })(); const ga4Items = products.map((p, i) => ({ item_name: p.title || "", item_id: String(p.id || p.sku || ""), item_list_name: title || callerSection || "", item_list_id: callerSection || "", item_category: "", item_brand: "gadnic", price: p.rawPrice || 0, quantity: 1, })); const viewObs = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting && !entry.target.dataset.bdwGa4Viewed) { entry.target.dataset.bdwGa4Viewed = "1"; window.dataLayer.push({ event: "ga4.trackEvent", eventName: "view_item_list", eventParams: { currency: currency, items: ga4Items, }, }); observer.unobserve(entry.target); } }); }, { threshold: 0.5 } ); viewObs.observe(container); } // Initialize BlazeSlider if the library is available const sliderEl = container.querySelector(".blaze-slider"); if (sliderEl && typeof BlazeSlider !== "undefined") { try { return new BlazeSlider(sliderEl, { all: { slidesToShow: slidesToShow, slideGap: slideGap + "px", loop: true, transitionDuration: 300, }, "(max-width: 1024px)": { slidesToShow: 3, }, "(max-width: 768px)": { slidesToShow: 2, }, "(max-width: 480px)": { slidesToShow: 1, }, }); } catch (e) { console.warn("BDW BlazeSlider init error", e); } } else if (typeof BlazeSlider === "undefined") { console.warn("BDW Slider: BlazeSlider library not loaded"); } return null; }; // ===================== BDW CACHE SYSTEM ===================== const _BDW_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes, adjustable const _BDW_CACHE_KEY_RULE = "bdw_cache_rule"; const _BDW_CACHE_KEY_PRODUCTS = "bdw_cache_products"; const _BDW_CACHE_KEY_BANNERS = "bdw_cache_banners"; const _BDW_USE_MOCK = false; // set to false when APIs are ready const bdwCacheGet = (cacheKey) => { try { const raw = localStorage.getItem(cacheKey); if (!raw) return null; const cached = JSON.parse(raw); if (!cached || !cached.ts || !cached.data) return null; if (Date.now() - cached.ts > _BDW_CACHE_TTL_MS) { localStorage.removeItem(cacheKey); return null; } return cached.data; } catch { return null; } }; const bdwCacheSet = (cacheKey, data) => { try { localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data: data })); } catch (e) { console.warn("BDW cache write error", e); } }; // ===================== BDW S2 TRACKING API ===================== // Build the "u" param from vrein-info context, based on page section const bdwBuildUParam = (section) => { const brainObj = SetBrainObject(); const lastCat = brainObj.current?.category?.id || ""; const lastProds = (brainObj.history?.productViewed || []).map((p) => p.id).join(","); const cartProds = (brainObj.current?.cartOpen || []).map((p) => p.id || p.sku).join(","); const searchTerm = brainObj.current?.searchTerm || ""; switch (section) { case "Home": return "/home/1/" + lastCat + "/" + lastProds + "/" + cartProds + "/" + searchTerm; case "PLP": return "/category/" + lastCat + "//" + lastProds + "/" + cartProds; case "PDP": { const pid = brainObj.current?.product?.id || ""; const catCode = brainObj.current?.category?.id || ""; const lastSku = brainObj.current?.product?.sku || ""; return "product/" + pid + "/" + catCode + "/" + catCode + "//" + lastProds + "/" + lastSku + "/" + cartProds; } case "Search": return "/search/" + searchTerm + "/" + lastCat; case "SearchNoResult": return "/searchnoresult/" + searchTerm + "/" + lastCat; default: return "/home/1/" + lastCat + "/" + lastProds + "/" + cartProds + "/" + searchTerm; } }; // Build the full S2 tracking URL for a section fetch const bdwBuildS2Url = async (sectionId) => { const email = getEmail() || ""; const pageSection = getSectionFromDataLayer() || "Home"; const uParam = bdwBuildUParam(pageSection); const guid = _resolvedGuid || (await bdwResolveGuid()); const params = [ "HASH=" + encodeURIComponent(_HASHBNDW), "email=" + encodeURIComponent(email), "branchOffice=", "format=json", "hs=" + Date.now(), "upath=" + encodeURIComponent(window.location.pathname), "sid=" + encodeURIComponent(guid || ""), ]; params.push("u=" + encodeURIComponent(uParam)); if (sectionId) { const cleanSectionId = sectionId.replace(/^[#.]+/, "").toLowerCase(); params.push("sectionId=" + encodeURIComponent(cleanSectionId)); } return _NSURLBNDW + "/tracking/track?" + params.join("&"); }; // Normalize S2 tracking response const bdwNormalizeS2Response = (data) => { if (!data) return []; const sections = Array.isArray(data) ? data : data.sections || data.Sections || data.results || [data]; return sections .filter((s) => s && (s.Products || s.products) && (s.Products || s.products).length > 0) .map((s) => ({ Section: s.Section || s.section || "", Title: s.Title || s.title || "Recomendados", Products: s.Products || s.products || [], GaEventAction: s.GaEventAction || s.gaEventAction || s.Section || "", GaEventCategory: s.GaEventCategory || s.gaEventCategory || "ga-braindw", GaEventLabel: s.GaEventLabel || s.gaEventLabel || "", })); }; // Fetch a single section from S2 tracking API const bdwFetchSection = async (sectionId) => { const cacheKey = _BDW_CACHE_KEY_RULE + "_" + sectionId; const cached = bdwCacheGet(cacheKey); if (cached) return cached; const url = await bdwBuildS2Url(sectionId); const response = await fetch(url, { mode: "cors", credentials: "include", }); if (!response.ok) { throw new Error(`S2 HTTP ${response.status}`); } const data = await response.json(); const normalized = bdwNormalizeS2Response(data); bdwCacheSet(cacheKey, normalized); return normalized; }; // ===================== BDW PRODUCT DETAIL API (GraphQL) ===================== // Select the best variant from a product's variants array // Variants only carry code and stock; pricing lives at product level const bdwSelectVariant = (variants) => { if (!variants || variants.length === 0) return null; // Prefer a variant with stock > 0 for (let i = 0; i < variants.length; i++) { if (variants[i].stock > 0) return variants[i]; } // Fallback to first variant return variants[0]; }; // Process raw GraphQL products into a map keyed by product ID // pricing and discountTagText are product-level fields per the API schema const bdwProcessGraphQLProducts = (products) => { const map = {}; for (let i = 0; i < products.length; i++) { const p = products[i]; if (!p || !p.id) continue; // pricing is at product level const rawSale = p.pricing ? p.pricing.salePrice : 0; const listPrice = p.pricing ? p.pricing.listPrice : 0; // salePrice=0 means no promo — real price is listPrice const salePrice = rawSale > 0 ? rawSale : listPrice; let discount = 0; if (listPrice > 0 && rawSale > 0 && listPrice > rawSale) { discount = Math.round((1 - rawSale / listPrice) * 100); } // variants only carry code + stock const variant = bdwSelectVariant(p.variants || []); map[String(p.id)] = { id: String(p.id), name: p.name || "", image: p.image || "", link: p.link || "", salePrice: salePrice, listPrice: listPrice, discount: discount, discountTag: p.discountTagText || "", sku: variant ? variant.code || "" : "", stock: variant ? variant.stock || 0 : 0, withoutTaxPrice: p.pricing ? p.pricing.withoutTaxPrice || 0 : 0, promotions: p.promotions || [], rating: p.rating || 0, installments: p.installments || [], freeShipping: Array.isArray(p.promotions) && p.promotions.some((promo) => promo.value === "Envio Gratis"), cucarda: Array.isArray(p.installments) && p.installments.length > 0 ? p.installments[0].value : "", cucardaKey: Array.isArray(p.installments) && p.installments.length > 0 ? p.installments[0].key : "", }; } return map; }; // Fetch product details via GraphQL for a batch of product IDs const bdwFetchProductDetails = async (productIds) => { if (!productIds || productIds.length === 0) return {}; // Deduplicate const uniqueIds = [...new Set(productIds.map(String))]; // Check cache: resolve as many as possible from cache const cachedProducts = bdwCacheGet(_BDW_CACHE_KEY_PRODUCTS) || {}; const missingIds = uniqueIds.filter((id) => !cachedProducts[id]); if (missingIds.length > 0) { const query = `query {\n product(\n tenantId: "${_TENANT_ID}",\n productIds: ${JSON.stringify(missingIds)},\n includeVariants: false\n ) {\n id\n name\n image\n link\n discountTagText\n pricing {\n salePrice\n listPrice\n withoutTaxPrice\n }\n variants {\n code\n stock\n }\n promotions\n rating\n installments\n }\n}`; const response = await fetch(_GRAPHQL_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: query }), }); if (!response.ok) { throw new Error(`GraphQL HTTP ${response.status}`); } const json = await response.json(); if (json.errors) { console.warn("BDW GraphQL errors:", json.errors); } const products = (json.data && json.data.product) || []; const processed = bdwProcessGraphQLProducts(products); // Merge into cache map Object.keys(processed).forEach((pid) => { cachedProducts[pid] = processed[pid]; }); // Persist updated product cache bdwCacheSet(_BDW_CACHE_KEY_PRODUCTS, cachedProducts); } return cachedProducts; }; // Format a numeric price to ARS currency string const bdwFormatPrice = (val) => { if (typeof val === "string" && val.startsWith("$")) return val; if (typeof val === "number" || (typeof val === "string" && !isNaN(parseFloat(val)))) { return parseFloat(val).toLocaleString("es-AR", { style: "currency", currency: "ARS", minimumFractionDigits: 0, }).replace(/\s+/g, ""); } return val || ""; }; // Hardcoded function for cucardaKeys function getCuotasLabel(numeroCuotas) { const num = Number(numeroCuotas); // Optional: add Number.isInteger(num) if you only want whole numbers if (Number.isInteger(num) && num > 1) { return `${num} CUOTAS SIN INTERÉS`; } return null; } // Map a GraphQL product detail to the card format used by buildBdwProductCard const bdwMapProductToCard = (raw) => { const discountText = raw.discount > 0 ? raw.discount + "% OFF" : (raw.discountTag || ""); return { id: raw.id || "", sku: raw.sku || raw.id || "", title: raw.name || "", url: raw.link || "", image: raw.image || "", price: bdwFormatPrice(raw.salePrice), rawPrice: raw.salePrice || 0, oldPrice: raw.listPrice > 0 && raw.listPrice !== raw.salePrice ? bdwFormatPrice(raw.listPrice) : "", discount: discountText, priceWithoutTax: raw.withoutTaxPrice > 0 ? bdwFormatPrice(raw.withoutTaxPrice) : "", freeShipping: raw.freeShipping ?? false, cucarda: raw.cucarda || "", cucardaKey: raw.cucardaKey || "", rating: raw.rating || 0, }; }; // ===================== MAIN SLIDER ORCHESTRATOR ===================== // Fetch products from S2 tracking API and build sliders progressively // sections: array of section IDs e.g. ["bdw-home-carrusel-1", "bdw-home-carrusel-2"] const getProductsIdFromBraindw = async (sections, options = {}) => { // sections = sections.map((s) => s.trim().toLowerCase()); try { if (_BDW_USE_MOCK) { // Mock mode: render all sliders with mock data for (const sectionId of sections) { const selector = sectionId.startsWith("#") || sectionId.startsWith(".") ? sectionId : "#" + sectionId; buildBdwSlider(selector, _BDW_MOCK_PRODUCTS, sectionId, options); } return; } // Progressive pipeline: fetch each section, collect all product IDs, then batch-fetch details const sectionResults = []; const allProductIds = []; for (const sectionId of sections) { // Skip if the container element does not exist in the DOM const selector = sectionId.startsWith("#") || sectionId.startsWith(".") ? sectionId : "#" + sectionId; if (!document.querySelector(selector)) continue; try { const normalized = await bdwFetchSection(sectionId); for (const section of normalized) { sectionResults.push(section); (section.Products || []).forEach((pid) => { const pidStr = pid.toString(); if (!allProductIds.includes(pidStr)) { allProductIds.push(pidStr); } }); } } catch (e) { console.warn("BDW S2 fetch error for", sectionId, e.message); } } if (sectionResults.length === 0 || allProductIds.length === 0) { console.warn("BDW: S2 returned no sections with products"); return; } // Single batch request for all product details const productDetailsMap = await bdwFetchProductDetails(allProductIds); // Build each slider with its section products for (const section of sectionResults) { const sectionId = section.Section || ""; // Match section to requested ID (case-insensitive), stripping leading # or . const matchedId = sections.find( (s) => s.replace(/^[#.]+/, "").toLowerCase() === sectionId.replace(/^[#.]+/, "").toLowerCase(), ); if (!matchedId) continue; const sectionProducts = (section.Products || []) .map((pid) => productDetailsMap[pid.toString()]) .filter(Boolean) .map(bdwMapProductToCard); if (sectionProducts.length === 0) continue; const selector = matchedId.startsWith("#") || matchedId.startsWith(".") ? matchedId : "#" + matchedId; buildBdwSlider(selector, sectionProducts, sectionId, { ...options, title: section.Title || options.title || "", sourceType: "bdw", sourceCaller: section.Section || sectionId || "", sourceTitle: section.Title || "", sourceEndpoint: section.GaEventAction || section.GaEventLabel || "", }); } } catch (e) { console.warn("getProductsIdFromBraindw error", e); // Fallback to mock on error for (const sectionId of sections) { const selector = sectionId.startsWith("#") || sectionId.startsWith(".") ? sectionId : "#" + sectionId; buildBdwSlider(selector, _BDW_MOCK_PRODUCTS, sectionId, options); } } }; // ===================== END BDW SLIDER ===================== // ===================== BDW SMART IMAGE / BANNER SECTION ===================== // Push a GA4 banner event to window.dataLayer const bdwPushBannerGA4Event = (eventName, trackingData) => { if (!Array.isArray(window.dataLayer)) return; window.dataLayer.push({ event: "ga4.trackEvent", eventName: eventName, eventParams: { creative_name: trackingData.creative_name || "", creative_slot: trackingData.creative_slot || "", banner_id: trackingData.banner_id || "", banner_name: trackingData.banner_name || "", }, }); }; // Fetch banner data from the SmartImage API for a given sectionId const bdwFetchSmartImage = async (sectionId) => { const email = getEmail() || ""; const url = `${_NSURLBNDW}/tracking/SmartImage` + `?HASH=${encodeURIComponent(_HASHBNDW)}` + `&email=${encodeURIComponent(email)}` + `&branchOffice=` + `&whitelabel=` + `§ionid=${encodeURIComponent(sectionId)}` + `&idcategory=`; const response = await fetch(url, { credentials: "include" }); if (!response.ok) throw new Error(`SmartImage HTTP ${response.status}`); return response.json(); }; // Start a live countdown inside `el`, targeting `dateEnd` in the given `timezoneOffset` (e.g. -4 = UTC-4). // Returns the interval ID so the caller can clear it when needed. const bdwStartCountdown = (el, dateEnd, timezoneOffset) => { // Parse dateEnd as UTC (append "Z"), then shift by timezoneOffset to get the real UTC target. // e.g. "2026-05-09T12:00:00" + offset -4 → target is 2026-05-09T16:00:00Z const targetUtcMs = Date.parse(dateEnd + "Z") - timezoneOffset * 3600 * 1000; const pad = (n) => String(n).padStart(2, "0"); const update = () => { const diff = targetUtcMs - Date.now(); if (diff <= 0) { el.textContent = "00d 00h 00m 00s"; clearInterval(interval); return; } const days = Math.floor(diff / 86400000); const hours = Math.floor((diff % 86400000) / 3600000); const minutes = Math.floor((diff % 3600000) / 60000); const seconds = Math.floor((diff % 60000) / 1000); el.innerHTML = `${pad(days)}d ` + `${pad(hours)}h ` + `${pad(minutes)}m ` + `${pad(seconds)}s`; }; update(); const interval = setInterval(update, 1000); return interval; }; // Build a single banner slide element (link + picture) for one image entry. // trackingData: { creative_name, creative_slot, banner_id, banner_name } const bdwBuildBannerSlide = (img, altText, trackingData) => { const wrapper = document.createElement("div"); wrapper.className = "bdw-banner-item"; wrapper.style.cssText = "display:block;width:100%;"; const link = document.createElement("a"); link.href = img.Link || "#"; link.style.cssText = "display:block;width:100%;"; const picture = document.createElement("picture"); const mobileSource = document.createElement("source"); mobileSource.media = "(max-width: 768px)"; mobileSource.srcset = img.UrlMobile || img.UrlDesktop || ""; const imgEl = document.createElement("img"); imgEl.src = img.UrlDesktop || img.UrlMobile || ""; imgEl.alt = altText || ""; imgEl.loading = "lazy"; imgEl.style.cssText = "width:100%;display:block;"; picture.appendChild(mobileSource); picture.appendChild(imgEl); link.appendChild(picture); wrapper.appendChild(link); // GA4 select_banner — fire when the banner link is clicked if (trackingData) { wrapper.addEventListener("click", () => { bdwPushBannerGA4Event("select_banner", trackingData); }, true); } return wrapper; }; // Build and attach a SmartCountdown overlay element onto a container. const bdwAttachCountdown = (countdown, targetEl, isMobile) => { if (!countdown || !countdown.Enabled) return; const positionStr = isMobile ? String(countdown.PositionMobile || "2.2") : String(countdown.PositionDesktop || "2.2"); const [rawRow, rawCol] = positionStr.split("."); const row = Math.min(3, Math.max(1, parseInt(rawRow) || 2)); const col = Math.min(3, Math.max(1, parseInt(rawCol) || 2)); const rowStyles = row === 1 ? { top: "5%", bottom: "auto" } : row === 3 ? { top: "auto", bottom: "5%" } : { top: "50%", bottom: "auto" }; const colStyles = col === 1 ? { left: "5%", right: "auto", textAlign: "left" } : col === 3 ? { left: "auto", right: "5%", textAlign: "right" } : { left: "50%", right: "auto", textAlign: "center" }; const tx = col === 2 ? "-50%" : "0"; const ty = row === 2 ? "-50%" : "0"; const transform = (tx === "0" && ty === "0") ? "none" : `translate(${tx},${ty})`; const fontSize = isMobile ? (countdown.FontSizeMobile || 15) : (countdown.FontSizeDesktop || 17); const fontColor = countdown.FontColor || "#000000"; const cdEl = document.createElement("div"); cdEl.className = "bdw-smart-countdown"; cdEl.style.cssText = [ "position:absolute", `top:${rowStyles.top}`, `bottom:${rowStyles.bottom}`, `left:${colStyles.left}`, `right:${colStyles.right}`, `transform:${transform}`, `font-size:${fontSize}px`, `color:${fontColor}`, `text-align:${colStyles.textAlign}`, "font-weight:bold", "white-space:nowrap", "pointer-events:none", "z-index:10", ].join(";"); targetEl.appendChild(cdEl); bdwStartCountdown(cdEl, countdown.DateEnd, countdown.TimeZoneOffset || 0); }; // Render a single banner API item (images + optional countdown) into `containerEl`. // When the item has multiple images they are rendered as a BlazeSlider banner carousel. // sectionId is used as creative_slot in GA4 events. const bdwRenderBannerItem = (item, containerEl, sectionId) => { const isMobile = window.innerWidth <= 768; const images = item.Images || []; const buildTrackingData = (img) => ({ creative_name: item.Title || "", creative_slot: item.Title || "", banner_id: sectionId || "", banner_name: isMobile ? img.UrlMobile : img.UrlDesktop || "", }); if (images.length <= 1) { // Single image — original behaviour const wrapper = document.createElement("div"); wrapper.className = "bdw-banner-item container"; wrapper.style.cssText = "position:relative;display:block;width:100%;overflow:hidden;"; const slide = bdwBuildBannerSlide(images[0], item.Title, buildTrackingData(images[0])); wrapper.appendChild(slide); // GA4 view_banner — fire once when the banner enters the viewport if (typeof IntersectionObserver !== "undefined") { const obs = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting && !entry.target.dataset.bdwGa4Viewed) { entry.target.dataset.bdwGa4Viewed = "1"; bdwPushBannerGA4Event("view_banner", buildTrackingData(images[0])); observer.unobserve(entry.target); } }); }, { threshold: 0.5 } ); obs.observe(wrapper); } bdwAttachCountdown(item.SmartCountdown, wrapper, isMobile); containerEl.appendChild(wrapper); return; } // Multiple images — BlazeSlider banner const outer = document.createElement("div"); outer.className = "bdw-banner-slider-wrapper container"; outer.style.cssText = "position:relative;width:100%;overflow:hidden;"; const blazeEl = document.createElement("div"); blazeEl.className = "blaze-slider bdw-banner-slider"; blazeEl.style.cssText = "--slides-to-show:1;--slide-gap:0px;position:relative;"; const blazeContainer = document.createElement("div"); blazeContainer.className = "blaze-container"; const trackContainer = document.createElement("div"); trackContainer.className = "blaze-track-container"; const track = document.createElement("div"); track.className = "blaze-track"; images.forEach((img) => { const slide = bdwBuildBannerSlide(img, item.Title, buildTrackingData(img)); track.appendChild(slide); }); trackContainer.appendChild(track); blazeContainer.appendChild(trackContainer); blazeEl.appendChild(blazeContainer); // Prev / next controls const controls = document.createElement("div"); controls.className = "controls controls-banner"; controls.innerHTML = ` `; blazeEl.appendChild(controls); outer.appendChild(blazeEl); // SmartCountdown sits over the whole slider (visible on whichever slide is active) bdwAttachCountdown(item.SmartCountdown, outer, isMobile); containerEl.appendChild(outer); // GA4 view_banner — fire once when the banner slider enters the viewport if (typeof IntersectionObserver !== "undefined") { const obs = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting && !entry.target.dataset.bdwGa4Viewed) { entry.target.dataset.bdwGa4Viewed = "1"; bdwPushBannerGA4Event("view_banner", buildTrackingData(images[0])); observer.unobserve(entry.target); } }); }, { threshold: 0.5 } ); obs.observe(outer); } // Initialize BlazeSlider if (typeof BlazeSlider !== "undefined") { try { new BlazeSlider(blazeEl, { all: { slidesToShow: 1, slideGap: "0px", loop: true, transitionDuration: 400, enableAutoplay: true, autoplayInterval: 3000, }, }); } catch (e) { console.warn("BDW banner BlazeSlider init error", e); } } }; // Fetch SmartImage data and render the banner into `containerSelector`. // containerSelector: CSS selector matching the host element (e.g. "#BDW-HOME-IMAGES-1") // sectionId: the sectionid value sent to the API (e.g. "BDW-HOME-IMAGES-1") const buildBdwBanner = async (containerSelector, sectionId) => { const container = document.querySelector(containerSelector); if (!container) return; try { const cacheKey = _BDW_CACHE_KEY_BANNERS + "_" + sectionId; let items = bdwCacheGet(cacheKey); if (!items) { const data = await bdwFetchSmartImage(sectionId); items = Array.isArray(data) ? data : [data]; // Only cache when no item has an active SmartCountdown (countdowns need fresh data) const hasActiveCountdown = items.some( (item) => item && item.SmartCountdown && item.SmartCountdown.Enabled ); if (!hasActiveCountdown) { bdwCacheSet(cacheKey, items); } } container.innerHTML = ""; container.classList.add("app", "container"); container.style.setProperty("padding", "0px", "important"); items.forEach((item) => { if (item && item.Images && item.Images.length > 0) { bdwRenderBannerItem(item, container, sectionId); } }); } catch (e) { console.warn("BDW buildBdwBanner error for", sectionId, e); } }; // Load multiple banner sections. // Each entry can be a plain string (used as both selector and sectionId, "BDW-HOME-IMAGES-1" // becomes selector "#BDW-HOME-IMAGES-1"), or an object { selector, sectionId }. const getBannersFromBraindw = async (bannerSections) => { for (const entry of bannerSections) { let selector, sectionId; if (typeof entry === "string") { selector = entry.startsWith("#") || entry.startsWith(".") ? entry : "#" + entry; sectionId = entry.replace(/^[#.]+/, ""); } else { selector = entry.selector; sectionId = entry.sectionId; } if (!document.querySelector(selector)) continue; await buildBdwBanner(selector, sectionId); } }; // ===================== END BDW SMART IMAGE / BANNER SECTION ===================== // ===================== METRICS & TRACKING ===================== const BDW_ABTEST_BASE = "https://metrics.vrein.ai"; // Cache for abtest flag status (5 min TTL) let _abtestFlagCache = null; let _abtestFlagCacheTimestamp = 0; const ABTEST_CACHE_TTL = 15 * 60 * 1000; // 5 minutes in ms const _bdwCarouselProductCallers = {}; function getLocalStorageValue(key) { try { return localStorage.getItem(key) || ''; } catch (e) { return ''; } } // ----------------------------------------------- // ABTest Flag Status (cached 15 min) // ----------------------------------------------- async function getAbtestFlagStatus() { const now = Date.now(); // Return cached value if still valid if ( _abtestFlagCache !== null && now - _abtestFlagCacheTimestamp < ABTEST_CACHE_TTL ) { return _abtestFlagCache; } try { const url = `${BDW_ABTEST_BASE}/api/abtest/GetFlagStatus?hashclient=${_HASHBNDW}`; const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { console.warn('BDW: ABTest flag status request failed:', response.status); _abtestFlagCache = false; _abtestFlagCacheTimestamp = now; return false; } const data = await response.json(); // Expected shape: // { isSuccess, isFailure, value: { abTestenabled, hashclient }, error } let flag = false; if (typeof data === 'boolean') { flag = data; } else if (data && data.isSuccess === true && data.value) { if (typeof data.value.abTestenabled === 'boolean') { flag = data.value.abTestenabled; } else if (typeof data.value.abtestenabled === 'boolean') { // fallback in case backend casing changes flag = data.value.abtestenabled; } } _abtestFlagCache = flag; _abtestFlagCacheTimestamp = now; return flag; } catch (e) { console.warn('BDW: Error fetching ABTest flag status', e); _abtestFlagCache = false; _abtestFlagCacheTimestamp = now; return false; } } // click event tracking for BDW carousels, with ABTest flag check async function trackCarouselClick(sectionId, productId) { const isAbtestEnabled = await getAbtestFlagStatus(); try { const normalizedSectionId = bdwNormalizeSectionKey(sectionId); const sourceMeta = _bdwCarouselSourceBySection[normalizedSectionId] || { type: "bdw", caller: sectionId || "", title: "", endpoint: "", }; const productMeta = (_bdwCarouselProductBySection[normalizedSectionId] || {})[String(productId)] || { productid: String(productId || ""), productname: "", }; const sessionId = _resolvedGuid || (await bdwResolveGuid()); const payload = { eventtype: "clic", abtestenabled: isAbtestEnabled, bdwsession: !!sessionId, sessionid: sessionId, timestamp: new Date().toISOString(), email: getEmail() || "", source: { type: sourceMeta.type || "bdw", caller: sourceMeta.caller || "", title: sourceMeta.title || "", endpoint: sourceMeta.endpoint || "", }, products: [ { productid: productMeta.productid, productname: productMeta.productname, }, ], tenant: _HASHBNDW, eventcontextkey: sessionId, }; const url = `${BDW_ABTEST_BASE}/api/events/Capture?hashclient=${_HASHBNDW}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(payload), }); if (!response.ok) { console.warn('BDW: click event capture failed:', response.status); } else { console.log('BDW: click event captured successfully'); } } catch (e) { console.error('BDW: Error capturing click event', e); } } // test if the script is loaded const scriptLoaded = () => { const brainObject = SetBrainObject(); getEmailFromPage(); const email = getEmail(); if (email) { trackEmail(email); brainObject.session.email = email; } else { console.log("No email found in local storage"); } brainObject.enable = 1; localStorage.setItem("vrein-info", JSON.stringify(brainObject)); getUrlLocation(); // capture url location for all sections and send to persistence if (window.location.pathname.split("/").indexOf("order-received") == -1) { setTimeout(() => { initCartObserver(); onRemoveProductFromCart(); }, 1000); } setTimeout(async () => { switch (getSectionFromDataLayer()) { case "Home": getProductsIdFromBraindw([ "#BDW-Home-Carrusel-1", "#BDW-Home-Carrusel-2", "#BDW-Home-Carrusel-3", "#BDW-Home-Carrusel-4", "#BDW-Home-Carrusel-5", "#BDW-Home-Carrusel-6", "#BDW-Home-Carrusel-7", "#BDW-Home-Carrusel-8", "#BDW-Home-Carrusel-9", "#BDW-Home-Carrusel-10", "#BDW-Home-Carrusel-11", "#BDW-Home-Carrusel-12", ]); getBannersFromBraindw([ "#BDW-HOME-IMAGES-1", "BDW-HOME-IMAGES-2", "BDW-HOME-IMAGES-3", "BDW-HOME-IMAGES-4", "BDW-HOME-IMAGES-5", ]); break; case "PLP": // Code for PLP await captureProductFromAllSections(); getProductsIdFromBraindw([ "bdw-plp-carrusel", "bdw-plp-carrusel-1", "bdw-plp-carrusel-2", ]); break; case "PDP": // Code for PDP await captureProductFromAllSections(); getProductsIdFromBraindw([ "#bdw-pdp-carrusel-1", "#bdw-pdp-carrusel-2", "#bdw-pdp-carrusel-3", "#bdw-pdp-carrusel-4", "#bdw-pdp-carrusel-5", ]); break; case "Checkout": onRemoveProductFromPage(); // The cart observer owns the Checkout snapshot too; calling // captureOpenCartFromPage() here would emit a duplicate cartOpened. getProductsIdFromBraindw([ "bdw-checkout-carrusel-1", "bdw-checkout-carrusel-2", "bdw-checkout-carrusel-3", "bdw-checkout-carrusel-4", ]); break; case "TyPage": if ( window.location.pathname.split("/").indexOf("order-received") > -1 ) { // Code for Order Received // TODO Cart Closed //getCloseCart(); } // Code for Checkout break; case "Search": getProductsIdFromBraindw(["bdw-sr-carrusel-1", "bdw-sr-carrusel-2"]); break; case "SearchNoResult": getProductsIdFromBraindw([ "bdw-snr-carrusel-1", "bdw-snr-carrusel-2", "bdw-snr-carrusel-3", "bdw-snr-carrusel-4", ]); break; default: break; } }, 600); }; try { scriptLoaded(); // getAbtestFlagStatus(); // warm up ABTest flag cache } catch { console.log("ERROR loading bdw-script") }