WIP: development_backend #4
@@ -340,6 +340,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)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,21 +23,24 @@
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <!-- Info Alert -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-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>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <!-- Proposal Form and Guidelines in Flex Layout -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <!-- Proposal Form Column -->
 | 
			
		||||
@@ -47,34 +50,42 @@
 | 
			
		||||
                    <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
 | 
			
		||||
                                      placeholder="Provide a detailed description of your proposal..."></textarea>
 | 
			
		||||
                                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>
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        <div class="row mb-3">
 | 
			
		||||
                            <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>
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <div class="form-check">
 | 
			
		||||
                                <input class="form-check-input" type="checkbox" id="draft" name="draft" value="true">
 | 
			
		||||
@@ -83,7 +94,7 @@
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        <div class="d-grid gap-2">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">Submit Proposal</button>
 | 
			
		||||
                            <a href="/governance" class="btn btn-outline-secondary">Cancel</a>
 | 
			
		||||
@@ -92,7 +103,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        <!-- Guidelines Column -->
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card bg-light h-100">
 | 
			
		||||
@@ -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>
 | 
			
		||||
                        {% 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") }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        Not set
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </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 %}
 | 
			
		||||
                            <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 %}
 | 
			
		||||
                            <div class="text-center w-100">Not set</div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </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 }}%">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        <!-- 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 }}%">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        <!-- 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>
 | 
			
		||||
                                style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
 | 
			
		||||
                                aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
 | 
			
		||||
                </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>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label class="form-label">Vote Type</label>
 | 
			
		||||
                            <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>
 | 
			
		||||
                    <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="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>
 | 
			
		||||
                            </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>
 | 
			
		||||
                            </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>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <button type="submit" class="btn btn-primary w-100">Submit Vote</button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </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>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Votes List -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Votes ({{ votes | length }})</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if votes | length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Voter</th>
 | 
			
		||||
                                    <th>Vote</th>
 | 
			
		||||
                                    <th>Comment</th>
 | 
			
		||||
                                    <th>Date</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for vote in votes %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>{{ vote.voter_name }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span
 | 
			
		||||
                                            class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% 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>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                            {% 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">
 | 
			
		||||
                                <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" 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 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 text-secondary" for="voteAbstain"><i
 | 
			
		||||
                                                class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <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"><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="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>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <p class="text-center">No votes have been cast yet.</p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Votes List -->
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <div class="col-12">
 | 
			
		||||
                <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="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 table-hover align-middle mb-0">
 | 
			
		||||
                                <thead class="table-light">
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <th class="ps-3">Voter</th>
 | 
			
		||||
                                        <th>Vote</th>
 | 
			
		||||
                                        <th>Comment</th>
 | 
			
		||||
                                        <th class="text-end pe-3">Date</th>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </thead>
 | 
			
		||||
                                <tbody>
 | 
			
		||||
                                    {% if votes | length == 0 %}
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <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 %} 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 %}
 | 
			
		||||
                                            <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>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
    {% 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 %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user