245 lines
12 KiB
JavaScript
245 lines
12 KiB
JavaScript
import { useEffect, useState, useMemo } from "react";
|
|
import ReactGA from "react-ga4";
|
|
import { useParams } from 'react-router-dom';
|
|
|
|
import { Box, Container, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
|
|
import Paper from "../../components/Paper/Paper";
|
|
import PageTitle from "../../components/PageTitle/PageTitle";
|
|
import LinearProgressBar from "../../components/Progress/LinearProgressBar";
|
|
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
|
import Chip from "../../components/Chip/Chip";
|
|
import { SecondaryButton } from "../../components/Button";
|
|
|
|
import { formatNumber } from "../../helpers";
|
|
import { prettifySecondsInDays } from "../../helpers/timeUtil";
|
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
|
|
|
import ProposalDiscussionModal from "./components/ProposalDiscussionModal";
|
|
import ProposalDiscussion from "./components/ProposalDiscussion";
|
|
import { convertStatusToTemplate } from "./helpers";
|
|
|
|
import { useTokenSymbol, useTotalSupply, useBalance } from "../../hooks/tokens";
|
|
import {
|
|
useProposalStatus,
|
|
useProposalProposer,
|
|
useProposalLocked,
|
|
useProposalQuorum,
|
|
useProposalVotes,
|
|
useProposalSnapshot,
|
|
useProposalDeadline,
|
|
useProposalVotingDelay
|
|
} from "../../hooks/governance";
|
|
|
|
///////////////////////////////////////////////////////
|
|
import Timeline from '@mui/lab/Timeline';
|
|
import TimelineItem from '@mui/lab/TimelineItem';
|
|
import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
|
import TimelineConnector from '@mui/lab/TimelineConnector';
|
|
import TimelineContent from '@mui/lab/TimelineContent';
|
|
import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent';
|
|
import TimelineDot from '@mui/lab/TimelineDot';
|
|
///////////////////////////
|
|
import FastfoodIcon from '@mui/icons-material/Fastfood';
|
|
import LaptopMacIcon from '@mui/icons-material/LaptopMac';
|
|
import HotelIcon from '@mui/icons-material/Hotel';
|
|
import RepeatIcon from '@mui/icons-material/Repeat';
|
|
|
|
const HUNDRED = new DecimalBigNumber(100n, 0);
|
|
|
|
const ProposalDetails = ({ chainId, address }) => {
|
|
const { id } = useParams();
|
|
const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined);
|
|
|
|
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
|
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
|
|
|
const theme = useTheme();
|
|
|
|
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
|
const { balance } = useBalance(chainId, "GHST", address);
|
|
const { totalSupply } = useTotalSupply(chainId, "GHST"); // TODO: revisit
|
|
|
|
const { status: proposalStatus } = useProposalStatus(chainId, id);
|
|
const { proposer: proposalProposer } = useProposalProposer(chainId, id);
|
|
const { locked: proposalLocked } = useProposalLocked(chainId, id);
|
|
const { quorum: proposalQuorum } = useProposalQuorum(chainId, id);
|
|
const { forVotes, againstVotes } = useProposalVotes(chainId, id);
|
|
|
|
useEffect(() => {
|
|
ReactGA.send({ hitType: "pageview", page: `/governance/${id}` });
|
|
}, []);
|
|
|
|
const isDiscussionModalOpened = useMemo(() => {
|
|
return selectedDiscussionUrl !== undefined;
|
|
}, [selectedDiscussionUrl]);
|
|
|
|
return (
|
|
<>
|
|
<ProposalDiscussionModal
|
|
url={selectedDiscussionUrl}
|
|
isOpened={isDiscussionModalOpened}
|
|
closeModal={() => setSelectedDiscussionUrl(undefined)}
|
|
/>
|
|
<Box>
|
|
<PageTitle name={`GBP: ${id} - NAME`} subtitle={`By: ${proposalProposer} | BONDED: $${proposalLocked} ${ghstSymbol}`} />
|
|
<Container
|
|
style={{
|
|
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
|
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
|
minHeight: "calc(100vh - 128px)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
justifyContent: "center"
|
|
}}
|
|
>
|
|
<Box sx={{ mt: "15px" }}>
|
|
<Box display="flex" justifyContent="space-between" gap="20px">
|
|
<Paper
|
|
fullWidth
|
|
enableBackground
|
|
headerContent={
|
|
<Box display="flex" alignItems="center" flexDirection="row" gap="10px">
|
|
<Typography variant="h6">
|
|
Progress
|
|
</Typography>
|
|
<Chip
|
|
sx={{ marginTop: "4px", width: "88px" }}
|
|
label={proposalStatus}
|
|
template={convertStatusToTemplate(proposalStatus)}
|
|
/>
|
|
</Box>
|
|
}
|
|
topRight={
|
|
<ProposalDiscussion onClick={() => setSelectedDiscussionUrl("dicks")} />
|
|
}
|
|
>
|
|
<Box height="220px" display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
|
|
<Box display="flex" flexDirection="column">
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Typography variant="body2" color={theme.colors.feedback.success}>
|
|
For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forVotes * HUNDRED / proposalQuorum, 1)}%)
|
|
</Typography>
|
|
<Typography variant="body2" color={theme.colors.feedback.error}>
|
|
Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstVotes * HUNDRED / proposalQuorum, 1)}%)
|
|
</Typography>
|
|
</Box>
|
|
<LinearProgressBar
|
|
barColor={theme.colors.feedback.success}
|
|
barBackground={theme.colors.feedback.error}
|
|
variant="determinate"
|
|
value={69}
|
|
target={Math.floor(Math.random() * 101)}
|
|
/>
|
|
</Box>
|
|
|
|
<Box display="flex" flexDirection="column">
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Box display="flex" flexDirection="row">
|
|
<Typography>Quorum</Typography>
|
|
<InfoTooltip message="Minimum number of voting power required to be present to make proposal executable" />
|
|
</Box>
|
|
<Typography>{formatNumber(proposalQuorum.toString(), 4)}</Typography>
|
|
</Box>
|
|
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Box display="flex" flexDirection="row">
|
|
<Typography>Total</Typography>
|
|
<InfoTooltip message="Total number of votes available for proposal" />
|
|
</Box>
|
|
<Typography>{formatNumber(totalSupply.toString(), 4)}</Typography>
|
|
</Box>
|
|
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Box display="flex" flexDirection="row">
|
|
<Typography>Votes</Typography>
|
|
<InfoTooltip message="Voting power of the connected wallet" />
|
|
</Box>
|
|
<Typography>{formatNumber(balance.toString(), 4)}</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box display="flex" gap="20px">
|
|
<SecondaryButton fullWidth onClick={() => alert("For vote casted")}>For</SecondaryButton>
|
|
<SecondaryButton fullWidth onClick={() => alert("Against vote casted")}>Against</SecondaryButton>
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
|
|
<Paper
|
|
fullWidth
|
|
enableBackground
|
|
headerContent={
|
|
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
|
<Typography variant="h6">
|
|
Timeline
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
<VotingTimeline chainId={chainId} proposalId={id} />
|
|
</Paper>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Paper
|
|
fullWidth
|
|
enableBackground
|
|
headerContent={
|
|
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
|
<Typography variant="h6">
|
|
Executable Code
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
Here will be a list of decoded calldatas
|
|
</Paper>
|
|
</Container>
|
|
</Box>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const VotingTimeline = ({ proposalId, chainId }) => {
|
|
const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId);
|
|
const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId);
|
|
const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId);
|
|
|
|
const voteStarted = useMemo(() => {
|
|
if (proposalSnapshot && propsalVotingDelay) {
|
|
return proposalSnapshot > propsalVotingDelay ? proposalSnapshot - propsalVotingDelay : 0;
|
|
}
|
|
return 0;
|
|
}, [proposalSnapshot, propsalVotingDelay]);
|
|
|
|
return (
|
|
<Timeline sx={{ margin: 0, padding: 0 }}>
|
|
<VotingTimelineItem time={voteStarted} message="Proposed on:" isFirst />
|
|
<VotingTimelineItem time={proposalSnapshot} message="Voting started:" />
|
|
<VotingTimelineItem time={proposalDeadline} message="Voting ends:" />
|
|
</Timeline>
|
|
)
|
|
}
|
|
|
|
const VotingTimelineItem = ({ isFirst, isLast, time, message }) => {
|
|
return (
|
|
<TimelineItem>
|
|
<TimelineOppositeContent sx={{ display: "none" }} />
|
|
<TimelineSeparator>
|
|
{!isFirst && <TimelineConnector sx={{ background: "#fff" }} />}
|
|
<TimelineDot sx={{ width: "15px", height: "15px", background: "#fff" }}></TimelineDot>
|
|
{!isLast && <TimelineConnector sx={{ background: "#fff" }} />}
|
|
</TimelineSeparator>
|
|
|
|
<TimelineContent>
|
|
<Typography>{message}</Typography>
|
|
<Typography component="span">{new Date(time * 1000).toLocaleString()}</Typography>
|
|
</TimelineContent>
|
|
</TimelineItem>
|
|
)
|
|
}
|
|
|
|
export default ProposalDetails;
|