DEQMemberList.tsx
1 import { useQuery } from '@tanstack/react-query' 2 import type { Chain } from '../../types/vote' 3 import type { DEQMember } from '../../types/emergency' 4 import * as emergencyService from '../../services/emergency' 5 6 interface DEQMemberListProps { 7 chain: Chain 8 } 9 10 export default function DEQMemberList({ chain }: DEQMemberListProps) { 11 const { data: members = [], isLoading } = useQuery({ 12 queryKey: ['deqMembers', chain], 13 queryFn: () => emergencyService.getDEQMembers(chain), 14 }) 15 16 const { data: config } = useQuery({ 17 queryKey: ['emergencyConfig', chain], 18 queryFn: () => emergencyService.getEmergencyConfig(chain), 19 }) 20 21 const activeMembers = members.filter((m) => m.status === 'active') 22 const backupMembers = members.filter((m) => m.role === 'backup') 23 24 if (isLoading) { 25 return ( 26 <div className="bg-white border rounded-lg p-6"> 27 <div className="flex justify-center"> 28 <div className="animate-spin h-6 w-6 border-2 border-gray-400 border-t-transparent rounded-full" /> 29 </div> 30 </div> 31 ) 32 } 33 34 return ( 35 <div className="bg-white border rounded-lg"> 36 <div className="p-4 border-b"> 37 <h3 className="font-medium">DEQ Members</h3> 38 <p className="text-sm text-gray-600 mt-1"> 39 Delegated Emergency Quorum - {config?.requiredSignatures || 3} of{' '} 40 {config?.totalDEQMembers || 5} required for emergency actions 41 </p> 42 </div> 43 44 {/* Summary */} 45 <div className="p-4 bg-gray-50 border-b"> 46 <div className="flex items-center gap-6"> 47 <div> 48 <div className="text-2xl font-bold text-green-600"> 49 {activeMembers.length} 50 </div> 51 <div className="text-xs text-gray-500">Active</div> 52 </div> 53 <div> 54 <div className="text-2xl font-bold text-blue-600"> 55 {backupMembers.length} 56 </div> 57 <div className="text-xs text-gray-500">Backup</div> 58 </div> 59 <div> 60 <div className="text-2xl font-bold text-gray-600"> 61 {config?.requiredSignatures || 3} 62 </div> 63 <div className="text-xs text-gray-500">Required Sigs</div> 64 </div> 65 </div> 66 </div> 67 68 {/* Member List */} 69 <div className="divide-y"> 70 {members.length === 0 ? ( 71 <div className="p-4 text-center text-gray-500"> 72 No DEQ members found for {chain} chain. 73 </div> 74 ) : ( 75 members.map((member) => ( 76 <DEQMemberCard key={member.address} member={member} /> 77 )) 78 )} 79 </div> 80 81 {/* Info */} 82 <div className="p-4 bg-gray-50 border-t text-sm text-gray-600"> 83 <h4 className="font-medium text-gray-800 mb-2">About DEQ</h4> 84 <ul className="space-y-1"> 85 <li>• DEQ members are nominated by governance votes</li> 86 <li>• Primary members are the first signers for emergency actions</li> 87 <li>• Backup members can sign if primary members are unavailable</li> 88 <li>• Members can be revoked by governance if they act maliciously</li> 89 </ul> 90 </div> 91 </div> 92 ) 93 } 94 95 function DEQMemberCard({ member }: { member: DEQMember }) { 96 const statusColors = { 97 active: 'bg-green-100 text-green-700', 98 revoked: 'bg-red-100 text-red-700', 99 expired: 'bg-gray-100 text-gray-700', 100 } 101 102 const roleColors = { 103 primary: 'bg-blue-100 text-blue-700', 104 backup: 'bg-gray-100 text-gray-600', 105 } 106 107 return ( 108 <div className="p-4"> 109 <div className="flex items-center justify-between"> 110 <div className="flex items-center gap-3"> 111 <div 112 className={`w-10 h-10 rounded-full flex items-center justify-center ${ 113 member.status === 'active' ? 'bg-green-100' : 'bg-gray-100' 114 }`} 115 > 116 <KeyIcon 117 className={`h-5 w-5 ${ 118 member.status === 'active' ? 'text-green-600' : 'text-gray-400' 119 }`} 120 /> 121 </div> 122 <div> 123 <div className="font-mono text-sm"> 124 {member.address.slice(0, 12)}...{member.address.slice(-8)} 125 </div> 126 <div className="text-xs text-gray-500"> 127 Added {new Date(member.addedAt * 1000).toLocaleDateString()} 128 </div> 129 </div> 130 </div> 131 132 <div className="flex items-center gap-2"> 133 <span className={`text-xs px-2 py-0.5 rounded ${roleColors[member.role]}`}> 134 {member.role} 135 </span> 136 <span className={`text-xs px-2 py-0.5 rounded ${statusColors[member.status]}`}> 137 {member.status} 138 </span> 139 </div> 140 </div> 141 142 {/* Public Key (truncated) */} 143 <div className="mt-2 text-xs text-gray-500"> 144 Public Key: {member.publicKey.slice(0, 20)}... 145 </div> 146 147 {/* Expiration */} 148 {member.expiresAt && ( 149 <div className="mt-1 text-xs text-yellow-600"> 150 Expires: {new Date(member.expiresAt * 1000).toLocaleDateString()} 151 </div> 152 )} 153 154 {/* Governance link */} 155 <div className="mt-2 text-xs"> 156 <span className="text-gray-500">Nominated by proposal: </span> 157 <a href="#" className="text-blue-600 hover:underline"> 158 {member.addedBy} 159 </a> 160 </div> 161 </div> 162 ) 163 } 164 165 function KeyIcon({ className }: { className?: string }) { 166 return ( 167 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 168 <path 169 strokeLinecap="round" 170 strokeLinejoin="round" 171 strokeWidth={2} 172 d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" 173 /> 174 </svg> 175 ) 176 } 177 178 /** 179 * Compact badge showing DEQ member count 180 */ 181 export function DEQStatusBadge({ chain }: { chain: Chain }) { 182 const { data: count = 0 } = useQuery({ 183 queryKey: ['activeDEQCount', chain], 184 queryFn: () => emergencyService.getActiveDEQCount(chain), 185 }) 186 187 const { data: config } = useQuery({ 188 queryKey: ['emergencyConfig', chain], 189 queryFn: () => emergencyService.getEmergencyConfig(chain), 190 }) 191 192 const required = config?.requiredSignatures || 3 193 194 return ( 195 <div 196 className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs ${ 197 count >= required ? 'bg-green-50 text-green-700' : 'bg-yellow-50 text-yellow-700' 198 }`} 199 > 200 <KeyIcon className="h-3.5 w-3.5" /> 201 <span> 202 DEQ: {count}/{config?.totalDEQMembers || 5} 203 </span> 204 </div> 205 ) 206 }