diff --git a/apps/dapp/components/CopyToClipboard.tsx b/apps/dapp/components/CopyToClipboard.tsx index b2e461b70..7a1bca5f8 100644 --- a/apps/dapp/components/CopyToClipboard.tsx +++ b/apps/dapp/components/CopyToClipboard.tsx @@ -17,19 +17,20 @@ function concatAddressImpl( return first + '...' + last } -function concatAddress(address: string) { - const takeN = 7 +function concatAddress(address: string, takeN = 7): string { return concatAddressImpl(address, takeN, takeN) } interface CopyToClipboardProps { value: string success?: string + takeN?: number } export function CopyToClipboard({ value, success = 'Copied to clipboard!', + takeN, }: CopyToClipboardProps) { const [copied, setCopied] = useState(false) return ( @@ -47,7 +48,7 @@ export function CopyToClipboard({ ) : ( )} - {concatAddress(value)} + {concatAddress(value, takeN)} ) } diff --git a/apps/dapp/components/ProposalDetailsSidebar.tsx b/apps/dapp/components/ProposalDetailsSidebar.tsx index 9b3fc851c..4b76e9229 100644 --- a/apps/dapp/components/ProposalDetailsSidebar.tsx +++ b/apps/dapp/components/ProposalDetailsSidebar.tsx @@ -30,26 +30,33 @@ import { TriangleUp } from './icons/TriangleUp' import { Progress } from './Progress' import { ProposalStatus } from './ProposalStatus' +const YouTooltip = ({ label }: { label: string }) => ( + +

+ ? +

+
+) + const PASSING_THRESHOLD_TOOLTIP = "A proposal must attain this proportion of 'Yes' votes to pass." const QUORUM_TOOLTIP = 'This proportion of voting weight must vote for a proposal to pass.' -export function ProposalDetailsSidebar({ - contractAddress, - proposalId, - multisig, -}: { +interface ProposalDetailsProps { contractAddress: string + multisig: boolean proposalId: number - multisig?: boolean -}) { +} + +export const ProposalDetailsCard = ({ + contractAddress, + multisig, + proposalId, +}: ProposalDetailsProps) => { const proposal = useRecoilValue( proposalSelector({ contractAddress, proposalId }) ) - const proposalTally = useRecoilValue( - proposalTallySelector({ contractAddress, proposalId }) - ) const { state: proposalExecutionTXHashState, contents: txHashContents } = useRecoilValueLoadable( proposalExecutionTXHashSelector({ contractAddress, proposalId }) @@ -57,9 +64,6 @@ export function ProposalDetailsSidebar({ const proposalExecutionTXHash: string | null = proposalExecutionTXHashState === 'hasValue' ? txHashContents : null - const sigConfig = useRecoilValue( - contractConfigSelector({ contractAddress, multisig: !!multisig }) - ) const walletVote = useRecoilValue( walletVoteSelector({ contractAddress, proposalId }) ) @@ -70,19 +74,140 @@ export function ProposalDetailsSidebar({ const votingPower = useRecoilValue( votingPowerAtHeightSelector({ contractAddress, - multisig: !!multisig, + multisig, height, }) ) const memberWhenProposalCreated = votingPower > 0 + if (!proposal) { + return null + } + + return ( +
+
+
+

+ Proposal +

+ +

+ # {proposal.id.toString().padStart(6, '0')} +

+
+ +
+ +
+

+ Status +

+ +

+ +

+
+ +
+ +
+

+ You +

+ + {!memberWhenProposalCreated ? ( + + ) : walletVote === WalletVote.Yes ? ( +

+ Yes +

+ ) : walletVote === WalletVote.No ? ( +

+ No +

+ ) : walletVote === WalletVote.Abstain ? ( +

+ Abstain +

+ ) : walletVote === WalletVote.Veto ? ( +

+ Veto +

+ ) : walletVote ? ( +

+ Unknown: {walletVote} +

+ ) : proposal.status === 'open' ? ( + + ) : ( + + )} +
+
+ +
+

Proposer

+ + + + {proposal.status === 'executed' && + proposalExecutionTXHashState === 'loading' ? ( + <> +

TX

+

Loading...

+ + ) : !!proposalExecutionTXHash ? ( + <> + {CHAIN_TXN_URL_PREFIX ? ( + + TX + + + ) : ( +

TX

+ )} + + + + ) : null} +
+
+ ) +} + +export const ProposalDetailsVoteStatus = ({ + contractAddress, + multisig, + proposalId, +}: ProposalDetailsProps) => { + const proposal = useRecoilValue( + proposalSelector({ contractAddress, proposalId }) + ) + const proposalTally = useRecoilValue( + proposalTallySelector({ contractAddress, proposalId }) + ) + + const config = useRecoilValue( + contractConfigSelector({ contractAddress, multisig }) + ) + const { threshold, quorum } = useThresholdQuorum( contractAddress, proposalId, - !!multisig + multisig ) - const configWrapper = new ContractConfigWrapper(sigConfig) + const configWrapper = new ContractConfigWrapper(config) const tokenDecimals = configWrapper.gov_token_decimals const localeOptions = { maximumSignificantDigits: 3 } @@ -134,12 +259,12 @@ export function ProposalDetailsSidebar({ const totalAbstainPercent = (abstainVotes / totalWeight) * 100 if (!proposal) { - return
Error, no proposal
+ return null } const maxVotingSeconds = - 'time' in sigConfig.config.max_voting_period - ? sigConfig.config.max_voting_period.time + 'time' in config.config.max_voting_period + ? config.config.max_voting_period.time : undefined const expiresInSeconds = proposal.expires && 'at_time' in proposal.expires @@ -147,464 +272,382 @@ export function ProposalDetailsSidebar({ : undefined return ( -
-

Details

-
-

- Proposal -

-

- # {proposal.id.toString().padStart(6, '0')} -

-

- Status -

-
- -
-

- Proposer -

-

- -

- {proposal.status === 'executed' && - proposalExecutionTXHashState === 'loading' ? ( - <> -

TX

-

Loading...

- - ) : !!proposalExecutionTXHash ? ( - <> - {CHAIN_TXN_URL_PREFIX ? ( - - TX - - - ) : ( -

TX

- )} -

- -

- - ) : null} - {memberWhenProposalCreated && ( +
+ {threshold ? ( + quorum ? ( <> -

- Your vote +

+ Ratio of votes

- {walletVote === WalletVote.Yes ? ( -

- Yes -

- ) : walletVote === WalletVote.No ? ( -

- No -

- ) : walletVote === WalletVote.Abstain ? ( -

- Abstain -

- ) : walletVote === WalletVote.Veto ? ( -

- Veto -

- ) : walletVote ? ( -

- Unknown: {walletVote} -

- ) : ( -

- {proposal.status === 'open' ? 'Pending...' : 'None'} -

- )} - - )} -
- -

Referendum status

- -
- {threshold ? ( - quorum ? ( - <> -

- Ratio of votes -

- -
- {[ -

- Yes{' '} - {turnoutYesPercent.toLocaleString(undefined, localeOptions)} - % -

, -

- No{' '} - {turnoutNoPercent.toLocaleString(undefined, localeOptions)}% -

, - ] - .sort(() => yesVotes - noVotes) - .map((elem, idx) => ( -
- {elem} -
- ))} -

- Abstain{' '} - {turnoutAbstainPercent.toLocaleString( - undefined, - localeOptions - )} +

+ {[ +

+ Yes{' '} + {turnoutYesPercent.toLocaleString(undefined, localeOptions)}% +

, +

+ No {turnoutNoPercent.toLocaleString(undefined, localeOptions)} % -

-
- -
- b.value - a.value), - { - value: Number(turnoutAbstainPercent), - // Secondary is dark with 80% opacity. - color: 'rgba(var(--dark), 0.8)', - }, - ], - }, - ]} - verticalBars={ - threshold && [ - { - value: threshold.percent, - color: 'rgba(var(--dark), 0.5)', - }, - ] - } - /> -
- -
- 90 - ? 'calc(100% - 32px)' - : `calc(${threshold.percent}% - 17px)`, - }} - width="36px" - /> - - -
-

- Passing threshold:{' '} - {threshold.display} -

- -

- {turnoutYesPercent >= threshold.percent ? ( - <> - Reached{' '} - - - ) : ( - <> - Not met{' '} - - - )} -

+

, + ] + .sort(() => yesVotes - noVotes) + .map((elem, idx) => ( +
+ {elem}
- -
- -
-

- Turnout -

- -

- {turnoutPercent.toLocaleString(undefined, localeOptions)}% -

-
- -
- + Abstain{' '} + {turnoutAbstainPercent.toLocaleString(undefined, localeOptions)} + % +

+
+ +
+ -
- -
- 90 - ? 'calc(100% - 32px)' - : `calc(${quorum.percent}% - 17px)`, - }} - width="36px" - /> - - -
-

- Quorum:{' '} - {quorum.display} -

- -

- {turnoutPercent >= quorum.percent ? ( - <> - Reached{' '} - - - ) : ( - <> - Not met{' '} - - - )} -

-
-
-
- - ) : ( - <> -

- Turnout -

- -
- {[ -

- Yes{' '} - {totalYesPercent.toLocaleString(undefined, localeOptions)}% -

, -

- No {totalNoPercent.toLocaleString(undefined, localeOptions)} - % -

, - ] - .sort(() => yesVotes - noVotes) - .map((elem, idx) => ( -
- {elem} -
- ))} -

- Abstain{' '} - {totalAbstainPercent.toLocaleString(undefined, localeOptions)} - % -

-
- -
- b.value - a.value), { - value: Number(totalAbstainPercent), - // Secondary is dark with 80% opacity. - color: 'rgba(var(--dark), 0.8)', + value: Number(turnoutNoPercent), + color: 'rgb(var(--error))', }, - ], - }, - ]} - verticalBars={[ + ].sort((a, b) => b.value - a.value), + { + value: Number(turnoutAbstainPercent), + // Secondary is dark with 80% opacity. + color: 'rgba(var(--dark), 0.8)', + }, + ], + }, + ]} + verticalBars={ + threshold && [ { value: threshold.percent, color: 'rgba(var(--dark), 0.5)', }, - ]} - /> -
- -
- 90 - ? 'calc(100% - 32px)' - : `calc(${threshold.percent}% - 17px)`, - }} - width="36px" - /> - - -
-

- Passing threshold:{' '} - {threshold.display} -

- -

- {totalYesPercent >= threshold.percent ? ( - <> - Reached{' '} - - - ) : ( - <> - Not met{' '} - - - )} -

-
-
-
- - ) - ) : null} + ] + } + /> +
+ +
+ 90 + ? 'calc(100% - 32px)' + : `calc(${threshold.percent}% - 17px)`, + }} + width="36px" + /> + + +
+

+ Passing threshold:{' '} + {threshold.display} +

+ +

+ {turnoutYesPercent >= threshold.percent ? ( + <> + Reached{' '} + + + ) : ( + <> + Not met{' '} + + + )} +

+
+
+
+ +
+

+ Turnout +

- {expiresInSeconds !== undefined && expiresInSeconds > 0 && ( +

+ {turnoutPercent.toLocaleString(undefined, localeOptions)}% +

+
+ +
+ +
+ +
+ 90 + ? 'calc(100% - 32px)' + : `calc(${quorum.percent}% - 17px)`, + }} + width="36px" + /> + + +
+

+ Quorum: {quorum.display} +

+ +

+ {turnoutPercent >= quorum.percent ? ( + <> + Reached{' '} + + + ) : ( + <> + Not met{' '} + + + )} +

+
+
+
+ + ) : ( <> -

- Time left -

- -

- {secondsToWdhms(expiresInSeconds, 2)} +

+ Turnout

- {maxVotingSeconds !== undefined && ( -
- + {[ +

+ Yes {totalYesPercent.toLocaleString(undefined, localeOptions)} + % +

, +

+ No {totalNoPercent.toLocaleString(undefined, localeOptions)}% +

, + ] + .sort(() => yesVotes - noVotes) + .map((elem, idx) => ( +
+ {elem} +
+ ))} +

+ Abstain{' '} + {totalAbstainPercent.toLocaleString(undefined, localeOptions)}% +

+
+ +
+ -
- )} + { + value: Number(totalNoPercent), + color: 'rgb(var(--error))', + }, + ].sort((a, b) => b.value - a.value), + { + value: Number(totalAbstainPercent), + // Secondary is dark with 80% opacity. + color: 'rgba(var(--dark), 0.8)', + }, + ], + }, + ]} + verticalBars={[ + { + value: threshold.percent, + color: 'rgba(var(--dark), 0.5)', + }, + ]} + /> +
+ +
+ 90 + ? 'calc(100% - 32px)' + : `calc(${threshold.percent}% - 17px)`, + }} + width="36px" + /> + + +
+

+ Passing threshold:{' '} + {threshold.display} +

+ +

+ {totalYesPercent >= threshold.percent ? ( + <> + Reached{' '} + + + ) : ( + <> + Not met{' '} + + + )} +

+
+
+
- )} - - {threshold?.percent === 50 && yesVotes === noVotes && ( -
-

Tie clarification

- -

{"'Yes' will win a tie vote."}

-
- )} + ) + ) : null} + + {expiresInSeconds !== undefined && expiresInSeconds > 0 && ( + <> +

+ Time left +

+ +

+ {secondsToWdhms(expiresInSeconds, 2)} +

+ + {maxVotingSeconds !== undefined && ( +
+ +
+ )} + + )} + + {threshold?.percent === 50 && yesVotes === noVotes && ( +
+

Tie clarification

+ +

{"'Yes' will win a tie vote."}

+
+ )} - {abstainVotes === turnoutTotal && ( -
-

All abstain clarification

+ {abstainVotes === turnoutTotal && ( +
+

All abstain clarification

-

- When all abstain, a proposal will fail. -

-
- )} -
+

+ When all abstain, a proposal will fail. +

+
+ )}
) } + +export const ProposalDetailsSidebar = (props: ProposalDetailsProps) => ( +
+

Details

+ + +

Referendum status

+ +
+) diff --git a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx index 06c3f6173..9105722a6 100644 --- a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx +++ b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx @@ -5,7 +5,11 @@ import { useRecoilValue } from 'recoil' import { Breadcrumbs } from 'components/Breadcrumbs' import { ProposalDetails } from 'components/ProposalDetails' -import { ProposalDetailsSidebar } from 'components/ProposalDetailsSidebar' +import { + ProposalDetailsSidebar, + ProposalDetailsCard, + ProposalDetailsVoteStatus, +} from 'components/ProposalDetailsSidebar' import { daoSelector } from 'selectors/daos' import { cw20TokenInfo } from 'selectors/treasury' @@ -16,8 +20,14 @@ const Proposal: NextPage = () => { const daoInfo = useRecoilValue(daoSelector(contractAddress)) const govTokenInfo = useRecoilValue(cw20TokenInfo(daoInfo.gov_token)) + const proposalDetailsProps = { + contractAddress, + multisig: false, + proposalId: Number(proposalKey), + } + return ( -
+
{ [router.asPath, `Proposal ${proposalKey}`], ]} /> + +
+ +
+ { }} proposalId={Number(proposalKey)} /> + +
+

Referendum status

+ + +
-
- +
+
) diff --git a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx index 9e23e5bec..ace15b90e 100644 --- a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx +++ b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx @@ -5,7 +5,11 @@ import { useRecoilValue } from 'recoil' import { Breadcrumbs } from 'components/Breadcrumbs' import { ProposalDetails } from 'components/ProposalDetails' -import { ProposalDetailsSidebar } from 'components/ProposalDetailsSidebar' +import { + ProposalDetailsSidebar, + ProposalDetailsCard, + ProposalDetailsVoteStatus, +} from 'components/ProposalDetailsSidebar' import { sigSelector } from 'selectors/multisigs' const MultisigProposal: NextPage = () => { @@ -14,8 +18,14 @@ const MultisigProposal: NextPage = () => { const contractAddress = router.query.contractAddress as string const sigInfo = useRecoilValue(sigSelector(contractAddress)) + const proposalDetailsProps = { + contractAddress, + multisig: true, + proposalId: Number(proposalKey), + } + return ( -
+
{ [router.asPath, `Proposal ${proposalKey}`], ]} /> + +
+ +
+ { multisig proposalId={Number(proposalKey)} /> + +
+

Referendum status

+ + +
-
- +
+
)