Okay Wish I could say I know exactly. Will do better next time promise lol
This commit is contained in:
262
mkdocs/docs/javascripts/ad-widgets.js
Normal file
262
mkdocs/docs/javascripts/ad-widgets.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
})();
|
||||
479
mkdocs/docs/javascripts/docs-comments.js
Normal file
479
mkdocs/docs/javascripts/docs-comments.js
Normal file
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* Docs Comments Widget — Gitea Issues-backed comments for MkDocs pages
|
||||
*
|
||||
* Loads approved comments from the Express API proxy, supports:
|
||||
* - Anonymous comments (with moderation queue)
|
||||
* - Gitea OAuth2 login for instant comments
|
||||
* - Dark/light theme via MkDocs Material CSS vars
|
||||
* - SPA re-init via document$.subscribe()
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// --- Config ---
|
||||
function getApiUrl() {
|
||||
// env-config.js sets window.PAYMENT_API_URL to the resolved API URL
|
||||
return window.PAYMENT_API_URL || 'http://localhost:4000';
|
||||
}
|
||||
|
||||
var API_BASE = '';
|
||||
var SESSION_KEY = 'docs-comment-gitea-token';
|
||||
var USER_KEY = 'docs-comment-gitea-user';
|
||||
var STATE_KEY = 'docs-comment-oauth-state';
|
||||
var RETURN_KEY = 'docs-comment-return-url';
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal inline markdown: **bold**, *italic*, `code`, [text](url)
|
||||
*/
|
||||
function renderInlineMarkdown(text) {
|
||||
var html = escapeHtml(text);
|
||||
// Code (backticks)
|
||||
html = html.replace(/`([^`]+)`/g, '<code class="dc-inline-code">$1</code>');
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
// Italic
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
// Links
|
||||
html = html.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
||||
);
|
||||
// Newlines
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
return html;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
var now = Date.now();
|
||||
var then = new Date(dateStr).getTime();
|
||||
var diff = Math.floor((now - then) / 1000);
|
||||
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago';
|
||||
return new Date(dateStr).toLocaleDateString();
|
||||
}
|
||||
|
||||
function getInitials(name) {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.map(function (w) { return w[0]; })
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
function getToken() {
|
||||
try { return sessionStorage.getItem(SESSION_KEY); } catch { return null; }
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
try {
|
||||
var data = sessionStorage.getItem(USER_KEY);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function setAuth(token, user) {
|
||||
try {
|
||||
sessionStorage.setItem(SESSION_KEY, token);
|
||||
sessionStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
} catch { /* sessionStorage unavailable */ }
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
try {
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(USER_KEY);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
|
||||
function fetchComments(pagePath, page, callback) {
|
||||
var url = API_BASE + '/api/docs-comments/comments?pagePath=' +
|
||||
encodeURIComponent(pagePath) + '&page=' + (page || 1);
|
||||
|
||||
fetch(url)
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) { callback(null, data); })
|
||||
.catch(function (err) { callback(err); });
|
||||
}
|
||||
|
||||
function postAnonymous(payload, callback) {
|
||||
fetch(API_BASE + '/api/docs-comments/comments/anonymous', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) return res.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) { callback(null, data); })
|
||||
.catch(function (err) { callback(err); });
|
||||
}
|
||||
|
||||
function postAuthenticated(payload, token, callback) {
|
||||
fetch(API_BASE + '/api/docs-comments/comments/authenticated', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Gitea-Token': token,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) return res.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) { callback(null, data); })
|
||||
.catch(function (err) { callback(err); });
|
||||
}
|
||||
|
||||
function fetchOAuthConfig(callback) {
|
||||
fetch(API_BASE + '/api/docs-comments/oauth/config')
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) { callback(null, data); })
|
||||
.catch(function (err) { callback(err); });
|
||||
}
|
||||
|
||||
function exchangeCode(code, redirectUri, callback) {
|
||||
fetch(API_BASE + '/api/docs-comments/oauth/exchange', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: code, redirectUri: redirectUri }),
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('OAuth exchange failed');
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) { callback(null, data); })
|
||||
.catch(function (err) { callback(err); });
|
||||
}
|
||||
|
||||
// --- Render ---
|
||||
|
||||
function renderAvatar(comment) {
|
||||
if (comment.avatarUrl) {
|
||||
return '<img class="dc-avatar" src="' + escapeHtml(comment.avatarUrl) + '" alt="" loading="lazy">';
|
||||
}
|
||||
return '<span class="dc-avatar dc-avatar--initials">' + escapeHtml(getInitials(comment.authorName)) + '</span>';
|
||||
}
|
||||
|
||||
function renderComment(comment) {
|
||||
var fullDate = new Date(comment.createdAt).toLocaleString();
|
||||
return (
|
||||
'<div class="dc-comment">' +
|
||||
'<div class="dc-comment__header">' +
|
||||
renderAvatar(comment) +
|
||||
'<span class="dc-comment__author">' + escapeHtml(comment.authorName) + '</span>' +
|
||||
(comment.isAnonymous ? '<span class="dc-comment__badge">Guest</span>' : '') +
|
||||
'<time class="dc-comment__time" title="' + escapeHtml(fullDate) + '">' + timeAgo(comment.createdAt) + '</time>' +
|
||||
'</div>' +
|
||||
'<div class="dc-comment__body">' + renderInlineMarkdown(comment.body) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function renderCommentList(data) {
|
||||
if (!data.comments || data.comments.length === 0) {
|
||||
return '<p class="dc-empty">No comments yet. Be the first to share your thoughts!</p>';
|
||||
}
|
||||
return data.comments.map(renderComment).join('');
|
||||
}
|
||||
|
||||
function renderForm(pagePath, oauthConfig) {
|
||||
var token = getToken();
|
||||
var user = getUser();
|
||||
|
||||
if (token && user) {
|
||||
// Authenticated form
|
||||
return (
|
||||
'<div class="dc-form dc-form--authenticated">' +
|
||||
'<div class="dc-form__user">' +
|
||||
'<img class="dc-avatar" src="' + escapeHtml(user.avatarUrl || '') + '" alt="">' +
|
||||
'<span>Commenting as <strong>' + escapeHtml(user.name) + '</strong></span>' +
|
||||
'<button class="dc-btn dc-btn--text dc-logout-btn" type="button">Sign out</button>' +
|
||||
'</div>' +
|
||||
'<textarea class="dc-textarea dc-auth-body" placeholder="Write a comment... (Markdown supported)" rows="3"></textarea>' +
|
||||
'<div class="dc-form__actions">' +
|
||||
'<button class="dc-btn dc-btn--primary dc-submit-auth" type="button">Post Comment</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
// Anonymous form with optional OAuth login
|
||||
var loginBtn = '';
|
||||
if (oauthConfig && oauthConfig.oauthEnabled) {
|
||||
loginBtn = '<button class="dc-btn dc-btn--outline dc-login-btn" type="button">Sign in with Gitea</button>';
|
||||
}
|
||||
|
||||
return (
|
||||
'<div class="dc-form dc-form--anonymous">' +
|
||||
'<div class="dc-form__row">' +
|
||||
'<input class="dc-input dc-anon-name" type="text" placeholder="Your name *" maxlength="100">' +
|
||||
'<input class="dc-input dc-anon-email" type="email" placeholder="Email (optional)" maxlength="255">' +
|
||||
'</div>' +
|
||||
// Honeypot — hidden from humans
|
||||
'<input class="dc-honeypot" type="text" name="website" tabindex="-1" autocomplete="off">' +
|
||||
'<textarea class="dc-textarea dc-anon-body" placeholder="Write a comment... (Markdown supported, 10+ characters)" rows="3"></textarea>' +
|
||||
'<div class="dc-form__actions">' +
|
||||
'<button class="dc-btn dc-btn--primary dc-submit-anon" type="button">Post as Guest</button>' +
|
||||
loginBtn +
|
||||
'</div>' +
|
||||
'<p class="dc-form__note">Guest comments are reviewed before appearing.</p>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
// --- Main Init ---
|
||||
|
||||
function initWidget(container) {
|
||||
var pagePath = container.getAttribute('data-page-path') || '';
|
||||
if (!pagePath) return;
|
||||
|
||||
API_BASE = getApiUrl();
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="dc-widget">' +
|
||||
'<h3 class="dc-title">Comments</h3>' +
|
||||
'<div class="dc-comments-list dc-loading">Loading comments...</div>' +
|
||||
'<div class="dc-form-container"></div>' +
|
||||
'</div>';
|
||||
|
||||
var listEl = container.querySelector('.dc-comments-list');
|
||||
var formContainer = container.querySelector('.dc-form-container');
|
||||
var oauthConfig = null;
|
||||
|
||||
// Load OAuth config + comments in parallel
|
||||
fetchOAuthConfig(function (err, config) {
|
||||
if (!err && config) oauthConfig = config;
|
||||
renderFormSection();
|
||||
});
|
||||
|
||||
loadComments(1);
|
||||
|
||||
function loadComments(page) {
|
||||
listEl.classList.add('dc-loading');
|
||||
listEl.innerHTML = 'Loading comments...';
|
||||
|
||||
fetchComments(pagePath, page, function (err, data) {
|
||||
listEl.classList.remove('dc-loading');
|
||||
if (err) {
|
||||
listEl.innerHTML = '<p class="dc-error">Comments unavailable.</p>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = renderCommentList(data);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFormSection() {
|
||||
formContainer.innerHTML = renderForm(pagePath, oauthConfig);
|
||||
bindFormEvents();
|
||||
}
|
||||
|
||||
function bindFormEvents() {
|
||||
// Anonymous submit
|
||||
var submitAnon = formContainer.querySelector('.dc-submit-anon');
|
||||
if (submitAnon) {
|
||||
submitAnon.addEventListener('click', function () {
|
||||
var name = formContainer.querySelector('.dc-anon-name').value.trim();
|
||||
var email = formContainer.querySelector('.dc-anon-email').value.trim();
|
||||
var body = formContainer.querySelector('.dc-anon-body').value.trim();
|
||||
var honeypot = formContainer.querySelector('.dc-honeypot').value;
|
||||
|
||||
if (!name) { showFormError('Please enter your name.'); return; }
|
||||
if (body.length < 10) { showFormError('Comment must be at least 10 characters.'); return; }
|
||||
|
||||
submitAnon.disabled = true;
|
||||
submitAnon.textContent = 'Posting...';
|
||||
|
||||
postAnonymous(
|
||||
{ pagePath: pagePath, authorName: name, authorEmail: email || undefined, body: body, website: honeypot },
|
||||
function (err) {
|
||||
submitAnon.disabled = false;
|
||||
submitAnon.textContent = 'Post as Guest';
|
||||
if (err) { showFormError(err.message || 'Failed to post comment.'); return; }
|
||||
formContainer.querySelector('.dc-anon-body').value = '';
|
||||
showFormSuccess('Your comment has been submitted for review.');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Authenticated submit
|
||||
var submitAuth = formContainer.querySelector('.dc-submit-auth');
|
||||
if (submitAuth) {
|
||||
submitAuth.addEventListener('click', function () {
|
||||
var body = formContainer.querySelector('.dc-auth-body').value.trim();
|
||||
var token = getToken();
|
||||
|
||||
if (body.length < 10) { showFormError('Comment must be at least 10 characters.'); return; }
|
||||
if (!token) { showFormError('Session expired. Please sign in again.'); clearAuth(); renderFormSection(); return; }
|
||||
|
||||
submitAuth.disabled = true;
|
||||
submitAuth.textContent = 'Posting...';
|
||||
|
||||
postAuthenticated(
|
||||
{ pagePath: pagePath, body: body },
|
||||
token,
|
||||
function (err) {
|
||||
submitAuth.disabled = false;
|
||||
submitAuth.textContent = 'Post Comment';
|
||||
if (err) {
|
||||
if (err.message && err.message.indexOf('401') !== -1) {
|
||||
clearAuth();
|
||||
renderFormSection();
|
||||
showFormError('Session expired. Please sign in again.');
|
||||
return;
|
||||
}
|
||||
showFormError(err.message || 'Failed to post comment.');
|
||||
return;
|
||||
}
|
||||
formContainer.querySelector('.dc-auth-body').value = '';
|
||||
loadComments(1);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// OAuth login
|
||||
var loginBtn = formContainer.querySelector('.dc-login-btn');
|
||||
if (loginBtn && oauthConfig && oauthConfig.oauthEnabled) {
|
||||
loginBtn.addEventListener('click', function () {
|
||||
var state = Math.random().toString(36).slice(2);
|
||||
try {
|
||||
sessionStorage.setItem(STATE_KEY, state);
|
||||
sessionStorage.setItem(RETURN_KEY, window.location.href);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
var redirectUri = window.location.origin + '/comments/callback/';
|
||||
var url = oauthConfig.authorizeUrl +
|
||||
'?client_id=' + encodeURIComponent(oauthConfig.clientId) +
|
||||
'&redirect_uri=' + encodeURIComponent(redirectUri) +
|
||||
'&response_type=code' +
|
||||
'&state=' + encodeURIComponent(state);
|
||||
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
|
||||
// Logout
|
||||
var logoutBtn = formContainer.querySelector('.dc-logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', function () {
|
||||
clearAuth();
|
||||
renderFormSection();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showFormError(msg) {
|
||||
removeFormMessages();
|
||||
var el = document.createElement('p');
|
||||
el.className = 'dc-form-message dc-form-message--error';
|
||||
el.textContent = msg;
|
||||
formContainer.appendChild(el);
|
||||
setTimeout(function () { el.remove(); }, 5000);
|
||||
}
|
||||
|
||||
function showFormSuccess(msg) {
|
||||
removeFormMessages();
|
||||
var el = document.createElement('p');
|
||||
el.className = 'dc-form-message dc-form-message--success';
|
||||
el.textContent = msg;
|
||||
formContainer.appendChild(el);
|
||||
setTimeout(function () { el.remove(); }, 5000);
|
||||
}
|
||||
|
||||
function removeFormMessages() {
|
||||
var msgs = formContainer.querySelectorAll('.dc-form-message');
|
||||
for (var i = 0; i < msgs.length; i++) msgs[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
// --- OAuth Callback Handler ---
|
||||
|
||||
function handleOAuthCallback() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var code = params.get('code');
|
||||
var state = params.get('state');
|
||||
|
||||
if (!code) return;
|
||||
|
||||
// Verify state
|
||||
var savedState;
|
||||
try { savedState = sessionStorage.getItem(STATE_KEY); } catch { /* ignore */ }
|
||||
if (savedState && state !== savedState) {
|
||||
console.warn('[DocsComments] OAuth state mismatch');
|
||||
return;
|
||||
}
|
||||
|
||||
API_BASE = getApiUrl();
|
||||
var redirectUri = window.location.origin + '/comments/callback/';
|
||||
|
||||
exchangeCode(code, redirectUri, function (err, data) {
|
||||
if (err || !data || !data.accessToken) {
|
||||
console.error('[DocsComments] OAuth exchange failed:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
setAuth(data.accessToken, data.user);
|
||||
|
||||
// Redirect back to original page
|
||||
var returnUrl;
|
||||
try { returnUrl = sessionStorage.getItem(RETURN_KEY); } catch { /* ignore */ }
|
||||
try {
|
||||
sessionStorage.removeItem(STATE_KEY);
|
||||
sessionStorage.removeItem(RETURN_KEY);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
window.location.href = returnUrl || '/';
|
||||
});
|
||||
}
|
||||
|
||||
// --- SPA Init ---
|
||||
|
||||
function init() {
|
||||
// Check if this is the OAuth callback page
|
||||
if (window.location.pathname.indexOf('/comments/callback') !== -1) {
|
||||
handleOAuthCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById('docs-comments');
|
||||
if (container) {
|
||||
initWidget(container);
|
||||
}
|
||||
}
|
||||
|
||||
// MkDocs Material SPA navigation support
|
||||
if (typeof document$ !== 'undefined') {
|
||||
document$.subscribe(function () {
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
// Fallback for non-SPA or non-Material builds
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user