feat: Enhance proposal creation and display
- Improve proposal creation form with input validation and default date settings for a better user experience. - Add context variables to the proposals template for consistent display across governance pages. - Enhance proposal detail page with visual improvements, voting results display, and user voting functionality. - Add styles for better visual presentation of proposal details and voting information.
This commit is contained in:
parent
fad288f67d
commit
52fbc77e3e
@ -341,6 +341,11 @@ impl GovernanceController {
|
||||
};
|
||||
ctx.insert("proposals", &proposals);
|
||||
|
||||
// Add the required context variables for the proposals template
|
||||
ctx.insert("active_tab", "proposals");
|
||||
ctx.insert("status_filter", &None::<String>);
|
||||
ctx.insert("search_filter", &None::<String>);
|
||||
|
||||
render_template(&tmpl, "governance/proposals.html", &ctx)
|
||||
}
|
||||
|
||||
|
@ -30,9 +30,12 @@
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
|
||||
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals clearly state the problem, solution, and implementation details. The community will review and vote on your proposal, so be thorough and thoughtful in your submission.</p>
|
||||
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
|
||||
clearly state the problem, solution, and implementation details. The community will review and vote
|
||||
on your proposal, so be thorough and thoughtful in your submission.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-earmark-text"></i> Proposal Templates</a>
|
||||
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
|
||||
class="bi bi-file-earmark-text"></i> Proposal Templates</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,18 +50,22 @@
|
||||
<h5 class="mb-0">New Proposal</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/governance/create" method="post">
|
||||
<form action="/governance/create" method="post" id="proposalForm" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required
|
||||
placeholder="Enter a clear, concise title for your proposal">
|
||||
<input type="text" class="form-control" id="title" name="title" required minlength="5"
|
||||
maxlength="100" placeholder="Enter a clear, concise title for your proposal">
|
||||
<div class="invalid-feedback">Please provide a title (5-100 characters).</div>
|
||||
<div class="form-text">Make it descriptive and specific</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="8" required
|
||||
minlength="50" maxlength="5000"
|
||||
placeholder="Provide a detailed description of your proposal..."></textarea>
|
||||
<div class="invalid-feedback">Please provide a detailed description (at least 50
|
||||
characters).</div>
|
||||
<div class="form-text">Explain the purpose, benefits, and implementation details</div>
|
||||
</div>
|
||||
|
||||
@ -66,11 +73,15 @@
|
||||
<div class="col-md-6">
|
||||
<label for="voting_start_date" class="form-label">Voting Start Date</label>
|
||||
<input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
|
||||
<div class="invalid-feedback" id="start_date_feedback">Please select a valid start date.
|
||||
</div>
|
||||
<div class="form-text">When should voting begin?</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="voting_end_date" class="form-label">Voting End Date</label>
|
||||
<input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
|
||||
<div class="invalid-feedback" id="end_date_feedback">End date must be after start date.
|
||||
</div>
|
||||
<div class="form-text">When should voting end?</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,4 +133,111 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('proposalForm');
|
||||
const startDateInput = document.getElementById('voting_start_date');
|
||||
const endDateInput = document.getElementById('voting_end_date');
|
||||
const startDateFeedback = document.getElementById('start_date_feedback');
|
||||
const endDateFeedback = document.getElementById('end_date_feedback');
|
||||
|
||||
// Set default dates
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const nextWeek = new Date(today);
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
|
||||
// Format dates for input fields
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Set default values
|
||||
startDateInput.value = formatDate(tomorrow);
|
||||
endDateInput.value = formatDate(nextWeek);
|
||||
|
||||
// Validate dates when they change
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
const currentDate = new Date();
|
||||
currentDate.setHours(0, 0, 0, 0); // Reset time to start of day
|
||||
|
||||
let startValid = true;
|
||||
let endValid = true;
|
||||
|
||||
// Validate start date is not in the past
|
||||
if (startDate < currentDate) {
|
||||
startDateInput.classList.add('is-invalid');
|
||||
startDateFeedback.textContent = 'Start date cannot be in the past.';
|
||||
startValid = false;
|
||||
} else {
|
||||
startDateInput.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate end date is after start date
|
||||
if (endDate < startDate) {
|
||||
endDateInput.classList.add('is-invalid');
|
||||
endDateFeedback.textContent = 'End date must be after start date.';
|
||||
endValid = false;
|
||||
} else {
|
||||
endDateInput.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
return startValid && endValid;
|
||||
}
|
||||
|
||||
// Validate on input
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
|
||||
// Form submission validation
|
||||
form.addEventListener('submit', function (event) {
|
||||
let formValid = true;
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
formValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Check minlength if specified
|
||||
if (field.minLength && field.value.length < field.minLength) {
|
||||
field.classList.add('is-invalid');
|
||||
formValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Validate dates
|
||||
const datesValid = validateDates();
|
||||
formValid = formValid && datesValid;
|
||||
|
||||
// If form is not valid, prevent submission
|
||||
if (!formValid) {
|
||||
event.preventDefault();
|
||||
// Scroll to the first invalid element
|
||||
const firstInvalid = form.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
firstInvalid.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initial validation
|
||||
validateDates();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
@ -2,6 +2,37 @@
|
||||
|
||||
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comment-text:hover {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.progress {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4">
|
||||
@ -30,44 +61,62 @@
|
||||
|
||||
<!-- Proposal Details -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">{{ proposal.title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span
|
||||
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
|
||||
<i
|
||||
class="bi {% if proposal.status == 'Active' %}bi-check-circle{% elif proposal.status == 'Approved' %}bi-trophy{% elif proposal.status == 'Rejected' %}bi-x-circle{% elif proposal.status == 'Draft' %}bi-pencil{% else %}bi-exclamation-circle{% endif %} me-1"></i>
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
<small class="text-muted">Created by {{ proposal.creator_name }}
|
||||
<!-- on {{ proposal.created_at | date(format="%Y-%m-%d") }} --></small>
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>Created by {{ proposal.creator_name
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<h5>Description</h5>
|
||||
<p class="mb-4">{{ proposal.description }}</p>
|
||||
<div class="flex-grow-1">
|
||||
<h5><i class="bi bi-file-text me-2"></i>Description</h5>
|
||||
<div class="p-3 bg-light rounded mb-4">{{ proposal.description }}</div>
|
||||
</div>
|
||||
|
||||
<h5>Voting Period</h5>
|
||||
<p>
|
||||
<div class="mt-auto">
|
||||
<h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
|
||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
|
||||
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||
<div>
|
||||
<div class="text-muted mb-1">Start Date</div>
|
||||
<div class="fw-bold">{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<i class="bi bi-arrow-right fs-4 text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted mb-1">End Date</div>
|
||||
<div class="fw-bold">{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
Not set
|
||||
<div class="text-center w-100">Not set</div>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Voting Results</h5>
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4 shadow-sm h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Voting Results Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="border-bottom pb-2 mb-3">Results</h6>
|
||||
|
||||
{% set yes_percent = 0 %}
|
||||
{% set no_percent = 0 %}
|
||||
{% set abstain_percent = 0 %}
|
||||
@ -78,131 +127,269 @@
|
||||
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
|
||||
{% endif %}
|
||||
|
||||
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%">
|
||||
<!-- Yes votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-success"><i class="bi bi-check-circle-fill me-1"></i> Yes</span>
|
||||
<span class="badge bg-success rounded-pill">{{ results.yes_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
|
||||
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="{{ yes_percent }}% of votes"></div>
|
||||
</div>
|
||||
|
||||
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%">
|
||||
<!-- No votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-danger"><i class="bi bi-x-circle-fill me-1"></i> No</span>
|
||||
<span class="badge bg-danger rounded-pill">{{ results.no_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
|
||||
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="{{ no_percent }}% of votes"></div>
|
||||
</div>
|
||||
|
||||
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<!-- Abstain votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-secondary"><i class="bi bi-dash-circle-fill me-1"></i>
|
||||
Abstain</span>
|
||||
<span class="badge bg-secondary rounded-pill">{{ results.abstain_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-secondary" role="progressbar"
|
||||
style="width: {{ abstain_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
|
||||
style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
|
||||
aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote Form -->
|
||||
{% if proposal.status == "Active" and user and user.id %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Cast Your Vote</h5>
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
|
||||
<div class="text-center">
|
||||
<h4 class="mb-0">{{ results.total_votes }}</h4>
|
||||
<small class="text-muted">Total Votes</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{% if proposal.status == "Active" %}
|
||||
<div class="text-center">
|
||||
<div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
|
||||
<svg width="60" height="60">
|
||||
<circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
|
||||
</circle>
|
||||
<circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
|
||||
stroke-dasharray="157"
|
||||
stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
|
||||
transform="rotate(-90 30 30)"></circle>
|
||||
</svg>
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
|
||||
{{ yes_percent }}%</div>
|
||||
</div>
|
||||
<small class="text-muted">Approval Rate</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote Form Section -->
|
||||
{% if proposal.status == "Active" and user and user.id %}
|
||||
<div class="mt-auto">
|
||||
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
|
||||
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Vote Type</label>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes"
|
||||
checked>
|
||||
<label class="form-check-label" for="voteYes">
|
||||
Yes - I support this proposal
|
||||
</label>
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteYes"
|
||||
value="Yes" required>
|
||||
<label class="form-check-label text-success" for="voteYes"><i
|
||||
class="bi bi-check-circle-fill me-1"></i>Yes</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
|
||||
<label class="form-check-label" for="voteNo">
|
||||
No - I oppose this proposal
|
||||
</label>
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteNo"
|
||||
value="No">
|
||||
<label class="form-check-label text-danger" for="voteNo"><i
|
||||
class="bi bi-x-circle-fill me-1"></i>No</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
|
||||
value="Abstain">
|
||||
<label class="form-check-label" for="voteAbstain">
|
||||
Abstain - I choose not to vote
|
||||
</label>
|
||||
<label class="form-check-label text-secondary" for="voteAbstain"><i
|
||||
class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="comment" class="form-label">Comment (Optional)</label>
|
||||
<textarea class="form-control" id="comment" name="comment" rows="3"
|
||||
placeholder="Explain your vote..."></textarea>
|
||||
<textarea class="form-control" id="comment" name="comment" rows="2"
|
||||
placeholder="Add your thoughts about this proposal (optional)..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-send me-2"></i>Submit
|
||||
Vote</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif proposal.status != "Active" %}
|
||||
<div class="mt-auto text-center p-3 bg-light rounded">
|
||||
<i class="bi bi-info-circle fs-4 text-muted"></i>
|
||||
<p class="mb-0 mt-2">Voting is {{ proposal.status | lower }} for this proposal</p>
|
||||
</div>
|
||||
{% elif not user or not user.id %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p>You must be logged in to vote.</p>
|
||||
<a href="/login" class="btn btn-primary">Login to Vote</a>
|
||||
</div>
|
||||
</div>
|
||||
{% elif proposal.status != "Active" %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning mb-0">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<strong>Note:</strong> Voting is only available for proposals with an Active status.
|
||||
This proposal's current status is <strong>{{ proposal.status }}</strong>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto text-center p-3 bg-light rounded">
|
||||
<i class="bi bi-person-lock fs-4 text-muted"></i>
|
||||
<p class="mb-0 mt-2">You must be logged in to vote</p>
|
||||
<a href="/login" class="btn btn-primary btn-sm mt-2">Login to Vote</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Votes List -->
|
||||
<div class="row mb-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Votes ({{ votes | length }})</h5>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap">
|
||||
<h5 class="mb-0 mb-md-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="input-group input-group-sm me-2 d-none d-md-flex" style="width: 200px;">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-search"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control border-start-0" id="voteSearch"
|
||||
placeholder="Search votes...">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if votes | length > 0 %}
|
||||
<div class="btn-group" role="group" aria-label="Filter votes">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary active"
|
||||
data-filter="all">All</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success"
|
||||
data-filter="yes">Yes</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-filter="no">No</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-filter="abstain">Abstain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Voter</th>
|
||||
<th class="ps-3">Voter</th>
|
||||
<th>Vote</th>
|
||||
<th>Comment</th>
|
||||
<th>Date</th>
|
||||
<th class="text-end pe-3">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vote in votes %}
|
||||
{% if votes | length == 0 %}
|
||||
<tr>
|
||||
<td>{{ vote.voter_name }}</td>
|
||||
<td colspan="4" class="text-center py-4">
|
||||
<div class="py-3">
|
||||
<i class="bi bi-inbox fs-1 text-muted"></i>
|
||||
<p class="mt-2 mb-0">No votes have been cast yet</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for vote in votes %}
|
||||
<tr class="vote-row" data-vote-type="{{ vote.vote_type | lower }}">
|
||||
<td class="ps-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2 bg-primary text-white">
|
||||
U
|
||||
</div>
|
||||
<span>{{ vote.voter_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %} rounded-pill px-3 py-2">
|
||||
{% if vote.vote_type == 'Yes' %}
|
||||
<i class="bi bi-check-circle-fill me-1"></i>
|
||||
{% elif vote.vote_type == 'No' %}
|
||||
<i class="bi bi-x-circle-fill me-1"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-dash-circle-fill me-1"></i>
|
||||
{% endif %}
|
||||
{{ vote.vote_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
|
||||
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
|
||||
<td>
|
||||
{% if vote.comment %}
|
||||
<div class="comment-text">{{ vote.comment }}</div>
|
||||
{% else %}
|
||||
<span class="text-muted fst-italic">No comment provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="d-flex flex-column align-items-end">
|
||||
<span>{{ vote.created_at | date(format="%Y-%m-%d") }}</span>
|
||||
<small class="text-muted">{{ vote.created_at | date(format="%H:%M")
|
||||
}}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No votes have been cast yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Vote filtering using data-filter attributes
|
||||
const filterButtons = document.querySelectorAll('[data-filter]');
|
||||
const voteRows = document.querySelectorAll('.vote-row');
|
||||
const searchInput = document.getElementById('voteSearch');
|
||||
|
||||
// Filter votes by type
|
||||
filterButtons.forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
// Update active button
|
||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const filterType = this.getAttribute('data-filter');
|
||||
|
||||
voteRows.forEach(row => {
|
||||
if (filterType === 'all') {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
const voteType = row.getAttribute('data-vote-type');
|
||||
row.style.display = (voteType === filterType) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function () {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
voteRows.forEach(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
|
||||
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize tooltips for all elements with title attributes
|
||||
const tooltipElements = document.querySelectorAll('[title]');
|
||||
if (tooltipElements.length > 0) {
|
||||
[].slice.call(tooltipElements).map(function (el) {
|
||||
return new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
||||
{% endblock content %}
|
Loading…
Reference in New Issue
Block a user