update governance ui

This commit is contained in:
timurgordon 2025-05-16 14:07:20 +03:00
parent 9468595395
commit 2fd74defab
5 changed files with 238 additions and 154 deletions

View File

@ -12,9 +12,24 @@ pub struct GovernanceController;
impl GovernanceController {
/// Helper function to get user from session
/// For testing purposes, this will always return a mock user
fn get_user_from_session(session: &Session) -> Option<Value> {
session.get::<String>("user").ok().flatten().and_then(|user_json| {
// Try to get user from session first
let session_user = session.get::<String>("user").ok().flatten().and_then(|user_json| {
serde_json::from_str(&user_json).ok()
});
// If user is not in session, return a mock user for testing
session_user.or_else(|| {
// Create a mock user
let mock_user = serde_json::json!({
"id": 1,
"username": "test_user",
"email": "test@example.com",
"name": "Test User",
"role": "member"
});
Some(mock_user)
})
}
@ -23,14 +38,32 @@ impl GovernanceController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get mock proposals for the dashboard
let proposals = Self::get_mock_proposals();
ctx.insert("proposals", &proposals);
let mut proposals = Self::get_mock_proposals();
// Filter for active proposals only
let active_proposals: Vec<Proposal> = proposals.into_iter()
.filter(|p| p.status == ProposalStatus::Active)
.collect();
// Sort active proposals by voting end date (ascending)
let mut sorted_active_proposals = active_proposals.clone();
sorted_active_proposals.sort_by(|a, b| a.voting_ends_at.cmp(&b.voting_ends_at));
ctx.insert("proposals", &sorted_active_proposals);
// Get the nearest deadline proposal for the voting pane
if let Some(nearest_proposal) = sorted_active_proposals.first() {
ctx.insert("nearest_proposal", nearest_proposal);
}
// Get recent activity for the timeline
let recent_activity = Self::get_mock_recent_activity();
ctx.insert("recent_activity", &recent_activity);
// Get some statistics
let stats = Self::get_mock_statistics();
@ -106,13 +139,9 @@ impl GovernanceController {
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "create");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
} else {
// Redirect to login if not logged in
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
render_template(&tmpl, "governance/create_proposal.html", &ctx)
}
@ -123,18 +152,12 @@ impl GovernanceController {
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
// Check if user is logged in
if Self::get_user_from_session(&session).is_none() {
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
}
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// In a real application, we would save the proposal to a database
// For now, we'll just redirect to the proposals page with a success message
@ -204,19 +227,77 @@ impl GovernanceController {
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "my_votes");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
// Get mock votes for this user
let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data
ctx.insert("votes", &votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
} else {
// Redirect to login if not logged in
Ok(HttpResponse::Found().append_header(("Location", "/login")).finish())
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get mock votes for this user
let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data
ctx.insert("votes", &votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
}
/// Generate mock recent activity data for the dashboard
fn get_mock_recent_activity() -> Vec<serde_json::Value> {
vec![
serde_json::json!({
"type": "vote",
"user": "Sarah Johnson",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(2)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "comment",
"user": "Michael Chen",
"proposal_id": "prop-003",
"proposal_title": "Weekly Community Calls",
"action": "commented",
"comment": "I think this would greatly improve communication.",
"timestamp": (Utc::now() - Duration::hours(5)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
serde_json::json!({
"type": "vote",
"user": "Robert Callingham",
"proposal_id": "prop-005",
"proposal_title": "Security Audit Implementation",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(8)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "proposal",
"user": "Emma Rodriguez",
"proposal_id": "prop-004",
"proposal_title": "Sustainability Roadmap",
"action": "created proposal",
"timestamp": (Utc::now() - Duration::hours(12)).to_rfc3339(),
"icon": "bi-file-earmark-text-fill text-info"
}),
serde_json::json!({
"type": "vote",
"user": "David Kim",
"proposal_id": "prop-002",
"proposal_title": "Governance Framework Update",
"action": "voted No",
"timestamp": (Utc::now() - Duration::hours(16)).to_rfc3339(),
"icon": "bi-x-circle-fill text-danger"
}),
serde_json::json!({
"type": "comment",
"user": "Lisa Wang",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "commented",
"comment": "I'd like to volunteer to help coordinate this effort.",
"timestamp": (Utc::now() - Duration::hours(24)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
]
}
// Mock data generation methods

View File

@ -65,8 +65,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
.route("/governance/create-proposal", web::get().to(GovernanceController::create_proposal_form))
.route("/governance/create-proposal", web::post().to(GovernanceController::submit_proposal))
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
// Flow routes

View File

@ -3,51 +3,8 @@
{% block title %}Governance Dashboard{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Governance Dashboard</h1>
<p class="lead">Participate in the decision-making process by voting on proposals and creating new ones.</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<h5 class="card-title">Total Proposals</h5>
<p class="card-text display-6">{{ stats.total_proposals }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<h5 class="card-title">Active Proposals</h5>
<p class="card-text display-6">{{ stats.active_proposals }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-info h-100">
<div class="card-body">
<h5 class="card-title">Total Votes</h5>
<p class="card-text display-6">{{ stats.total_votes }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body">
<h5 class="card-title">Participation Rate</h5>
<p class="card-text display-6">{{ stats.participation_rate }}%</p>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
@ -66,43 +23,109 @@
</div>
</div>
<!-- Active Proposals Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<!-- Info Alert -->
<div class="row mb-2">
<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 Governance</h5>
<p>The governance system allows token holders to participate in decision-making processes by voting on proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction of our decentralized ecosystem.</p>
<div class="mt-2">
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
</div>
</div>
</div>
</div>
<!-- Dashboard Main Content -->
<div class="row mb-3">
<!-- Voting Pane for Nearest Deadline Proposal -->
<div class="col-lg-8 mb-4 mb-lg-0">
{% if nearest_proposal is defined %}
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Active Proposals</h5>
<a href="/governance/proposals" class="btn btn-sm btn-outline-primary">View All</a>
<h5 class="mb-0">Urgent: Voting Closes Soon</h5>
<div>
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
<a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Title</th>
<th>Creator</th>
<th>Status</th>
<th>Voting Ends</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in proposals %}
{% if proposal.status == "Active" %}
<tr>
<td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td>
<td><span class="badge bg-success">{{ proposal.status }}</span></td>
<td>{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<h4 class="card-title">{{ nearest_proposal.title }}</h4>
<h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
<div class="mb-4">
<p>{{ nearest_proposal.description }}</p>
</div>
<div class="progress mb-3" style="height: 25px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">65% Yes</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div>
</div>
<div class="d-flex justify-content-between text-muted small mb-4">
<span>26 votes cast</span>
<span>Quorum: 75% reached</span>
</div>
<div class="mb-4">
<h5 class="mb-3">Cast Your Vote</h5>
<form>
<div class="mb-3">
<input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment">
</div>
<div class="d-flex justify-content-between">
<button type="submit" name="vote" value="yes" class="btn btn-success">Vote Yes</button>
<button type="submit" name="vote" value="no" class="btn btn-danger">Vote No</button>
<button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button>
</div>
</form>
</div>
</div>
</div>
{% else %}
<div class="card h-100">
<div class="card-body text-center py-5">
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
<h5>No active proposals requiring votes</h5>
<p class="text-muted">When new proposals are created, they will appear here for voting.</p>
<a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
</div>
</div>
{% endif %}
</div>
<!-- Recent Activity Timeline -->
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Recent Activity</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for activity in recent_activity %}
<div class="list-group-item border-start-0 border-end-0 py-3">
<div class="d-flex">
<div class="me-3">
<i class="bi {{ activity.icon }} fs-4"></i>
</div>
<div>
<div class="d-flex justify-content-between align-items-center">
<strong>{{ activity.user }}</strong>
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small>
</div>
<p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p>
{% if activity.type == "comment" and activity.comment is defined %}
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card-footer text-center">
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
</div>
</div>
</div>
@ -113,7 +136,7 @@
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Recent Proposals</h5>
<h5 class="mb-0">Active Proposals (Ending Soon)</h5>
</div>
<div class="card-body">
<div class="row">
@ -133,8 +156,8 @@
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
</div>
</div>
<div class="card-footer text-muted">
Created: {{ proposal.created_at | date(format="%Y-%m-%d") }}
<div class="card-footer text-muted text-center">
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
</div>
</div>
</div>
@ -146,17 +169,4 @@
</div>
</div>
</div>
<!-- Call to Action -->
<div class="row mb-4">
<div class="col-12">
<div class="card bg-light">
<div class="card-body text-center">
<h4 class="mb-3">Have an idea to improve our platform?</h4>
<p class="mb-4">Create a proposal and let the community vote on it.</p>
<a href="/governance/create" class="btn btn-primary">Create Proposal</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,14 +3,6 @@
{% block title %}My Votes - Governance Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">My Votes</h1>
<p class="lead">View all proposals you have voted on.</p>
</div>
</div>
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="col-12">
@ -52,7 +44,7 @@
</tr>
</thead>
<tbody>
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
<tr>
<td>{{ proposal.title }}</td>
<td>
@ -96,7 +88,7 @@
<h5 class="card-title">Yes Votes</h5>
<p class="display-4">
{% set yes_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Yes' %}
{% set yes_count = yes_count + 1 %}
{% endif %}
@ -112,7 +104,7 @@
<h5 class="card-title">No Votes</h5>
<p class="display-4">
{% set no_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'No' %}
{% set no_count = no_count + 1 %}
{% endif %}
@ -128,7 +120,7 @@
<h5 class="card-title">Abstain Votes</h5>
<p class="display-4">
{% set abstain_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Abstain' %}
{% set abstain_count = abstain_count + 1 %}
{% endif %}
@ -140,5 +132,4 @@
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -3,14 +3,6 @@
{% block title %}Proposals - Governance Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Governance Proposals</h1>
<p class="lead">View and vote on all proposals in the system.</p>
</div>
</div>
<!-- Success message if present -->
{% if success %}
<div class="row mb-4">
@ -24,7 +16,7 @@
{% endif %}
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
@ -43,6 +35,17 @@
</div>
</div>
<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 Proposals</h5>
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
<div class="mt-2">
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
</div>
</div>
</div>
<!-- Filter Controls -->
<div class="row mb-4">
<div class="col-12">
@ -124,5 +127,4 @@
</div>
</div>
</div>
</div>
{% endblock %}