263 lines
9.6 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
})();
|