/ frontend / src / components / voting / VoteProgress.tsx
VoteProgress.tsx
 1  import type { PullRequest } from '../../types/vote'
 2  import { calculateVoteProgress } from '../../services/chain'
 3  
 4  interface VoteProgressProps {
 5    pr: PullRequest
 6    totalGovernors: number
 7  }
 8  
 9  export default function VoteProgress({ pr, totalGovernors }: VoteProgressProps) {
10    const progress = calculateVoteProgress(pr, totalGovernors)
11  
12    // Format time remaining
13    const formatTimeRemaining = (seconds: number | null): string => {
14      if (seconds === null || seconds <= 0) return 'Ended'
15  
16      const days = Math.floor(seconds / 86400)
17      const hours = Math.floor((seconds % 86400) / 3600)
18      const minutes = Math.floor((seconds % 3600) / 60)
19  
20      if (days > 0) return `${days}d ${hours}h remaining`
21      if (hours > 0) return `${hours}h ${minutes}m remaining`
22      return `${minutes}m remaining`
23    }
24  
25    const totalVotes = pr.yesVotes + pr.noVotes
26    const yesWidth = totalVotes > 0 ? (pr.yesVotes / totalVotes) * 100 : 0
27    const noWidth = totalVotes > 0 ? (pr.noVotes / totalVotes) * 100 : 0
28  
29    return (
30      <div className="space-y-3">
31        {/* Progress bar */}
32        <div className="h-4 bg-gray-200 rounded-full overflow-hidden flex">
33          <div
34            className="bg-green-500 transition-all duration-300"
35            style={{ width: `${yesWidth}%` }}
36          />
37          <div
38            className="bg-red-500 transition-all duration-300"
39            style={{ width: `${noWidth}%` }}
40          />
41        </div>
42  
43        {/* Vote counts */}
44        <div className="flex justify-between text-sm">
45          <div className="flex items-center gap-2">
46            <span className="inline-block w-3 h-3 bg-green-500 rounded-full" />
47            <span className="text-gray-700">
48              Yes: <span className="font-semibold">{pr.yesVotes}</span>
49            </span>
50            <span className="text-gray-400">({progress.percentYes.toFixed(1)}%)</span>
51          </div>
52          <div className="flex items-center gap-2">
53            <span className="text-gray-400">({progress.percentNo.toFixed(1)}%)</span>
54            <span className="text-gray-700">
55              No: <span className="font-semibold">{pr.noVotes}</span>
56            </span>
57            <span className="inline-block w-3 h-3 bg-red-500 rounded-full" />
58          </div>
59        </div>
60  
61        {/* Threshold indicator */}
62        <div className="flex items-center justify-between text-sm">
63          <div className="text-gray-500">
64            Threshold: <span className="font-medium">{progress.threshold}</span> votes (67%)
65          </div>
66          <div
67            className={`font-medium ${
68              progress.isPassing ? 'text-green-600' : 'text-gray-500'
69            }`}
70          >
71            {progress.isPassing ? 'Passing' : 'Not passing'}
72          </div>
73        </div>
74  
75        {/* Time remaining */}
76        {pr.status === 'voting' && (
77          <div className="text-sm text-gray-500 text-center">
78            {formatTimeRemaining(progress.timeRemaining)}
79          </div>
80        )}
81      </div>
82    )
83  }