/ frontend / src / pages / VotesPage.tsx
VotesPage.tsx
  1  import { useEffect, useState } from 'react'
  2  import { Link } from 'react-router-dom'
  3  import { useVotesStore } from '../store/votes'
  4  import VoteProgress from '../components/voting/VoteProgress'
  5  import type { Chain, PRStatus } from '../types/vote'
  6  
  7  export default function VotesPage() {
  8    const { activeVotes, isLoadingVotes, loadActiveVotes } = useVotesStore()
  9    const [chainFilter, setChainFilter] = useState<Chain | 'all'>('all')
 10    const [statusFilter, setStatusFilter] = useState<PRStatus | 'all'>('voting')
 11  
 12    // Mock total governors for now - would come from chain
 13    const totalGovernors = 10
 14  
 15    useEffect(() => {
 16      loadActiveVotes(chainFilter === 'all' ? undefined : chainFilter)
 17    }, [chainFilter, loadActiveVotes])
 18  
 19    const filteredVotes = activeVotes.filter((pr) => {
 20      if (statusFilter !== 'all' && pr.status !== statusFilter) return false
 21      if (chainFilter !== 'all' && pr.chain !== chainFilter) return false
 22      return true
 23    })
 24  
 25    // Format deadline
 26    const formatDeadline = (deadline: number | null): string => {
 27      if (!deadline) return 'N/A'
 28      const date = new Date(deadline * 1000)
 29      return date.toLocaleDateString('en-US', {
 30        month: 'short',
 31        day: 'numeric',
 32        hour: '2-digit',
 33        minute: '2-digit',
 34      })
 35    }
 36  
 37    return (
 38      <div>
 39        <div className="flex items-center justify-between mb-6">
 40          <h1 className="text-2xl font-bold">Active Votes</h1>
 41  
 42          {/* Filters */}
 43          <div className="flex gap-4">
 44            <select
 45              value={chainFilter}
 46              onChange={(e) => setChainFilter(e.target.value as Chain | 'all')}
 47              className="px-3 py-2 border border-border-primary bg-bg-secondary text-text-primary rounded-lg text-sm"
 48            >
 49              <option value="all">All Chains</option>
 50              <option value="alpha">Alpha (Tech)</option>
 51              <option value="delta">Delta (Code)</option>
 52            </select>
 53  
 54            <select
 55              value={statusFilter}
 56              onChange={(e) => setStatusFilter(e.target.value as PRStatus | 'all')}
 57              className="px-3 py-2 border border-border-primary bg-bg-secondary text-text-primary rounded-lg text-sm"
 58            >
 59              <option value="voting">Active</option>
 60              <option value="passed">Passed</option>
 61              <option value="failed">Failed</option>
 62              <option value="draft">Draft</option>
 63              <option value="all">All</option>
 64            </select>
 65          </div>
 66        </div>
 67  
 68        {isLoadingVotes ? (
 69          <div className="flex justify-center py-12">
 70            <div className="animate-spin h-8 w-8 border-4 border-alpha-500 border-t-transparent rounded-full" />
 71          </div>
 72        ) : filteredVotes.length > 0 ? (
 73          <div className="space-y-4">
 74            {filteredVotes.map((pr) => (
 75              <div
 76                key={pr.prHash}
 77                className="bg-bg-secondary border border-border-primary rounded-lg p-6 hover:shadow-md transition-all"
 78              >
 79                <div className="flex items-start justify-between mb-4">
 80                  <div>
 81                    <div className="flex items-center gap-2 mb-1">
 82                      <span
 83                        className={`px-2 py-0.5 rounded text-xs font-medium ${
 84                          pr.chain === 'alpha'
 85                            ? 'bg-alpha-100 text-alpha-700'
 86                            : 'bg-delta-100 text-delta-700'
 87                        }`}
 88                      >
 89                        {pr.chain === 'alpha' ? 'Alpha' : 'Delta'}
 90                      </span>
 91                      <span
 92                        className={`px-2 py-0.5 rounded text-xs font-medium ${
 93                          pr.status === 'voting'
 94                            ? 'bg-blue-100 text-blue-700'
 95                            : pr.status === 'passed'
 96                            ? 'bg-green-100 text-green-700'
 97                            : pr.status === 'failed'
 98                            ? 'bg-red-100 text-red-700'
 99                            : 'bg-bg-tertiary text-text-secondary'
100                        }`}
101                      >
102                        {pr.status}
103                      </span>
104                      {pr.behaviorFlags > 0 && (
105                        <span className="px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">
106                          Flagged
107                        </span>
108                      )}
109                    </div>
110                    <h2 className="text-lg font-semibold">
111                      PR: {pr.prHash.slice(0, 16)}...
112                    </h2>
113                    <p className="text-sm text-text-tertiary font-mono">
114                      Repo: {pr.repoId.slice(0, 12)}...
115                    </p>
116                  </div>
117  
118                  <div className="text-right text-sm text-text-tertiary">
119                    <div>Submitter: {pr.submitter.slice(0, 10)}...</div>
120                    {pr.sponsor && <div>Sponsor: {pr.sponsor.slice(0, 10)}...</div>}
121                    {pr.voteDeadline && (
122                      <div>Deadline: {formatDeadline(pr.voteDeadline)}</div>
123                    )}
124                  </div>
125                </div>
126  
127                {/* Vote progress */}
128                <VoteProgress pr={pr} totalGovernors={totalGovernors} />
129  
130                {/* Action link */}
131                <div className="mt-4 flex justify-end">
132                  <Link
133                    to={`/votes/${pr.prHash}`}
134                    className="text-alpha-600 hover:text-alpha-700 text-sm font-medium"
135                  >
136                    View Details &rarr;
137                  </Link>
138                </div>
139              </div>
140            ))}
141          </div>
142        ) : (
143          <div className="text-center py-12 text-text-tertiary">
144            <p>No votes found matching your filters.</p>
145            <p className="text-sm mt-2">
146              {statusFilter === 'voting'
147                ? 'No active votes at the moment.'
148                : 'Try changing the filters.'}
149            </p>
150          </div>
151        )}
152      </div>
153    )
154  }