/ frontend / src / components / voting / SponsorButton.tsx
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  }