Your own product gallery: API + iframe
This example loads your tenant's products via the REST API and shows them as a gallery on your page. Clicking a card switches the embedded iframe to that product, and your own export button next to it produces the MP4. It takes a few lines of JavaScript without a framework, and you can test it all locally first.
For AI coding agents
This example is available as Markdown too. Prompt for your coding agent:
Prompt Prompt
Read https://virtualcampaign.dev/embed/gallery.md and build a product gallery on our website that drives the embedded Creative Simulator. Remote-controlled export
Your gallery (from GET /v1/products)
Cards from the API response; active once the iframe reports ready.
Loading products from the API …
Your gallery (own images)
Static list { code, name, image } in your code – no API call. Click → setProduct(code).
Event log (iframe → your page)
- No events received yet …
index.html (gallery from the API) index.html (gallery from the API)
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kreativsimulator – Produkt-Galerie</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; background: #111; color: #eee; }
.layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); gap: 16px; padding: 16px; }
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
iframe { width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000; border-radius: 8px; }
#gallery { display: grid; gap: 8px; align-content: start; }
.card { display: flex; gap: 10px; align-items: center; text-align: left; padding: 6px;
border: 2px solid #333; border-radius: 8px; background: #1b1b1b; color: #eee; cursor: pointer; }
.card:disabled { opacity: .4; cursor: default; }
.card.active { border-color: #ff5722; }
.card img { width: 96px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 4px; background: #000; }
.bar { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
button, select { font: inherit; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #222; color: #eee; }
button:not(:disabled):hover { border-color: #ff5722; }
#status { font-size: 12px; color: #999; margin-left: auto; }
/* Notice dialog — dark, orange accent (matches the VirtualCampaign look) */
.notice { border: 1px solid #2a2a2a; border-radius: 16px; background: #141414; color: #eee;
padding: 0; margin: auto; width: min(640px, calc(100vw - 32px)); box-shadow: 0 30px 80px rgba(0,0,0,.6); }
.notice::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px); }
.notice form { position: relative; padding: 32px 36px 28px; margin: 0; }
.notice h2 { margin: 0 0 14px; font-size: 28px; font-weight: 600; letter-spacing: -.01em; }
.notice p { margin: 0 0 26px; color: #b5b5b5; line-height: 1.65; font-size: 15px; }
.notice-actions { display: flex; justify-content: flex-end; gap: 10px; }
.notice-primary { background: #ff5722; color: #fff; border: 0; border-radius: 999px; padding: 12px 24px;
font: inherit; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; }
.notice-primary:hover { background: #e64a19; }
.notice-secondary { background: transparent; color: #b5b5b5; border: 1px solid #333; border-radius: 999px;
padding: 12px 20px; font: inherit; cursor: pointer; }
.notice-secondary:hover { color: #fff; border-color: #666; }
.notice-close { position: absolute; top: 14px; right: 16px; background: none; border: 0; color: #ff5722;
font-size: 26px; line-height: 1; cursor: pointer; }
</style>
</head>
<body>
<div class="layout">
<div>
<!-- Only the canvas: header, sidebar and the built-in export button are hidden. -->
<iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk" title="Kreativsimulator" allow="fullscreen"></iframe>
<div class="bar">
<select id="res"></select>
<button id="export" disabled>Export MP4</button>
<button id="still" disabled>Save image (JPG)</button>
<span id="status">Loading …</span>
</div>
</div>
<div id="gallery"></div>
</div>
<!-- Notice dialog — shown before every download. Wording is yours. -->
<dialog id="notice" class="notice">
<form method="dialog">
<button type="button" class="notice-close" aria-label="Schließen" data-cancel>×</button>
<h2>Hinweis</h2>
<p>
Die erstellte Simulation ist urheberrechtlich geschützt. Sie darf nur intern von Ihnen
bzw. bei Agenturen auch gegenüber (potentiellen) Kunden zu Präsentationszwecken genutzt
werden. Eine darüber hinausgehende Nutzung, insbesondere eine Veröffentlichung
(Website, Social Media etc.), ist nicht gestattet.
</p>
<div class="notice-actions">
<button type="button" class="notice-secondary" data-cancel>Abbrechen</button>
<button type="submit" value="confirm" class="notice-primary">Bestätigen →</button>
</div>
</form>
</dialog>
<script>
const API = "https://api.virtualcampaign.dev";
const API_KEY = "vc_4b3efe1be755a77ba77b95e67067d8e84176fdf7621ebd3b2d94e"; // demo key – replace with your own
const SIMULATOR = "https://demo.kreativsimulator.com/"; // your tenant: https://<tenant>.kreativsimulator.com/
const P = "kreativsimulator:";
const iframe = document.getElementById("ks");
const gallery = document.getElementById("gallery");
const res = document.getElementById("res");
const status = document.getElementById("status");
const exportBtn = document.getElementById("export");
const stillBtn = document.getElementById("still");
const send = (msg) => iframe.contentWindow.postMessage(msg, "*");
let simulatorCodes = null;
// ── 1. Gallery from the REST API ────────────────────────────────────
async function loadProducts() {
const r = await fetch(API + "/v1/products?limit=50", {
headers: { Authorization: "Bearer " + API_KEY },
});
if (!r.ok) throw new Error("API error " + r.status);
return (await r.json()).products;
}
function renderGallery(products) {
gallery.innerHTML = "";
for (const p of products) {
const card = document.createElement("button");
card.type = "button";
card.className = "card";
card.dataset.code = p.productCode;
card.disabled = true; // until the iframe says `ready`
const img = document.createElement("img");
img.src = API + p.thumbnailUrl + "?res=mdpi";
img.alt = "";
const name = document.createElement("span");
name.textContent = p.productName;
card.append(img, name);
card.onclick = () => send({ type: P + "setProduct", code: p.productCode });
gallery.appendChild(card);
}
syncCards();
}
function syncCards(activeCode) {
for (const card of gallery.children) {
const known = simulatorCodes && simulatorCodes.has(card.dataset.code);
card.disabled = !known;
card.title = simulatorCodes && !known ? "Not available in this simulator tenant" : "";
card.classList.toggle("active", card.dataset.code === activeCode);
}
}
// ── 2. Events from the iframe ───────────────────────────────────────
window.addEventListener("message", (e) => {
const m = e.data;
if (!m || m.source !== "kreativsimulator") return;
// Canvas shape + export presets: ready/productChanged carry `render`,
// stageChanged carries the fields itself. A photo product is NOT 16:9.
const shape = m.type === "stageChanged" ? m : m.render;
if (shape) {
iframe.style.aspectRatio = shape.width + " / " + shape.height;
res.innerHTML = "";
for (const r of shape.presets) {
const o = document.createElement("option");
o.value = r.width + "x" + r.height;
o.textContent = r.label;
o.selected = r.width === shape.width;
res.appendChild(o);
}
}
if (m.type === "ready") {
// With the product library, ready.products holds only the first 30 (total in productsTotal).
// Then any code of the catalog may be valid, so check nothing.
simulatorCodes = m.productsTotal > m.products.length
? { has: () => true }
: new Set(m.products.map((p) => p.code));
exportBtn.disabled = stillBtn.disabled = false;
syncCards(m.product);
status.textContent = "Ready";
}
if (m.type === "productChanged") syncCards(m.code);
if (m.type === "error") status.textContent = "Error: " + m.message;
});
// ── 3. Notice dialog + remote export (works with the built-in button hidden)
// Ask first, download afterwards: confirmThen(fn) opens the dialog and runs fn
// only when the user confirms. Works for export (MP4) and exportImage (JPG) alike.
const notice = document.getElementById("notice");
let pendingAction = null;
function confirmThen(action) {
pendingAction = action;
notice.showModal();
}
// "Bestätigen" submits the <form method="dialog"> (click or Enter) → run the action.
notice.querySelector("form").addEventListener("submit", () => {
if (pendingAction) pendingAction();
pendingAction = null;
});
// "Abbrechen" / × / Esc just close the dialog — nothing is sent.
notice.querySelectorAll("[data-cancel]").forEach((b) => (b.onclick = () => notice.close()));
notice.addEventListener("cancel", () => (pendingAction = null));
const size = () => res.value.split("x").map(Number);
exportBtn.onclick = () => {
const [width, height] = size();
confirmThen(() => send({ type: P + "export", filename: "kreation", width, height }));
};
stillBtn.onclick = () => {
const [width, height] = size();
confirmThen(() => send({ type: P + "exportImage", filename: "kreation", width, height }));
};
loadProducts()
.then(renderGallery)
.catch((err) => (status.textContent = "Could not load products: " + err.message));
</script>
</body>
</html> Own thumbnails Own thumbnails
<!-- Your own images (hosted by you) mapped to product codes. -->
<div style="display:grid; grid-template-columns: 2fr 1fr; gap: 16px;">
<iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk"
title="Kreativsimulator" style="width:100%; aspect-ratio:16/9; border:0"
allow="fullscreen"></iframe>
<div id="gallery" style="display:grid; gap:8px; align-content:start"></div>
</div>
<script>
// The product code is the key. Take it from GET /v1/products (productCode),
// from the iframe's `ready` event (products[].code) or from the VirtualCampaign UI.
const MY_PRODUCTS = [
{ code: "29a5c31b", name: "City-Light München", image: "/img/citylight-muenchen.jpg" },
{ code: "2aff892d", name: "Billboard Bursa", image: "/img/billboard-bursa.jpg" },
{ code: "e6bc7001", name: "ePanel Bahnhof", image: "/img/epanel.jpg" },
];
const iframe = document.getElementById("ks");
const send = (msg) => iframe.contentWindow.postMessage(msg, "*");
const gallery = document.getElementById("gallery");
let simulatorCodes = null;
for (const p of MY_PRODUCTS) {
const card = document.createElement("button");
card.type = "button";
card.dataset.code = p.code;
card.disabled = true; // until `ready`
card.style.cssText = "display:flex; gap:10px; align-items:center; text-align:left; padding:6px; border:2px solid #ddd; border-radius:8px; background:#fff; cursor:pointer";
const img = document.createElement("img");
img.src = p.image; // your own file – any host, no CORS needed for <img>
img.alt = "";
img.style.cssText = "width:96px; aspect-ratio:16/9; object-fit:cover; border-radius:4px";
const name = document.createElement("span");
name.textContent = p.name;
card.append(img, name);
card.onclick = () => send({ type: "kreativsimulator:setProduct", code: p.code });
gallery.appendChild(card);
}
function syncCards(activeCode) {
for (const card of gallery.children) {
const known = simulatorCodes && simulatorCodes.has(card.dataset.code);
card.disabled = !known;
card.title = simulatorCodes && !known ? "Unknown product code in this tenant" : "";
card.style.borderColor = card.dataset.code === activeCode ? "#ff5722" : "#ddd";
}
}
window.addEventListener("message", (e) => {
const m = e.data;
if (!m || m.source !== "kreativsimulator") return;
// Canvas shape: ready/productChanged carry `render`, stageChanged carries it
// itself. A photo product is NOT 16:9 — follow it, then there is no letterbox.
const r = m.type === "stageChanged" ? m : m.render;
if (r) iframe.style.aspectRatio = r.width + " / " + r.height;
if (m.type === "ready") {
// With the product library, ready.products holds only the first 30 (total in productsTotal).
// Then any code of the catalog may be valid, so check nothing.
simulatorCodes = m.productsTotal > m.products.length
? { has: () => true }
: new Set(m.products.map((p) => p.code));
syncCards(m.product);
}
if (m.type === "productChanged") syncCards(m.code);
if (m.type === "error") console.warn("Kreativsimulator:", m.message);
});
</script> Want your own API key and domain whitelisted?
Send us your domains. We'll set up your tenant, send you your API key and whitelist the domains in the same step.