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 }