/**
* Straw Poll Widget Hydration for MkDocs
*
* Supports two modes:
* .straw-poll-inline — Full voting UI embedded in docs page
* .straw-poll-card — Preview card linking to the full poll lander
*
* Uses the lightweight /api/straw-polls/widget/:slug endpoint (cached).
* Follows the scheduling-poll.js hydration pattern.
*/
(function () {
'use strict';
function getApiUrl() {
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 COLORS = ['#1890ff', '#52c41a', '#faad14', '#ff4d4f', '#722ed1', '#13c2c2', '#eb2f96'];
var YNA_COLORS = { Yes: '#52c41a', No: '#ff4d4f', Abstain: '#8c8c8c' };
function tokenKey(slug) {
return 'straw_poll_voter_token_' + slug;
}
// ===== Card Mode =====
function renderCard(block, poll, appUrl) {
var html = '
';
html += '
';
html += (poll.type === 'YES_NO_ABSTAIN' ? 'Yes / No / Abstain' : 'Single Choice') + ' Poll
';
html += '
' + poll.title + ' ';
html += '
' + poll.totalVotes + ' vote' + (poll.totalVotes !== 1 ? 's' : '') + '
';
if (poll.status === 'ACTIVE') {
html += '
';
html += 'Vote Now → ';
} else {
html += '
Closed ';
}
html += '
';
block.innerHTML = html;
}
// ===== Inline Mode =====
function renderInline(block, poll, apiUrl) {
var slug = poll.slug;
var storedToken = localStorage.getItem(tokenKey(slug));
var hasVoted = !!storedToken;
var showResults = poll.totalVotes > 0;
var html = '';
// Title
html += '
' + poll.title + ' ';
html += '
';
html += (poll.type === 'YES_NO_ABSTAIN' ? 'Yes / No / Abstain' : 'Single Choice');
html += ' · ' + poll.totalVotes + ' vote' + (poll.totalVotes !== 1 ? 's' : '') + '
';
if (poll.status === 'ACTIVE' && !hasVoted) {
// Vote form
html += '
';
poll.options.forEach(function (opt, i) {
var isYNA = poll.type === 'YES_NO_ABSTAIN';
var color = isYNA ? (YNA_COLORS[opt.label] || COLORS[i]) : COLORS[i % COLORS.length];
html += '';
html += opt.label + ' ';
});
html += ' ';
html += '';
html += 'Submit Vote ';
html += '
';
}
// Results
if (showResults && (hasVoted || poll.status !== 'ACTIVE')) {
html += renderResultsHtml(poll);
}
if (hasVoted && poll.status === 'ACTIVE') {
html += '
✓ You\'ve voted
';
}
html += '
';
block.innerHTML = html;
// Wire up vote form
if (poll.status === 'ACTIVE' && !hasVoted) {
wireVoteForm(block, poll, apiUrl, slug);
}
}
function renderResultsHtml(poll) {
var html = '';
poll.options.forEach(function (opt, i) {
var pct = poll.totalVotes > 0 ? Math.round((opt.voteCount / poll.totalVotes) * 100) : 0;
var color = poll.type === 'YES_NO_ABSTAIN' ? (YNA_COLORS[opt.label] || COLORS[i]) : COLORS[i % COLORS.length];
html += '
';
html += '
';
html += '' + opt.label + ' ' + opt.voteCount + ' (' + pct + '%)
';
html += '
';
});
html += '
';
return html;
}
function wireVoteForm(block, poll, apiUrl, slug) {
var selectedId = null;
var buttons = block.querySelectorAll('.sp-opt-btn');
var submitBtn = block.querySelector('#sp-submit-' + slug);
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
selectedId = btn.getAttribute('data-option-id');
buttons.forEach(function (b) { b.style.borderWidth = '2px'; b.style.fontWeight = 'normal'; });
btn.style.borderWidth = '3px';
btn.style.fontWeight = '600';
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
submitBtn.addEventListener('click', function () {
if (!selectedId) return;
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
var voterName = (block.querySelector('#sp-voter-name-' + slug) || {}).value || '';
var body = { optionId: selectedId };
if (voterName) body.voterName = voterName;
var storedToken = localStorage.getItem(tokenKey(slug));
if (storedToken) body.voterToken = storedToken;
fetch(apiUrl + '/api/straw-polls/public/' + encodeURIComponent(slug) + '/vote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data.voterToken) localStorage.setItem(tokenKey(slug), data.voterToken);
// Re-fetch and re-render with results
return fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug)).then(function (r) { return r.json(); });
})
.then(function (updated) {
renderInline(block, updated, apiUrl);
})
.catch(function () {
submitBtn.textContent = 'Error — try again';
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
}
// ===== Hydration =====
function hydrateBlocks() {
var apiUrl = getApiUrl();
var appUrl = getAppUrl();
// Inline embeds
document.querySelectorAll('.straw-poll-inline').forEach(function (block) {
if (block.getAttribute('data-hydrated') === 'true') return;
var slug = block.getAttribute('data-poll-slug');
if (!slug) return;
block.setAttribute('data-hydrated', 'true');
block.innerHTML = 'Loading poll...
';
fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug))
.then(function (res) { if (!res.ok) throw new Error(); return res.json(); })
.then(function (poll) { renderInline(block, poll, apiUrl); })
.catch(function () {
block.innerHTML = 'Poll unavailable
';
});
});
// Card links
document.querySelectorAll('.straw-poll-card').forEach(function (block) {
if (block.getAttribute('data-hydrated') === 'true') return;
var slug = block.getAttribute('data-poll-slug');
if (!slug) return;
block.setAttribute('data-hydrated', 'true');
block.innerHTML = 'Loading...
';
fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug))
.then(function (res) { if (!res.ok) throw new Error(); return res.json(); })
.then(function (poll) { renderCard(block, poll, appUrl); })
.catch(function () {
block.innerHTML = 'Poll unavailable
';
});
});
}
// 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); });
}
})();