ProposalLink.tsx
1 import { useQuery } from '@tanstack/react-query' 2 import type { PullRequest, CrossChainAttestation } from '../../types/vote' 3 import { 4 getProposal, 5 getCrossChainAttestation, 6 formatProposalStatus, 7 PROPOSAL_STATUS, 8 } from '../../services/governance' 9 10 interface ProposalLinkProps { 11 pr: PullRequest 12 } 13 14 export default function ProposalLink({ pr }: ProposalLinkProps) { 15 // Fetch linked governance proposal if exists 16 const { data: proposal } = useQuery({ 17 queryKey: ['governanceProposal', pr.governanceProposalId, pr.chain], 18 queryFn: () => getProposal(pr.chain, pr.governanceProposalId!), 19 enabled: !!pr.governanceProposalId, 20 }) 21 22 // Fetch cross-chain attestation for shared repos 23 const { data: attestation } = useQuery({ 24 queryKey: ['crossChainAttestation', pr.prHash, pr.chain], 25 queryFn: () => getCrossChainAttestation(pr.chain, pr.prHash), 26 enabled: pr.isSharedRepo && pr.status === 'passed', 27 }) 28 29 // No governance link yet 30 if (!pr.governanceProposalId && pr.status !== 'passed') { 31 return null 32 } 33 34 // Shared repo waiting for both chains 35 if (pr.isSharedRepo && pr.status === 'passed' && attestation) { 36 const fullAttestation: CrossChainAttestation = { 37 prHash: pr.prHash, 38 alphaPassed: attestation.alphaPassed, 39 deltaPassed: attestation.deltaPassed, 40 alphaProposalId: attestation.alphaProposalId, 41 deltaProposalId: attestation.deltaProposalId, 42 finalizedAt: null, 43 } 44 return ( 45 <SharedRepoStatus 46 attestation={fullAttestation} 47 prChain={pr.chain} 48 /> 49 ) 50 } 51 52 // PR passed, proposal pending 53 if (pr.status === 'passed' && !pr.governanceProposalId) { 54 return ( 55 <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> 56 <h3 className="font-semibold text-blue-800 mb-2"> 57 Governance Proposal Pending 58 </h3> 59 <p className="text-sm text-blue-700"> 60 This PR has passed the Forge vote. A governance proposal will be created 61 to finalize the code changes. 62 </p> 63 </div> 64 ) 65 } 66 67 // Show linked proposal 68 if (proposal) { 69 return ( 70 <div className="bg-purple-50 border border-purple-200 rounded-lg p-4"> 71 <div className="flex items-center justify-between mb-3"> 72 <h3 className="font-semibold text-purple-800"> 73 Linked Governance Proposal 74 </h3> 75 <StatusBadge status={proposal.status} /> 76 </div> 77 78 <dl className="grid grid-cols-2 gap-3 text-sm"> 79 <div> 80 <dt className="text-purple-600">Proposal ID</dt> 81 <dd className="font-mono text-purple-900">{proposal.proposalId}</dd> 82 </div> 83 <div> 84 <dt className="text-purple-600">Type</dt> 85 <dd className="text-purple-900 capitalize">{proposal.proposalType}</dd> 86 </div> 87 <div> 88 <dt className="text-purple-600">Votes</dt> 89 <dd className="text-purple-900"> 90 {proposal.yesVotes} Yes / {proposal.noVotes} No 91 </dd> 92 </div> 93 <div> 94 <dt className="text-purple-600">Status</dt> 95 <dd className="text-purple-900">{formatProposalStatus(proposal.status)}</dd> 96 </div> 97 </dl> 98 99 {proposal.status === PROPOSAL_STATUS.PASSED && ( 100 <div className="mt-3 pt-3 border-t border-purple-200"> 101 <p className="text-sm text-purple-700"> 102 Timelock ends:{' '} 103 {new Date(proposal.timelockEndsAt * 1000).toLocaleString()} 104 </p> 105 </div> 106 )} 107 108 {proposal.status === PROPOSAL_STATUS.EXECUTED && ( 109 <div className="mt-3 pt-3 border-t border-purple-200 bg-green-50 -mx-4 -mb-4 px-4 py-3 rounded-b-lg"> 110 <p className="text-sm text-green-700 font-medium"> 111 Code changes have been officially executed and recorded on-chain. 112 </p> 113 </div> 114 )} 115 </div> 116 ) 117 } 118 119 return null 120 } 121 122 function SharedRepoStatus({ 123 attestation, 124 prChain, 125 }: { 126 attestation: CrossChainAttestation 127 prChain: 'alpha' | 'delta' 128 }) { 129 const bothPassed = attestation.alphaPassed && attestation.deltaPassed 130 131 return ( 132 <div className="bg-gradient-to-r from-alpha-50 to-delta-50 border border-gray-200 rounded-lg p-4"> 133 <h3 className="font-semibold text-gray-800 mb-3"> 134 Cross-Chain Attestation Required 135 </h3> 136 137 <p className="text-sm text-gray-600 mb-4"> 138 This is a shared repository. Changes require approval from both Alpha Tech 139 Governors and Delta Code Governors. 140 </p> 141 142 <div className="grid grid-cols-2 gap-4"> 143 {/* Alpha Chain Status */} 144 <div 145 className={`p-3 rounded-lg ${ 146 attestation.alphaPassed 147 ? 'bg-green-100 border border-green-300' 148 : 'bg-gray-100 border border-gray-300' 149 }`} 150 > 151 <div className="flex items-center gap-2 mb-1"> 152 {attestation.alphaPassed ? ( 153 <CheckIcon className="h-5 w-5 text-green-600" /> 154 ) : ( 155 <ClockIcon className="h-5 w-5 text-gray-400" /> 156 )} 157 <span className="font-medium text-sm">Alpha Tech</span> 158 </div> 159 <p className="text-xs text-gray-600"> 160 {attestation.alphaPassed 161 ? 'Approved' 162 : prChain === 'alpha' 163 ? 'Passed locally' 164 : 'Awaiting vote'} 165 </p> 166 </div> 167 168 {/* Delta Chain Status */} 169 <div 170 className={`p-3 rounded-lg ${ 171 attestation.deltaPassed 172 ? 'bg-green-100 border border-green-300' 173 : 'bg-gray-100 border border-gray-300' 174 }`} 175 > 176 <div className="flex items-center gap-2 mb-1"> 177 {attestation.deltaPassed ? ( 178 <CheckIcon className="h-5 w-5 text-green-600" /> 179 ) : ( 180 <ClockIcon className="h-5 w-5 text-gray-400" /> 181 )} 182 <span className="font-medium text-sm">Delta Code</span> 183 </div> 184 <p className="text-xs text-gray-600"> 185 {attestation.deltaPassed 186 ? 'Approved' 187 : prChain === 'delta' 188 ? 'Passed locally' 189 : 'Awaiting vote'} 190 </p> 191 </div> 192 </div> 193 194 {bothPassed && ( 195 <div className="mt-4 pt-3 border-t border-gray-200"> 196 <p className="text-sm text-green-700 font-medium"> 197 Both chains have approved. A joint governance proposal will be created. 198 </p> 199 </div> 200 )} 201 202 {!bothPassed && ( 203 <div className="mt-4 pt-3 border-t border-gray-200"> 204 <p className="text-sm text-gray-600"> 205 Governance proposal will be created after both chains approve. 206 </p> 207 </div> 208 )} 209 </div> 210 ) 211 } 212 213 function StatusBadge({ status }: { status: number | typeof PROPOSAL_STATUS[keyof typeof PROPOSAL_STATUS] }) { 214 const getStatusStyle = () => { 215 switch (status) { 216 case PROPOSAL_STATUS.PENDING: 217 return 'bg-gray-100 text-gray-700' 218 case PROPOSAL_STATUS.ACTIVE: 219 return 'bg-blue-100 text-blue-700' 220 case PROPOSAL_STATUS.PASSED: 221 return 'bg-green-100 text-green-700' 222 case PROPOSAL_STATUS.FAILED: 223 return 'bg-red-100 text-red-700' 224 case PROPOSAL_STATUS.EXECUTED: 225 return 'bg-purple-100 text-purple-700' 226 case PROPOSAL_STATUS.VETOED: 227 return 'bg-orange-100 text-orange-700' 228 case PROPOSAL_STATUS.EXPIRED: 229 return 'bg-gray-100 text-gray-500' 230 default: 231 return 'bg-gray-100 text-gray-700' 232 } 233 } 234 235 return ( 236 <span 237 className={`px-2 py-1 rounded text-xs font-medium ${getStatusStyle()}`} 238 > 239 {formatProposalStatus(status as typeof PROPOSAL_STATUS[keyof typeof PROPOSAL_STATUS])} 240 </span> 241 ) 242 } 243 244 function CheckIcon({ className }: { className?: string }) { 245 return ( 246 <svg 247 className={className} 248 fill="none" 249 viewBox="0 0 24 24" 250 stroke="currentColor" 251 strokeWidth={2} 252 > 253 <path 254 strokeLinecap="round" 255 strokeLinejoin="round" 256 d="M5 13l4 4L19 7" 257 /> 258 </svg> 259 ) 260 } 261 262 function ClockIcon({ className }: { className?: string }) { 263 return ( 264 <svg 265 className={className} 266 fill="none" 267 viewBox="0 0 24 24" 268 stroke="currentColor" 269 strokeWidth={2} 270 > 271 <path 272 strokeLinecap="round" 273 strokeLinejoin="round" 274 d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" 275 /> 276 </svg> 277 ) 278 }