361 lines
13 KiB
JavaScript
361 lines
13 KiB
JavaScript
import { useState, useMemo } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import {
|
|
Box,
|
|
Link,
|
|
Tabs,
|
|
Tab,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
useTheme,
|
|
useMediaQuery
|
|
} from "@mui/material";
|
|
import { getBlockNumber } from "@wagmi/core";
|
|
|
|
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
|
|
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
|
|
|
|
import { networkAvgBlockSpeed } from "../../../constants";
|
|
import { prettifySecondsInDays, prettifySeconds } from "../../../helpers/timeUtil";
|
|
|
|
import Chip from "../../../components/Chip/Chip";
|
|
import Modal from "../../../components/Modal/Modal";
|
|
import Paper from "../../../components/Paper/Paper";
|
|
import LinearProgressBar from "../../../components/Progress/LinearProgressBar";
|
|
import { PrimaryButton, TertiaryButton } from "../../../components/Button";
|
|
|
|
import ProposalInfoText from "./ProposalInfoText";
|
|
import {
|
|
convertStatusToTemplate,
|
|
convertStatusToLabel,
|
|
MY_PROPOSALS_PREFIX,
|
|
VOTED_PROPOSALS_PREFIX
|
|
} from "../helpers";
|
|
|
|
import { useScreenSize } from "../../../hooks/useScreenSize";
|
|
|
|
import {
|
|
useProposals,
|
|
} from "../../../hooks/governance";
|
|
|
|
const MAX_PROPOSALS_TO_SHOW = 10;
|
|
|
|
const ProposalsList = ({ chainId, address, config }) => {
|
|
const isSmallScreen = useScreenSize("md");
|
|
const navigate = useNavigate();
|
|
const theme = useTheme();
|
|
|
|
const [proposalsFilter, setProposalFilter] = useState("active");
|
|
|
|
const myStoredProposals = localStorage.getItem(MY_PROPOSALS_PREFIX);
|
|
const [myProposals, setMyProposals] = useState(
|
|
myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : []
|
|
);
|
|
|
|
const storedVotedProposals = localStorage.getItem(VOTED_PROPOSALS_PREFIX);
|
|
const [votedProposals, setVotedProposals] = useState(
|
|
storedVotedProposals ? JSON.parse(storedVotedProposals).map(id => BigInt(id)) : []
|
|
);
|
|
|
|
const searchedIndexes = useMemo(() => {
|
|
switch (proposalsFilter) {
|
|
case "voted":
|
|
return votedProposals;
|
|
case "created":
|
|
return myProposals;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}, [proposalsFilter]);
|
|
|
|
const [blockNumber, setBlockNumber] = useState(0n);
|
|
const { proposals } = useProposals(chainId, MAX_PROPOSALS_TO_SHOW, searchedIndexes);
|
|
|
|
getBlockNumber(config).then(block => setBlockNumber(block));
|
|
|
|
if (proposals?.length === 0 && proposalsFilter === "active") {
|
|
return (
|
|
<Box display="flex" justifyContent="center">
|
|
<Typography variant="h4">No proposals yet</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (isSmallScreen) {
|
|
return (
|
|
<Paper
|
|
fullWidth
|
|
enableBackground
|
|
headerContent={
|
|
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
|
<Typography variant="h6">
|
|
Proposals
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
<Box display="flex" flexDirection="column" gap="40px">
|
|
{proposals?.map(proposal => (
|
|
<ProposalCard
|
|
key={proposal.hashes.short}
|
|
proposal={proposal}
|
|
blockNumber={blockNumber}
|
|
chainId={chainId}
|
|
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
|
|
/>
|
|
))}
|
|
</Box>
|
|
|
|
{proposalsFilter === "active" && <Box my="24px" textAlign="center">
|
|
<ProposalInfoText />
|
|
</Box>}
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Paper
|
|
fullWidth
|
|
enableBackground
|
|
headerContent={
|
|
<Box
|
|
width="100%"
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
flexDirection="row"
|
|
gap="5px"
|
|
>
|
|
<Typography variant="h6">
|
|
Proposals
|
|
</Typography>
|
|
|
|
<PrimaryButton
|
|
variant="text"
|
|
href="https://forum.ghostchain.io"
|
|
>
|
|
View Forum
|
|
</PrimaryButton>
|
|
</Box>
|
|
}
|
|
>
|
|
<ProposalFilterTrigger trigger={proposalsFilter} setTrigger={setProposalFilter} />
|
|
|
|
<ProposalTable>
|
|
{proposals?.map(proposal => (
|
|
<ProposalRow
|
|
key={proposal.hashes.short}
|
|
proposal={proposal}
|
|
blockNumber={blockNumber}
|
|
chainId={chainId}
|
|
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
|
|
/>
|
|
))}
|
|
</ProposalTable>
|
|
|
|
{proposalsFilter === "active" && <Box mt="24px" textAlign="center" width="70%" mx="auto">
|
|
<ProposalInfoText />
|
|
</Box>}
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const ProposalTable = ({ children }) => (
|
|
<TableContainer>
|
|
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell align="center" style={{ width: "130px", padding: "8px 0" }}>Proposal ID</TableCell>
|
|
<TableCell align="center" style={{ width: "130px", padding: "8px 0" }}>Status</TableCell>
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>Vote Ends</TableCell>
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>Voting Stats</TableCell>
|
|
<TableCell align="center" style={{ padding: "8px 0" }}></TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
|
|
<TableBody>{children}</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
|
|
const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
|
|
const theme = useTheme();
|
|
|
|
const voteValue = useMemo(() => {
|
|
const againstVotes = proposal?.votes?.at(0)?._value ?? 0n;
|
|
const forVotes = proposal?.votes?.at(1)?._value ?? 0n;
|
|
const totalVotes = againstVotes + forVotes;
|
|
if (totalVotes == 0) {
|
|
return 0;
|
|
}
|
|
return Number(forVotes * 100n / totalVotes);
|
|
}, [proposal]);
|
|
|
|
const voteTarget = useMemo(() => {
|
|
const againstVotes = proposal?.votes?.at(0)?._value ?? 0n;
|
|
const forVotes = proposal?.votes?.at(1)?._value ?? 0n;
|
|
const totalVotes = againstVotes + forVotes;
|
|
|
|
const first = (5n * againstVotes + forVotes);
|
|
const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes))));
|
|
const bias = 3n * first + second;
|
|
const denominator = totalVotes + bias;
|
|
|
|
if (denominator === 0n) {
|
|
return 80;
|
|
}
|
|
|
|
return Number(totalVotes * 100n / denominator);
|
|
}, [proposal]);
|
|
|
|
return (
|
|
<TableRow id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>
|
|
<Typography>GDP-{proposal.hashes.short}</Typography>
|
|
</TableCell>
|
|
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>
|
|
<Chip
|
|
sx={{ width: "100px" }}
|
|
label={convertStatusToLabel(proposal.state)}
|
|
template={convertStatusToTemplate(proposal.state)}
|
|
/>
|
|
</TableCell>
|
|
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>
|
|
<Typography>
|
|
{convertDeadline(
|
|
proposal.deadline,
|
|
blockNumber,
|
|
chainId
|
|
)}
|
|
</Typography>
|
|
</TableCell>
|
|
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>
|
|
<Box marginLeft="15px" marginRight="15px">
|
|
<LinearProgressBar
|
|
barColor={theme.colors.feedback.success}
|
|
barBackground={theme.colors.feedback.error}
|
|
variant="determinate"
|
|
value={voteValue}
|
|
target={voteTarget}
|
|
/>
|
|
</Box>
|
|
</TableCell>
|
|
|
|
<TableCell align="center" style={{ padding: "8px 0" }}>
|
|
{(proposal.state === "Active" || proposal.state === "Succeeded") && <PrimaryButton
|
|
fullWidth
|
|
onClick={() => openProposal()}
|
|
sx={{ maxWidth: "130px" }}
|
|
>
|
|
{proposal.state === "Succeeded" ? "Execute" : "Vote"}
|
|
</PrimaryButton>}
|
|
{(proposal.state !== "Active" && proposal.state !== "Succeeded") && <TertiaryButton
|
|
fullWidth
|
|
onClick={() => openProposal()}
|
|
sx={{ alignSelf: "right", maxWidth: "130px" }}
|
|
>
|
|
View
|
|
</TertiaryButton>}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
|
|
const theme = useTheme();
|
|
const isSmallScreen = useMediaQuery('(max-width: 450px)');
|
|
|
|
return (
|
|
<Box id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
|
|
<Box display="flex" flexDirection={isSmallScreen ? "column" : "row"} justifyContent="space-between">
|
|
<Box display="flex" flexDirection="column" width="100%">
|
|
<Box display="flex" flexDirection="row" alignItems="center" width="100%" gap="10px">
|
|
<Typography variant="h3">GIP-{proposal.hashes.short}</Typography>
|
|
<Chip
|
|
sx={{ width: "88px" }}
|
|
label={convertStatusToLabel(proposal.state)}
|
|
template={convertStatusToTemplate(proposal.state)}
|
|
/>
|
|
</Box>
|
|
<Typography>
|
|
{convertDeadline(
|
|
proposal.deadline,
|
|
blockNumber,
|
|
chainId
|
|
)}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box marginTop="15px" marginBottom="15px">
|
|
<LinearProgressBar
|
|
barColor={theme.colors.feedback.success}
|
|
barBackground={theme.colors.feedback.error}
|
|
variant="determinate"
|
|
value={69}
|
|
target={Math.floor(Math.random() * 101)}
|
|
/>
|
|
</Box>
|
|
<Box marginBottom="20px">
|
|
{(proposal.state === "Active" || proposal.state === "Succeeded") && <PrimaryButton
|
|
fullWidth
|
|
onClick={() => openProposal()}
|
|
>
|
|
{proposal.state === "Succeeded" ? "Execute" : "Vote"}
|
|
</PrimaryButton>}
|
|
{(proposal.state !== "Active" && proposal.state !== "Succeeded") && <TertiaryButton
|
|
fullWidth
|
|
onClick={() => openProposal()}
|
|
>
|
|
View
|
|
</TertiaryButton>}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const ProposalFilterTrigger = ({ trigger, setTrigger }) => {
|
|
return (
|
|
<Tabs
|
|
centered
|
|
textColor="primary"
|
|
indicatorColor="primary"
|
|
value={trigger}
|
|
aria-label="Proposal filter tabs"
|
|
onChange={(_, view) => setTrigger(view)}
|
|
TabIndicatorProps={{ style: { display: "none" } }}
|
|
>
|
|
<Tab aria-label="proposal-filter-active-button" value="active" label="Active" style={{ fontSize: "1rem" }} />
|
|
<Tab aria-label="proposal-filter-voted-button" value="voted" label="Voted" style={{ fontSize: "1rem" }} />
|
|
<Tab aria-label="proposal-filter-created-button" value="created" label="Created" style={{ fontSize: "1rem" }} />
|
|
</Tabs>
|
|
)
|
|
}
|
|
|
|
const convertDeadline = (deadline, blockNumber, chainId) => {
|
|
const diff = blockNumber > deadline ? blockNumber - deadline : deadline - blockNumber;
|
|
const voteSeconds = Number(diff * networkAvgBlockSpeed(chainId));
|
|
|
|
const result = prettifySeconds(voteSeconds, "mins");
|
|
if (result === "now") {
|
|
return new Date(Date.now()).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
return `in ${result}`;
|
|
}
|
|
|
|
export default ProposalsList;
|