ghost-dao-interface/src/containers/Governance/ProposalDetails.jsx
Uncle Fatso 20f2e78ae7
draft implementation of governance page
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-01-30 15:13:52 +03:00

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;