/* ============================================================================= * Prology – Related Products Carousel (vanilla JS, embeddable) [SERVER-ONLY] * ----------------------------------------------------------------------------- * Public-facing widget. It NEVER touches Magento directly: no admin * credentials, no admin token, no Magento REST URLs live in this file. * All of that stays on YOUR server. This script only: * - detects keywords from the post title * - detects the visitor's store by geo (au/us) — geo only, no Magento info * - calls YOUR endpoint and renders the returned products in a carousel * * EMBED: * * * * Your server returns a JSON array of: * { name, image, url, price, qty } (price already formatted, url absolute) * See server/server.js for the reference implementation. * ========================================================================== */ window.PROLOGY_BANNER = { endpoint: "http://localhost:8787/api/related-products", }; (function () { "use strict"; // ----- Defaults (override via window.PROLOGY_BANNER) ----------------------- var CFG = Object.assign( { // REQUIRED: your server endpoint (does the Magento login + stock filtering). endpoint: null, count: 16, // how many products to show (10-20) // Keywords for related products. Empty -> auto-detect from the post title. keywords: null, // e.g. ['Catalyst 9300', 'Catalyst 9200', 'Cisco'] // Store hint passed to the server. null -> detect by visitor geo (au/us). // This is GEO only (Cloudflare trace); it carries no Magento information. store: null, // 'au' | 'us' | null = auto-detect traceUrl: "/cdn-cgi/trace", // Where to inject: CSS selector of the post content area. mountSelector: ".entry-content", // Placement: 'after-first-table' | 'end' | 'start' placement: "after-first-table", }, window.PROLOGY_BANNER || {}, ); // --------------------------------------------------------------------------- // 1. Detect related keywords from the page // --------------------------------------------------------------------------- function detectKeywords() { if (CFG.keywords && CFG.keywords.length) return CFG.keywords.slice(); var h1 = document.querySelector("h1"); var text = ((h1 && h1.textContent) || document.title || "").trim(); var BRANDS = [ "Cisco", "Catalyst", "Meraki", "Aruba", "HPE", "Juniper", "Ubiquiti", "UniFi", "Fortinet", "Netgear", "MikroTik", ]; var found = []; var re = /([A-Z][a-zA-Z]+)\s*(\d{3,4}[A-Za-z]?)/g, m; while ((m = re.exec(text)) !== null) found.push(m[1] + " " + m[2]); BRANDS.forEach(function (b) { if (new RegExp("\\b" + b + "\\b", "i").test(text)) found.push(b); }); var seen = {}, out = []; found.forEach(function (k) { var key = k.toLowerCase(); if (!seen[key]) { seen[key] = 1; out.push(k); } }); return out.length ? out : ["Cisco"]; } // --------------------------------------------------------------------------- // 2. Detect store by visitor geo (AU -> 'au', otherwise -> 'us'). Geo only. // The server validates this and can override it (e.g. via CF-IPCountry). // --------------------------------------------------------------------------- function fallbackStore() { try { if ( /Australia/i.test( Intl.DateTimeFormat().resolvedOptions().timeZone || "", ) ) return "au"; } catch (e) {} return /-au\b/i.test(navigator.language || "") ? "au" : "us"; } function resolveStore() { if (CFG.store) return Promise.resolve(CFG.store); return fetch(CFG.traceUrl) .then(function (r) { return r.ok ? r.text() : ""; }) .then(function (t) { var m = /(?:^|\n)loc=([A-Z]{2})/.exec(t || ""); return m ? (m[1] === "AU" ? "au" : "us") : fallbackStore(); }) .catch(fallbackStore); } // --------------------------------------------------------------------------- // 3. Load products from YOUR server (the only network call to your backend) // --------------------------------------------------------------------------- function loadProducts(store) { var q = encodeURIComponent(detectKeywords().join(",")); var sep = CFG.endpoint.indexOf("?") >= 0 ? "&" : "?"; var url = CFG.endpoint + sep + "q=" + q + "&count=" + CFG.count + (store ? "&store=" + encodeURIComponent(store) : ""); return fetch(url) .then(function (r) { return r.json(); }) .then(function (list) { return Array.isArray(list) ? list : (list && list.items) || []; }); } // --------------------------------------------------------------------------- // 4. Render (Shadow DOM) – carousel with prev/next arrows, no header // --------------------------------------------------------------------------- var STYLE = '\ :host{all:initial;display:block!important;width:100%;max-width:100%;min-width:0}\ *{box-sizing:border-box}\ .pb{position:relative;width:100%;max-width:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;margin:32px 0;color:#0f2747}\ .pb-spon{display:block;text-align:right;font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:#9aa7b8;margin:0 2px 4px}\ .pb-track{display:flex;gap:14px;width:100%;min-width:0;max-width:100%;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;\ padding:6px 2px 12px;-webkit-overflow-scrolling:touch;scrollbar-width:none}\ .pb-track::-webkit-scrollbar{display:none}\ .pb-card{scroll-snap-align:start;flex:0 0 200px;display:flex;flex-direction:column;\ border:1px solid #eef2f7;border-radius:12px;overflow:hidden;text-decoration:none;color:inherit;background:#fff;\ transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease}\ .pb-card:hover{transform:translateY(-3px);box-shadow:0 10px 22px rgba(16,42,77,.12);border-color:#cfe0f5}\ .pb-thumb{position:relative;aspect-ratio:1/1;background:#f5f8fc;display:flex;align-items:center;justify-content:center}\ .pb-thumb img{width:100%;height:100%;object-fit:contain;padding:10px}\ .pb-stock{position:absolute;top:8px;left:8px;font-size:10px;font-weight:600;color:#0a7d3c;\ background:#e6f7ec;border:1px solid #b6e6c6;border-radius:20px;padding:2px 8px}\ .pb-body{padding:11px 12px 13px;display:flex;flex-direction:column;gap:7px;flex:1}\ .pb-name{font-size:12.5px;line-height:1.35;font-weight:600;color:#13294a;\ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:34px}\ .pb-foot{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:8px}\ .pb-price{font-size:14px;font-weight:800;color:#0a4a8f}\ .pb-cta{font-size:11px;font-weight:700;color:#0a4a8f;background:#eaf2fc;border-radius:7px;padding:5px 9px;white-space:nowrap}\ .pb-card:hover .pb-cta{background:#0a4a8f;color:#fff}\ .pb-nav{position:absolute;top:38%;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;\ border:1px solid #e3e9f1;background:#fff;color:#0a4a8f;cursor:pointer;z-index:3;\ display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px rgba(16,42,77,.16);\ transition:opacity .15s ease,background .15s ease,color .15s ease}\ .pb-nav:hover{background:#0a4a8f;color:#fff}\ .pb-nav svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}\ .pb-prev{left:6px}.pb-next{right:6px}\ .pb-nav[disabled]{opacity:0;pointer-events:none}\ .pb-empty,.pb-load{padding:26px 20px;text-align:center;color:#6b7a90;font-size:13px}\ .pb-sk{flex:0 0 200px;background:linear-gradient(90deg,#f0f3f8 25%,#e7ecf3 37%,#f0f3f8 63%);\ background-size:400% 100%;animation:pbsh 1.3s ease infinite;border-radius:12px;aspect-ratio:.74/1}\ @keyframes pbsh{0%{background-position:100% 0}100%{background-position:-100% 0}}\ @media(max-width:680px){.pb-card,.pb-sk{flex:0 0 158px}.pb-nav{width:34px;height:34px}.pb-prev{left:4px}.pb-next{right:4px}}\ @media(max-width:380px){.pb-card,.pb-sk{flex:0 0 144px}}'; var ARROW_L = ''; var ARROW_R = ''; function el(host) { var root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host; var st = document.createElement("style"); st.textContent = STYLE; root.appendChild(st); var wrap = document.createElement("div"); wrap.className = "pb"; root.appendChild(wrap); return wrap; } function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[c]; }); } function skeleton(wrap) { var cards = ""; for (var i = 0; i < 6; i++) cards += '
'; wrap.innerHTML = '