diff --git a/apps/web/public/images/cake-staking/not-enough-veCAKE.png b/apps/web/public/images/cake-staking/not-enough-veCAKE.png new file mode 100644 index 0000000000000..4715ab12f5174 Binary files /dev/null and b/apps/web/public/images/cake-staking/not-enough-veCAKE.png differ diff --git a/apps/web/src/state/types.ts b/apps/web/src/state/types.ts index 2ce2c6c4b5938..f860ef52fb4c7 100644 --- a/apps/web/src/state/types.ts +++ b/apps/web/src/state/types.ts @@ -391,6 +391,11 @@ export enum ProposalState { CLOSED = 'closed', } +export enum ProposalTypeName { + SINGLE_CHOICE = 'single-choice', + WEIGHTED = 'weighted', +} + export interface Proposal { author: string body: string @@ -403,6 +408,9 @@ export interface Proposal { state: ProposalState title: string ipfs: string + type: ProposalTypeName + scores: number[] + scores_total: number } export interface Vote { diff --git a/apps/web/src/state/voting/helpers.ts b/apps/web/src/state/voting/helpers.ts index 2f9699bab15d7..392934be3de88 100644 --- a/apps/web/src/state/voting/helpers.ts +++ b/apps/web/src/state/voting/helpers.ts @@ -50,6 +50,9 @@ export const getProposal = async (id: string): Promise => { author votes ipfs + type + scores + scores_total } } `, diff --git a/apps/web/src/views/Voting/CreateProposal/index.tsx b/apps/web/src/views/Voting/CreateProposal/index.tsx index ebc59f4635323..b9399cea34449 100644 --- a/apps/web/src/views/Voting/CreateProposal/index.tsx +++ b/apps/web/src/views/Voting/CreateProposal/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@pancakeswap/localization' import { AutoRenewIcon, Box, @@ -15,19 +16,18 @@ import { useModal, useToast, } from '@pancakeswap/uikit' -import snapshot from '@snapshot-labs/snapshot.js' -import isEmpty from 'lodash/isEmpty' -import times from 'lodash/times' -import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from 'react' -import { useInitialBlock } from 'state/block/hooks' - -import { useTranslation } from '@pancakeswap/localization' import truncateHash from '@pancakeswap/utils/truncateHash' +import snapshot from '@snapshot-labs/snapshot.js' import ConnectWalletButton from 'components/ConnectWalletButton' import Container from 'components/Layout/Container' +import isEmpty from 'lodash/isEmpty' +import times from 'lodash/times' import dynamic from 'next/dynamic' import Link from 'next/link' import { useRouter } from 'next/router' +import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from 'react' +import { useInitialBlock } from 'state/block/hooks' +import { ProposalTypeName } from 'state/types' import { getBlockExploreLink } from 'utils' import { DatePicker, DatePickerPortal, TimePicker } from 'views/Voting/components/DatePicker' import { useAccount, useWalletClient } from 'wagmi' @@ -96,7 +96,7 @@ const CreateProposal = () => { const data: any = await client.proposal(web3 as any, account, { space: PANCAKE_SPACE, - type: 'single-choice', + type: ProposalTypeName.SINGLE_CHOICE, // TODO title: name, body, start: combineDateAndTime(startDate, startTime) || 0, diff --git a/apps/web/src/views/Voting/Proposal/Details.tsx b/apps/web/src/views/Voting/Proposal/Details.tsx index 65e44bdd1dd27..4774bf3766520 100644 --- a/apps/web/src/views/Voting/Proposal/Details.tsx +++ b/apps/web/src/views/Voting/Proposal/Details.tsx @@ -1,23 +1,15 @@ +import { useTranslation } from '@pancakeswap/localization' import { Box, Card, CardBody, CardHeader, Flex, Heading, LinkExternal, ScanLink, Text } from '@pancakeswap/uikit' -import { styled } from 'styled-components' +import truncateHash from '@pancakeswap/utils/truncateHash' import dayjs from 'dayjs' -import { Proposal } from 'state/types' +import { Proposal, ProposalTypeName } from 'state/types' import { getBlockExploreLink } from 'utils' -import { useTranslation } from '@pancakeswap/localization' -import truncateHash from '@pancakeswap/utils/truncateHash' import { IPFS_GATEWAY } from '../config' -import { ProposalStateTag } from '../components/Proposals/tags' interface DetailsProps { proposal: Proposal } -const DetailBox = styled(Box)` - background-color: ${({ theme }) => theme.colors.background}; - border: 1px solid ${({ theme }) => theme.colors.cardBorder}; - border-radius: 16px; -` - const Details: React.FC> = ({ proposal }) => { const { t } = useTranslation() const startDate = new Date(proposal.start * 1000) @@ -25,45 +17,56 @@ const Details: React.FC> = ({ proposal }) return ( - + {t('Details')} - {t('Identifier')} - - {proposal.ipfs.slice(0, 8)} - - - - {t('Creator')} + + {t('Creator')} + {truncateHash(proposal.author)} - - {t('Snapshot')} + + + {t('Voting system')} + + {proposal.type === ProposalTypeName.SINGLE_CHOICE ? t('Binary') : t('Weighted')} + + + + {t('Identifier')} + + + {proposal.ipfs.slice(0, 8)} + + + + + {t('Snapshot')} + {proposal.snapshot} - - - - + + + {t('Start Date')} {dayjs(startDate).format('YYYY-MM-DD HH:mm')} - - + + {t('End Date')} {dayjs(endDate).format('YYYY-MM-DD HH:mm')} - + ) diff --git a/apps/web/src/views/Voting/Proposal/Overview.tsx b/apps/web/src/views/Voting/Proposal/Overview.tsx index 74da5d91b4562..79336dfb5a603 100644 --- a/apps/web/src/views/Voting/Proposal/Overview.tsx +++ b/apps/web/src/views/Voting/Proposal/Overview.tsx @@ -1,5 +1,14 @@ import { useTranslation } from '@pancakeswap/localization' -import { ArrowBackIcon, Box, Button, Flex, Heading, NotFound, ReactMarkdown } from '@pancakeswap/uikit' +import { + ArrowBackIcon, + Box, + Button, + Flex, + Heading, + NotFound, + ReactMarkdown, + useMatchBreakpoints, +} from '@pancakeswap/uikit' import { useQuery } from '@tanstack/react-query' import Container from 'components/Layout/Container' import PageLoader from 'components/Loader/PageLoader' @@ -23,6 +32,7 @@ const Overview = () => { const id = query.id as string const { t } = useTranslation() const { address: account } = useAccount() + const { isDesktop } = useMatchBreakpoints() const { status: proposalLoadingStatus, @@ -56,8 +66,12 @@ const Overview = () => { }) const votes = useMemo(() => data || [], [data]) - - const hasAccountVoted = account && votes && votes.some((vote) => vote.voter.toLowerCase() === account.toLowerCase()) + const hasAccountVoted = + account && + votes && + proposal && + proposal.state === ProposalState.ACTIVE && + votes.some((vote) => vote.voter.toLowerCase() === account.toLowerCase()) const isPageLoading = votesLoadingStatus === 'pending' || proposalLoadingStatus === 'pending' @@ -96,19 +110,44 @@ const Overview = () => { {proposal.body} - {!isPageLoading && !hasAccountVoted && proposal.state === ProposalState.ACTIVE && ( - + {!isPageLoading && ( + + )} + {!isDesktop && ( + +
+ + )} - -
- - + {isDesktop && ( + +
+ + + )} ) diff --git a/apps/web/src/views/Voting/Proposal/ResultType/SingleVoteResults.tsx b/apps/web/src/views/Voting/Proposal/ResultType/SingleVoteResults.tsx new file mode 100644 index 0000000000000..f6326564eea08 --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/ResultType/SingleVoteResults.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Box, Flex, Progress, Text } from '@pancakeswap/uikit' +import { formatNumber } from '@pancakeswap/utils/formatBalance' +import { Vote } from 'state/types' +import TextEllipsis from '../../components/TextEllipsis' +import { calculateVoteResults, getTotalFromVotes } from '../../helpers' + +interface SingleVoteResultsProps { + choices: string[] + votes: Vote[] +} + +export const SingleVoteResults: React.FC = ({ votes, choices }) => { + const { t } = useTranslation() + const results = calculateVoteResults(votes) + const totalVotes = getTotalFromVotes(votes) + + return ( + <> + {choices.map((choice, index) => { + const choiceVotes = results[choice] || [] + const totalChoiceVote = getTotalFromVotes(choiceVotes) + const progress = totalVotes === 0 ? 0 : (totalChoiceVote / totalVotes) * 100 + + return ( + 0 ? '24px' : '0px'}> + + + {choice} + + + + + + + {t('%total% Votes', { total: formatNumber(totalChoiceVote, 0, 2) })} + {progress.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}% + + + ) + })} + + ) +} diff --git a/apps/web/src/views/Voting/Proposal/ResultType/WeightedVoteResults.tsx b/apps/web/src/views/Voting/Proposal/ResultType/WeightedVoteResults.tsx new file mode 100644 index 0000000000000..0664c5d779df1 --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/ResultType/WeightedVoteResults.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Box, Flex, Progress, Text } from '@pancakeswap/uikit' +import { formatNumber } from '@pancakeswap/utils/formatBalance' +import { useMemo } from 'react' +import { WeightedVoteState } from 'views/Voting/Proposal/VoteType/types' +import TextEllipsis from '../../components/TextEllipsis' + +interface WeightedVoteResultsProps { + choices: string[] + sortData?: boolean + choicesVotes: WeightedVoteState[] +} + +export const WeightedVoteResults: React.FC = ({ choices, sortData, choicesVotes }) => { + const { t } = useTranslation() + + const totalSum = useMemo( + () => choicesVotes.reduce((sum, item) => sum + Object.values(item).reduce((a, b) => a + b, 0), 0), + [choicesVotes], + ) + + const percentageResults = useMemo( + () => + choicesVotes.reduce((acc, item) => { + Object.entries(item).forEach(([key, value]) => { + // eslint-disable-next-line no-param-reassign + acc[key] = (acc[key] || 0) + value + }) + return acc + }, {}), + [choicesVotes], + ) + + const sortedChoices = useMemo(() => { + const list = choices.map((choice, index) => { + const totalChoiceVote = percentageResults[index + 1] ?? 0 + const progress = (totalChoiceVote / totalSum) * 100 + return { choice, totalChoiceVote, progress } + }) + + return sortData ? list.sort((a, b) => b.progress - a.progress) : list + }, [choices, percentageResults, sortData, totalSum]) + + return ( + <> + {sortedChoices.map(({ choice, totalChoiceVote, progress }, index) => ( + 0 ? '24px' : '0px'}> + + + {choice} + + + + + + + {t('%total% Votes', { total: formatNumber(totalChoiceVote, 0, 2) })} + + {totalChoiceVote === 0 && totalSum === 0 + ? '0.00%' + : `${progress.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + %`} + + + + ))} + + ) +} diff --git a/apps/web/src/views/Voting/Proposal/Results.tsx b/apps/web/src/views/Voting/Proposal/Results.tsx index d852d65a094ba..00565eff7947d 100644 --- a/apps/web/src/views/Voting/Proposal/Results.tsx +++ b/apps/web/src/views/Voting/Proposal/Results.tsx @@ -1,64 +1,41 @@ -import { Box, Text, Flex, Card, CardBody, CardHeader, Heading, Progress, Skeleton } from '@pancakeswap/uikit' -import { FarmWidget } from '@pancakeswap/widgets-internal' -import { useAccount } from 'wagmi' -import { Vote } from 'state/types' -import { formatNumber } from '@pancakeswap/utils/formatBalance' import { useTranslation } from '@pancakeswap/localization' +import { Box, Card, CardBody, CardHeader, Flex, Heading, Skeleton } from '@pancakeswap/uikit' import { FetchStatus, TFetchStatus } from 'config/constants/types' -import { calculateVoteResults, getTotalFromVotes } from '../helpers' +import { Proposal, ProposalTypeName, Vote } from 'state/types' +import { SingleVoteResults } from 'views/Voting/Proposal/ResultType/SingleVoteResults' +import { WeightedVoteResults } from 'views/Voting/Proposal/ResultType/WeightedVoteResults' import TextEllipsis from '../components/TextEllipsis' -const { VotedTag } = FarmWidget.Tags - interface ResultsProps { + proposal: Proposal choices: string[] votes: Vote[] votesLoadingStatus: TFetchStatus } -const Results: React.FC> = ({ choices, votes, votesLoadingStatus }) => { +const Results: React.FC> = ({ proposal, choices, votes, votesLoadingStatus }) => { const { t } = useTranslation() - const results = calculateVoteResults(votes) - const { address: account } = useAccount() - const totalVotes = getTotalFromVotes(votes) return ( - + {t('Current Results')} - {votesLoadingStatus === FetchStatus.Fetched && - choices.map((choice, index) => { - const choiceVotes = results[choice] || [] - const totalChoiceVote = getTotalFromVotes(choiceVotes) - const progress = totalVotes === 0 ? 0 : (totalChoiceVote / totalVotes) * 100 - const hasVoted = choiceVotes.some((vote) => { - return account && vote.voter.toLowerCase() === account.toLowerCase() - }) - - return ( - 0 ? '24px' : '0px'}> - - - {choice} - - {hasVoted && } - - - - - - {t('%total% Votes', { total: formatNumber(totalChoiceVote, 0, 2) })} - - {progress.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}% - - - - ) - })} + {votesLoadingStatus === FetchStatus.Fetched && ( + <> + {proposal.type === ProposalTypeName.SINGLE_CHOICE && } + {proposal.type === ProposalTypeName.WEIGHTED && ( + ({ [index + 1]: proposal?.scores?.[index] ?? 0 }))} + /> + )} + + )} {votesLoadingStatus === FetchStatus.Fetching && choices.map((choice, index) => { diff --git a/apps/web/src/views/Voting/Proposal/Vote.tsx b/apps/web/src/views/Voting/Proposal/Vote.tsx index f41f3c2b4ffcf..3eb0794bc41a2 100644 --- a/apps/web/src/views/Voting/Proposal/Vote.tsx +++ b/apps/web/src/views/Voting/Proposal/Vote.tsx @@ -1,60 +1,81 @@ import { useTranslation } from '@pancakeswap/localization' import { + Balance, Button, Card, CardBody, CardHeader, CardProps, + Flex, Heading, - Radio, + Image, + Message, + MessageText, Text, useModal, useToast, + VoteIcon, } from '@pancakeswap/uikit' +import { BigNumber } from 'bignumber.js' import ConnectWalletButton from 'components/ConnectWalletButton' -import { useState } from 'react' -import { Proposal } from 'state/types' -import { styled } from 'styled-components' +import { useEffect, useMemo, useState } from 'react' +import { Proposal, ProposalState, ProposalTypeName, Vote } from 'state/types' +import { VECAKE_VOTING_POWER_BLOCK } from 'views/Voting/helpers' +import useGetVotingPower from 'views/Voting/hooks/useGetVotingPower' +import { SingleVote } from 'views/Voting/Proposal/VoteType/SingleVote' +import { SingleVoteState, VoteState, WeightedVoteState } from 'views/Voting/Proposal/VoteType/types' +import { WeightedVote } from 'views/Voting/Proposal/VoteType/WeightedVote' import { useAccount } from 'wagmi' import CastVoteModal from '../components/CastVoteModal' interface VoteProps extends CardProps { proposal: Proposal + votes: Vote[] + hasAccountVoted: boolean onSuccess?: () => void } -interface State { - label: string - value: number -} - -const Choice = styled.label<{ isChecked: boolean; isDisabled: boolean }>` - align-items: center; - border: 1px solid ${({ theme, isChecked }) => theme.colors[isChecked ? 'success' : 'cardBorder']}; - border-radius: 16px; - cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')}; - display: flex; - margin-bottom: 16px; - padding: 16px; -` - -const ChoiceText = styled.div` - flex: 1; - padding-left: 16px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 0; -` - -const Vote: React.FC> = ({ proposal, onSuccess, ...props }) => { - const [vote, setVote] = useState({ +const VoteComponent: React.FC> = ({ + proposal, + votes, + hasAccountVoted, + onSuccess, + ...props +}) => { + const [vote, setVote] = useState({ label: '', value: 0, }) const { t } = useTranslation() const { toastSuccess } = useToast() const { address: account } = useAccount() + const { total } = useGetVotingPower(Number(proposal.snapshot)) + + useEffect(() => { + const { type, choices } = proposal + if (type === ProposalTypeName.WEIGHTED && account) { + let newData: null | WeightedVoteState = null + const voteData = votes.find((i) => i.voter.toLowerCase() === account.toLowerCase()) + + if (voteData) { + newData = choices.reduce((acc, _, index) => { + // eslint-disable-next-line no-param-reassign + acc[index + 1] = voteData?.choice[index + 1] ?? 0 + return acc + }, {}) + } else { + newData = choices.reduce((acc, _, index) => { + // eslint-disable-next-line no-param-reassign + acc[index + 1] = 0 + return acc + }, {}) + } + + if (newData) { + setVote(newData) + } + } + }, [account]) const handleSuccess = async () => { toastSuccess(t('Vote cast!')) @@ -62,50 +83,122 @@ const Vote: React.FC> = ({ proposal, onSucces } const [presentCastVoteModal] = useModal( - , + , + ) + + const isVeCakeVersion = useMemo( + () => proposal.snapshot && BigInt(proposal.snapshot) >= VECAKE_VOTING_POWER_BLOCK, + [proposal], ) + const notEnoughVeCake = useMemo(() => { + if (isVeCakeVersion) { + return total === undefined || new BigNumber(total).lte(0) + } + + return false + }, [isVeCakeVersion, total]) + + const isAbleToVote = useMemo(() => { + if (proposal.type === ProposalTypeName.SINGLE_CHOICE) { + return (vote as SingleVoteState).value > 0 + } + + // ProposalTypeName.WEIGHTED + const totalVote = Object.values(vote).reduce((acc, value) => acc + value, 0) + return totalVote > 0 + }, [proposal, vote]) + return ( - - - {t('Cast your vote')} - + + + + {t('Cast your vote')} + + {account && isVeCakeVersion && ( + + {t('veCake Balance')}: + + + + )} + - {proposal.choices.map((choice, index) => { - const isChecked = index + 1 === vote.value - - const handleChange = () => { - setVote({ - label: choice, - value: index + 1, - }) - } - - return ( - -
- -
- - - {choice} - - -
- ) - })} + {proposal.type === ProposalTypeName.SINGLE_CHOICE && ( + + )} + {proposal.type === ProposalTypeName.WEIGHTED && ( + + )} {account ? ( - + <> + {proposal.state === ProposalState.ACTIVE && ( + <> + {hasAccountVoted ? ( + + + {t('You cast your vote! Please wait until the voting ends to see the end results.')} + + + ) : notEnoughVeCake ? ( + + ) : !isAbleToVote ? ( + + ) : ( + + )} + + )} + ) : ( - + )}
) } -export default Vote +export default VoteComponent diff --git a/apps/web/src/views/Voting/Proposal/VoteType/SingleVote.tsx b/apps/web/src/views/Voting/Proposal/VoteType/SingleVote.tsx new file mode 100644 index 0000000000000..8c33f3be6e10a --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/VoteType/SingleVote.tsx @@ -0,0 +1,50 @@ +import { Radio, Text } from '@pancakeswap/uikit' +import { Dispatch } from 'react' +import { Proposal, ProposalState } from 'state/types' +import { SingleVoteState } from 'views/Voting/Proposal/VoteType/types' +import { Choice, ChoiceText } from 'views/Voting/Proposal/VoteType/VoteStyle' +import { useAccount } from 'wagmi' + +interface SingleVoteProps { + proposal: Proposal + vote: SingleVoteState + setVote: Dispatch +} + +export const SingleVote: React.FC = ({ proposal, vote, setVote }) => { + const { address: account } = useAccount() + + return ( + <> + {proposal.choices.map((choice, index) => { + const isChecked = index + 1 === vote.value + + const handleChange = () => { + setVote({ + label: choice, + value: index + 1, + }) + } + + return ( + +
+ +
+ + + {choice} + + +
+ ) + })} + + ) +} diff --git a/apps/web/src/views/Voting/Proposal/VoteType/VoteStyle.tsx b/apps/web/src/views/Voting/Proposal/VoteType/VoteStyle.tsx new file mode 100644 index 0000000000000..1ac822658ee88 --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/VoteType/VoteStyle.tsx @@ -0,0 +1,20 @@ +import { styled } from 'styled-components' + +export const Choice = styled.label<{ isChecked?: boolean; isDisabled?: boolean }>` + align-items: center; + border: 1px solid ${({ theme, isChecked }) => theme.colors[isChecked ? 'success' : 'cardBorder']}; + border-radius: 16px; + cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')}; + display: flex; + margin-bottom: 16px; + padding: 16px; +` + +export const ChoiceText = styled.div` + flex: 1; + padding-left: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 0; +` diff --git a/apps/web/src/views/Voting/Proposal/VoteType/WeightedVote.tsx b/apps/web/src/views/Voting/Proposal/VoteType/WeightedVote.tsx new file mode 100644 index 0000000000000..75a3f19f8ac97 --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/VoteType/WeightedVote.tsx @@ -0,0 +1,170 @@ +import { AddIcon, Box, Flex, IconButton, Input, MinusIcon, Text } from '@pancakeswap/uikit' +import { Dispatch, useCallback, useMemo } from 'react' +import { Proposal, ProposalState } from 'state/types' +import { styled } from 'styled-components' +import { WeightedVoteState } from 'views/Voting/Proposal/VoteType/types' + +const Choice = styled.label` + max-width: 100%; + overflow: hidden; + display: flex; + padding: 8px 16px; + border-radius: 16px; + margin-bottom: 16px; + border: 1px solid ${({ theme }) => theme.colors.cardBorder}; + flex-direction: column; + + ${({ theme }) => theme.mediaQueries.md} { + flex-direction: row; + align-items: center; + } +` + +const ChoiceText = styled.div` + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + white-space: initial; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + margin-bottom: 16px; + + ${({ theme }) => theme.mediaQueries.md} { + margin: 0 auto 0 0; + } +` + +const IconButtonStyle = styled(IconButton)` + border-radius: 8px; + width: 26px; + min-width: 26px; + height: 26px; +` + +const ContainerStyled = styled(Flex)` + padding: 8px; + max-width: 140px; + min-width: 140px; + border-radius: 16px; + background: ${({ theme }) => theme.colors.input}; + border: 1px solid ${({ theme }) => theme.colors.inputSecondary}; +` + +const InputStyled = styled(Input)` + border: 0px; + height: 24px; + box-shadow: none; + text-align: center; + + &:focus { + box-shadow: none !important; + } +` + +const DisableText = styled(Box)` + max-width: 140px; + min-width: 140px; + padding: 8px; + border-radius: 16px; + text-align: center; + height: 42px; + line-height: 24px; + color: ${({ theme }) => theme.colors.textDisabled}; + background: ${({ theme }) => theme.colors.disabled}; +` + +interface WeightedVoteProps { + proposal: Proposal + hasAccountVoted: boolean + vote: WeightedVoteState + notEnoughVeCake: boolean + setVote: Dispatch +} + +export const WeightedVote: React.FC = ({ + proposal, + hasAccountVoted, + vote, + notEnoughVeCake, + setVote, +}) => { + const totalVote = useMemo(() => Object.values(vote).reduce((acc, value) => acc + value, 0), [vote]) + + const percentageDisplay = useMemo(() => { + const percentage = {} + Object.keys(vote).forEach((key) => { + percentage[key] = totalVote === 0 ? '0.00%' : `${((vote[key] / totalVote) * 100).toFixed(2)}%` + }) + return percentage + }, [totalVote, vote]) + + const handleButton = (index: number, value: number) => { + const newVoteData = { + ...vote, + [index]: value <= 0 ? 0 : value, + } as WeightedVoteState + + setVote(newVoteData) + } + + const handleInput = useCallback( + (e: React.ChangeEvent, index: number) => { + if (e.currentTarget.validity.valid) { + const newVoteData = { + ...vote, + [index]: Number(e.target.value), + } as WeightedVoteState + + setVote(newVoteData) + } + }, + [setVote, vote], + ) + + return ( + <> + {proposal.choices.map((choice, index) => { + const choiceIndex = index + 1 + const inputValue = vote[choiceIndex] + + return ( + + {choice} + + {percentageDisplay[choiceIndex]} + {hasAccountVoted || proposal.state === ProposalState.CLOSED ? ( + {inputValue} + ) : ( + + handleButton(choiceIndex, inputValue - 1)} + > + + + handleInput(e, choiceIndex)} + /> + handleButton(choiceIndex, inputValue + 1)} + > + + + + )} + + + ) + })} + + ) +} diff --git a/apps/web/src/views/Voting/Proposal/VoteType/types.ts b/apps/web/src/views/Voting/Proposal/VoteType/types.ts new file mode 100644 index 0000000000000..cc11e77ed4404 --- /dev/null +++ b/apps/web/src/views/Voting/Proposal/VoteType/types.ts @@ -0,0 +1,10 @@ +export interface SingleVoteState { + label: string + value: number +} + +export interface WeightedVoteState { + [key: string]: number +} + +export type VoteState = SingleVoteState | WeightedVoteState diff --git a/apps/web/src/views/Voting/Proposal/Votes.tsx b/apps/web/src/views/Voting/Proposal/Votes.tsx index a2c69ff899768..4c9ec177a4285 100644 --- a/apps/web/src/views/Voting/Proposal/Votes.tsx +++ b/apps/web/src/views/Voting/Proposal/Votes.tsx @@ -13,13 +13,14 @@ import { import { FetchStatus, TFetchStatus } from 'config/constants/types' import orderBy from 'lodash/orderBy' import { useState } from 'react' -import { Vote } from 'state/types' +import { Proposal, Vote } from 'state/types' import { useAccount } from 'wagmi' import VoteRow from '../components/Proposal/VoteRow' import VotesLoading from '../components/Proposal/VotesLoading' interface VotesProps { votes: Vote[] + proposal: Proposal totalVotes?: number votesLoadingStatus: TFetchStatus } @@ -30,7 +31,7 @@ const parseVotePower = (incomingVote: Vote) => { return votingPower } -const Votes: React.FC> = ({ votes, votesLoadingStatus, totalVotes }) => { +const Votes: React.FC> = ({ votes, proposal, votesLoadingStatus, totalVotes }) => { const [showAll, setShowAll] = useState(false) const { isMobile } = useMatchBreakpoints() const { t } = useTranslation() @@ -47,7 +48,7 @@ const Votes: React.FC> = ({ votes, votesLoad return ( - + {t('Votes (%count%)', { count: totalVotes || '-' })} @@ -61,7 +62,7 @@ const Votes: React.FC> = ({ votes, votesLoad <> {displayVotes.map((vote) => { const isVoter = account && vote.voter.toLowerCase() === account.toLowerCase() - return + return })}