/ frontend / src / pages / VoteDetailPage.tsx
VoteDetailPage.tsx
  1  import { useParams, Link } from 'react-router-dom'
  2  import { useQuery } from '@tanstack/react-query'
  3  import VotePanel from '../components/voting/VotePanel'
  4  import SponsorButton from '../components/voting/SponsorButton'
  5  import ProposalLink from '../components/voting/ProposalLink'
  6  import CooldownStatus from '../components/voting/CooldownStatus'
  7  import type { PullRequest } from '../types/vote'
  8  
  9  async function fetchPR(prHash: string): Promise<PullRequest | null> {
 10    const res = await fetch(`/api/cache/prs/${prHash}`)
 11    if (!res.ok) return null
 12    return res.json()
 13  }
 14  
 15  async function fetchGovernorCount(chain: string): Promise<number> {
 16    try {
 17      const res = await fetch(`/api/cache/governors?chain=${chain}`)
 18      if (!res.ok) return 10 // Default
 19      const governors = await res.json()
 20      return governors.length || 10
 21    } catch {
 22      return 10
 23    }
 24  }
 25  
 26  export default function VoteDetailPage() {
 27    const { prHash } = useParams<{ prHash: string }>()
 28  
 29    const { data: pr, isLoading, error } = useQuery({
 30      queryKey: ['pr', prHash],
 31      queryFn: () => fetchPR(prHash!),
 32      enabled: !!prHash,
 33    })
 34  
 35    const { data: governorCount = 10 } = useQuery({
 36      queryKey: ['governorCount', pr?.chain],
 37      queryFn: () => fetchGovernorCount(pr!.chain),
 38      enabled: !!pr,
 39    })
 40  
 41    if (isLoading) {
 42      return (
 43        <div className="flex justify-center py-12">
 44          <div className="animate-spin h-8 w-8 border-4 border-alpha-500 border-t-transparent rounded-full" />
 45        </div>
 46      )
 47    }
 48  
 49    if (error || !pr) {
 50      return (
 51        <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
 52          PR not found or failed to load.
 53        </div>
 54      )
 55    }
 56  
 57    return (
 58      <div className="max-w-4xl mx-auto">
 59        <div className="mb-6">
 60          <Link to="/votes" className="text-alpha-600 hover:underline text-sm">
 61            &larr; Back to votes
 62          </Link>
 63        </div>
 64  
 65        {/* PR Header */}
 66        <div className="bg-white border rounded-lg p-6 mb-6">
 67          <div className="flex items-start justify-between mb-4">
 68            <div>
 69              <div className="flex items-center gap-2 mb-2">
 70                <span
 71                  className={`px-2 py-1 rounded text-xs font-medium ${
 72                    pr.chain === 'alpha'
 73                      ? 'bg-alpha-100 text-alpha-700'
 74                      : 'bg-delta-100 text-delta-700'
 75                  }`}
 76                >
 77                  {pr.chain === 'alpha' ? 'Alpha Tech' : 'Delta Code'}
 78                </span>
 79                <span
 80                  className={`px-2 py-1 rounded text-xs font-medium ${
 81                    pr.status === 'voting'
 82                      ? 'bg-blue-100 text-blue-700'
 83                      : pr.status === 'passed'
 84                      ? 'bg-green-100 text-green-700'
 85                      : pr.status === 'failed'
 86                      ? 'bg-red-100 text-red-700'
 87                      : 'bg-gray-100 text-gray-700'
 88                  }`}
 89                >
 90                  {pr.status.toUpperCase()}
 91                </span>
 92              </div>
 93              <h1 className="text-2xl font-bold mb-2">Pull Request</h1>
 94              <p className="text-sm text-gray-500 font-mono">{pr.prHash}</p>
 95            </div>
 96          </div>
 97  
 98          <dl className="grid grid-cols-2 gap-4 text-sm">
 99            <div>
100              <dt className="text-gray-500">Commit Hash</dt>
101              <dd className="font-mono">{pr.commitHash.slice(0, 16)}...</dd>
102            </div>
103            <div>
104              <dt className="text-gray-500">Repository</dt>
105              <dd className="font-mono">{pr.repoId.slice(0, 16)}...</dd>
106            </div>
107            <div>
108              <dt className="text-gray-500">Submitter</dt>
109              <dd className="font-mono">{pr.submitter}</dd>
110            </div>
111            <div>
112              <dt className="text-gray-500">Sponsor</dt>
113              <dd className="font-mono">{pr.sponsor || 'Not sponsored'}</dd>
114            </div>
115            {pr.sponsoredAt && (
116              <div>
117                <dt className="text-gray-500">Sponsored At</dt>
118                <dd>{new Date(pr.sponsoredAt * 1000).toLocaleString()}</dd>
119              </div>
120            )}
121            {pr.voteDeadline && (
122              <div>
123                <dt className="text-gray-500">Vote Deadline</dt>
124                <dd>{new Date(pr.voteDeadline * 1000).toLocaleString()}</dd>
125              </div>
126            )}
127          </dl>
128        </div>
129  
130        {/* Sponsor or Vote Panel */}
131        {pr.status === 'draft' ? (
132          <div className="bg-white border rounded-lg p-6">
133            <h2 className="text-lg font-semibold mb-4">Sponsor This PR</h2>
134            <p className="text-gray-600 mb-4">
135              This PR is waiting for a governor to sponsor it for voting.
136              Sponsoring will start a 7-day voting period.
137            </p>
138            <SponsorButton pr={pr} />
139          </div>
140        ) : (
141          <VotePanel pr={pr} totalGovernors={governorCount} />
142        )}
143  
144        {/* Governance Proposal Link */}
145        {(pr.status === 'passed' || pr.status === 'proposal_created' || pr.status === 'executed') && (
146          <div className="mt-6">
147            <ProposalLink pr={pr} />
148          </div>
149        )}
150  
151        {/* Resubmission Cooldown Status */}
152        {pr.status === 'failed' && (
153          <CooldownStatus
154            commitHash={pr.commitHash}
155            chain={pr.chain}
156            className="mt-6"
157          />
158        )}
159  
160        {/* Behavior flags */}
161        {pr.behaviorFlags > 0 && (
162          <div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
163            <h3 className="font-semibold text-yellow-800 mb-2">Flagged Concerns</h3>
164            <ul className="text-sm text-yellow-700 space-y-1">
165              {(pr.behaviorFlags & 0b001) !== 0 && (
166                <li>Security concern flagged</li>
167              )}
168              {(pr.behaviorFlags & 0b010) !== 0 && (
169                <li>Political concern flagged</li>
170              )}
171              {(pr.behaviorFlags & 0b100) !== 0 && (
172                <li>Economic concern flagged</li>
173              )}
174            </ul>
175          </div>
176        )}
177  
178        {/* Shared repo indicator */}
179        {pr.isSharedRepo && (
180          <div className="mt-6 bg-gradient-to-r from-alpha-50 to-delta-50 border border-gray-200 rounded-lg p-4">
181            <div className="flex items-center gap-2">
182              <span className="text-sm font-medium text-gray-700">Shared Repository</span>
183              <span className="text-xs text-gray-500">
184                Requires approval from both Alpha Tech and Delta Code Governors
185              </span>
186            </div>
187          </div>
188        )}
189      </div>
190    )
191  }