diff --git a/package.json b/package.json index 4e976a5..b7d74be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.5.7", + "version": "0.5.8", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/Select/Select.jsx b/src/components/Select/Select.jsx new file mode 100644 index 0000000..2a940b9 --- /dev/null +++ b/src/components/Select/Select.jsx @@ -0,0 +1,72 @@ +import { Box, MenuItem, Select as MuiSelect, Typography, useTheme } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + +const StyledSelectInput = styled(MuiSelect, { + shouldForwardProp: prop => prop !== "inputWidth" +})(({ theme, inputWidth }) => ({ + width: "100%", + "& .MuiSelect-select": { + padding: 0, + height: "24px !important", + minHeight: "24px !important", + display: "flex", + alignItems: "center", + fontSize: "18px", + fontWeight: 500, + paddingRight: "24px", + }, + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + "& .MuiSelect-icon": { + right: 0, + position: "absolute", + color: theme.colors.gray[500], + } +})); + +const Select = ({ + label, + value, + onChange, + options, + inputWidth, + width = "100%", +}) => { + const theme = useTheme(); + return ( + + {label && ( + + {label} + + )} + + + + {options.map((opt) => ( + + {opt.label} + + ))} + + + + ); +}; + +export default Select; diff --git a/src/components/Swap/SwapCard.jsx b/src/components/Swap/SwapCard.jsx index 4f7b416..160549e 100644 --- a/src/components/Swap/SwapCard.jsx +++ b/src/components/Swap/SwapCard.jsx @@ -7,7 +7,7 @@ import Token from "../Token/Token"; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })( +export const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })( ({ inputWidth, inputFontSize }) => ({ "& .MuiInputBase-input": { padding: 0, diff --git a/src/containers/Bond/Bonds.jsx b/src/containers/Bond/Bonds.jsx index 20ee595..55c7ad0 100644 --- a/src/containers/Bond/Bonds.jsx +++ b/src/containers/Bond/Bonds.jsx @@ -1,4 +1,4 @@ -import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material"; +import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material"; import { useEffect, useState } from "react"; import ReactGA from "react-ga4"; diff --git a/src/containers/Governance/NewProposal.jsx b/src/containers/Governance/NewProposal.jsx index b377792..c6dbfd8 100644 --- a/src/containers/Governance/NewProposal.jsx +++ b/src/containers/Governance/NewProposal.jsx @@ -1,75 +1,153 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { Box, Container, + TableContainer, + Table, + TableRow, + TableBody, + TableHead, + TableCell, Typography, + Link, OutlinedInput, InputLabel, FormControl, useMediaQuery, + useTheme } from "@mui/material"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; + import Paper from "../../components/Paper/Paper"; import PageTitle from "../../components/PageTitle/PageTitle"; -import { PrimaryButton } from "../../components/Button"; +import { PrimaryButton, TertiaryButton } from "../../components/Button"; + +import ProposalModal from "./components/ProposalModal"; +import { parseFunctionCalldata } from "./components/functions/index"; const NewProposal = ({ config, address, connect, chainId }) => { const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); - const [descriptionUrl, setDescriptionUrl] = useState(""); + const theme = useTheme(); + + const [isModalOpened, setIsModalOpened] = useState(false); + const [proposalFunctions, setProposalFunctions] = useState([]); + + const addCalldata = (calldata) => setProposalFunctions(prev => [...prev, calldata]); + const removeCalldata = (index) => setProposalFunctions(prev => prev.filter((_, i) => i !== index)); + + const nativeCurrency = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.nativeCurrency?.symbol; + }, [config]); + + const submitProposal = () => { + alert("Proposal created"); + setProposalFunctions([]); + } return ( - - - - - - - Proposal Overview - + <> + setIsModalOpened(false)} /> + + + + + + + Proposal Functions + + + } + > + + + {proposalFunctions.length === 0 && + + Create new proposal by adding functions below + + + Learn more  + + + } + + + + setIsModalOpened(true)}>Add New + submitProposal()}>Submit Proposal + - } - > - - - - - - ) -} + -const BasicInput = ({ id, label, value, eventHandler }) => { - return ( - - {label} - - - eventHandler(event.currentTarget.value)} - /> - + {proposalFunctions.length > 0 && + + Proposal Functions + + + } + > + + + + + Function + Target + Value + + + {proposalFunctions.map((calldata, index) => { + return parseFunctionCalldata(calldata, index, nativeCurrency, removeCalldata); + })} +
+
+ } +
+ - + ) } diff --git a/src/containers/Governance/ProposalDetails.jsx b/src/containers/Governance/ProposalDetails.jsx index 88a3e06..3a21b63 100644 --- a/src/containers/Governance/ProposalDetails.jsx +++ b/src/containers/Governance/ProposalDetails.jsx @@ -2,7 +2,7 @@ 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 { Box, Container, Typography, Link, useMediaQuery, useTheme } from "@mui/material"; import Paper from "../../components/Paper/Paper"; import PageTitle from "../../components/PageTitle/PageTitle"; @@ -11,12 +11,13 @@ import InfoTooltip from "../../components/Tooltip/InfoTooltip"; import Chip from "../../components/Chip/Chip"; import { SecondaryButton } from "../../components/Button"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; + 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"; @@ -65,7 +66,7 @@ const ProposalDetails = ({ chainId, address }) => { const { proposer: proposalProposer } = useProposalProposer(chainId, id); const { locked: proposalLocked } = useProposalLocked(chainId, id); const { quorum: proposalQuorum } = useProposalQuorum(chainId, id); - const { forVotes, againstVotes } = useProposalVotes(chainId, id); + const { forVotes, againstVotes, totalVotes } = useProposalVotes(chainId, id); useEffect(() => { ReactGA.send({ hitType: "pageview", page: `/governance/${id}` }); @@ -75,130 +76,153 @@ const ProposalDetails = ({ chainId, address }) => { return selectedDiscussionUrl !== undefined; }, [selectedDiscussionUrl]); + const quorumPercentage = useMemo(() => { + if (totalSupply._value === 0n) return 0; + return proposalQuorum / totalSupply * HUNDRED; + }, [proposalQuorum, totalSupply]); + + const votePercentage = useMemo(() => { + if (totalSupply._value === 0n) return 0; + return totalVotes / totalSupply * HUNDRED; + }, [totalVotes, totalSupply]); + + const voteWeightPercentage = useMemo(() => { + if (totalSupply._value === 0n) return 0; + return balance / totalSupply * HUNDRED; + }, [balance, totalSupply]); + return ( - <> - setSelectedDiscussionUrl(undefined)} - /> - - - - - - - - Progress + + + + + + + + Progress + + + + } + topRight={ + + View Forum  + + + } + > + + + + + For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forVotes * HUNDRED / proposalQuorum, 1)}%) + + + Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstVotes * HUNDRED / proposalQuorum, 1)}%) - - } - topRight={ - setSelectedDiscussionUrl("dicks")} /> - } - > - - - - - For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forVotes * HUNDRED / proposalQuorum, 1)}%) - - - Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstVotes * HUNDRED / proposalQuorum, 1)}%) - + + + + + + + Quorum + - + {formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%) - - - - Quorum - - - {formatNumber(proposalQuorum.toString(), 4)} - - - - - Total - - - {formatNumber(totalSupply.toString(), 4)} - - - - - Votes - - - {formatNumber(balance.toString(), 4)} + + + Total + + {formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%) - - alert("For vote casted")}>For - alert("Against vote casted")}>Against + + + Votes + + + {formatNumber(balance.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%) - - - - Timeline - - - } - > - - - - - - - - Executable Code - + + alert("For vote casted")}>For + alert("Against vote casted")}>Against + - } - > - Here will be a list of decoded calldatas - - - - + + + + + Timeline + + + } + > + + + + + + + + Executable Code + + + } + > + Here will be a list of decoded calldatas + + + ) } diff --git a/src/containers/Governance/components/Metric.jsx b/src/containers/Governance/components/Metric.jsx index 1b79ad7..9ee5822 100644 --- a/src/containers/Governance/components/Metric.jsx +++ b/src/containers/Governance/components/Metric.jsx @@ -37,7 +37,7 @@ export const ProposalThreshold = props => { const _props = { ...props, - label: `$${props.ghstSymbol} Threshold`, + label: "Min Collateral", tooltip: `Minimum $${props.ghstSymbol} required to be locked to create a proposal`, }; @@ -56,7 +56,7 @@ export const ProposalsCount = props => { tooltip: `Total proposals created`, }; - if (proposalsCount) _props.metric = proposalsCount.toString(); + if (proposalsCount) _props.metric = `${formatNumber(proposalsCount.toString(), 0)}`; else _props.isLoading = true; return ; diff --git a/src/containers/Governance/components/ProposalDiscussion.jsx b/src/containers/Governance/components/ProposalDiscussion.jsx deleted file mode 100644 index 56d4f54..0000000 --- a/src/containers/Governance/components/ProposalDiscussion.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Link } from "@mui/material"; -import GhostStyledIcon from "../../../components/Icon/GhostIcon"; -import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react"; - -const ProposalDiscussion = (linkProps) => { - return ( - - Learn more  - - - ) -} - -export default ProposalDiscussion; diff --git a/src/containers/Governance/components/ProposalDiscussionModal.jsx b/src/containers/Governance/components/ProposalDiscussionModal.jsx deleted file mode 100644 index ac2e902..0000000 --- a/src/containers/Governance/components/ProposalDiscussionModal.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState } from "react"; -import { Box, Typography, Link } from "@mui/material"; - -import Modal from "../../../components/Modal/Modal"; -import { PrimaryButton } from "../../../components/Button"; - -const ProposalDiscussionModal = ({ isOpened, closeModal, url }) => { - const [isCopied, setIsCopied] = useState(false); - - const copyToClipboard = () => { - navigator.clipboard.writeText(url) - .then(() => { - isCopied(true); - setTimeout(() => setIsCopied(false), 2000); - }) - .catch(err => console.error(err)); - } - - return ( - - Discussion URL - - } - open={isOpened} - onClose={closeModal} - maxWidth="460px" - minHeight="200px" - > - - - - You are leaving the ghost dao app. Check the link on your own, we are not in charge of your destiny. - - - {url} - - - - window.open(url, '_blank', 'noopener,noreferrer')} - > - Open - - - - ) -} - -export default ProposalDiscussionModal; diff --git a/src/containers/Governance/components/ProposalModal.jsx b/src/containers/Governance/components/ProposalModal.jsx new file mode 100644 index 0000000..8d94de1 --- /dev/null +++ b/src/containers/Governance/components/ProposalModal.jsx @@ -0,0 +1,100 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { Box, Typography } from "@mui/material"; + +import Modal from "../../../components/Modal/Modal"; +import Select from "../../../components/Select/Select"; +import { PrimaryButton, TertiaryButton } from "../../../components/Button"; + +import { + getFunctionArguments, + getFunctionCalldata, + getFunctionDescription, + allPossibleFunctions +} from "./functions"; + +const ProposalModal = ({ isOpened, closeModal, addCalldata }) => { + const [selectedOption, setSelectedOption] = useState(); + const [renderArguments, setRenderArguments] = useState(false); + + const handleChange = (event) => { + setSelectedOption(event.target.value); + }; + + const headerLabel = useMemo(() => { + const data = allPossibleFunctions.find(obj => obj.value === selectedOption); + if (data && renderArguments) { + return data.label; + } + return "Executable Code"; + }, [selectedOption, renderArguments]); + + const handleCalldata = useCallback(() => { + addCalldata(getFunctionCalldata(selectedOption)); + setSelectedOption(null); + closeModal(); + }, [selectedOption]); + + const handleClose = () => { + setSelectedOption(null); + setRenderArguments(false); + closeModal(); + } + + const handleAddCalldata = (calldata) => { + addCalldata(calldata); + handleClose(); + } + + const ArgumentsSteps = useMemo(() => getFunctionArguments(selectedOption), [selectedOption]); + + return ( + + {headerLabel} + + } + open={isOpened} + onClose={handleClose} + maxWidth="460px" + minHeight="200px" + > + + {renderArguments + ? setRenderArguments(false)} + /> + : setRenderArguments(true)} + ready={ArgumentsSteps !== null} + /> + } + + + ) +} + +const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProceed, ready }) => { + const functionDescription = useMemo(() => getFunctionDescription(selectedOption), [selectedOption]); + return ( + +