ghost-dao-interface/src/containers/Governance/components/ProposalsList.jsx
Uncle Fatso a79a877838
fix for the percentage calculations
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-02-18 18:38:49 +03:00

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;