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