/ frontend / src / components / emergency / DEQMemberList.tsx
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  }