-
Loading proposal...
+
@@ -21,6 +27,31 @@ const { id } = Astro.params;
let quadraticAllocations = {};
let starRatings = {};
+ function escapeHtml(value) {
+ return String(value || '').replace(/[&<>"']/g, function(ch) {
+ switch (ch) {
+ case '&': return '&';
+ case '<': return '<';
+ case '>': return '>';
+ case '"': return '"';
+ case "'": return ''';
+ default: return ch;
+ }
+ });
+ }
+
+ function normalizeStatus(status) {
+ const s = String(status || '').toLowerCase();
+ if (s === 'draft' || s === 'discussion' || s === 'voting' || s === 'closed') return s;
+ return 'draft';
+ }
+
+ function normalizeMethod(method) {
+ const m = String(method || '').toLowerCase();
+ if (m === 'approval' || m === 'ranked_choice' || m === 'quadratic' || m === 'star' || m === 'schulze') return m;
+ return 'approval';
+ }
+
function getVotingHint(method) {
const hints = {
'approval': 'Click options to select, then submit your vote',
@@ -42,124 +73,166 @@ const { id } = Astro.params;
const data = await res.json();
currentProposal = data;
- const isDraft = data.proposal.status === 'draft';
- const isDiscussion = data.proposal.status === 'discussion';
- const isVoting = data.proposal.status === 'voting';
- const isClosed = data.proposal.status === 'closed';
+ const statusKey = normalizeStatus(data.proposal.status);
+ const methodKey = normalizeMethod(data.proposal.voting_method);
+ const isDraft = statusKey === 'draft';
+ const isDiscussion = statusKey === 'discussion';
+ const isVoting = statusKey === 'voting';
+ const isClosed = statusKey === 'closed';
const canStartDiscussion = token && isDraft;
const canStartVoting = token && (isDraft || isDiscussion);
const canCloseVoting = token && isVoting;
+ const safeTitle = escapeHtml(data.proposal.title);
+ const safeAuthor = escapeHtml(data.author_name);
+ const safeDescription = escapeHtml(data.proposal.description);
+ const safeMethodLabel = escapeHtml(methodKey.replace('_', ' '));
+
container.innerHTML = `
-
-
- by ${data.author_name}
- on ${new Date(data.proposal.created_at).toLocaleDateString()}
- ${data.proposal.voting_starts_at ? ` | Voting started: ${new Date(data.proposal.voting_starts_at).toLocaleDateString()}` : ''}
- ${data.proposal.voting_ends_at ? ` | Ends: ${new Date(data.proposal.voting_ends_at).toLocaleDateString()}` : ''}
-
-
${data.proposal.description}
-
-
-
Options
-
- ${(() => {
- const maxVotes = Math.max(...data.options.map(o => o.vote_count));
- const hasVotes = maxVotes > 0;
- const method = data.proposal.voting_method;
- return data.options.map(opt => {
- const isWinner = isClosed && hasVotes && opt.vote_count === maxVotes;
- let votingUI = '';
- if (isVoting && token) {
- if (method === 'quadratic') {
- votingUI = `
-
- 0
-
-
`;
- } else if (method === 'star') {
- votingUI = `
- ${[1,2,3,4,5].map(s => `☆`).join('')}
-
`;
- } else if (method === 'ranked_choice') {
- votingUI = `
`;
- }
- }
- return `
-
-
- ${opt.label} ${isWinner ? '🏆' : ''}
- ${opt.description ? `${opt.description}` : ''}
-
- ${votingUI}
-
${opt.vote_count} votes
-
- `;
- }).join('');
- })()}
-
-
-
- ${isVoting && token ? `
-
-
${getVotingHint(data.proposal.voting_method)}
-
Method: ${data.proposal.voting_method.replace('_', ' ')}
- ${data.proposal.voting_method === 'quadratic' ? '
Credits used: 0/100
' : ''}
-
-
- ` : ''}
-
- ${canStartDiscussion ? `
-
-
-
-
-
-
- ` : ''}
-
- ${isDiscussion && token ? `
-
-
-
-
- ` : ''}
-
- ${canCloseVoting ? `
-
-
-
- ` : ''}
-
- ${isClosed ? `
-
-
📊 Voting Results
-
-
Loading detailed results...
+
+
- ` : ''}
+
${safeDescription}
+
- ${!token && isVoting ? `
-
-
Login to vote on this proposal
-
- ` : ''}
+
+
+
+ Options
+ Your choices
+
+
+
+ ${(() => {
+ const maxVotes = Math.max(...data.options.map(o => o.vote_count));
+ const hasVotes = maxVotes > 0;
+ const method = methodKey;
+ return data.options.map(opt => {
+ const isWinner = isClosed && hasVotes && opt.vote_count === maxVotes;
+ let votingUI = '';
+ if (isVoting && token) {
+ if (method === 'quadratic') {
+ votingUI = `
+
+ 0
+
+
`;
+ } else if (method === 'star') {
+ votingUI = `
+ ${[1,2,3,4,5].map(s => `☆`).join('')}
+
`;
+ } else if (method === 'ranked_choice') {
+ votingUI = `
`;
+ }
+ }
-
+
+ ${isVoting && token ? `
+
+
${escapeHtml(getVotingHint(methodKey))}
+
Method: ${safeMethodLabel}
+ ${methodKey === 'quadratic' ? '
Credits used: 0/100
' : ''}
+
+
+ ` : ''}
+
+ ${!token && isVoting ? `
+
+
Login to vote on this proposal
+
+ ` : ''}
+
+
+
+ ${canStartDiscussion ? `
+
+
+
+
+
+
+
+
+ ` : ''}
+
+ ${isDiscussion && token ? `
+
+
+
+
+
+
+ ` : ''}
+
+ ${canCloseVoting ? `
+
+ ` : ''}
+
+ ${isClosed ? `
+
+
+ Voting Results
+ Summary + method details
+
+
+
+ ` : ''}
+
+
+
+ Discussion
+ Comments
+
+
+ ${token ? `
+
+ ` : `
Login to comment
`}
+
+
+
`;
@@ -258,142 +331,26 @@ const { id } = Astro.params;
try {
if (!token) {
- container.innerHTML = 'Login required to view detailed results
';
+ container.innerHTML = 'Login required to view results
';
return;
}
- const res = await fetch(`${apiBase}/api/proposals/${id}/results`, {
- headers: { 'Authorization': `Bearer ${token}` },
- });
- if (!res.ok) {
- container.innerHTML = 'Could not load detailed results
';
+ const host = document.getElementById('results-chart-host');
+ const chart = host?.querySelector('.results-chart');
+ if (!host || !chart) {
+ container.innerHTML = 'Results chart unavailable
';
return;
}
- const data = await res.json();
- const method = currentProposal?.proposal?.voting_method || 'approval';
-
- let resultsHTML = `
-
-
- ${data.total_voters || 0}
- Total Voters
-
-
- ${data.total_votes || 0}
- Total Votes
-
- ${data.winner ? `
-
- 🏆 ${data.winner.label}
- Winner
-
- ` : ''}
-
- `;
+ host.style.display = 'block';
+ host.setAttribute('aria-hidden', 'false');
- // Method-specific details
- if (method === 'ranked_choice' && data.rounds) {
- resultsHTML += `
-
-
Instant Runoff Rounds
-
- ${data.rounds.map((round, i) => `
-
-
Round ${i + 1}
-
- ${round.counts.map((c) => `
- - ${c.label}: ${c.votes} votes ${c.eliminated ? '(eliminated)' : ''}
- `).join('')}
-
-
- `).join('')}
-
-
- `;
- } else if (method === 'schulze' && data.pairwise_matrix) {
- resultsHTML += `
-
-
Schulze Method - Pairwise Comparison
-
Winner determined by Condorcet-consistent pairwise comparison
-
- ${data.ranking?.map((r, i) => `
-
- #${i + 1}
- ${r.label}
- ${r.score} wins
-
- `).join('') || ''}
-
-
- `;
- } else if (method === 'star' && data.scores) {
- resultsHTML += `
-
-
STAR Voting Results
-
Score Then Automatic Runoff
-
- ${data.scores.map((s) => `
-
-
${s.label}
-
-
${s.average.toFixed(2)} avg (${s.total} total)
-
- `).join('')}
-
- ${data.runoff ? `
-
-
Automatic Runoff
-
${data.runoff.winner} vs ${data.runoff.runner_up}: ${data.runoff.winner_votes} to ${data.runoff.runner_up_votes}
-
- ` : ''}
-
- `;
- } else if (method === 'quadratic' && data.allocations) {
- resultsHTML += `
-
-
Quadratic Voting Results
-
Credits spent: cost = votes² (intensity-weighted)
-
- ${data.allocations.map((a) => `
-
-
${a.label}
-
-
${a.effective_votes} effective votes (${a.total_credits} credits)
-
- `).join('')}
-
-
- `;
- } else {
- // Default approval voting display
- resultsHTML += `
-
-
Approval Voting Results
-
- ${(data.options || currentProposal?.options || []).sort((a, b) => b.vote_count - a.vote_count).map((o) => {
- const maxVotes = Math.max(...(data.options || currentProposal?.options || []).map((x) => x.vote_count));
- const pct = maxVotes > 0 ? (o.vote_count / maxVotes) * 100 : 0;
- return `
-
-
${o.label}
-
-
${o.vote_count} votes
-
- `;
- }).join('')}
-
-
- `;
+ if (!container.contains(chart)) {
+ container.innerHTML = '';
+ container.appendChild(chart);
}
- container.innerHTML = resultsHTML;
+ chart.dataset.enabled = '1';
} catch (error) {
container.innerHTML = 'Error loading results
';
}
@@ -561,15 +518,20 @@ const { id } = Astro.params;
return;
}
- container.innerHTML = comments.map(c => `
-
Discussion
- ${token ? ` - - ` : `Login to comment
`} -Loading comments...
-