draft implementation of governance page

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-01-30 15:13:52 +03:00
parent a2a4b86ccc
commit 20f2e78ae7
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
17 changed files with 1232 additions and 11 deletions

View File

@ -1,7 +1,7 @@
{
"name": "ghost-dao-interface",
"private": true,
"version": "0.4.4",
"version": "0.5.1",
"type": "module",
"scripts": {
"dev": "vite",
@ -16,6 +16,7 @@
"@ethersproject/bignumber": "^5.8.0",
"@ethersproject/units": "^5.8.0",
"@mui/icons-material": "^6.4.7",
"@mui/lab": "6.0.1-beta.36",
"@mui/material": "^6.4.7",
"@mui/utils": "^6.4.6",
"@polkadot-api/metadata-builders": "0.13.0",

View File

@ -26,6 +26,9 @@ importers:
'@mui/icons-material':
specifier: ^6.4.7
version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@mui/lab':
specifier: 6.0.1-beta.36
version: 6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mui/material':
specifier: ^6.4.7
version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -525,6 +528,21 @@ packages:
'@ethersproject/units@5.8.0':
resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==}
'@floating-ui/core@1.7.4':
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
'@floating-ui/dom@1.7.5':
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
'@floating-ui/react-dom@2.1.7':
resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -638,6 +656,18 @@ packages:
resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==}
engines: {node: '>=16.0.0'}
'@mui/base@5.0.0-beta.70':
resolution: {integrity: sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ==}
engines: {node: '>=14.0.0'}
deprecated: This package has been replaced by @base-ui/react
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/core-downloads-tracker@6.4.7':
resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==}
@ -652,6 +682,27 @@ packages:
'@types/react':
optional: true
'@mui/lab@6.0.1-beta.36':
resolution: {integrity: sha512-af9lDmA9SZGEWF1XXk0EVBpfCITk9IKsvh9lLOZGdYaaHfQeCsqxGEDMvNO66j0P8EYoxpyry84LFCJYuLVtVw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/material': ^6.5.0
'@mui/material-pigment-css': ^6.5.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/material-pigment-css':
optional: true
'@types/react':
optional: true
'@mui/material@6.4.7':
resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==}
engines: {node: '>=14.0.0'}
@ -682,6 +733,16 @@ packages:
'@types/react':
optional: true
'@mui/private-theming@6.4.9':
resolution: {integrity: sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/styled-engine@6.4.6':
resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==}
engines: {node: '>=14.0.0'}
@ -695,6 +756,19 @@ packages:
'@emotion/styled':
optional: true
'@mui/styled-engine@6.5.0':
resolution: {integrity: sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/system@6.4.7':
resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==}
engines: {node: '>=14.0.0'}
@ -711,6 +785,22 @@ packages:
'@types/react':
optional: true
'@mui/system@6.5.0':
resolution: {integrity: sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/types@7.2.21':
resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
peerDependencies:
@ -719,6 +809,14 @@ packages:
'@types/react':
optional: true
'@mui/types@7.2.24':
resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@6.4.6':
resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==}
engines: {node: '>=14.0.0'}
@ -729,6 +827,16 @@ packages:
'@types/react':
optional: true
'@mui/utils@6.4.9':
resolution: {integrity: sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@noble/ciphers@1.2.1':
resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==}
engines: {node: ^14.21.3 || >=16}
@ -3423,6 +3531,23 @@ snapshots:
'@ethersproject/constants': 5.8.0
'@ethersproject/logger': 5.8.0
'@floating-ui/core@1.7.4':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.5':
dependencies:
'@floating-ui/core': 1.7.4
'@floating-ui/utils': 0.2.10
'@floating-ui/react-dom@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@floating-ui/dom': 1.7.5
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@floating-ui/utils@0.2.10': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -3615,6 +3740,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mui/base@5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@floating-ui/react-dom': 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mui/types': 7.2.24(@types/react@19.0.10)
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
'@popperjs/core': 2.11.8
clsx: 2.1.1
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.10
'@mui/core-downloads-tracker@6.4.7': {}
'@mui/icons-material@6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
@ -3625,6 +3764,23 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
'@mui/lab@6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/base': 5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mui/system': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@mui/types': 7.2.24(@types/react@19.0.10)
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
clsx: 2.1.1
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@types/react': 19.0.10
'@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.9
@ -3648,16 +3804,38 @@ snapshots:
'@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.9
'@babel/runtime': 7.27.6
'@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0)
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.10
'@mui/private-theming@6.4.9(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.10
'@mui/styled-engine@6.4.6(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.9
'@babel/runtime': 7.27.6
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
csstype: 3.1.3
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@mui/styled-engine@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
@ -3684,10 +3862,30 @@ snapshots:
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@types/react': 19.0.10
'@mui/system@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/private-theming': 6.4.9(@types/react@19.0.10)(react@19.0.0)
'@mui/styled-engine': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)
'@mui/types': 7.2.24(@types/react@19.0.10)
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
'@types/react': 19.0.10
'@mui/types@7.2.21(@types/react@19.0.10)':
optionalDependencies:
'@types/react': 19.0.10
'@mui/types@7.2.24(@types/react@19.0.10)':
optionalDependencies:
'@types/react': 19.0.10
'@mui/utils@6.4.6(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.9
@ -3700,6 +3898,18 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
'@mui/utils@6.4.9(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/types': 7.2.24(@types/react@19.0.10)
'@types/prop-types': 15.7.14
clsx: 2.1.1
prop-types: 15.8.1
react: 19.0.0
react-is: 19.0.0
optionalDependencies:
'@types/react': 19.0.10
'@noble/ciphers@1.2.1': {}
'@noble/ciphers@1.3.0': {}
@ -5042,7 +5252,7 @@ snapshots:
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.26.9
'@babel/runtime': 7.27.6
cosmiconfig: 7.1.0
resolve: 1.22.10
@ -5238,7 +5448,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.26.9
'@babel/runtime': 7.27.6
csstype: 3.1.3
dot-case@3.0.4:

View File

@ -31,6 +31,8 @@ const Wrapper = lazy(() => import("./containers/WethWrapper/WethWrapper"));
const Dex = lazy(() => import("./containers/Dex/Dex"));
const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
const NotFound = lazy(() => import("./containers/NotFound/NotFound"));
const Governance = lazy(() => import("./containers/Governance/Governance"));
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
const PREFIX = "App";
@ -213,6 +215,8 @@ function App() {
}
<Route path="/bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/dex/:name" element={<Dex connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
</>
}
<Route path="/empty" element={<NotFound

View File

@ -0,0 +1,39 @@
import { Box, LinearProgress as MuiLinearProgress } from "@mui/material";
import { styled } from "@mui/material/styles";
const PREFIX = "MuiLinearProgress";
const classes = {
chip: `${PREFIX}-bar`,
};
const StyledMuiLinearProgress = styled(MuiLinearProgress, {
shouldForwardProp: (prop) => prop !== "barBackground" && prop !== 'barColor' && prop !== 'height'
})(({ theme, barColor, barBackground, height }) => ({
height: height || 8,
borderRadius: 4,
backgroundColor: barBackground || theme.palette.grey[300],
'& .MuiLinearProgress-bar': {
backgroundColor: barColor || theme.palette.primary.main
}
}));
const LinearProgressBar = (props) => {
return (
<Box sx={{ position: 'relative', width: '100%', py: 1 }}>
<StyledMuiLinearProgress {...props} />
{props.target && <Box
sx={{
position: 'absolute',
left: `${props.target}%`,
top: props.targetTop || 0,
bottom: props.targetBottom || 0,
width: props.targetWidth || "2px",
backgroundColor: props.targetBackgroundColor || 'white',
}}
></Box>}
</Box>
)
}
export default LinearProgressBar;

View File

@ -24,6 +24,8 @@ import TelegramIcon from '@mui/icons-material/Telegram';
import HowToVoteIcon from '@mui/icons-material/HowToVote';
import HubIcon from '@mui/icons-material/Hub';
import PublicIcon from '@mui/icons-material/Public';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import GavelIcon from '@mui/icons-material/Gavel';
import ForumIcon from '@mui/icons-material/Forum';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import BookIcon from '@mui/icons-material/Book';
@ -177,7 +179,8 @@ const NavContent = ({ chainId, addressChainId }) => {
}
/>
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
<NavItem icon={PublicIcon} label={`Bridge`} to="/bridge" />
<NavItem icon={ForkRightIcon} label={`Bridge`} to="/bridge" />
<NavItem icon={GavelIcon} label={`Governance`} to="/governance" />
<Box className="menu-divider">
<Divider />
</Box>

View File

@ -1,6 +1,5 @@
import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper";
@ -21,7 +20,6 @@ import { useTokenSymbol } from "../../hooks/tokens";
const Bonds = ({ chainId, address, connect }) => {
const [isZoomed] = useState(false);
const navigate = useNavigate();
const [secondsTo, setSecondsTo] = useState(0);
const isSmallScreen = useMediaQuery("(max-width: 650px)");
@ -29,7 +27,7 @@ const Bonds = ({ chainId, address, connect }) => {
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/bonds" });
}, [])
}, []);
const { liveBonds } = useLiveBonds(chainId);
const totalReserves = useTotalReserves(chainId);

View File

@ -0,0 +1,92 @@
import { useEffect } from "react";
import ReactGA from "react-ga4";
import { Box, Container, Grid, Divider, Typography, useMediaQuery } from "@mui/material";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import { PrimaryButton } from "../../components/Button";
import GovernanceInfoText from "./components/GovernanceInfoText";
import ProposalsList from "./components/ProposalsList";
import { ProposalsCount, MinQuorumPercentage, ProposalThreshold } from "./components/Metric";
import { useTokenSymbol } from "../../hooks/tokens";
const Governance = ({ connect, config, address, chainId }) => {
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const handleModal = () => {
alert("proposal modal here");
}
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/governance" });
}, []);
return (
<Box>
<PageTitle name="ghostDAO Governance" subtitle={`Vote for proposals and suggest new with $${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" }}>
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Requirements
</Typography>
</Box>
}
>
<Grid container spacing={1}>
<Grid item xs={isSmallScreen ? 12 : 4}>
<ProposalsCount chainId={chainId} />
</Grid>
<Grid item xs={isSmallScreen ? 12 : 4}>
<MinQuorumPercentage chainId={chainId} />
</Grid>
<Grid item xs={isSmallScreen ? 12 : 4}>
<ProposalThreshold chainId={chainId} ghstSymbol={ghstSymbol} />
</Grid>
</Grid>
<Divider sx={{ marginTop: "30px" }} />
<Box display="flex" justifyContent="center">Claimes for locked funds could be here</Box>
<Divider />
<Box mt="15px" display="flex" flexDirection="column" alignItems="center" justifyContent="center">
<PrimaryButton
fullWidth
onClick={() => handleModal(true)}
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
>
Create Proposal
</PrimaryButton>
<Box textAlign="center" mt="15px">
<GovernanceInfoText />
</Box>
</Box>
</Paper>
<ProposalsList config={config} chainId={chainId} />
</Box>
</Container>
</Box>
)
}
export default Governance;

View File

@ -0,0 +1,244 @@
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;

View File

@ -0,0 +1,23 @@
import { Link, Typography, useTheme } from "@mui/material";
const GovernanceInfoText = () => {
const theme = useTheme();
return (
<Typography
variant="body2"
color="textSecondary"
fontSize="0.875em"
lineHeight="15px"
>
ghostDAO's adaptive governance system algorithmically sets proposal threshold based on activity.
&nbsp;<Link
color={theme.colors.primary[300]}
href="https://ghostchain.io/ghostdao_litepaper"
target="_blank"
rel="noopener noreferrer"
>Learn more here.</Link>
</Typography>
)
};
export default GovernanceInfoText;

View File

@ -0,0 +1,53 @@
import Metric from "../../../components/Metric/Metric";
import { formatCurrency, formatNumber } from "../../../helpers";
import {
useMinQuorum,
useProposalThreshold,
useProposalCount
} from "../../../hooks/governance";
export const MinQuorumPercentage = props => {
const { percentage } = useMinQuorum(props.chainId);
const _props = {
...props,
label: `Min Quorum`,
tooltip: `Minimum quorum needed for proposal to be succeeded.`,
};
if (percentage) _props.metric = `${formatNumber(percentage, 2)}%`;
else _props.isLoading = true;
return <Metric {..._props} />;
};
export const ProposalThreshold = props => {
const { threshold } = useProposalThreshold(props.chainId, props.ghstSymbol);
const _props = {
...props,
label: `$${props.ghstSymbol} Threshold`,
tooltip: `Minimum $${props.ghstSymbol} amount to be locked to create proposal.`,
};
if (threshold) _props.metric = `${formatCurrency(threshold.toString(), 0, props.ghstSymbol)}`;
else _props.isLoading = true;
return <Metric {..._props} />;
}
export const ProposalsCount = props => {
const { proposalsCount } = useProposalCount(props.chainId);
const _props = {
...props,
label: `Proposals Count`,
tooltip: `How much proposals already passed.`,
};
if (proposalsCount) _props.metric = proposalsCount.toString();
else _props.isLoading = true;
return <Metric {..._props} />;
}

View File

@ -0,0 +1,29 @@
import { Link } from "@mui/material";
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
const ProposalDiscussion = (linkProps) => {
return (
<Link
{...linkProps}
underline="hover"
sx={{ fontFamily: "Ubuntu" }}
display="flex"
flexDirection="row"
alignItems="center"
alignContent="center"
justifyContent={linkProps.isSmallScreen ? "start" : "center"}
className="link-container"
>
Learn more&nbsp;
<GhostStyledIcon
style={{ marginTop: "7px" }}
viewBox="0 0 30 30"
className="external-site-link-icon"
component={ArrowUpIcon}
/>
</Link>
)
}
export default ProposalDiscussion;

View File

@ -0,0 +1,64 @@
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 (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">Discussion URL</Typography>
</Box>
}
open={isOpened}
onClose={closeModal}
maxWidth="460px"
minHeight="200px"
>
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column">
<Box marginBottom="20px" display="flex" flexDirection="column" alignItems="center" gap="10px">
<Typography align="center">
You are leaving the ghost dao app. Check the link on your own, we are not in charge of your destiny.
</Typography>
<Link
onClick={copyToClipboard}
underline="hover"
sx={{
maxWidth: '360px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'middle',
fontStyle: 'italic'
}}
>
{url}
</Link>
</Box>
<PrimaryButton
fullWidth
onClick={() => window.open(url, '_blank', 'noopener,noreferrer')}
>
Open
</PrimaryButton>
</Box>
</Modal>
)
}
export default ProposalDiscussionModal;

View File

@ -0,0 +1,23 @@
import { Link, Typography, useTheme } from "@mui/material";
const ProposalInfoText = () => {
const theme = useTheme();
return (
<Typography
variant="body2"
color="textSecondary"
fontSize="0.875em"
lineHeight="15px"
>
Important: We display only the 10 most recent proposals. Only one proposal can be active at a time.
&nbsp;<Link
color={theme.colors.primary[300]}
href="https://ghostchain.io/ghostdao_litepaper"
target="_blank"
rel="noopener noreferrer"
>Learn more here.</Link>
</Typography>
)
};
export default ProposalInfoText;

View File

@ -0,0 +1,316 @@
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 { NavLink } from "react-router-dom";
import { getBlockNumber } from "@wagmi/core";
import { networkAvgBlockSpeed } from "../../../constants";
import { prettifySecondsInDays } 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 ProposalDiscussionModal from "./ProposalDiscussionModal";
import ProposalDiscussion from "./ProposalDiscussion";
import ProposalInfoText from "./ProposalInfoText";
import { convertStatusToTemplate } from "../helpers";
import { useScreenSize } from "../../../hooks/useScreenSize";
import {
useProposals,
} from "../../../hooks/governance";
const MAX_PROPOSALS_TO_SHOW = 10;
const ProposalsList = ({ chainId, config }) => {
const isSmallScreen = useScreenSize("md");
const navigate = useNavigate();
const [blockNumber, setBlockNumber] = useState(0n);
const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined);
const [proposalsFilter, setProposalFilter] = useState("active");
const { proposals } = useProposals(chainId, MAX_PROPOSALS_TO_SHOW);
getBlockNumber(config).then(block => setBlockNumber(block));
const isDiscussionModalOpened = useMemo(() => {
return selectedDiscussionUrl !== undefined;
}, [selectedDiscussionUrl]);
const filteredProposals = useMemo(() => {
switch (proposalsFilter) {
case "voted":
return proposals.filter(obj => obj.status === "Succeeded" || obj.status === "Defeated");
case "created":
return proposals.filter(obj => obj.status === "Executed");
default:
return proposals;
}
}, [proposals, proposalsFilter]);
if (proposals?.length === 0) {
return (
<Box display="flex" justifyContent="center">
<Typography variant="h4">No proposals yet</Typography>
</Box>
);
}
if (isSmallScreen) {
return (
<>
<ProposalDiscussionModal
url={selectedDiscussionUrl}
isOpened={isDiscussionModalOpened}
closeModal={() => setSelectedDiscussionUrl(undefined)}
/>
<Paper headerText="Proposals" fullWidth enableBackground>
<Box my="24px" textAlign="center">
<ProposalInfoText />
</Box>
<Box display="flex" flexDirection="column" gap="40px">
{filteredProposals?.map(proposal => (
<ProposalCard
key={proposal.id}
proposal={proposal}
setActive={setSelectedDiscussionUrl}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.id}`)}
/>
))}
</Box>
</Paper>
</>
);
}
return (
<>
<ProposalDiscussionModal
url={selectedDiscussionUrl}
isOpened={isDiscussionModalOpened}
closeModal={() => setSelectedDiscussionUrl(undefined)}
/>
<Paper headerText="Proposals" fullWidth enableBackground>
<ProposalFilterTrigger trigger={proposalsFilter} setTrigger={setProposalFilter} />
<ProposalTable>
{filteredProposals?.map(proposal => (
<ProposalRow
key={proposal.id}
proposal={proposal}
setActive={setSelectedDiscussionUrl}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.id}`)}
/>
))}
</ProposalTable>
<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 style={{ width: "100px", padding: "8px 0" }}>Proposal ID</TableCell>
<TableCell align="center" style={{ width: "120px", padding: "8px 0" }}>Status</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Discussion</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, setActive, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
return (
<TableRow id={proposal.id + `--proposal`} data-testid={proposal.id + `--proposal`}>
<TableCell style={{ padding: "8px 0" }}>
<Typography>GDP-{proposal.id}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Chip
sx={{ width: "88px" }}
label={proposal.status}
template={convertStatusToTemplate(proposal.status)}
/>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<ProposalDiscussion onClick={() => setActive(proposal.discussion)} />
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>
{convertVoteEnds(
proposal.id % 2n === 0n,
proposal.voteEnds,
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={69}
target={Math.floor(Math.random() * 101)}
/>
</Box>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
{(proposal.status === "Active" || proposal.status === "Succeeded") && <PrimaryButton
fullWidth
onClick={() => openProposal()}
sx={{ maxWidth: "150px" }}
>
{proposal.status === "Succeeded" ? "Execute" : "Vote"}
</PrimaryButton>}
{(proposal.status !== "Active" && proposal.status !== "Succeeded") && <TertiaryButton
fullWidth
onClick={() => openProposal()}
sx={{ maxWidth: "150px" }}
>
View
</TertiaryButton>}
</TableCell>
</TableRow>
);
}
const ProposalCard = ({ proposal, setActive, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery('(max-width: 450px)');
return (
<Box id={proposal.id + `--proposal`} data-testid={proposal.id + `--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.id}</Typography>
<Chip
sx={{ width: "88px" }}
label={proposal.status}
template={convertStatusToTemplate(proposal.status)}
/>
</Box>
<Typography>
{convertVoteEnds(
proposal.id % 2n === 0n,
proposal.voteEnds,
blockNumber,
chainId
)}
</Typography>
</Box>
<Box width="150px">
<ProposalDiscussion
isSmallScreen={isSmallScreen}
onClick={() => setActive(proposal.discussion)}
/>
</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.status === "Active" || proposal.status === "Succeeded") && <PrimaryButton
fullWidth
onClick={() => openProposal()}
>
{proposal.status === "Succeeded" ? "Execute" : "Vote"}
</PrimaryButton>}
{(proposal.status !== "Active" && proposal.status !== "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 convertVoteEnds = (tmp, voteEnds, blockNumber, chainId) => {
const tmpVoteSeconds = Number(voteEnds * networkAvgBlockSpeed(chainId));
const tmpSeconds = (tmp ? tmpVoteSeconds : -tmpVoteSeconds);
const result = prettifySecondsInDays(tmpSeconds);
if (result === "now") {
return new Date(Date.now()).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
return `in ${result}`;
}
export default ProposalsList;

View File

@ -0,0 +1,14 @@
export const convertStatusToTemplate = (status) => {
switch (status.toUpperCase()) {
case "EXECUTED":
return 'info';
case "CANCELED":
return 'warning';
case "SUCCEEDED":
return 'success';
case "DEFEATED":
return 'error';
default:
return 'info';
}
}

View File

@ -1,5 +1,5 @@
import { Dispatch, SetStateAction, useState, useEffect } from "react";
import { Box, Container, Grid, Divider, Typography, Link, useMediaQuery, useTheme } from "@mui/material";
import { Box, Container, Grid, Divider, Typography, useMediaQuery, useTheme } from "@mui/material";
import ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper";
@ -47,7 +47,7 @@ export const StakeContainer = ({ chainId, address, connect }) => {
}
return (
<Box >
<Box>
<PageTitle name="Protocol Staking" subtitle={`Stake your ${ftsoSymbol} to earn rebase yields`} />
<Container
style={{

View File

@ -0,0 +1,108 @@
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { getTokenDecimals } from "../helpers";
export const useMinQuorum = (chainId) => {
const numerator = 69n;
const denominator = 100n;
let percentage = 0;
if (numerator && denominator && denominator > 0n) {
percentage = Number(100n * numerator / denominator) / 100;
}
return { numerator, denominator, percentage }
}
export const useProposalThreshold = (chainId, name) => {
const decimals = getTokenDecimals(name);
const threshold = new DecimalBigNumber(420_000_000_000_000_000_000n, decimals);
return { threshold };
}
export const useProposalCount = (chainId) => {
const proposalsCount = 1337n;
return { proposalsCount };
}
export const useProposalStatus = (chainId, proposalId) => {
const status = "Succeeded";
return { status };
}
export const useProposalProposer = (chainId, proposalId) => {
const proposer = "0x71C7656EC7ab88b098defB751B7401B5f6d8976F";
return { proposer };
}
export const useProposalLocked = (chainId, proposalId) => {
const decimals = getTokenDecimals(name);
const locked = new DecimalBigNumber(420_000_000_000_000_000_000n, decimals);
return { locked }
}
export const useProposalQuorum = (chainId, proposalId) => {
const decimals = getTokenDecimals(name);
const quorum = new DecimalBigNumber(1337_000_000_000_000_000_000n, decimals);
return { quorum }
}
export const useProposalVotes = (chainId, proposalId) => {
const decimals = getTokenDecimals(name);
const forVotes = new DecimalBigNumber(420_000_000_000_000_000_000n, decimals);
const againstVotes = new DecimalBigNumber(69_000_000_000_000_000_000n, decimals);
return { forVotes, againstVotes }
}
export const useProposalSnapshot = (chainId, proposalId) => {
const snapshot = Math.floor((Date.now() - (3 * 24 * 60 * 60 * 1000)) / 1000);
return { snapshot };
}
export const useProposalDeadline = (chainId, proposalId) => {
const deadline = Math.floor(Date.now() / 1000);
return { deadline };
}
export const useProposalVotingDelay = (chainId, proposalId) => {
const delay = 1;
return { delay };
}
export const useProposals = (chainId, depth) => {
const decimals = getTokenDecimals(name);
const { proposalsCount } = useProposalCount(chainId);
let iterator = proposalsCount ? proposalsCount : 0n;
const bigIntDepth = BigInt(depth);
const edgeProposalId = iterator > bigIntDepth ? iterator - bigIntDepth : 0n;
const statuses = [
"Active",
"Executed",
"Canceled",
"Succeeded",
"Defeated"
];
let proposals = [];
while (iterator > proposalsCount - bigIntDepth) {
iterator -= 1n;
const voteEnds = 50n;
const yesVotes = new DecimalBigNumber(1337_000_000_000_000_000_000, decimals);
const noVotes = new DecimalBigNumber(420_000_000_000_000_000_000, decimals);
proposals.push({
id: iterator,
discussion: "https://google.com",
status: statuses[Number(iterator) % statuses.length],
voteEnds,
yesVotes,
noVotes
});
}
return { proposals };
}