335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
import { useState, useMemo } from "react";
|
|
import { useNavigate, useParams } 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 { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
|
|
|
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 {
|
|
convertStatusToColor,
|
|
convertStatusToLabel,
|
|
MY_PROPOSALS_PREFIX,
|
|
VOTED_PROPOSALS_PREFIX
|
|
} from "../helpers";
|
|
|
|
import { useScreenSize } from "../../../hooks/useScreenSize";
|
|
|
|
import { useProposals } from "../../../hooks/governance";
|
|
import { useLocalStorage } from "../../../hooks/localstorage";
|
|
|
|
const MAX_PROPOSALS_TO_SHOW = 10;
|
|
|
|
const ProposalsList = ({ chainId, address, config }) => {
|
|
const isSmallScreen = useScreenSize("md");
|
|
const navigate = useNavigate();
|
|
const theme = useTheme();
|
|
|
|
const { network } = useParams();
|
|
const { getStorageValue, setStorageValue } = useLocalStorage();
|
|
|
|
const [proposalsFilter, setProposalFilter] = useState("active");
|
|
|
|
const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []);
|
|
const [myProposals, setMyProposals] = useState(() => myStoredProposals.map(id => BigInt(id)));
|
|
|
|
const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []);
|
|
const [votedProposals, setVotedProposals] = useState(() => 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(`${network}/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(`/${network}/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={{ width: "180px", padding: "8px 0" }}></TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
|
|
<TableBody>{children}</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
|
|
const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
|
|
const theme = useTheme();
|
|
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)}
|
|
background={convertStatusToColor(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={proposal?.voteValue ?? 0}
|
|
target={proposal?.voteTarget ?? 50}
|
|
/>
|
|
</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)}
|
|
background={convertStatusToColor(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={proposal?.voteValue ?? 0}
|
|
target={proposal?.voteTarget ?? 50}
|
|
/>
|
|
</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 alreadyHappened = blockNumber > deadline;
|
|
const diff = alreadyHappened ? blockNumber - deadline : deadline - blockNumber;
|
|
const voteSeconds = Number(diff * networkAvgBlockSpeed(chainId));
|
|
|
|
const result = prettifySeconds(voteSeconds, "min");
|
|
if (result === "now") {
|
|
return new Date(Date.now()).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
const prefix = alreadyHappened ? "" : "in ";
|
|
const postfix = alreadyHappened ? " ago" : "";
|
|
|
|
return `${prefix}${result}${postfix}`;
|
|
}
|
|
|
|
export default ProposalsList;
|