diff --git a/package.json b/package.json index 9e60d39..c86a47e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183862c..28d471f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/App.jsx b/src/App.jsx index 3363c68..f6b3f03 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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() { } } /> } /> + } /> + } /> } 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 ( + + + {props.target && } + + ) +} + +export default LinearProgressBar; diff --git a/src/components/Sidebar/NavContent.jsx b/src/components/Sidebar/NavContent.jsx index a4f7ab4..6d0f834 100644 --- a/src/components/Sidebar/NavContent.jsx +++ b/src/components/Sidebar/NavContent.jsx @@ -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 }) => { } /> - + + diff --git a/src/containers/Bond/Bonds.jsx b/src/containers/Bond/Bonds.jsx index 058c831..e880a4b 100644 --- a/src/containers/Bond/Bonds.jsx +++ b/src/containers/Bond/Bonds.jsx @@ -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); diff --git a/src/containers/Governance/Governance.jsx b/src/containers/Governance/Governance.jsx new file mode 100644 index 0000000..fe46b63 --- /dev/null +++ b/src/containers/Governance/Governance.jsx @@ -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 ( + + + + + + + Proposal Requirements + + + } + > + + + + + + + + + + + + + + Claimes for locked funds could be here + + + + handleModal(true)} + sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }} + > + Create Proposal + + + + + + + + + + + ) +} + +export default Governance; diff --git a/src/containers/Governance/ProposalDetails.jsx b/src/containers/Governance/ProposalDetails.jsx new file mode 100644 index 0000000..bff84fa --- /dev/null +++ b/src/containers/Governance/ProposalDetails.jsx @@ -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 ( + <> + setSelectedDiscussionUrl(undefined)} + /> + + + + + + + + Progress + + + + } + 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)} + + + + + Total + + + {formatNumber(totalSupply.toString(), 4)} + + + + + Votes + + + {formatNumber(balance.toString(), 4)} + + + + + alert("For vote casted")}>For + alert("Against vote casted")}>Against + + + + + + + Timeline + + + } + > + + + + + + + + Executable Code + + + } + > + Here will be a list of decoded calldatas + + + + + ) +} + +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 ( + + + + + + ) +} + +const VotingTimelineItem = ({ isFirst, isLast, time, message }) => { + return ( + + + + {!isFirst && } + + {!isLast && } + + + + {message} + {new Date(time * 1000).toLocaleString()} + + + ) +} + +export default ProposalDetails; diff --git a/src/containers/Governance/components/GovernanceInfoText.jsx b/src/containers/Governance/components/GovernanceInfoText.jsx new file mode 100644 index 0000000..114c516 --- /dev/null +++ b/src/containers/Governance/components/GovernanceInfoText.jsx @@ -0,0 +1,23 @@ +import { Link, Typography, useTheme } from "@mui/material"; + +const GovernanceInfoText = () => { + const theme = useTheme(); + return ( + + ghostDAO's adaptive governance system algorithmically sets proposal threshold based on activity. +  Learn more here. + + ) +}; + +export default GovernanceInfoText; diff --git a/src/containers/Governance/components/Metric.jsx b/src/containers/Governance/components/Metric.jsx new file mode 100644 index 0000000..8c39cdd --- /dev/null +++ b/src/containers/Governance/components/Metric.jsx @@ -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 ; +}; + +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 ; +} + +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 ; +} diff --git a/src/containers/Governance/components/ProposalDiscussion.jsx b/src/containers/Governance/components/ProposalDiscussion.jsx new file mode 100644 index 0000000..9bf829b --- /dev/null +++ b/src/containers/Governance/components/ProposalDiscussion.jsx @@ -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 ( + + Learn more  + + + ) +} + +export default ProposalDiscussion; diff --git a/src/containers/Governance/components/ProposalDiscussionModal.jsx b/src/containers/Governance/components/ProposalDiscussionModal.jsx new file mode 100644 index 0000000..ac2e902 --- /dev/null +++ b/src/containers/Governance/components/ProposalDiscussionModal.jsx @@ -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 ( + + 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/ProposalInfoText.jsx b/src/containers/Governance/components/ProposalInfoText.jsx new file mode 100644 index 0000000..a2ea4c9 --- /dev/null +++ b/src/containers/Governance/components/ProposalInfoText.jsx @@ -0,0 +1,23 @@ +import { Link, Typography, useTheme } from "@mui/material"; + +const ProposalInfoText = () => { + const theme = useTheme(); + return ( + + Important: We display only the 10 most recent proposals. Only one proposal can be active at a time. +  Learn more here. + + ) +}; + +export default ProposalInfoText; diff --git a/src/containers/Governance/components/ProposalsList.jsx b/src/containers/Governance/components/ProposalsList.jsx new file mode 100644 index 0000000..9f0b567 --- /dev/null +++ b/src/containers/Governance/components/ProposalsList.jsx @@ -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 ( + + No proposals yet + + ); + } + + if (isSmallScreen) { + return ( + <> + setSelectedDiscussionUrl(undefined)} + /> + + + + + + + {filteredProposals?.map(proposal => ( + navigate(`/governance/${proposal.id}`)} + /> + ))} + + + + ); + } + + return ( + <> + setSelectedDiscussionUrl(undefined)} + /> + + + + + {filteredProposals?.map(proposal => ( + navigate(`/governance/${proposal.id}`)} + /> + ))} + + + + + + + + ); +} + +const ProposalTable = ({ children }) => ( + + + + + Proposal ID + Status + Discussion + Vote Ends + Voting Stats + + + + + {children} +
+
+); + +const ProposalRow = ({ proposal, setActive, blockNumber, openProposal, chainId }) => { + const theme = useTheme(); + + return ( + + + GDP-{proposal.id} + + + + + + + + setActive(proposal.discussion)} /> + + + + + {convertVoteEnds( + proposal.id % 2n === 0n, + proposal.voteEnds, + blockNumber, + chainId + )} + + + + + + + + + + + {(proposal.status === "Active" || proposal.status === "Succeeded") && openProposal()} + sx={{ maxWidth: "150px" }} + > + {proposal.status === "Succeeded" ? "Execute" : "Vote"} + } + {(proposal.status !== "Active" && proposal.status !== "Succeeded") && openProposal()} + sx={{ maxWidth: "150px" }} + > + View + } + + + ); +} + +const ProposalCard = ({ proposal, setActive, blockNumber, openProposal, chainId }) => { + const theme = useTheme(); + const isSmallScreen = useMediaQuery('(max-width: 450px)'); + + return ( + + + + + GIP-{proposal.id} + + + + {convertVoteEnds( + proposal.id % 2n === 0n, + proposal.voteEnds, + blockNumber, + chainId + )} + + + + setActive(proposal.discussion)} + /> + + + + + + + + {(proposal.status === "Active" || proposal.status === "Succeeded") && openProposal()} + > + {proposal.status === "Succeeded" ? "Execute" : "Vote"} + } + {(proposal.status !== "Active" && proposal.status !== "Succeeded") && openProposal()} + > + View + } + + + ); +}; + +const ProposalFilterTrigger = ({ trigger, setTrigger }) => { + return ( + setTrigger(view)} + TabIndicatorProps={{ style: { display: "none" } }} + > + + + + + ) +} + +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; diff --git a/src/containers/Governance/helpers.js b/src/containers/Governance/helpers.js new file mode 100644 index 0000000..505d17a --- /dev/null +++ b/src/containers/Governance/helpers.js @@ -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'; + } +} diff --git a/src/containers/Stake/StakeContainer.jsx b/src/containers/Stake/StakeContainer.jsx index 49e3f4a..e404bcc 100644 --- a/src/containers/Stake/StakeContainer.jsx +++ b/src/containers/Stake/StakeContainer.jsx @@ -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 ( - + { + 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 }; +}