diff --git a/components/TreasuryVotingWidget/index.tsx b/components/TreasuryVotingWidget/index.tsx index 879af615..167c1194 100644 --- a/components/TreasuryVotingWidget/index.tsx +++ b/components/TreasuryVotingWidget/index.tsx @@ -4,10 +4,12 @@ import { ProposalExtended } from "@lib/api/treasury"; import { ProposalVotingPower } from "@lib/api/types/get-treasury-proposal"; import dayjs from "@lib/dayjs"; import { abbreviateNumber, fromWei, shortenAddress } from "@lib/utils"; -import { Box, Button, Flex, Heading, Text } from "@livepeer/design-system"; +import { Box, Button, Flex, Link, Text } from "@livepeer/design-system"; +import { InfoCircledIcon } from "@modulz/radix-icons"; import { useAccountAddress } from "hooks"; import numbro from "numbro"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { zeroAddress } from "viem"; import VoteButton from "../VoteButton"; @@ -19,328 +21,430 @@ type Props = { const formatPercent = (percent: number) => numbro(percent).format({ output: "percent", - mantissa: 4, + mantissa: 2, }); const formatLPT = (lpt: string | undefined) => abbreviateNumber(fromWei(lpt ?? "0"), 4); +const SectionLabel = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + const TreasuryVotingWidget = ({ proposal, vote, ...props }: Props) => { const accountAddress = useAccountAddress(); const [reason, setReason] = useState(""); + const hasVotingPower = + !!vote && !!vote.self && parseFloat(vote.self.votes) > 0; + const canVoteNow = + proposal.state === "Active" && vote?.self?.hasVoted === false; + const isIneligible = canVoteNow && !hasVotingPower; + const hasDelegate = + !!vote?.delegate && vote.delegate.address.toLowerCase() !== zeroAddress; + + const userVoteStatus = useMemo(() => { + if (!vote?.self) return null; + if (isIneligible) return "Ineligible"; + if (vote.self.hasVoted) return "Voted"; + return "Not voted"; + }, [vote?.self, isIneligible]); + return ( - - - - Do you support{" "} - {proposal.attributes?.lip - ? `LIP-${proposal.attributes?.lip}` - : "this proposal"} - ? - + + {/* ========== RESULTS SECTION ========== */} + + Results - - + {/* Vote bars - read-only styled */} + + {/* Against bar */} + + + Against + - - Against - - - {formatPercent(proposal.votes.percent.against)} + + + {formatPercent(proposal.votes.percent.against)} + + + + {/* For bar */} + + + For + - - For - - - {formatPercent(proposal.votes.percent.for)} + + + {formatPercent(proposal.votes.percent.for)} + + + + {/* Abstain bar */} + + + Abstain + - - Abstain - - - {formatPercent(proposal.votes.percent.abstain)} + - - - {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} - {proposal.state !== "Pending" && proposal.state !== "Active" - ? "Final Results" - : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + - " left"} - + + {formatPercent(proposal.votes.percent.abstain)} + + - {accountAddress ? ( - <> - - {vote && !vote?.delegate ? null : ( - - - Your Delegate{" "} - {vote?.delegate && - `(${shortenAddress(vote?.delegate.address)})`} - {vote?.delegate && - ` ${ - vote.delegate.hasVoted - ? "already voted" - : "hasn't voted yet" - }`} - - - {vote?.delegate ? formatLPT(vote.delegate.votes) : "N/A"} - - - )} - + {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} + {proposal.state !== "Pending" && proposal.state !== "Active" + ? "Final Results" + : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + + " left"} + + + + {/* ========== YOUR VOTE SECTION ========== */} + {accountAddress ? ( + + Your vote + + {/* Delegate vote status */} + {hasDelegate && ( + + + Delegate vote ({shortenAddress(vote.delegate!.address)}) + + + {vote.delegate!.hasVoted ? "Voted" : "Not voted"} + + + )} + + {/* User vote status */} + + Status + + {userVoteStatus} + + + + {/* Voting power */} + + Voting power + + {vote?.self ? formatLPT(vote.self.votes) : "0"} LPT + + + + {/* ========== ACTION AREA ========== */} + + {/* Eligible: show vote buttons + reason */} + {canVoteNow && hasVotingPower && ( + + - - You ( - {accountAddress.replace(accountAddress.slice(5, 39), "…")}) - {vote?.self && - ` ${ - vote.self.hasVoted - ? "already voted" - : "haven't voted yet" - }`} - - - {vote?.self ? formatLPT(vote.self.votes) : "N/A"} - - - {!vote?.self.hasVoted && proposal.state === "Active" && ( - - - My Voting Power - - - {formatLPT(vote?.self.votes)} LPT - - - )} + Against + + + For + + + Abstain + + + + + + )} - {proposal?.state === "Active" && - vote?.self.hasVoted === false && ( + {/* Ineligible: show info banner + links, no buttons, no reason */} + {isIneligible && ( + + - 0)} - variant="red" - size="4" - choiceId={0} - proposalId={proposal?.id} - reason={reason} - > - Against - - 0)} - variant="primary" - choiceId={1} - size="4" - proposalId={proposal?.id} - reason={reason} + /> + + - For - - 0)} - variant="gray" - size="4" - choiceId={2} - proposalId={proposal?.id} - reason={reason} + Ineligible to vote + + - Abstain - - - 0)} - /> + You had 0 LPT staked on{" "} + {proposal.votes.voteStartTime.format("MMM D, YYYY")} when + this proposal was created. + + + + Learn about stake snapshots + + - )} + + + )} - {["Succeeded", "Queued"].includes(proposal?.state) && ( - - - - )} - - ) : ( + {/* Queue/Execute buttons for passed proposals */} + {["Succeeded", "Queued"].includes(proposal?.state) && ( + + + + )} + + ) : ( + /* No wallet connected */ + + Your vote - + Connect your wallet to vote. - )} - + + )} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed8a1625..97f96d23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8015,8 +8015,8 @@ packages: numbro@2.5.0: resolution: {integrity: sha512-xDcctDimhzko/e+y+Q2/8i3qNC9Svw1QgOkSkQoO0kIPI473tR9QRbo2KP88Ty9p8WbPy+3OpTaAIzehtuHq+A==} - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} obj-multiplex@1.0.0: resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} @@ -19797,7 +19797,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 + nwsapi: 2.2.23 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -20929,7 +20929,7 @@ snapshots: dependencies: bignumber.js: 9.3.1 - nwsapi@2.2.22: {} + nwsapi@2.2.23: {} obj-multiplex@1.0.0: dependencies: @@ -22515,14 +22515,6 @@ snapshots: tmpl@1.0.5: {} - tldts-core@6.1.86: {} - - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - - tmpl@1.0.5: {} - to-buffer@1.2.2: dependencies: isarray: 2.0.5