From cf13fa3073752463a0c9c4ac8884ad8328b0620d Mon Sep 17 00:00:00 2001 From: MrX-SNX Date: Fri, 6 Sep 2024 11:07:14 +0100 Subject: [PATCH] Tweaks for voting (#441) * wip * feat: implemeted voting and ranking * ref: sort func * ref: clean up * ref: more clean up * fix: epoch query --- .../cypress/cypress/tasks/changePeriod.js | 1 + .../CouncilInformation/CouncilInformation.tsx | 18 +- .../CouncilMembers/CouncilMembers.tsx | 32 +-- .../CouncilNominees/CouncilNominees.tsx | 76 ++---- .../UserTableView/UserTableView.tsx | 71 ++---- governance/ui/src/queries/useGetEpochIndex.ts | 2 +- .../ui/src/queries/useGetHistoricalVotes.ts | 236 +++++++++++++++--- .../src/queries/useGetNextElectionSettings.ts | 2 +- .../ui/src/queries/useGetUserDetailsQuery.ts | 5 +- governance/ui/src/utils/sort-users.ts | 103 ++++++++ governance/ui/src/utils/table-border.ts | 33 +++ governance/ui/webpack.config.js | 2 +- 12 files changed, 402 insertions(+), 179 deletions(-) create mode 100644 governance/ui/src/utils/sort-users.ts create mode 100644 governance/ui/src/utils/table-border.ts diff --git a/governance/cypress/cypress/tasks/changePeriod.js b/governance/cypress/cypress/tasks/changePeriod.js index ad95e98d1..9b8fe9812 100644 --- a/governance/cypress/cypress/tasks/changePeriod.js +++ b/governance/cypress/cypress/tasks/changePeriod.js @@ -16,6 +16,7 @@ export async function changePeriod({ council, period }) { const electionId = await proxy.spartan.connect(signer).getEpochIndex(); if (period === 'admin') { + console.log(block.timestamp); await proxy[council] .connect(signer) .Epoch_setEpochDates( diff --git a/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx b/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx index c13eb61ef..522e38c29 100644 --- a/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx +++ b/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx @@ -1,4 +1,4 @@ -import { Button, Flex, Heading, Icon, Text } from '@chakra-ui/react'; +import { Flex, Heading, Icon, Text } from '@chakra-ui/react'; import councils, { CouncilSlugs } from '../../utils/councils'; import { CouncilImage } from '../CouncilImage'; import { Link } from 'react-router-dom'; @@ -56,8 +56,16 @@ export default function CouncilInformation({ activeCouncil }: { activeCouncil: C > Stipends: {council?.stipends}/month - - - + + diff --git a/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx b/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx index d8fa06fb3..3bad09517 100644 --- a/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx +++ b/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx @@ -20,6 +20,7 @@ import { ArrowUpDownIcon } from '@chakra-ui/icons'; import SortArrows from '../SortArrows/SortArrows'; import { useGetCouncilMembers, useGetUserDetailsQuery } from '../../queries'; import TableLoading from '../TableLoading/TableLoading'; +import { sortUsers } from '../../utils/sort-users'; export default function CouncilMembers({ activeCouncil }: { activeCouncil: CouncilSlugs }) { const [sortConfig, setSortConfig] = useState<[boolean, string]>([false, 'start']); @@ -34,29 +35,8 @@ export default function CouncilMembers({ activeCouncil }: { activeCouncil: Counc const nextEpoch = calculateNextEpoch(councilSchedule); const sortedNominees = useMemo(() => { - // TODO @dev - // Sort user by voting power and add a place key to the object - if (!!councilMemberDetails?.length) { - if (sortConfig[1] === 'ranking') { - return councilMemberDetails.reverse(); - } - if (sortConfig[1] === 'name') { - return councilMemberDetails.sort((a, b) => { - if (a.username && b.username) { - return sortConfig[0] - ? a.username.localeCompare(b.username) - : a.username.localeCompare(b.username) * -1; - } else { - return sortConfig[0] - ? a?.address.localeCompare(b.address) - : a?.address.localeCompare(b.address) * -1; - } - }); - } - return councilMemberDetails; - } - return []; - }, [sortConfig, councilMemberDetails]); + return sortUsers(activeCouncil, '', sortConfig, councilMemberDetails); + }, [sortConfig, councilMemberDetails, activeCouncil]); return ( { setSortConfig([!sortConfig[0], 'votes']); - // sortedNominees = sortedNominees.sort((a, b) => { - // TODO implement sorting for most votes when subgraph is ready - // }); }} > Votes {sortConfig[1] === 'votes' && } @@ -146,9 +123,6 @@ export default function CouncilMembers({ activeCouncil }: { activeCouncil: Counc px="6" onClick={() => { setSortConfig([!sortConfig[0], 'votingPower']); - // sortedNominees = sortedNominees.sort((a, b) => { - // TODO implement sorting for most votes when subgraph is ready - // }); }} > Voting Power{' '} diff --git a/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx b/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx index c0bb23ef2..4a6715258 100644 --- a/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx +++ b/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx @@ -23,7 +23,6 @@ import UserTableView from '../UserTableView/UserTableView'; import { useGetNomineesDetails } from '../../queries/useGetNomineesDetails'; import { useGetCurrentPeriod } from '../../queries/useGetCurrentPeriod'; import { useMemo, useState } from 'react'; -import { utils } from 'ethers'; import SortArrows from '../SortArrows/SortArrows'; import { useNetwork, useWallet } from '../../queries/useWallet'; import { CouncilImage } from '../CouncilImage'; @@ -32,6 +31,7 @@ import { CloseIcon } from '@chakra-ui/icons'; import { useVoteContext } from '../../context/VoteContext'; import { useGetEpochIndex, useGetHistoricalVotes } from '../../queries'; import { getVoteSelectionState } from '../../utils/localstorage'; +import { sortUsers } from '../../utils/sort-users'; export default function CouncilNominees({ activeCouncil }: { activeCouncil: CouncilSlugs }) { const [search, setSearch] = useState(''); @@ -55,53 +55,10 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun ); const council = councils.find((council) => council.slug === activeCouncil); const epoch = calculateNextEpoch(councilSchedule, nextEpochDuration); - const votesForCouncil = votes && votes[activeCouncil]; const sortedNominees = useMemo(() => { - if (councilNomineesDetails?.length && votesForCouncil) { - return councilNomineesDetails - .map((nominee) => { - const vote = votesForCouncil.find( - (vote) => - epochId === vote.id && vote.voter.toLowerCase() === nominee.address.toLowerCase() - ); - if (vote) return (nominee = { ...nominee, vote }); - return nominee; - }) - .filter((nominee) => { - if (utils.isAddress(search)) { - return nominee.address.toLowerCase().includes(search); - } - if (search) { - if (nominee.username) { - return nominee.username.toLowerCase().includes(search); - } else { - return nominee.address.toLowerCase().includes(search); - } - } - return true; - }) - .sort((a, b) => { - if (sortConfig[1] === 'name') { - if (a.username && b.username) { - return sortConfig[0] - ? a.username.localeCompare(b.username) - : a.username.localeCompare(b.username) * -1; - } - if (a.username && !b.username) { - return -1; - } else if (b.username && !a.username) { - return 1; - } - return sortConfig[0] - ? a.address.localeCompare(b.address) - : a.address.localeCompare(b.address) * -1; - } - return 0; - }); - } - return []; - }, [search, councilNomineesDetails, sortConfig, votesForCouncil, epochId]); + return sortUsers(activeCouncil, search, sortConfig, councilNomineesDetails, votes); + }, [search, councilNomineesDetails, sortConfig, activeCouncil, votes]); return ( { - setSortConfig([!sortConfig[0], 'ranking']); - // sortedNominees = sortedNominees.sort((a, b) => { - // TODO implement sorting for most votes when subgraph is ready - // }); + setSortConfig([true, 'ranking']); }} > N°{' '} @@ -232,9 +186,6 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun pl="6" onClick={() => { setSortConfig([!sortConfig[0], 'votes']); - // sortedNominees = sortedNominees.sort((a, b) => { - // TODO implement sorting for most votes when subgraph is ready - // }); }} > Votes {sortConfig[1] === 'votes' && } @@ -248,9 +199,6 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun pl="6" onClick={() => { setSortConfig([!sortConfig[0], 'votingPower']); - // sortedNominees = sortedNominees.sort((a, b) => { - // TODO implement sorting for most votes when subgraph is ready - // }); }} > Voting Power{' '} @@ -264,9 +212,9 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun {!!sortedNominees?.length ? ( - sortedNominees.map((councilNominee, index) => ( + sortedNominees.map((councilNominee) => ( )) ) : isLoading ? ( @@ -289,3 +238,14 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun ); } + +function totalVotingPowerForCouncil(council: CouncilSlugs) { + switch (council) { + case 'spartan': + return 'totalVotingPowerSpartan'; + case 'ambassador': + return 'totalVotingPowerAmbassador'; + case 'treasury': + return 'totalVotingPowerTreasury'; + } +} diff --git a/governance/ui/src/components/UserTableView/UserTableView.tsx b/governance/ui/src/components/UserTableView/UserTableView.tsx index eef5cef5c..71b52d192 100644 --- a/governance/ui/src/components/UserTableView/UserTableView.tsx +++ b/governance/ui/src/components/UserTableView/UserTableView.tsx @@ -9,52 +9,22 @@ import { prettyString } from '@snx-v3/format'; import { useGetEpochIndex, useGetUserBallot, useNetwork } from '../../queries'; import { getVoteSelectionState } from '../../utils/localstorage'; import { useVoteContext } from '../../context/VoteContext'; - -function renderCorrectBorder( - column: 'place' | 'name' | 'votes' | 'power' | 'badge', - position: 'left' | 'right' | 'bottom', - period: string | undefined, - isSelected: boolean -) { - if (column === 'place') { - if (position === 'left') { - return isSelected ? '1px solid' : ''; - } else if (position === 'bottom') { - return isSelected ? '1px solid' : ''; - } - } else if (column === 'name') { - if (position === 'left') { - if (period === '2') { - return ''; - } - if (period === '0') { - return ''; - } - return isSelected ? '1px solid' : ''; - } else if (position === 'bottom') { - return isSelected ? '1px solid' : ''; - } - } else if (column === 'votes') { - if (position === 'bottom') return isSelected ? '1px solid' : ''; - } else if (column === 'power') { - if (position === 'bottom') return isSelected ? '1px solid' : ''; - } else if (column === 'badge') { - if (position === 'bottom') return isSelected ? '1px solid' : ''; - if (position === 'right') return isSelected ? '1px solid' : ''; - } -} +import { BigNumber } from 'ethers'; +import { renderCorrectBorder } from '../../utils/table-border'; export default function UserTableView({ user, activeCouncil, place, isSelectedForVoting, + totalVotingPower, }: { isSelectedForVoting?: boolean; - place: number; + place?: number; user: GetUserDetails; activeCouncil: CouncilSlugs; isNomination?: boolean; + totalVotingPower?: BigNumber; }) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -72,6 +42,9 @@ export default function UserTableView({ activeCouncil ); const voteAddressState = typeof networkForState === 'string' ? networkForState : ''; + const totalVotingPowerPercentage = totalVotingPower + ? user.voteResult?.votePower.mul(100).div(totalVotingPower).toNumber().toFixed(2) + : 'N/A'; return ( - {place < 10 ? ( + {place === undefined ? ( + '-' + ) : place < + (activeCouncil === 'spartan' ? 8 : activeCouncil === 'ambassador' ? 5 : 4) ? ( - {place + 1} + {place} ) : ( @@ -139,7 +115,7 @@ export default function UserTableView({ fontSize="sm" fontWeight={700} > - 0 + {user.voteResult?.votesReceived || 0} )} {councilIsInAdminOrVoting && ( @@ -151,7 +127,10 @@ export default function UserTableView({ fontSize="sm" fontWeight={700} > - 0 + {totalVotingPowerPercentage ? totalVotingPowerPercentage + '%' : 'N/A'} + + {totalVotingPowerPercentage ? user.voteResult?.votePower.toString() : '—'} + )} {councilPeriod === '2' && ( @@ -203,20 +182,6 @@ export default function UserTableView({ )} - {councilPeriod === '0' && ( - - - 0 - - - )} ); } diff --git a/governance/ui/src/queries/useGetEpochIndex.ts b/governance/ui/src/queries/useGetEpochIndex.ts index 14663043f..e2158b874 100644 --- a/governance/ui/src/queries/useGetEpochIndex.ts +++ b/governance/ui/src/queries/useGetEpochIndex.ts @@ -11,7 +11,7 @@ export function useGetEpochIndex(council: CouncilSlugs) { queryKey: ['epochId', council, network?.id], queryFn: async () => { return await getCouncilContract(council) - .connect(motherShipProvider(network?.id || 13001)) + .connect(motherShipProvider(network?.id || 2192)) .getEpochIndex(); }, staleTime: 900000, diff --git a/governance/ui/src/queries/useGetHistoricalVotes.ts b/governance/ui/src/queries/useGetHistoricalVotes.ts index 9445234dc..ed74c6d79 100644 --- a/governance/ui/src/queries/useGetHistoricalVotes.ts +++ b/governance/ui/src/queries/useGetHistoricalVotes.ts @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query'; -import { useNetwork } from './useWallet'; import councils, { CouncilSlugs } from '../utils/councils'; +import { BigNumber } from 'ethers'; -const testnetURL = 'https://api.synthetix.io/v3/snax-testnet/votes'; +// const testnetURL = 'https://api.synthetix.io/v3/snax-testnet/votes'; const mainnetURL = 'https://api.synthetix.io/v3/snax/votes'; -export interface Vote { +interface VoteRaw { eventName: string; chainId: number; epochId: number; @@ -13,40 +13,218 @@ export interface Vote { blockTimestamp: number; id: string; transactionHash: string; - votingPower: string; + votingPower?: string; blockNumber: number; - candidates: string[]; + candidates?: string[]; contract: string; } +interface VoteParsed { + eventName: string; + chainId: number; + epochId: number; + voter: string; + blockTimestamp: number; + id: string; + transactionHash: string; + votingPower: BigNumber; + blockNumber: number; + candidates?: string[]; + contract: string; +} + +export interface Candidate { + votesReceived: number; + votePower: BigNumber; + candidate: string; +} + +export interface VotesResult { + spartan: Record; + ambassador: Record; + treasury: Record; + totalVotingPowerSpartan: BigNumber; + totalVotingPowerAmbassador: BigNumber; + totalVotingPowerTreasury: BigNumber; +} + export function useGetHistoricalVotes() { - const { network } = useNetwork(); return useQuery({ - queryKey: ['historical-votes', network?.id], + queryKey: ['historical-votes'], queryFn: async () => { - try { - const res = await fetch(network?.id !== 2192 ? mainnetURL : testnetURL); - let votesRaw: Record = await res.json(); - if (!('spartan' in votesRaw)) { - votesRaw = { spartan: votesRaw[0], ambassador: [], treasury: [] }; - } - return councils.reduce( - (cur, next) => { - cur[next.slug] = votesRaw[next.slug].sort((a, b) => { - if (a.epochId === b.epochId) { - return a.blockTimestamp - b.blockTimestamp; - } - return a.epochId - b.epochId; - }); - return cur; - }, - {} as Record - ); - } catch (err) { - console.error(err); - return { spartan: [], ambassador: [], treasury: [] } as Record; - } + const res = await fetch(mainnetURL); + const votesRaw: Record = await res.json(); + // TODO @dev remove for prod + // if (window.location.host.includes('localhost')) + // votesRaw.spartan = votesRaw.spartan.concat(fakeData); + const sortedEvents = councils.reduce( + (cur, next) => { + cur[next.slug] = votesRaw[next.slug].sort((a, b) => { + if (a.epochId === b.epochId) { + return a.blockTimestamp - b.blockTimestamp; + } + return a.epochId - b.epochId; + }); + return cur; + }, + {} as Record + ); + const spartan = parseVotes(sortedEvents.spartan); + const ambassador = parseVotes(sortedEvents.ambassador); + const treasury = parseVotes(sortedEvents.treasury); + + return { + spartan, + ambassador, + treasury, + totalVotingPowerSpartan: Object.keys(spartan).reduce( + (cur, candidate) => cur.add(spartan[candidate].votePower), + BigNumber.from(0) + ), + totalVotingPowerAmbassador: Object.keys(ambassador).reduce( + (cur, candidate) => cur.add(ambassador[candidate].votePower), + BigNumber.from(0) + ), + totalVotingPowerTreasury: Object.keys(treasury).reduce( + (cur, candidate) => cur.add(treasury[candidate].votePower), + BigNumber.from(0) + ), + }; }, staleTime: 900000, }); } + +const parseVotes = (votes: VoteRaw[]) => { + return votes + .reduce((cur, next) => { + if (next.eventName === 'VoteWithdrawn') { + const previousVoteIndex = cur.findIndex((vote) => { + return vote.contract === next.contract && vote.voter === next.voter; + }); + if (previousVoteIndex !== -1) { + cur.splice(previousVoteIndex, 1); + } else { + console.error('Could not find previous vote', cur[next.epochId], next); + } + } else { + const previousVoteIndex = cur.findIndex( + (vote) => vote.contract === next.contract && vote.voter === next.voter + ); + if (previousVoteIndex !== -1) { + cur.splice(previousVoteIndex, 1); + } + cur.push({ ...next, votingPower: BigNumber.from(next.votingPower) }); + } + + return cur; + }, [] as VoteParsed[]) + .reduce( + (cur, next) => { + const candidate = next.candidates ? next.candidates[0].toLowerCase() : ''; + if (cur[candidate]) { + cur[candidate] = { + ...cur, + votePower: cur[candidate].votePower.add(next.votingPower), + votesReceived: cur[candidate].votesReceived + 1, + candidate, + }; + } else { + cur[candidate] = { candidate, votePower: next.votingPower, votesReceived: 1 }; + } + + return cur; + }, + {} as Record + ); +}; + +// const fakeData: VoteRaw[] = [ +// { +// eventName: 'VoteRecorded', +// chainId: 2192, +// epochId: 0, +// voter: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// votingPower: '10', +// blockNumber: 595468, +// candidates: ['0xc3Cf311e04c1f8C74eCF6a795Ae760dc6312F345'], +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteRecorded', +// chainId: 2192, +// epochId: 0, +// voter: '0x47872B16557875850a02C94B28d959515F894913', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// votingPower: '10', +// blockNumber: 595469, +// candidates: ['0xc3Cf311e04c1f8C74eCF6a795Ae760dc6312F345'], +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteWithdrawn', +// chainId: 2192, +// epochId: 0, +// voter: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// blockNumber: 5954610, +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteWithdrawn', +// chainId: 2192, +// epochId: 0, +// voter: '0x47872B16557875850a02C94B28d959515F894913', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// blockNumber: 595469, +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteRecorded', +// chainId: 2192, +// epochId: 0, +// voter: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// votingPower: '10', +// blockNumber: 595468, +// candidates: ['0x98Ab20307fdABa1ce8b16d69d22461c6dbe85459'], +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteRecorded', +// chainId: 2192, +// epochId: 0, +// voter: '0x47872B16557875850a02C94B28d959515F894913', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// votingPower: '10', +// blockNumber: 595469, +// candidates: ['0x8F876c97AD1090004dC28294237003e571C76Ba7'], +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// { +// eventName: 'VoteRecorded', +// chainId: 2192, +// epochId: 0, +// voter: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', +// blockTimestamp: 1724766356000, +// id: '0000595468-a8656-000000', +// transactionHash: '0x3a995d6f45cd0f00c1a24aa4f9e4c816d39056c9dcb2ab61f9a864d6880b058b', +// votingPower: '10', +// blockNumber: 595499, +// candidates: ['0x8F876c97AD1090004dC28294237003e571C76Ba7'], +// contract: '0xbc85f11300a8ef619592fd678418ec4ef26fbdfd', +// }, +// ]; diff --git a/governance/ui/src/queries/useGetNextElectionSettings.ts b/governance/ui/src/queries/useGetNextElectionSettings.ts index 57375d1b1..3729beee3 100644 --- a/governance/ui/src/queries/useGetNextElectionSettings.ts +++ b/governance/ui/src/queries/useGetNextElectionSettings.ts @@ -10,7 +10,7 @@ export function useGetNextElectionSettings(council: CouncilSlugs) { queryKey: ['next-epoch-settings', council], queryFn: async () => { const schedule = await getCouncilContract(council) - .connect(motherShipProvider(network?.id || 13001)) + .connect(motherShipProvider(network?.id || 2192)) .getNextElectionSettings(); return Number(schedule.epochDuration.toString()) as number | undefined; }, diff --git a/governance/ui/src/queries/useGetUserDetailsQuery.ts b/governance/ui/src/queries/useGetUserDetailsQuery.ts index 226bb353a..9c6d478c9 100644 --- a/governance/ui/src/queries/useGetUserDetailsQuery.ts +++ b/governance/ui/src/queries/useGetUserDetailsQuery.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { motherShipProvider } from '../utils/providers'; import { profileContract } from '../utils/contracts'; import { utils } from 'ethers'; -import { Vote } from './useGetHistoricalVotes'; +import { Candidate } from './useGetHistoricalVotes'; export type GetUserDetails = { address: string; @@ -26,7 +26,8 @@ export type GetUserDetails = { delegationPitch: string; github: string; council?: string; - vote?: Vote; + voteResult?: Candidate; + place?: number; }; type UserPitch = { diff --git a/governance/ui/src/utils/sort-users.ts b/governance/ui/src/utils/sort-users.ts new file mode 100644 index 000000000..6ea98e1c4 --- /dev/null +++ b/governance/ui/src/utils/sort-users.ts @@ -0,0 +1,103 @@ +import { BigNumber, utils } from 'ethers'; +import { Candidate, GetUserDetails } from '../queries'; +import { CouncilSlugs } from './councils'; + +interface Votes { + spartan: Record; + ambassador: Record; + treasury: Record; + totalVotingPowerSpartan: BigNumber; + totalVotingPowerAmbassador: BigNumber; + totalVotingPowerTreasury: BigNumber; +} + +export const sortUsers = ( + activeCouncil: CouncilSlugs, + search: string, + sortConfig: [boolean, string], + councilNomineesDetails?: GetUserDetails[], + votes?: Votes +) => { + if (councilNomineesDetails?.length && votes) { + const candidatesByVotePowerRanking = votes + ? Object.keys(votes[activeCouncil]).sort((a, b) => { + return votes[activeCouncil][b].votePower + .sub(votes[activeCouncil][a].votePower) + .toNumber(); + }) + : []; + return councilNomineesDetails + .map((nominee) => { + if (votes[activeCouncil][nominee.address.toLowerCase()]) { + return { + ...nominee, + voteResult: votes[activeCouncil][nominee.address.toLowerCase()], + place: + candidatesByVotePowerRanking.findIndex( + (candidate) => candidate.toLowerCase() === nominee.address.toLowerCase() + ) + 1, + }; + } + return nominee; + }) + .filter((nominee) => { + if (utils.isAddress(search)) { + return nominee.address.toLowerCase().includes(search); + } + if (search) { + if (nominee.username) { + return nominee.username.toLowerCase().includes(search); + } else { + return nominee.address.toLowerCase().includes(search); + } + } + return true; + }) + .sort((a, b) => { + if (sortConfig[1] === 'name') { + if (a.username && b.username) { + return sortConfig[0] + ? a.username.localeCompare(b.username) + : a.username.localeCompare(b.username) * -1; + } + if (a.username && !b.username) { + return -1; + } else if (b.username && !a.username) { + return 1; + } + return sortConfig[0] + ? a.address.localeCompare(b.address) + : a.address.localeCompare(b.address) * -1; + } + if (sortConfig[1] === 'votingPower') { + if (sortConfig[0]) { + if (!b.voteResult?.votePower) return -1; + return b.voteResult?.votePower + .sub(a.voteResult?.votePower || BigNumber.from(0)) + .toNumber(); + } else { + if (!b.voteResult?.votePower) return 1; + return b.voteResult?.votePower + .sub(a.voteResult?.votePower || BigNumber.from(0)) + .mul(-1) + .toNumber(); + } + } + if (sortConfig[1] === 'votes') { + if (sortConfig[0]) { + return (b.voteResult?.votesReceived || 0) - (a.voteResult?.votesReceived || 0); + } else { + return ((b.voteResult?.votesReceived || 0) - (a.voteResult?.votesReceived || 0)) * -1; + } + } + if (sortConfig[1] === 'ranking') { + if (!b.voteResult?.votePower) return -1; + return b.voteResult?.votePower + .sub(a.voteResult?.votePower || BigNumber.from(0)) + .toNumber(); + } + return 0; + }); + } + return []; +}; diff --git a/governance/ui/src/utils/table-border.ts b/governance/ui/src/utils/table-border.ts new file mode 100644 index 000000000..8bbf10591 --- /dev/null +++ b/governance/ui/src/utils/table-border.ts @@ -0,0 +1,33 @@ +export function renderCorrectBorder( + column: 'place' | 'name' | 'votes' | 'power' | 'badge', + position: 'left' | 'right' | 'bottom', + period: string | undefined, + isSelected: boolean +) { + if (column === 'place') { + if (position === 'left') { + return isSelected ? '1px solid' : ''; + } else if (position === 'bottom') { + return isSelected ? '1px solid' : ''; + } + } else if (column === 'name') { + if (position === 'left') { + if (period === '2') { + return ''; + } + if (period === '0') { + return ''; + } + return isSelected ? '1px solid' : ''; + } else if (position === 'bottom') { + return isSelected ? '1px solid' : ''; + } + } else if (column === 'votes') { + if (position === 'bottom') return isSelected ? '1px solid' : ''; + } else if (column === 'power') { + if (position === 'bottom') return isSelected ? '1px solid' : ''; + } else if (column === 'badge') { + if (position === 'bottom') return isSelected ? '1px solid' : ''; + if (position === 'right') return isSelected ? '1px solid' : ''; + } +} diff --git a/governance/ui/webpack.config.js b/governance/ui/webpack.config.js index 9e7265de9..e3636b5c4 100644 --- a/governance/ui/webpack.config.js +++ b/governance/ui/webpack.config.js @@ -179,7 +179,7 @@ module.exports = { 'process.env.BOARDROOM_KEY': JSON.stringify( process.env.BOARDROOM_KEY ?? 'd9abe7a1ab45ace58e6bd91bb9771586' ), - 'process.env.CI': JSON.stringify(process.env.CI ?? false), + 'process.env.CI': JSON.stringify(process.env.CI ?? 'false'), }) ), resolve: {