Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Weighted voting UI #10858

Open
wants to merge 34 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
df733f5
feat: Add type
ChefMomota Oct 11, 2024
162a883
feat: Change voteType
ChefMomota Oct 16, 2024
257af07
feat: New vote details UI
ChefMomota Oct 21, 2024
d4a03c1
feat: Add veCake balance in Vote
ChefMomota Oct 21, 2024
ea3541d
feat: Add VoteIcon
ChefMomota Oct 21, 2024
ea6d3fe
feat: Update new layout
ChefMomota Oct 21, 2024
001d38a
feat: Add SingleVote component
ChefMomota Oct 21, 2024
513e875
feat: Add WeightedVote
ChefMomota Oct 21, 2024
3ab7163
feat: Update WeightedVote UI
ChefMomota Oct 21, 2024
79aba12
feat: Update UI
ChefMomota Oct 22, 2024
1d6cf64
feat: Binary UI update
ChefMomota Oct 22, 2024
94bf7fe
feat: Votes support WEIGHTED text
ChefMomota Oct 22, 2024
d730fc2
feat: Done Voting
ChefMomota Oct 22, 2024
e1bfbfe
fix: Bug
ChefMomota Oct 22, 2024
8f6aa81
fix: Conflict
ChefMomota Oct 24, 2024
1aa862e
fix: Bug
ChefMomota Oct 22, 2024
c2e5b3d
feat: Button UI
ChefMomota Oct 22, 2024
98492a4
fix: UI display
ChefMomota Oct 23, 2024
00961cf
feat: Add tooltips
ChefMomota Oct 23, 2024
764ad4a
fix: Current Results
ChefMomota Oct 23, 2024
683ed36
feat: Remove unused code
ChefMomota Oct 23, 2024
5f45d5c
fix: Number display
ChefMomota Oct 23, 2024
22811ec
fix: wording
ChefMomota Oct 23, 2024
12c6af3
feat: Add sort
ChefMomota Oct 23, 2024
45f2308
fix: Add block
ChefMomota Oct 23, 2024
4fecc4d
fix: All zero number
ChefMomota Oct 24, 2024
f48cf7f
feat: Remove zero
ChefMomota Oct 24, 2024
24c46a3
fix: Bug
ChefMomota Oct 24, 2024
5085bc0
fix: Bug
ChefMomota Oct 24, 2024
13d7ffd
fix: Tooltips
ChefMomota Oct 24, 2024
82d6a3f
revert: Whitelist address
ChefMomota Oct 24, 2024
09521fd
feat: Update vecake at snapshot time
ChefMomota Oct 25, 2024
f06f852
fix: snapshot time
ChefMomota Oct 25, 2024
38f2dd3
feat: Support old version
ChefMomota Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/web/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -403,6 +408,9 @@ export interface Proposal {
state: ProposalState
title: string
ipfs: string
type: ProposalTypeName
scores: number[]
scores_total: number
}

export interface Vote {
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/state/voting/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const getProposal = async (id: string): Promise<Proposal> => {
author
votes
ipfs
type
scores
scores_total
}
}
`,
Expand Down
16 changes: 8 additions & 8 deletions apps/web/src/views/Voting/CreateProposal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useTranslation } from '@pancakeswap/localization'
import {
AutoRenewIcon,
Box,
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
59 changes: 31 additions & 28 deletions apps/web/src/views/Voting/Proposal/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,72 @@
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<React.PropsWithChildren<DetailsProps>> = ({ proposal }) => {
const { t } = useTranslation()
const startDate = new Date(proposal.start * 1000)
const endDate = new Date(proposal.end * 1000)

return (
<Card mb="16px">
<CardHeader>
<CardHeader style={{ background: 'transparent' }}>
<Heading as="h3" scale="md">
{t('Details')}
</Heading>
</CardHeader>
<CardBody>
<Flex alignItems="center" mb="8px">
<Text color="textSubtle">{t('Identifier')}</Text>
<LinkExternal href={`${IPFS_GATEWAY}/${proposal.ipfs}`} ml="8px">
{proposal.ipfs.slice(0, 8)}
</LinkExternal>
</Flex>
<Flex alignItems="center" mb="8px">
<Text color="textSubtle">{t('Creator')}</Text>
<Text color="textSubtle" mr="auto">
{t('Creator')}
</Text>
<ScanLink useBscCoinFallback href={getBlockExploreLink(proposal.author, 'address')} ml="8px">
{truncateHash(proposal.author)}
</ScanLink>
</Flex>
<Flex alignItems="center" mb="16px">
<Text color="textSubtle">{t('Snapshot')}</Text>
<Flex mb="24px">
<Text color="textSubtle" mr="auto">
{t('Voting system')}
</Text>
<Text ml="8px">{proposal.type === ProposalTypeName.SINGLE_CHOICE ? t('Binary') : t('Weighted')}</Text>
</Flex>
<Flex alignItems="center" mb="8px">
<Text color="textSubtle" mr="auto">
{t('Identifier')}
</Text>
<LinkExternal href={`${IPFS_GATEWAY}/${proposal.ipfs}`} ml="8px">
{proposal.ipfs.slice(0, 8)}
</LinkExternal>
</Flex>
<Flex alignItems="center" mb="24px">
<Text color="textSubtle" mr="auto">
{t('Snapshot')}
</Text>
<ScanLink useBscCoinFallback href={getBlockExploreLink(proposal.snapshot, 'block')} ml="8px">
{proposal.snapshot}
</ScanLink>
</Flex>
<DetailBox p="16px">
<ProposalStateTag proposalState={proposal.state} mb="8px" />
<Flex alignItems="center">
<Text color="textSubtle" fontSize="14px">
<Box>
<Flex>
<Text color="textSubtle" mr="auto">
{t('Start Date')}
</Text>
<Text ml="8px">{dayjs(startDate).format('YYYY-MM-DD HH:mm')}</Text>
</Flex>
<Flex alignItems="center">
<Text color="textSubtle" fontSize="14px">
<Flex>
<Text color="textSubtle" mr="auto">
{t('End Date')}
</Text>
<Text ml="8px">{dayjs(endDate).format('YYYY-MM-DD HH:mm')}</Text>
</Flex>
</DetailBox>
</Box>
</CardBody>
</Card>
)
Expand Down
57 changes: 48 additions & 9 deletions apps/web/src/views/Voting/Proposal/Overview.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -96,19 +110,44 @@ const Overview = () => {
<ReactMarkdown>{proposal.body}</ReactMarkdown>
</Box>
</Box>
{!isPageLoading && !hasAccountVoted && proposal.state === ProposalState.ACTIVE && (
<Vote proposal={proposal} onSuccess={refetch} mb="16px" />
{!isPageLoading && (
<Vote
mb="16px"
proposal={proposal}
votes={votes}
hasAccountVoted={Boolean(hasAccountVoted)}
onSuccess={refetch}
/>
)}
{!isDesktop && (
<Box mb="16px">
<Details proposal={proposal} />
<Results
proposal={proposal}
choices={proposal.choices}
votes={votes || []}
votesLoadingStatus={votesLoadingStatus}
/>
</Box>
)}
<Votes
votes={votes || []}
proposal={proposal}
totalVotes={votes?.length ?? proposal.votes}
votesLoadingStatus={votesLoadingStatus}
/>
</Box>
<Box position="sticky" top="60px">
<Details proposal={proposal} />
<Results choices={proposal.choices} votes={votes || []} votesLoadingStatus={votesLoadingStatus} />
</Box>
{isDesktop && (
<Box position="sticky" top="60px">
<Details proposal={proposal} />
<Results
proposal={proposal}
choices={proposal.choices}
votes={votes || []}
votesLoadingStatus={votesLoadingStatus}
/>
</Box>
)}
</Layout>
</Container>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SingleVoteResultsProps> = ({ 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 (
<Box key={choice} mt={index > 0 ? '24px' : '0px'}>
<Flex alignItems="center" mb="8px">
<TextEllipsis mb="4px" title={choice}>
{choice}
</TextEllipsis>
</Flex>
<Box mb="4px">
<Progress primaryStep={progress} scale="sm" />
</Box>
<Flex alignItems="center" justifyContent="space-between">
<Text color="textSubtle">{t('%total% Votes', { total: formatNumber(totalChoiceVote, 0, 2) })}</Text>
<Text>{progress.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Text>
</Flex>
</Box>
)
})}
</>
)
}
Original file line number Diff line number Diff line change
@@ -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<WeightedVoteResultsProps> = ({ 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) => (
<Box key={choice} mt={index > 0 ? '24px' : '0px'}>
<Flex alignItems="center" mb="8px">
<TextEllipsis mb="4px" title={choice}>
{choice}
</TextEllipsis>
</Flex>
<Box mb="4px">
<Progress primaryStep={progress} scale="sm" />
</Box>
<Flex alignItems="center" justifyContent="space-between">
<Text color="textSubtle">{t('%total% Votes', { total: formatNumber(totalChoiceVote, 0, 2) })}</Text>
<Text>
{totalChoiceVote === 0 && totalSum === 0
? '0.00%'
: `${progress.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
%`}
</Text>
</Flex>
</Box>
))}
</>
)
}
Loading
Loading