190 lines
8.5 KiB
JavaScript
190 lines
8.5 KiB
JavaScript
/**
|
|
* Scheduling Poll Block Hydration for MkDocs
|
|
*
|
|
* Scans for .scheduling-poll-block elements, fetches poll data from the API,
|
|
* and renders a read-only poll summary with a "Vote Now" link to the full
|
|
* interactive voting page on the app.
|
|
*
|
|
* Follows the gancio-events.js hydration pattern.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
function getApiUrl() {
|
|
// env-config.js injects these globals
|
|
if (window.PAYMENT_API_URL) return window.PAYMENT_API_URL;
|
|
if (window.API_URL) return window.API_URL;
|
|
|
|
var host = window.location.hostname;
|
|
if (host !== 'localhost' && host.indexOf('.') !== -1) {
|
|
var parts = host.split('.');
|
|
var base = parts.slice(-2).join('.');
|
|
return window.location.protocol + '//api.' + base;
|
|
}
|
|
return 'http://localhost:4000';
|
|
}
|
|
|
|
function getAppUrl() {
|
|
if (window.APP_URL) return window.APP_URL;
|
|
|
|
var host = window.location.hostname;
|
|
if (host !== 'localhost' && host.indexOf('.') !== -1) {
|
|
var parts = host.split('.');
|
|
var base = parts.slice(-2).join('.');
|
|
return window.location.protocol + '//app.' + base;
|
|
}
|
|
return 'http://localhost:3000';
|
|
}
|
|
|
|
var STATUS_COLORS = {
|
|
OPEN: '#52c41a',
|
|
CLOSED: '#fa8c16',
|
|
FINALIZED: '#1890ff',
|
|
CANCELLED: '#ff4d4f',
|
|
};
|
|
|
|
var STATUS_LABELS = {
|
|
OPEN: 'Open for Voting',
|
|
CLOSED: 'Closed',
|
|
FINALIZED: 'Date Confirmed',
|
|
CANCELLED: 'Cancelled',
|
|
};
|
|
|
|
function formatDate(dateStr) {
|
|
var d = new Date(dateStr + 'T00:00:00');
|
|
var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
return days[d.getDay()] + ', ' + months[d.getMonth()] + ' ' + d.getDate();
|
|
}
|
|
|
|
function hydrateBlocks() {
|
|
var blocks = document.querySelectorAll('.scheduling-poll-block');
|
|
if (blocks.length === 0) return;
|
|
|
|
var apiUrl = getApiUrl();
|
|
var appUrl = getAppUrl();
|
|
|
|
blocks.forEach(function (block) {
|
|
// Skip if already hydrated
|
|
if (block.getAttribute('data-hydrated') === 'true') return;
|
|
|
|
var slug = block.getAttribute('data-poll-slug');
|
|
if (!slug) return;
|
|
|
|
var showComments = block.getAttribute('data-show-comments') !== 'false';
|
|
var title = block.getAttribute('data-title') || '';
|
|
|
|
block.setAttribute('data-hydrated', 'true');
|
|
block.innerHTML = '<div style="text-align:center; padding:20px; opacity:0.6;">Loading poll...</div>';
|
|
|
|
fetch(apiUrl + '/api/meeting-planner/public/' + encodeURIComponent(slug))
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error('Poll not found');
|
|
return res.json();
|
|
})
|
|
.then(function (poll) {
|
|
var statusColor = STATUS_COLORS[poll.status] || '#666';
|
|
var statusLabel = STATUS_LABELS[poll.status] || poll.status;
|
|
var isFinalized = poll.status === 'FINALIZED';
|
|
var options = poll.options || [];
|
|
var bestScore = 0;
|
|
options.forEach(function (o) {
|
|
if ((o.score || 0) > bestScore) bestScore = o.score || 0;
|
|
});
|
|
|
|
var html = '';
|
|
|
|
// Title
|
|
if (title) {
|
|
html += '<h2 style="text-align:center; margin:0 0 8px; font-size:1.5rem;">' + title + '</h2>';
|
|
}
|
|
|
|
// Poll title + status
|
|
html += '<h3 style="margin:0 0 8px; font-size:1.2rem;">' + poll.title + '</h3>';
|
|
if (poll.description) {
|
|
html += '<p style="opacity:0.75; margin:0 0 8px; line-height:1.5;">' + poll.description + '</p>';
|
|
}
|
|
html += '<div style="margin-bottom:12px;">';
|
|
html += '<span style="display:inline-block; padding:2px 10px; border-radius:4px; font-size:12px; font-weight:600; background:' + statusColor + '22; color:' + statusColor + '; border:1px solid ' + statusColor + '44;">' + statusLabel + '</span>';
|
|
if (poll.location) {
|
|
html += ' <span style="font-size:13px; opacity:0.65; margin-left:8px;">' + poll.location + '</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Finalized banner
|
|
if (isFinalized && poll.finalizedOption) {
|
|
html += '<div style="padding:10px 14px; border-radius:6px; background:rgba(82,196,26,0.1); border:1px solid rgba(82,196,26,0.3); margin-bottom:12px; color:#52c41a;">';
|
|
html += '<strong>Confirmed:</strong> ' + formatDate(poll.finalizedOption.date) + ' — ' + poll.finalizedOption.startTime + '–' + poll.finalizedOption.endTime;
|
|
html += '</div>';
|
|
}
|
|
|
|
// Options table
|
|
if (options.length > 0) {
|
|
html += '<div style="overflow-x:auto; margin-bottom:12px;">';
|
|
html += '<table style="width:100%; border-collapse:collapse; font-size:13px;">';
|
|
html += '<thead><tr>';
|
|
html += '<th style="padding:8px 12px; border-bottom:2px solid rgba(255,255,255,0.15); text-align:left;">Date / Time</th>';
|
|
html += '<th style="padding:8px 12px; border-bottom:2px solid rgba(255,255,255,0.15); text-align:center;">Yes</th>';
|
|
html += '<th style="padding:8px 12px; border-bottom:2px solid rgba(255,255,255,0.15); text-align:center;">If Need Be</th>';
|
|
html += '<th style="padding:8px 12px; border-bottom:2px solid rgba(255,255,255,0.15); text-align:center;">No</th>';
|
|
html += '<th style="padding:8px 12px; border-bottom:2px solid rgba(255,255,255,0.15); text-align:center;">Score</th>';
|
|
html += '</tr></thead><tbody>';
|
|
|
|
options.forEach(function (opt) {
|
|
var isBest = bestScore > 0 && (opt.score || 0) === bestScore;
|
|
var isConfirmed = isFinalized && poll.finalizedOptionId === opt.id;
|
|
var rowBg = isConfirmed ? 'rgba(82,196,26,0.08)' : isBest ? 'rgba(82,196,26,0.04)' : '';
|
|
html += '<tr style="background:' + rowBg + ';">';
|
|
html += '<td style="padding:8px 12px; border-bottom:1px solid rgba(255,255,255,0.1);">';
|
|
html += '<strong>' + formatDate(opt.date) + '</strong><br><span style="font-size:11px; opacity:0.7;">' + opt.startTime + '–' + opt.endTime + '</span>';
|
|
if (isConfirmed) html += ' <span style="color:#52c41a; font-size:10px; font-weight:600;">✓</span>';
|
|
html += '</td>';
|
|
html += '<td style="padding:8px 12px; border-bottom:1px solid rgba(255,255,255,0.1); text-align:center; color:#52c41a;">' + (opt.yesCount || 0) + '</td>';
|
|
html += '<td style="padding:8px 12px; border-bottom:1px solid rgba(255,255,255,0.1); text-align:center; color:#faad14;">' + (opt.ifNeedBeCount || 0) + '</td>';
|
|
html += '<td style="padding:8px 12px; border-bottom:1px solid rgba(255,255,255,0.1); text-align:center; color:#d9d9d9;">' + (opt.noCount || 0) + '</td>';
|
|
html += '<td style="padding:8px 12px; border-bottom:1px solid rgba(255,255,255,0.1); text-align:center; font-weight:600;">' + (opt.score || 0) + '</td>';
|
|
html += '</tr>';
|
|
});
|
|
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
// Comments count
|
|
if (showComments && poll.comments && poll.comments.length > 0) {
|
|
html += '<p style="font-size:13px; opacity:0.65;">' + poll.comments.length + ' comment' + (poll.comments.length !== 1 ? 's' : '') + '</p>';
|
|
}
|
|
|
|
// Vote Now CTA
|
|
if (poll.status === 'OPEN') {
|
|
html += '<div style="text-align:center; margin-top:16px;">';
|
|
html += '<a href="' + appUrl + '/poll/' + encodeURIComponent(slug) + '" target="_blank" rel="noopener noreferrer" ';
|
|
html += 'style="display:inline-block; padding:12px 32px; background:#fa8c16; color:#fff; text-decoration:none; border-radius:6px; font-weight:600; font-size:14px;">';
|
|
html += 'Vote Now →</a></div>';
|
|
}
|
|
|
|
block.innerHTML = '<div style="max-width:700px; margin:0 auto;">' + html + '</div>';
|
|
})
|
|
.catch(function () {
|
|
block.innerHTML = '<div style="text-align:center; padding:24px; opacity:0.5;">' +
|
|
'<p>Poll unavailable</p>' +
|
|
'<a href="' + appUrl + '/poll/' + encodeURIComponent(slug) + '" target="_blank" rel="noopener noreferrer" style="color:#fa8c16;">View poll →</a>' +
|
|
'</div>';
|
|
});
|
|
});
|
|
}
|
|
|
|
// Initial hydration
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', hydrateBlocks);
|
|
} else {
|
|
hydrateBlocks();
|
|
}
|
|
|
|
// Re-hydrate on MkDocs SPA navigation
|
|
if (typeof document$ !== 'undefined') {
|
|
document$.subscribe(function () {
|
|
setTimeout(hydrateBlocks, 100);
|
|
});
|
|
}
|
|
})();
|