// Reader vote widget: "which draft would you ship?" per reviewed skill. // The page itself is static (Gitea Pages), so this talks to a small // separate API — see /vote-service in the repository root. One vote per // source is enforced server-side by IP, not here; this module only renders // state and remembers the local choice so a returning visitor sees it // without re-voting. const API_BASE = (window.SKILLS_REVIEW_VOTE_API || '').replace(/\/$/, ''); const escape = (value) => value.replace(/[&<>"']/g, (character) => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[character]); function voterId() { let id = localStorage.getItem('skills-review-voter-id'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('skills-review-voter-id', id); } return id; } async function api(path, options = {}) { const response = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'X-Voter-Id': voterId(), ...options.headers } }); if (!response.ok) throw new Error(`vote API ${response.status}`); return response.json(); } function widgetMarkup(skillId, tally, you, unavailable) { const total = (tally.original || 0) + (tally.improved || 0); const share = (count) => total ? Math.round((count / total) * 100) : 0; if (unavailable) return `
READER VOTE

Voting is offline right now — the vote service is not configured or unreachable.

`; return `
WHICH DRAFT WOULD YOU SHIP?

${you ? `You voted ${you === 'original' ? 'original' : 'improved draft'}. Pick the other option to change it.` : 'One vote per visitor, tracked by network source.'}

`; } export async function renderVoteWidget(container, skillId) { if (!API_BASE) { container.innerHTML = widgetMarkup(skillId, {}, null, true); return; } container.innerHTML = widgetMarkup(skillId, {}, null, false); const cast = async (choice) => { container.innerHTML = widgetMarkup(skillId, {}, null, false); try { const result = await api('/api/votes', { method: 'POST', body: JSON.stringify({ skillId, choice }) }); container.innerHTML = widgetMarkup(skillId, { original: result.original, improved: result.improved }, result.you, false); bind(); } catch { container.innerHTML = widgetMarkup(skillId, {}, null, true); } }; function bind() { container.querySelectorAll('[data-vote]').forEach((button) => button.addEventListener('click', () => cast(button.dataset.vote))); } try { const result = await api(`/api/votes?skillId=${encodeURIComponent(skillId)}`); container.innerHTML = widgetMarkup(skillId, result.tallies?.[skillId] || {}, result.you, false); } catch { container.innerHTML = widgetMarkup(skillId, {}, null, true); } bind(); }