changemaker.lite/mkdocs/docs/assets/js/scheduling-poll.js
2026-03-01 15:22:27 -07:00

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) + ' &mdash; ' + poll.finalizedOption.startTime + '&ndash;' + 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 + '&ndash;' + opt.endTime + '</span>';
if (isConfirmed) html += ' <span style="color:#52c41a; font-size:10px; font-weight:600;">&#10003;</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 &rarr;</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 &rarr;</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);
});
}
})();