synor/apps/desktop-wallet/src/pages/Governance/GovernanceDashboard.tsx
Gulshan Yadav 81347ab15d feat(wallet): add comprehensive desktop wallet features
Add all-in-one desktop wallet with extensive feature set:

Infrastructure:
- Storage: IPFS-based decentralized file storage with upload/download
- Hosting: Domain registration and static site hosting
- Compute: GPU/CPU job marketplace for distributed computing
- Database: Multi-model database services (KV, Document, Vector, etc.)

Financial Features:
- Privacy: Confidential transactions with Pedersen commitments
- Bridge: Cross-chain transfers (Ethereum, Bitcoin, IBC/Cosmos)
- Governance: DAO proposals, voting, and delegation
- ZK-Rollup: L2 scaling with deposits, withdrawals, and transfers

UI/UX Improvements:
- Add ErrorBoundary component for graceful error handling
- Add LoadingStates components (spinners, skeletons, overlays)
- Add Animation components (FadeIn, SlideIn, CountUp, etc.)
- Update navigation with new feature sections

Testing:
- Add Playwright E2E smoke tests
- Test route accessibility and page rendering
- Verify build process and asset loading

Build:
- Fix TypeScript compilation errors
- Update Tauri plugin dependencies
- Successfully build macOS app bundle and DMG
2026-02-02 11:35:21 +05:30

501 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import {
Vote,
Plus,
RefreshCw,
AlertCircle,
Clock,
CheckCircle,
XCircle,
Users,
TrendingUp,
ChevronDown,
ChevronUp,
Zap,
} from 'lucide-react';
import {
useGovernanceStore,
getStatusLabel,
getStatusColor,
calculateVotePercentage,
} from '../../store/governance';
export default function GovernanceDashboard() {
const {
proposals,
votingPower,
isLoading,
isVoting,
error,
clearError,
fetchProposals,
fetchVotingPower,
createProposal,
vote,
delegate,
} = useGovernanceStore();
const [activeTab, setActiveTab] = useState<'proposals' | 'voting-power'>('proposals');
const [showCreateForm, setShowCreateForm] = useState(false);
const [showDelegateForm, setShowDelegateForm] = useState(false);
const [expandedProposal, setExpandedProposal] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
// Create form state
const [newTitle, setNewTitle] = useState('');
const [newDescription, setNewDescription] = useState('');
const [newActions, setNewActions] = useState('');
// Delegate form state
const [delegateAddress, setDelegateAddress] = useState('');
useEffect(() => {
fetchProposals();
fetchVotingPower();
}, [fetchProposals, fetchVotingPower]);
const handleCreateProposal = async () => {
if (!newTitle || !newDescription) return;
setIsCreating(true);
try {
// Parse actions as string array
let actions: string[] = [];
if (newActions) {
try {
actions = JSON.parse(newActions);
} catch {
// If not valid JSON, treat as single action
actions = [newActions];
}
}
await createProposal(newTitle, newDescription, actions);
setShowCreateForm(false);
setNewTitle('');
setNewDescription('');
setNewActions('');
fetchProposals();
} catch {
// Error handled by store
} finally {
setIsCreating(false);
}
};
const handleVote = async (proposalId: string, support: 'for' | 'against' | 'abstain') => {
await vote(proposalId, support);
fetchProposals();
};
const handleDelegate = async () => {
if (!delegateAddress) return;
await delegate(delegateAddress);
setShowDelegateForm(false);
setDelegateAddress('');
fetchVotingPower();
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
return <Clock size={16} className="text-yellow-400" />;
case 'active':
return <Vote size={16} className="text-blue-400" />;
case 'passed':
return <CheckCircle size={16} className="text-green-400" />;
case 'rejected':
return <XCircle size={16} className="text-red-400" />;
case 'executed':
return <Zap size={16} className="text-purple-400" />;
default:
return <Clock size={16} className="text-gray-400" />;
}
};
const formatVotes = (votes: string) => {
const num = parseFloat(votes);
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
return votes;
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Governance</h1>
<p className="text-gray-400 mt-1">Participate in Synor DAO decisions</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
fetchProposals();
fetchVotingPower();
}}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-gray-300 transition-colors disabled:opacity-50"
>
<RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
Refresh
</button>
<button
onClick={() => setShowCreateForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors"
>
<Plus size={16} />
Create Proposal
</button>
</div>
</div>
{/* Error Alert */}
{error && (
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="text-red-400" size={20} />
<p className="text-red-400 flex-1">{error}</p>
<button onClick={clearError} className="text-red-400 hover:text-red-300">
×
</button>
</div>
)}
{/* Create Proposal Modal */}
{showCreateForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 w-full max-w-lg max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold text-white mb-4">Create Proposal</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Title</label>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Proposal title"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Description</label>
<textarea
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
placeholder="Describe your proposal in detail..."
rows={4}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
Actions (optional JSON array)
</label>
<textarea
value={newActions}
onChange={(e) => setNewActions(e.target.value)}
placeholder='["action1", "action2"]'
rows={3}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono text-sm placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowCreateForm(false)}
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
>
Cancel
</button>
<button
onClick={handleCreateProposal}
disabled={!newTitle || !newDescription || isCreating}
className="flex-1 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
{isCreating ? 'Creating...' : 'Create'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Delegate Modal */}
{showDelegateForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 w-full max-w-md">
<h2 className="text-xl font-bold text-white mb-4">Delegate Voting Power</h2>
<div className="space-y-4">
<p className="text-sm text-gray-400">
Delegate your voting power to another address. They will be able to vote on your
behalf, but you retain ownership of your tokens.
</p>
<div>
<label className="block text-sm text-gray-400 mb-1">Delegate Address</label>
<input
type="text"
value={delegateAddress}
onChange={(e) => setDelegateAddress(e.target.value)}
placeholder="synor1..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowDelegateForm(false)}
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
>
Cancel
</button>
<button
onClick={handleDelegate}
disabled={!delegateAddress}
className="flex-1 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
Delegate
</button>
</div>
</div>
</div>
</div>
)}
{/* Voting Power Card */}
{votingPower && (
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-3 bg-synor-600/20 rounded-lg">
<TrendingUp size={24} className="text-synor-400" />
</div>
<div>
<p className="text-sm text-gray-400">Your Voting Power</p>
<p className="text-2xl font-bold text-white">{formatVotes(votingPower.votingPower)} SYN</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-xs text-gray-500">Delegated Out</p>
<p className="text-white">{formatVotes(votingPower.delegatedOut)}</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Delegated In</p>
<p className="text-white">{formatVotes(votingPower.delegatedIn)}</p>
</div>
<button
onClick={() => setShowDelegateForm(true)}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
>
<Users size={16} className="inline mr-2" />
Delegate
</button>
</div>
</div>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b border-gray-800">
<button
onClick={() => setActiveTab('proposals')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'proposals'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
Proposals ({proposals.length})
</button>
<button
onClick={() => setActiveTab('voting-power')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'voting-power'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
Voting Power
</button>
</div>
{/* Proposals Tab */}
{activeTab === 'proposals' && (
<div className="space-y-4">
{proposals.map((proposal) => {
const votePercentages = calculateVotePercentage(
proposal.forVotes,
proposal.againstVotes,
proposal.abstainVotes
);
return (
<div
key={proposal.id}
className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"
>
<div
className="flex items-start justify-between p-4 cursor-pointer hover:bg-gray-800/50"
onClick={() => setExpandedProposal(expandedProposal === proposal.id ? null : proposal.id)}
>
<div className="flex items-start gap-4">
{getStatusIcon(proposal.status)}
<div>
<h3 className="text-white font-medium">{proposal.title}</h3>
<p className="text-sm text-gray-400">
#{proposal.id.slice(0, 8)} by {proposal.proposer.slice(0, 8)}...
</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-2 py-1 text-xs rounded ${getStatusColor(proposal.status)}`}>
{getStatusLabel(proposal.status)}
</span>
{expandedProposal === proposal.id ? (
<ChevronUp size={20} className="text-gray-400" />
) : (
<ChevronDown size={20} className="text-gray-400" />
)}
</div>
</div>
{expandedProposal === proposal.id && (
<div className="p-4 border-t border-gray-800 bg-gray-800/30">
<p className="text-gray-300 mb-4">{proposal.description}</p>
{/* Vote Progress */}
<div className="mb-4 space-y-2">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-green-400">For</span>
<span className="text-white">{formatVotes(proposal.forVotes)} ({votePercentages.for.toFixed(1)}%)</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div className="h-full bg-green-500" style={{ width: `${votePercentages.for}%` }} />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-red-400">Against</span>
<span className="text-white">{formatVotes(proposal.againstVotes)} ({votePercentages.against.toFixed(1)}%)</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div className="h-full bg-red-500" style={{ width: `${votePercentages.against}%` }} />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-400">Abstain</span>
<span className="text-white">{formatVotes(proposal.abstainVotes)} ({votePercentages.abstain.toFixed(1)}%)</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div className="h-full bg-gray-500" style={{ width: `${votePercentages.abstain}%` }} />
</div>
</div>
</div>
{/* Timeline */}
<div className="grid grid-cols-2 gap-4 text-sm mb-4">
<div>
<p className="text-gray-500">Start Block</p>
<p className="text-white">#{proposal.startBlock.toLocaleString()}</p>
</div>
<div>
<p className="text-gray-500">End Block</p>
<p className="text-white">#{proposal.endBlock.toLocaleString()}</p>
</div>
</div>
{/* Voting Buttons */}
{proposal.status === 'active' && !proposal.userVoted && (
<div className="flex gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleVote(proposal.id, 'for');
}}
disabled={isVoting}
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
Vote For
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleVote(proposal.id, 'against');
}}
disabled={isVoting}
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
Vote Against
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleVote(proposal.id, 'abstain');
}}
disabled={isVoting}
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-500 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
Abstain
</button>
</div>
)}
{proposal.userVoted && (
<div className="text-center py-2 text-gray-400">
You have already voted on this proposal {proposal.userVote && `(${proposal.userVote})`}
</div>
)}
</div>
)}
</div>
);
})}
{proposals.length === 0 && (
<div className="text-center py-12 text-gray-500">
No governance proposals yet
</div>
)}
</div>
)}
{/* Voting Power Tab */}
{activeTab === 'voting-power' && votingPower && (
<div className="space-y-6">
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h3 className="text-lg font-semibold text-white mb-4">Voting Power Breakdown</h3>
<div className="space-y-4">
<div className="flex justify-between items-center p-4 bg-gray-800 rounded-lg">
<span className="text-gray-400">Your Voting Power</span>
<span className="text-white font-medium">{formatVotes(votingPower.votingPower)} SYN</span>
</div>
<div className="flex justify-between items-center p-4 bg-gray-800 rounded-lg">
<span className="text-gray-400">Delegated Out</span>
<span className="text-white font-medium">{formatVotes(votingPower.delegatedOut)} SYN</span>
</div>
<div className="flex justify-between items-center p-4 bg-gray-800 rounded-lg">
<span className="text-gray-400">Delegated In</span>
<span className="text-white font-medium">{formatVotes(votingPower.delegatedIn)} SYN</span>
</div>
</div>
</div>
{votingPower.delegate && (
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h3 className="text-lg font-semibold text-white mb-4">Delegation</h3>
<div className="flex items-center justify-between p-4 bg-gray-800 rounded-lg">
<div>
<p className="text-gray-400 text-sm">Your votes are delegated to</p>
<code className="text-white font-mono">{votingPower.delegate}</code>
</div>
<button
onClick={() => delegate('')}
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg text-white text-sm font-medium transition-colors"
>
Revoke
</button>
</div>
</div>
)}
</div>
)}
</div>
);
}