/** * * revi.io | https://revi.io/ * * Copyright (c) 2024 Revi. ALL RIGHTS RESERVED. * **/ (function () { // Dominio del widget window.reviDomain = { domain: "https://widgets.revi.io", }; // Evita doble inicialización if (window.reviWidgetInitialized) { return; } window.reviWidgetInitialized = true; const domain = window.reviDomain.domain; const CACHE_NAME = 'revi-widget-cache-v1'; const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 h en milisegundos const initializedWidgets = new Set(); /** * fetch con Cache API + fallback a localStorage. * @param {string} url * @param {{json: boolean, ttl: number}} options * @returns {Promise} */ async function fetchWithCache(url, { json = true, ttl = CACHE_TTL } = {}) { // 1) Intento Cache API if ('caches' in window) { const cache = await caches.open(CACHE_NAME); const cached = await cache.match(url); if (cached) { const tsHeader = cached.headers.get('SW-Fetched-Timestamp'); const ts = tsHeader ? new Date(tsHeader).getTime() : 0; if (Date.now() - ts < ttl) { return json ? cached.clone().json() : cached.clone().text(); } } // Si no hay o expiró, vamos a red const resp = await fetch(url); if (resp.ok) { // añadimos cabecera propia con timestamp const headers = new Headers(resp.headers); headers.set('SW-Fetched-Timestamp', new Date().toISOString()); const blob = await resp.clone().blob(); const clone = new Response(blob, { status: resp.status, statusText: resp.statusText, headers }); cache.put(url, clone); } return json ? resp.json() : resp.text(); } // 2) Fallback localStorage const key = `cw:${url}`; const keyTs = key + ':ts'; const stored = localStorage.getItem(key); const ts = parseInt(localStorage.getItem(keyTs), 10) || 0; if (stored && (Date.now() - ts < ttl)) { return json ? JSON.parse(stored) : stored; } const resp = await fetch(url); const body = json ? await resp.clone().json() : await resp.clone().text(); if (resp.ok) { localStorage.setItem(key, json ? JSON.stringify(body) : body); localStorage.setItem(keyTs, Date.now()); } return body; } // === Reemplazo de funciones con fetchWithCache === /** Carga el CSS del widget (usa Cache) */ async function loadWidgetStyles(parentContainer, widgetToken) { const model = parentContainer.getAttribute('data-model') || ''; const cssUrl = await fetchWithCache( `${domain}/api/getWidgetCSS/${widgetToken}/${model}`, { json: false } ); const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = cssUrl; document.head.appendChild(link); await new Promise(resolve => link.onload = resolve); } /** Obtiene el HTML del widget (usa Cache) */ async function fetchWidgetContent(parentContainer, widgetToken) { const lang = parentContainer.getAttribute('data-lang') || ''; const idProduct = parentContainer.getAttribute('data-id-product') || ''; const model = parentContainer.getAttribute('data-model') || ''; const numReviews = parentContainer.getAttribute('data-num-reviews')|| ''; const url = `${domain}/proxy-widget?widgetToken=${encodeURIComponent(widgetToken)}` + `&idProduct=${encodeURIComponent(idProduct)}` + `&lang=${encodeURIComponent(lang)}` + `&model=${encodeURIComponent(model)}` + `&numReviews=${encodeURIComponent(numReviews)}`; return fetchWithCache(url, { json: true }); } /** Carga el core JS del widget con Cache y evita duplicados */ async function loadWidgetImmediately(container, widgetToken) { try { const data = await fetchWithCache( `${domain}/api/getWidgetCoreFile/${widgetToken}`, { json: true } ); if (initializedWidgets.has(widgetToken)) { console.log(`Widget ${widgetToken} ya inicializado.`); return; } const src = `${domain}/app/${data.widgetCoreFile}.js`; loadScript(src, container, widgetToken); initializedWidgets.add(widgetToken); } catch (error) { console.error('Error cargando widget:', error); } } // === Funciones de carga originales (sin tocar) === function loadWidgetLazily(container, widgetToken) { const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { loadWidgetImmediately(container, widgetToken); obs.unobserve(container); } }); }, { root: null, rootMargin: '0px', threshold: 0.1 }); observer.observe(container); function onUserInteraction() { document.removeEventListener('scroll', onUserInteraction); document.removeEventListener('mousemove', onUserInteraction); document.removeEventListener('touchstart', onUserInteraction); loadWidgetImmediately(container, widgetToken); } document.addEventListener('scroll', onUserInteraction); document.addEventListener('mousemove', onUserInteraction); document.addEventListener('touchstart', onUserInteraction); } function loadScript(src, container, widgetToken) { const script = document.createElement('script'); script.type = 'text/javascript'; script.src = src; script.onload = () => { if (typeof window.initializeWidget === 'function') { window.initializeWidget(container, widgetToken); } }; document.head.appendChild(script); } function getWidgetTokenFromClass(className) { const match = className.match(/revi-widget-([^\s]+)/); return match ? match[1] : null; } function getLazyMode(container) { return container.getAttribute('data-revi-widget-lazy') || 'disabled'; } function initializeWidget() { const widgetContainers = document.querySelectorAll('div[class^="revi-widget-"]'); widgetContainers.forEach(container => { const widgetToken = getWidgetTokenFromClass(container.className); if (!widgetToken) { console.warn('Widget token no encontrado para el contenedor:', container); return; } const lazyMode = getLazyMode(container); if (lazyMode === 'enabled') { loadWidgetLazily(container, widgetToken); } else { loadWidgetImmediately(container, widgetToken); } }); } // Inicia al cargar el DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeWidget); } else { initializeWidget(); } })();