SponsorButton.tsx
1 import { useState } from 'react' 2 import { useQuery } from '@tanstack/react-query' 3 import type { PullRequest } from '../../types/vote' 4 import { useVotesStore } from '../../store/votes' 5 import { useAuthStore } from '../../store/auth' 6 import * as chainService from '../../services/chain' 7 8 const DELTA_MIN_GOVERNORS = 50 9 10 interface SponsorButtonProps { 11 pr: PullRequest 12 onSponsored?: () => void 13 } 14 15 export default function SponsorButton({ pr, onSponsored }: SponsorButtonProps) { 16 const { isConnected, chain } = useAuthStore() 17 const { sponsorPR } = useVotesStore() 18 19 const [isSponsoring, setIsSponsoring] = useState(false) 20 const [error, setError] = useState<string | null>(null) 21 22 // Check Delta governor count for minimum requirement 23 const { data: deltaGovernorCount = 0 } = useQuery({ 24 queryKey: ['governorCount', 'delta'], 25 queryFn: () => chainService.getGovernorCount('delta'), 26 enabled: pr.chain === 'delta', 27 }) 28 29 const isDeltaMinimumMet = pr.chain !== 'delta' || deltaGovernorCount >= DELTA_MIN_GOVERNORS 30 31 // Can only sponsor draft PRs on the same chain 32 // Delta chain also requires 50 minimum governors 33 const canSponsor = 34 isConnected && 35 chain === pr.chain && 36 pr.status === 'draft' && 37 isDeltaMinimumMet 38 39 const handleSponsor = async () => { 40 if (!canSponsor) return 41 42 setIsSponsoring(true) 43 setError(null) 44 45 try { 46 await sponsorPR(pr.prHash, pr.chain) 47 onSponsored?.() 48 } catch (err) { 49 setError(err instanceof Error ? err.message : 'Failed to sponsor PR') 50 } finally { 51 setIsSponsoring(false) 52 } 53 } 54 55 if (pr.status !== 'draft') { 56 return null 57 } 58 59 // Get appropriate button styling based on chain 60 const buttonColors = pr.chain === 'delta' 61 ? 'bg-delta-600 hover:bg-delta-700' 62 : 'bg-alpha-600 hover:bg-alpha-700' 63 64 return ( 65 <div className="space-y-3"> 66 {/* Delta 50-voter minimum warning */} 67 {pr.chain === 'delta' && !isDeltaMinimumMet && ( 68 <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3"> 69 <div className="flex items-start gap-2"> 70 <WarningIcon className="h-5 w-5 text-yellow-600 mt-0.5" /> 71 <div> 72 <p className="text-sm font-medium text-yellow-800"> 73 Minimum Governors Not Met 74 </p> 75 <p className="text-xs text-yellow-700 mt-1"> 76 Delta chain requires at least {DELTA_MIN_GOVERNORS} active Code 77 Governors before PRs can be sponsored. Currently:{' '} 78 <strong>{deltaGovernorCount}</strong> governors. 79 </p> 80 <p className="text-xs text-yellow-600 mt-2"> 81 {DELTA_MIN_GOVERNORS - deltaGovernorCount} more governors needed. 82 </p> 83 </div> 84 </div> 85 </div> 86 )} 87 88 <button 89 onClick={handleSponsor} 90 disabled={!canSponsor || isSponsoring} 91 className={`w-full py-2 px-4 text-white rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${buttonColors}`} 92 title={getButtonTitle(isConnected, chain, pr.chain, isDeltaMinimumMet)} 93 > 94 {isSponsoring ? 'Sponsoring...' : 'Sponsor for Vote'} 95 </button> 96 97 {error && <p className="text-red-600 text-sm text-center">{error}</p>} 98 99 <p className="text-xs text-gray-500 text-center"> 100 {pr.chain === 'delta' ? ( 101 <> 102 Sponsoring starts a 7-day voting period. Requires 50+ active governors 103 and you can only sponsor one PR at a time. 104 </> 105 ) : ( 106 <> 107 Sponsoring starts a 7-day voting period. You can only sponsor one PR 108 at a time. 109 </> 110 )} 111 </p> 112 </div> 113 ) 114 } 115 116 function getButtonTitle( 117 isConnected: boolean, 118 userChain: string | null | undefined, 119 prChain: string, 120 isDeltaMinimumMet: boolean 121 ): string { 122 if (!isConnected) return 'Connect wallet to sponsor' 123 if (userChain !== prChain) return `Switch to ${prChain} chain to sponsor` 124 if (!isDeltaMinimumMet) return 'Waiting for minimum 50 governors' 125 return 'Push this PR to a 7-day vote' 126 } 127 128 function WarningIcon({ className }: { className?: string }) { 129 return ( 130 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 131 <path 132 strokeLinecap="round" 133 strokeLinejoin="round" 134 strokeWidth={2} 135 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 136 /> 137 </svg> 138 ) 139 }