/ frontend / src / components / voting / VotePanel.tsx
VotePanel.tsx
  1  import { useState } from 'react'
  2  import type { PullRequest, VoteType, VoteFlag } from '../../types/vote'
  3  import { useVotesStore } from '../../store/votes'
  4  import { useAuthStore } from '../../store/auth'
  5  import VoteProgress from './VoteProgress'
  6  import FlagSelector from './FlagSelector'
  7  
  8  interface VotePanelProps {
  9    pr: PullRequest
 10    totalGovernors: number
 11  }
 12  
 13  export default function VotePanel({ pr, totalGovernors }: VotePanelProps) {
 14    const { isConnected, chain } = useAuthStore()
 15    const { castVote, hasVoted, getUserVote } = useVotesStore()
 16  
 17    const [selectedVote, setSelectedVote] = useState<VoteType | null>(null)
 18    const [selectedFlags, setSelectedFlags] = useState<VoteFlag[]>([])
 19    const [isSubmitting, setIsSubmitting] = useState(false)
 20    const [error, setError] = useState<string | null>(null)
 21  
 22    const userVote = getUserVote(pr.prHash)
 23    const alreadyVoted = hasVoted(pr.prHash)
 24  
 25    // Check if user can vote on this PR
 26    const canVote = isConnected && chain === pr.chain && pr.status === 'voting' && !alreadyVoted
 27  
 28    const handleSubmitVote = async () => {
 29      if (!selectedVote || !canVote) return
 30  
 31      setIsSubmitting(true)
 32      setError(null)
 33  
 34      try {
 35        await castVote(pr.prHash, selectedVote, selectedFlags, pr.chain)
 36      } catch (err) {
 37        setError(err instanceof Error ? err.message : 'Failed to submit vote')
 38      } finally {
 39        setIsSubmitting(false)
 40      }
 41    }
 42  
 43    return (
 44      <div className="bg-white border rounded-lg p-6 space-y-6">
 45        <div className="flex items-center justify-between">
 46          <h3 className="text-lg font-semibold">Vote</h3>
 47          <span
 48            className={`px-2 py-1 rounded text-xs font-medium ${
 49              pr.chain === 'alpha'
 50                ? 'bg-alpha-100 text-alpha-700'
 51                : 'bg-delta-100 text-delta-700'
 52            }`}
 53          >
 54            {pr.chain === 'alpha' ? 'Alpha Tech' : 'Delta Code'}
 55          </span>
 56        </div>
 57  
 58        {/* Vote Progress */}
 59        <VoteProgress pr={pr} totalGovernors={totalGovernors} />
 60  
 61        {/* Voting Interface */}
 62        {pr.status === 'voting' && (
 63          <>
 64            {alreadyVoted ? (
 65              <div className="text-center py-4">
 66                <p className="text-gray-600">
 67                  You voted:{' '}
 68                  <span
 69                    className={`font-semibold ${
 70                      userVote?.voteType === 'yes'
 71                        ? 'text-green-600'
 72                        : userVote?.voteType === 'no'
 73                        ? 'text-red-600'
 74                        : 'text-gray-600'
 75                    }`}
 76                  >
 77                    {userVote?.voteType.toUpperCase()}
 78                  </span>
 79                </p>
 80                {userVote?.flags && userVote.flags.length > 0 && (
 81                  <p className="text-sm text-gray-500 mt-1">
 82                    Flags: {userVote.flags.join(', ')}
 83                  </p>
 84                )}
 85              </div>
 86            ) : canVote ? (
 87              <div className="space-y-4">
 88                {/* Vote buttons */}
 89                <div className="flex gap-3">
 90                  <button
 91                    onClick={() => setSelectedVote('yes')}
 92                    className={`flex-1 py-3 rounded-lg font-medium transition-all ${
 93                      selectedVote === 'yes'
 94                        ? 'bg-green-500 text-white'
 95                        : 'bg-green-50 text-green-700 hover:bg-green-100'
 96                    }`}
 97                  >
 98                    Yes
 99                  </button>
100                  <button
101                    onClick={() => setSelectedVote('no')}
102                    className={`flex-1 py-3 rounded-lg font-medium transition-all ${
103                      selectedVote === 'no'
104                        ? 'bg-red-500 text-white'
105                        : 'bg-red-50 text-red-700 hover:bg-red-100'
106                    }`}
107                  >
108                    No
109                  </button>
110                  <button
111                    onClick={() => setSelectedVote('abstain')}
112                    className={`flex-1 py-3 rounded-lg font-medium transition-all ${
113                      selectedVote === 'abstain'
114                        ? 'bg-gray-500 text-white'
115                        : 'bg-gray-50 text-gray-700 hover:bg-gray-100'
116                    }`}
117                  >
118                    Abstain
119                  </button>
120                </div>
121  
122                {/* Flag selector (only for No votes) */}
123                {selectedVote === 'no' && (
124                  <FlagSelector
125                    selectedFlags={selectedFlags}
126                    onChange={setSelectedFlags}
127                    disabled={isSubmitting}
128                  />
129                )}
130  
131                {/* Submit button */}
132                <button
133                  onClick={handleSubmitVote}
134                  disabled={!selectedVote || isSubmitting}
135                  className="w-full py-3 bg-alpha-600 text-white rounded-lg font-medium hover:bg-alpha-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
136                >
137                  {isSubmitting ? 'Submitting...' : 'Submit Vote'}
138                </button>
139  
140                {error && (
141                  <p className="text-red-600 text-sm text-center">{error}</p>
142                )}
143              </div>
144            ) : (
145              <div className="text-center py-4 text-gray-500">
146                {!isConnected
147                  ? 'Connect wallet to vote'
148                  : chain !== pr.chain
149                  ? `Switch to ${pr.chain} chain to vote`
150                  : 'Voting not available'}
151              </div>
152            )}
153          </>
154        )}
155  
156        {/* Status for non-voting PRs */}
157        {pr.status !== 'voting' && (
158          <div
159            className={`text-center py-4 font-medium ${
160              pr.status === 'passed'
161                ? 'text-green-600'
162                : pr.status === 'failed'
163                ? 'text-red-600'
164                : 'text-gray-500'
165            }`}
166          >
167            {pr.status === 'passed' && 'Vote Passed'}
168            {pr.status === 'failed' && 'Vote Failed'}
169            {pr.status === 'draft' && 'Awaiting Sponsor'}
170          </div>
171        )}
172  
173        {/* Behavior flags warning */}
174        {pr.behaviorFlags > 0 && (
175          <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-700">
176            This PR has been flagged by voters. Review carefully.
177          </div>
178        )}
179      </div>
180    )
181  }