/* Demo player. Renders image or dom steps with hotspot overlay; emits privacy-first beacons. */ (function () { "use strict"; var token = location.pathname.split("/").pop(); var params = new URLSearchParams(location.search); var viewerToken = params.get("v") || null; // R7: identity only via explicit link token var state = { demo: null, i: 0 }; var snap = document.getElementById("snap"); var dots = document.getElementById("dots"); var titleEl = document.getElementById("title"); function beacon(event, stepId) { if (!state.demo) return; var payload = JSON.stringify({ demo_id: state.demo.demo_id, step_id: stepId || null, viewer_token: viewerToken, event: event, }); if (navigator.sendBeacon) { navigator.sendBeacon("/api/public/analytics", new Blob([payload], { type: "application/json" })); } else { fetch("/api/public/analytics", { method: "POST", headers: { "Content-Type": "application/json" }, body: payload }); } } function render() { var step = state.demo.steps[state.i]; snap.innerHTML = ""; var container; if (step.snapshot_type === "image") { container = document.createElement("img"); container.src = "/assets/" + step.asset_id; container.draggable = false; } else { container = document.createElement("iframe"); container.setAttribute("sandbox", ""); // R1/R2: fully inert document, no scripts container.srcdoc = step.dom_html || ""; } snap.appendChild(container); if (step.hotspot) { var h = document.createElement("div"); h.className = "hotspot"; h.style.left = step.hotspot.x + "%"; h.style.top = step.hotspot.y + "%"; h.style.width = step.hotspot.w + "%"; h.style.height = step.hotspot.h + "%"; h.addEventListener("click", next); snap.appendChild(h); if (step.annotation) { var t = document.createElement("div"); t.className = "tooltip"; var below = step.hotspot.y < 70; t.style.left = Math.min(step.hotspot.x, 70) + "%"; t.style.top = below ? (step.hotspot.y + step.hotspot.h + 2) + "%" : "auto"; if (!below) t.style.bottom = (100 - step.hotspot.y + 2) + "%"; t.innerHTML = (step.annotation.title ? "
" : "") + (step.annotation.body ? "" : ""); if (step.annotation.title) t.querySelector("h3").textContent = step.annotation.title; if (step.annotation.body) t.querySelector("p").textContent = step.annotation.body; snap.appendChild(t); } } dots.innerHTML = ""; state.demo.steps.forEach(function (_, j) { var d = document.createElement("div"); d.className = "dot" + (j === state.i ? " active" : ""); dots.appendChild(d); }); } function next() { var cur = state.demo.steps[state.i]; if (state.i >= state.demo.steps.length - 1) { beacon("complete", cur.id); return; } state.i++; beacon("advance", state.demo.steps[state.i].id); render(); } function prev() { if (state.i === 0) return; state.i--; beacon("back", state.demo.steps[state.i].id); render(); } document.getElementById("next").addEventListener("click", next); document.getElementById("prev").addEventListener("click", prev); document.addEventListener("keydown", function (e) { if (e.key === "ArrowRight") next(); if (e.key === "ArrowLeft") prev(); }); fetch("/api/public/demo/" + token) .then(function (r) { if (!r.ok) throw new Error("not found"); return r.json(); }) .then(function (demo) { state.demo = demo; titleEl.textContent = demo.name; document.title = demo.name; beacon("view", demo.steps.length ? demo.steps[0].id : null); render(); }) .catch(function () { snap.innerHTML = "