263 lines
9.6 KiB
JavaScript

/**
* Ad Widget Hydration for MkDocs
*
* Converts ad block placeholders (inserted via DocsPage toolbar) into
* rendered ad cards when pages are viewed in MkDocs.
*
* Supports:
* - .ad-specific-block[data-ad-id] — renders a specific ad by ID
* - .ad-slot-block[data-placement][data-variant] — renders a dynamic ad slot
*
* Reads API URL from window.PAYMENT_API_URL (set by env-config.js).
* Tracks impressions and clicks via POST /api/gallery-ads/track.
*/
(function () {
'use strict';
var API_URL = window.PAYMENT_API_URL || '';
/** Lighten/darken a hex color by an amount */
function adjustColor(hex, amount) {
function clamp(v) { return Math.max(0, Math.min(255, v)); }
var h = hex.replace('#', '');
var r = clamp(parseInt(h.substring(0, 2), 16) + amount);
var g = clamp(parseInt(h.substring(2, 4), 16) + amount);
var b = clamp(parseInt(h.substring(4, 6), 16) + amount);
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
}
/** Get or create a simple session ID for tracking */
function getSessionId() {
var key = 'cm_ad_session';
var id = sessionStorage.getItem(key);
if (!id) {
id = Math.random().toString(36).slice(2) + Date.now().toString(36);
sessionStorage.setItem(key, id);
}
return id;
}
/** Track an impression or click */
function trackEvent(adId, event) {
if (!API_URL) return;
fetch(API_URL + '/api/gallery-ads/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adId: adId, event: event, sessionId: getSessionId() }),
}).catch(function () {}); // silent
}
/** Render an ad card into a container element */
function renderAdCard(container, ad) {
container.innerHTML = '';
container.setAttribute('data-hydrated', 'true');
var isHighlight = ad.variant === 'highlight';
var isMinimal = ad.variant === 'minimal';
var defaultPrimary = '#1677ff';
var bgColor = ad.bgColor || defaultPrimary;
var bgGradient = ad.bgColor
? 'linear-gradient(135deg, ' + ad.bgColor + ' 0%, ' + adjustColor(ad.bgColor, -30) + ' 100%)'
: isHighlight
? 'linear-gradient(135deg, ' + defaultPrimary + ' 0%, ' + adjustColor(defaultPrimary, -40) + ' 100%)'
: 'linear-gradient(135deg, #1f1f2e 0%, #141422 100%)';
var borderStyle = isHighlight
? '2px solid ' + bgColor
: '1px solid rgba(255,255,255,0.08)';
var card = document.createElement('div');
card.style.cssText = 'border-radius:12px;overflow:hidden;border:' + borderStyle +
';cursor:' + (ad.linkUrl ? 'pointer' : 'default') +
';transition:all 0.2s ease;display:flex;flex-direction:column;max-width:400px;margin:16px auto;' +
(isHighlight ? 'box-shadow:0 0 20px ' + bgColor + '33;' : '');
// Click handler
card.addEventListener('click', function () {
trackEvent(ad.id, 'click');
if (ad.linkUrl) {
if (ad.linkUrl.indexOf('http') === 0) {
window.open(ad.linkUrl, '_blank', 'noopener');
} else {
window.location.href = ad.linkUrl;
}
}
});
// Top section (16:9 visual area) — skip for minimal variant
if (!isMinimal) {
var topBg = ad.imagePath
? 'url(' + ad.imagePath + ') center/cover no-repeat'
: bgGradient;
var top = document.createElement('div');
top.style.cssText = 'position:relative;padding-top:56.25%;background:' + topBg + ';';
var overlay = document.createElement('div');
overlay.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;text-align:center;' +
(ad.imagePath ? 'background:rgba(0,0,0,0.5);' : '');
if (ad.iconEmoji) {
var emoji = document.createElement('span');
emoji.style.cssText = 'font-size:36px;margin-bottom:8px;';
emoji.textContent = ad.iconEmoji;
overlay.appendChild(emoji);
}
var title = document.createElement('h4');
title.style.cssText = 'color:#fff;margin:0;text-shadow:0 1px 3px rgba(0,0,0,0.3);font-size:16px;font-weight:600;';
title.textContent = ad.title;
overlay.appendChild(title);
if (ad.subtitle) {
var subtitle = document.createElement('p');
subtitle.style.cssText = 'color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:13px;max-width:240px;';
subtitle.textContent = ad.subtitle;
overlay.appendChild(subtitle);
}
top.appendChild(overlay);
card.appendChild(top);
}
// Bottom section
var bottom = document.createElement('div');
bottom.style.cssText = 'padding:' + (isMinimal ? '20px 16px' : '12px 16px') +
';background:' + (isMinimal ? bgGradient : '#1b2838') +
';display:flex;flex-direction:column;gap:8px;';
if (isMinimal) {
if (ad.iconEmoji) {
var emojiMin = document.createElement('span');
emojiMin.style.fontSize = '24px';
emojiMin.textContent = ad.iconEmoji;
bottom.appendChild(emojiMin);
}
var titleMin = document.createElement('h5');
titleMin.style.cssText = 'color:#fff;margin:0;font-size:14px;font-weight:600;';
titleMin.textContent = ad.title;
bottom.appendChild(titleMin);
if (ad.subtitle) {
var subMin = document.createElement('p');
subMin.style.cssText = 'color:rgba(255,255,255,0.6);font-size:12px;margin:0;';
subMin.textContent = ad.subtitle;
bottom.appendChild(subMin);
}
}
if (ad.ctaText) {
var cta = document.createElement('a');
cta.textContent = ad.ctaText;
cta.href = ad.linkUrl || '#';
cta.style.cssText = 'display:block;text-align:center;padding:6px 16px;border-radius:6px;font-size:13px;font-weight:500;text-decoration:none;' +
(ad.ctaStyle === 'primary'
? 'background:' + defaultPrimary + ';color:#fff;'
: ad.ctaStyle === 'outline'
? 'background:transparent;color:' + defaultPrimary + ';border:1px solid ' + defaultPrimary + ';'
: 'background:transparent;color:' + defaultPrimary + ';');
cta.addEventListener('click', function (e) {
e.stopPropagation();
});
bottom.appendChild(cta);
}
var promo = document.createElement('p');
promo.style.cssText = 'font-size:10px;color:rgba(255,255,255,0.25);text-align:center;margin:0;';
promo.textContent = 'Promoted';
bottom.appendChild(promo);
card.appendChild(bottom);
container.appendChild(card);
// Impression tracking via IntersectionObserver
var impressionSent = false;
var timer = null;
var observer = new IntersectionObserver(function (entries) {
if (entries[0] && entries[0].isIntersecting) {
timer = setTimeout(function () {
if (!impressionSent) {
impressionSent = true;
trackEvent(ad.id, 'impression');
}
}, 1000);
} else if (timer) {
clearTimeout(timer);
timer = null;
}
}, { threshold: 0.5 });
observer.observe(card);
}
/** Hydrate all ad blocks on the page */
function hydrateAds() {
if (!API_URL) return;
var specificBlocks = document.querySelectorAll('.ad-specific-block:not([data-hydrated])');
var slotBlocks = document.querySelectorAll('.ad-slot-block:not([data-hydrated])');
if (specificBlocks.length === 0 && slotBlocks.length === 0) return;
// Fetch all active ads (for both specific and slot hydration)
fetch(API_URL + '/api/gallery-ads?placement=docs')
.then(function (r) { return r.json(); })
.then(function (ads) {
if (!Array.isArray(ads)) return;
// Hydrate specific ad blocks
specificBlocks.forEach(function (el) {
var adId = parseInt(el.getAttribute('data-ad-id') || '0', 10);
if (!adId) return;
var ad = ads.find(function (a) { return a.id === adId; });
if (!ad) {
// Ad not found or not active — try fetching all ads (placement filter may exclude it)
fetch(API_URL + '/api/gallery-ads')
.then(function (r) { return r.json(); })
.then(function (allAds) {
var found = allAds.find(function (a) { return a.id === adId; });
if (found) renderAdCard(el, found);
else el.style.display = 'none';
})
.catch(function () { el.style.display = 'none'; });
return;
}
renderAdCard(el, ad);
});
// Hydrate dynamic ad slot blocks
if (slotBlocks.length > 0 && ads.length > 0) {
var idx = 0;
slotBlocks.forEach(function (el) {
var variant = el.getAttribute('data-variant') || 'standard';
var match = ads.find(function (a) { return a.variant === variant; }) || ads[idx % ads.length];
idx++;
if (match) renderAdCard(el, match);
else el.style.display = 'none';
});
} else if (slotBlocks.length > 0) {
slotBlocks.forEach(function (el) { el.style.display = 'none'; });
}
})
.catch(function (err) {
console.warn('[Ad Widgets] Failed to fetch ads:', err);
specificBlocks.forEach(function (el) { el.style.display = 'none'; });
slotBlocks.forEach(function (el) { el.style.display = 'none'; });
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hydrateAds);
} else {
hydrateAds();
}
// Re-initialize on MkDocs SPA navigation
if (typeof window.document$ !== 'undefined') {
window.document$.subscribe(function () {
setTimeout(hydrateAds, 100);
});
}
})();