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 ← 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 }