{% extends "base.twig" %}
{% block title %}{{ t('ui.polls.title', {}, locale, ['common']) }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-vote-yea me-2"></i>
{{ t('ui.polls.title', {}, locale, ['common']) }}
</h5>
<a href="/polls/create" class="btn btn-sm btn-primary">
<i class="fas fa-file-circle-plus me-1"></i>
{{ t('ui.polls.create', {}, locale, ['common']) }}
</a>
</div>
<div class="card-body" id="votingBoothBody">
<div class="text-center">
<i class="fas fa-spinner fa-spin"></i>
{{ t('ui.polls.loading', {}, locale, ['common']) }}
</div>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="small text-muted">{{ t('ui.polls.description', {}, locale, ['common']) }}</div>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" id="pollPrevBtn" onclick="changeVotingPoll(-1)" title="{{ t('ui.polls.previous', {}, locale, ['common']) }}">
<i class="fas fa-chevron-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="pollNextBtn" onclick="changeVotingPoll(1)" title="{{ t('ui.polls.next', {}, locale, ['common']) }}">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let votingPolls = [];
let votingPollIndex = 0;
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
$(document).ready(function() {
loadI18nNamespaces(['common', 'errors']).then(function() { loadVotingBooth(); });
});
function loadVotingBooth(keepPollId = null) {
$.get('/api/polls/active')
.done(function(data) {
votingPolls = data.polls || [];
if (votingPolls.length === 0) {
$('#votingBoothBody').html(`<p class="text-muted mb-0">${uiT('ui.dashboard.polls.none_active', 'No active polls right now.')}</p>`);
updateVotingPollNav();
return;
}
if (keepPollId) {
const idx = votingPolls.findIndex(p => p.id === keepPollId);
votingPollIndex = idx >= 0 ? idx : 0;
} else {
const unansweredIndex = votingPolls.findIndex(p => !p.has_voted);
votingPollIndex = unansweredIndex >= 0 ? unansweredIndex : 0;
}
renderVotingBooth();
})
.fail(function() {
$('#votingBoothBody').html(`<p class="text-danger mb-0">${uiT('ui.dashboard.polls.load_failed', 'Failed to load poll.')}</p>`);
updateVotingPollNav();
});
}
function renderVotingBooth() {
if (votingPolls.length === 0) {
$('#votingBoothBody').html(`<p class="text-muted mb-0">${uiT('ui.dashboard.polls.none_active', 'No active polls right now.')}</p>`);
updateVotingPollNav();
return;
}
const poll = votingPolls[votingPollIndex];
const hasVoted = !!poll.has_voted;
if (hasVoted) {
const totalVotes = poll.total_votes || 0;
let resultsHtml = `<h5 class="mb-3">${escapeHtml(poll.question)}</h5>`;
resultsHtml += `<div class="small text-muted mb-3">${uiT('ui.dashboard.polls.results', 'Results')}</div>`;
if (totalVotes === 0) {
resultsHtml += `<p class="text-muted mb-0">${uiT('ui.dashboard.polls.no_votes', 'No votes yet.')}</p>`;
} else {
(poll.results || []).forEach(result => {
const percent = totalVotes > 0 ? Math.round((result.votes / totalVotes) * 100) : 0;
resultsHtml += `
<div class="mb-3">
<div class="d-flex justify-content-between small">
<span>${escapeHtml(result.option_text)}</span>
<span>${result.votes} (${percent}%)</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar" style="width: ${percent}%"></div>
</div>
</div>
`;
});
}
$('#votingBoothBody').html(resultsHtml);
updateVotingPollNav();
return;
}
let optionsHtml = `<h5 class="mb-3">${escapeHtml(poll.question)}</h5>`;
optionsHtml += `<div class="small text-muted mb-3">${uiT('ui.dashboard.polls.vote_to_see_results', 'Vote to see results')}</div>`;
optionsHtml += '<form id="votingBoothForm">';
(poll.options || []).forEach(option => {
optionsHtml += `
<div class="form-check mb-3">
<input class="form-check-input" type="radio" name="poll_option" id="pollOption${option.id}" value="${option.id}">
<label class="form-check-label" for="pollOption${option.id}">
${escapeHtml(option.option_text)}
</label>
</div>
`;
});
optionsHtml += `
<button type="button" class="btn btn-primary" onclick="submitPollVote(${poll.id})">
${uiT('ui.dashboard.polls.submit_vote', 'Submit Vote')}
</button>
`;
optionsHtml += '</form>';
$('#votingBoothBody').html(optionsHtml);
updateVotingPollNav();
}
function changeVotingPoll(delta) {
if (votingPolls.length <= 1) {
return;
}
const nextIndex = votingPollIndex + delta;
if (nextIndex < 0 || nextIndex >= votingPolls.length) {
return;
}
votingPollIndex = nextIndex;
renderVotingBooth();
}
function updateVotingPollNav() {
const hasMultiple = votingPolls.length > 1;
const prevBtn = document.getElementById('pollPrevBtn');
const nextBtn = document.getElementById('pollNextBtn');
if (!prevBtn || !nextBtn) {
return;
}
prevBtn.disabled = !hasMultiple || votingPollIndex === 0;
nextBtn.disabled = !hasMultiple || votingPollIndex === (votingPolls.length - 1);
}
function submitPollVote(pollId) {
const selected = document.querySelector('input[name="poll_option"]:checked');
if (!selected) {
alert(uiT('errors.polls.option_required', 'Please select an option.'));
return;
}
fetch(`/api/polls/${pollId}/vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ option_id: selected.value })
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadVotingBooth(pollId);
} else {
alert(apiError(data, uiT('errors.polls.vote_failed', 'Failed to submit vote.')));
}
})
.catch(() => alert(uiT('errors.polls.vote_failed', 'Failed to submit vote.')));
}
function escapeHtml(text) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
</script>
{% endblock %}
|