From 1b8160af201f3422a1a26811871503372efba6e9 Mon Sep 17 00:00:00 2001 From: Memas Deligeorgakis Date: Wed, 14 Sep 2022 16:24:35 +0200 Subject: [PATCH] #50 staking view and table component (#54) * Manually copied over the changes from feat/47_staking_gov_pgf, as there had been a big refactor and this could not be merged automatically * Had to add containers as we have `Appcomponents__ContentContainer` which is not display: flex and in the account views things would break in the current form, if this was to be changed to flex. Should be refactored though * Deleted left over files from merge * creating Table component * navigation and main components in Staking view * #55 Staking and Governance State (#56) * Initial files for staking and governance state * created types in Redux * moving fake data and table configurations away from a file next to the component * validator data through action and Redux * changed the way how to pass callbacks to table rows * removed console logs and added comments to indicate upcoming functionality * put back the placeholder view elements to new routes * Fixes based on PR feedback * Changed naming on PR#54 feedback --- apps/namada-interface/src/App/App.tsx | 2 +- apps/namada-interface/src/App/AppRoutes.tsx | 6 +- .../src/App/Staking/Staking.components.ts | 3 + .../src/App/Staking/Staking.tsx | 125 +++++++++++-- .../StakingOverview.components.ts | 13 ++ .../StakingOverview/StakingOverview.tsx | 176 ++++++++++++++++++ .../App/Staking/StakingOverview/fakeData.tsx | 70 +++++++ .../src/App/Staking/StakingOverview/index.ts | 1 + .../ValidatorDetails.components.ts | 13 ++ .../ValidatorDetails/ValidatorDetails.tsx | 17 ++ .../src/App/Staking/ValidatorDetails/index.ts | 1 + .../StakingAndGovernance.components.ts | 2 +- .../StakingAndGovernance.tsx | 38 +++- .../src/App/TopNavigation/topNavigation.tsx | 29 +-- .../TopNavigation/topNavigationLoggedIn.tsx | 2 +- apps/namada-interface/src/App/types.ts | 4 +- .../src/components/Table/Table.components.ts | 24 +++ .../src/components/Table/Table.tsx | 78 ++++++++ .../src/components/Table/index.ts | 3 + .../src/components/Table/types.ts | 16 ++ .../src/slices/StakingAndGovernance/README.md | 3 + .../slices/StakingAndGovernance/actions.ts | 39 ++++ .../slices/StakingAndGovernance/fakeData.ts | 124 ++++++++++++ .../src/slices/StakingAndGovernance/index.ts | 4 + .../slices/StakingAndGovernance/reducers.ts | 46 +++++ .../src/slices/StakingAndGovernance/types.ts | 44 +++++ apps/namada-interface/src/slices/index.ts | 1 + apps/namada-interface/src/store/index.ts | 1 + apps/namada-interface/src/store/mocks.ts | 3 + apps/namada-interface/src/store/store.ts | 3 + 30 files changed, 852 insertions(+), 39 deletions(-) create mode 100644 apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.components.ts create mode 100644 apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.tsx create mode 100644 apps/namada-interface/src/App/Staking/StakingOverview/fakeData.tsx create mode 100644 apps/namada-interface/src/App/Staking/StakingOverview/index.ts create mode 100644 apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.components.ts create mode 100644 apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.tsx create mode 100644 apps/namada-interface/src/App/Staking/ValidatorDetails/index.ts create mode 100644 apps/namada-interface/src/components/Table/Table.components.ts create mode 100644 apps/namada-interface/src/components/Table/Table.tsx create mode 100644 apps/namada-interface/src/components/Table/index.ts create mode 100644 apps/namada-interface/src/components/Table/types.ts create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/README.md create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/actions.ts create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/fakeData.ts create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/index.ts create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/reducers.ts create mode 100644 apps/namada-interface/src/slices/StakingAndGovernance/types.ts diff --git a/apps/namada-interface/src/App/App.tsx b/apps/namada-interface/src/App/App.tsx index 1f4a8109d59..039520b9a61 100644 --- a/apps/namada-interface/src/App/App.tsx +++ b/apps/namada-interface/src/App/App.tsx @@ -49,7 +49,7 @@ export const AnimatedTransition = (props: { // based on location we decide whether to use placeholder theme const getShouldUsePlaceholderTheme = (location: Location): boolean => { const topLevelRoute = locationToTopLevelRoute(location); - const isStaking = topLevelRoute === TopLevelRoute.Staking; + const isStaking = topLevelRoute === TopLevelRoute.StakingAndGovernance; return isStaking; }; diff --git a/apps/namada-interface/src/App/AppRoutes.tsx b/apps/namada-interface/src/App/AppRoutes.tsx index 3810f0cab55..b9cc0222db9 100644 --- a/apps/namada-interface/src/App/AppRoutes.tsx +++ b/apps/namada-interface/src/App/AppRoutes.tsx @@ -127,9 +127,11 @@ const AppRoutes = ({ store, persistor, password }: Props): JSX.Element => { } /> + } diff --git a/apps/namada-interface/src/App/Staking/Staking.components.ts b/apps/namada-interface/src/App/Staking/Staking.components.ts index 635a3f7001c..0c93242b030 100644 --- a/apps/namada-interface/src/App/Staking/Staking.components.ts +++ b/apps/namada-interface/src/App/Staking/Staking.components.ts @@ -7,6 +7,9 @@ export const StakingContainer = styled.div` align-items: center; width: 100%; height: 100%; + overflow-y: scroll; + padding: 0 32px; + box-sizing: border-box; color: ${(props) => props.theme.colors.utility2.main}; background-color: ${(props) => props.theme.colors.utility1.main80}; `; diff --git a/apps/namada-interface/src/App/Staking/Staking.tsx b/apps/namada-interface/src/App/Staking/Staking.tsx index e69bae492b1..9e0fb8eaa72 100644 --- a/apps/namada-interface/src/App/Staking/Staking.tsx +++ b/apps/namada-interface/src/App/Staking/Staking.tsx @@ -1,28 +1,125 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { Routes, Route, useNavigate, useLocation } from "react-router-dom"; import { MainContainerNavigation } from "App/StakingAndGovernance/MainContainerNavigation"; import { StakingContainer } from "./Staking.components"; -import { Button, ButtonVariant } from "components/Button"; +import { StakingOverview } from "./StakingOverview"; +import { ValidatorDetails } from "./ValidatorDetails"; +import { TopLevelRoute, StakingAndGovernanceSubRoute } from "App/types"; +import { Validator } from "slices/StakingAndGovernance"; const initialTitle = "Staking"; -export const Staking = (): JSX.Element => { + +// this is just a placeholder in real case we can use the +// navigation callback that we define in this file and pass +// down for the table +const breadcrumbsFromPath = (path: string): string[] => { + const pathInParts = path.split("/"); + const pathLength = pathInParts.length; + + if ( + `/${pathInParts[pathLength - 2]}` === + StakingAndGovernanceSubRoute.ValidatorDetails + ) { + return ["Staking", pathInParts[pathLength - 1]]; + } + return ["Staking"]; +}; + +const validatorNameFromUrl = (path: string): string | undefined => { + const pathInParts = path.split("/"); + const pathLength = pathInParts.length; + + if ( + `/${pathInParts[pathLength - 2]}` === + StakingAndGovernanceSubRoute.ValidatorDetails + ) { + return pathInParts[pathLength - 1]; + } +}; + +type Props = { + validators: Validator[]; + selectedValidator: string | undefined; + fetchValidators: () => void; + fetchValidatorDetails: (validatorId: string) => void; +}; + +export const Staking = (props: Props): JSX.Element => { const [breadcrumb, setBreadcrumb] = useState([initialTitle]); + const [validatorName, setValidatorName] = useState(); + const location = useLocation(); + const navigate = useNavigate(); + + const { fetchValidators, fetchValidatorDetails, validators } = props; + + // this is just so we can se the title/breadcrumb + // in real case we do this cleanly in a callback that + // we define here + const isStakingRoot = + location.pathname === + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}`; + + // from outside this view we just navigate here + // this view decides what is the default view + useEffect(() => { + if (isStakingRoot) { + navigate( + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}${StakingAndGovernanceSubRoute.StakingOverview}` + ); + } + }); + + useEffect(() => { + fetchValidators(); + }, []); + + useEffect(() => { + const newBreadcrumb = breadcrumbsFromPath(location.pathname); + const validatorName = validatorNameFromUrl(location.pathname); + if (validatorName) { + // triggers fetching of further details + // fetchValidatorDetails(validatorName); + + // placeholders + setBreadcrumb(newBreadcrumb); + setValidatorName(validatorName); + } + }, [location, JSON.stringify(breadcrumb)]); + + const navigateToValidatorDetails = (validatorId: string): void => { + navigate( + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}${StakingAndGovernanceSubRoute.ValidatorDetails}/${validatorId}` + ); + fetchValidatorDetails(validatorId); + }; return ( setBreadcrumb([initialTitle])} + navigateBack={() => { + navigate( + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}${StakingAndGovernanceSubRoute.StakingOverview}` + ); + setBreadcrumb([initialTitle]); + }} /> - {breadcrumb.length === 1 && ( - - )} + + + } + /> + } + /> + ); }; diff --git a/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.components.ts b/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.components.ts new file mode 100644 index 00000000000..65022a774ef --- /dev/null +++ b/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.components.ts @@ -0,0 +1,13 @@ +import styled from "styled-components/macro"; + +export const StakingOverviewContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + margin: 16px 0 16px; + overflow-y: scroll; + color: ${(props) => props.theme.colors.utility2.main}; + background-color: ${(props) => props.theme.colors.utility1.main80}; +`; diff --git a/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.tsx b/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.tsx new file mode 100644 index 00000000000..e14dca274f8 --- /dev/null +++ b/apps/namada-interface/src/App/Staking/StakingOverview/StakingOverview.tsx @@ -0,0 +1,176 @@ +import { StakingOverviewContainer } from "./StakingOverview.components"; +import { + Table, + TableLink, + TableDimmedCell, + TableConfigurations, +} from "components/Table"; + +import { myBalancesData, myValidatorData } from "./fakeData"; +import { + MyBalanceRow, + Validator, + MyStaking, +} from "slices/StakingAndGovernance"; + +// My Balances table row renderer and configuration +const myBalancesRowRenderer = (myBalanceRow: MyBalanceRow): JSX.Element => { + return ( + <> + {myBalanceRow.key} + {myBalanceRow.baseCurrency} + + {myBalanceRow.fiatCurrency} + + + ); +}; +const myBalancesConfigurations: TableConfigurations = { + title: "My Balances", + rowRenderer: myBalancesRowRenderer, + columns: [ + { uuid: "1", columnLabel: "", width: "30%" }, + { uuid: "2", columnLabel: "", width: "15%" }, + { uuid: "3", columnLabel: "", width: "55%" }, + ], +}; + +const MyValidatorsRowRenderer = ( + myValidatorRow: MyStaking, + callbacks?: ValidatorsCallbacks +): JSX.Element => { + return ( + <> + + { + const formattedValidatorName = myValidatorRow.name + .replace(" ", "-") + .toLowerCase(); + + // this function is defined at + // there it triggers a navigation. It then calls a callback + // that was passed to it by its' parent + // in that callback function that is defined in + // an action is dispatched to fetch validator data and make in available + callbacks && callbacks.onClickValidator(formattedValidatorName); + }} + > + {myValidatorRow.name} + + + {myValidatorRow.stakingStatus} + {myValidatorRow.stakedAmount} + + ); +}; + +const getMyValidatorsConfiguration = ( + navigateToValidatorDetails: (validatorId: string) => void +): TableConfigurations => { + return { + title: "My Validators", + rowRenderer: MyValidatorsRowRenderer, + columns: [ + { uuid: "1", columnLabel: "Validator", width: "30%" }, + { uuid: "2", columnLabel: "Status", width: "40%" }, + { uuid: "3", columnLabel: "Staked Amount", width: "30%" }, + ], + callbacks: { + onClickValidator: navigateToValidatorDetails, + }, + }; +}; + +// callbacks in this type are specific to a certain row type +type ValidatorsCallbacks = { + onClickValidator: (validatorId: string) => void; +}; + +// AllValidators table row renderer and configuration +// it contains callbacks defined in AllValidatorsCallbacks +const AllValidatorsRowRenderer = ( + validator: Validator, + callbacks?: ValidatorsCallbacks +): JSX.Element => { + // this is now as a placeholder but in real case it will be in StakingOverview + return ( + <> + + { + const formattedValidatorName = validator.name + .replace(" ", "-") + .toLowerCase(); + + callbacks && callbacks.onClickValidator(formattedValidatorName); + }} + > + {validator.name} + + + {validator.votingPower} + {validator.commission} + + ); +}; + +const getAllValidatorsConfiguration = ( + navigateToValidatorDetails: (validatorId: string) => void +): TableConfigurations => { + return { + title: "All Validators", + rowRenderer: AllValidatorsRowRenderer, + callbacks: { + onClickValidator: navigateToValidatorDetails, + }, + columns: [ + { uuid: "1", columnLabel: "Validator", width: "45%" }, + { uuid: "2", columnLabel: "Voting power", width: "25%" }, + { uuid: "3", columnLabel: "Commission", width: "30%" }, + ], + }; +}; + +type Props = { + navigateToValidatorDetails: (validatorId: string) => void; + validators: Validator[]; + ownValidators: Validator[]; +}; + +export const StakingOverview = (props: Props): JSX.Element => { + const { navigateToValidatorDetails, validators } = props; + + // we get the configurations for 2 tables that contain callbacks + const myValidatorsConfiguration = getMyValidatorsConfiguration( + navigateToValidatorDetails + ); + const allValidatorsConfiguration = getAllValidatorsConfiguration( + navigateToValidatorDetails + ); + + return ( + + {/* my balances */} + + + {/* my validators */} +
+ + {/* all validators */} +
+ + ); +}; diff --git a/apps/namada-interface/src/App/Staking/StakingOverview/fakeData.tsx b/apps/namada-interface/src/App/Staking/StakingOverview/fakeData.tsx new file mode 100644 index 00000000000..1ee52dff93c --- /dev/null +++ b/apps/namada-interface/src/App/Staking/StakingOverview/fakeData.tsx @@ -0,0 +1,70 @@ +import { RowBase } from "components/Table"; + +type Validator = RowBase & { + name: string; + homepageUrl: string; +}; + +// my balances +type MyBalanceRow = RowBase & { + key: string; + baseCurrency: string; + fiatCurrency: string; +}; + +export const myBalancesData: MyBalanceRow[] = [ + { + uuid: "1", + key: "Total Balance", + baseCurrency: "NAM 33.00", + fiatCurrency: "EUR 33.00", + }, + { + uuid: "2", + key: "Total Bonded", + baseCurrency: "NAM 10.00", + fiatCurrency: "EUR 10.00", + }, + { + uuid: "3", + key: "Pending Rewards", + baseCurrency: "NAM 23.00", + fiatCurrency: "EUR 23.00", + }, + { + uuid: "4", + key: "Available for Bonding", + baseCurrency: "NAM 10.00", + fiatCurrency: "EUR 10.00", + }, +]; + +// my validators +type MyValidatorsRow = Validator & { + stakingStatus: string; + stakedAmount: string; +}; + +export const myValidatorData: MyValidatorsRow[] = [ + { + uuid: "1", + name: "Polychain capital", + homepageUrl: "poly", + stakingStatus: "Bonded", + stakedAmount: "10.00", + }, + { + uuid: "2", + name: "Figment", + homepageUrl: "poly", + stakingStatus: "Bonded Pending", + stakedAmount: "3.00", + }, + { + uuid: "3", + name: "P2P", + homepageUrl: "poly", + stakingStatus: "Unboding (22 days left)", + stakedAmount: "20.00", + }, +]; diff --git a/apps/namada-interface/src/App/Staking/StakingOverview/index.ts b/apps/namada-interface/src/App/Staking/StakingOverview/index.ts new file mode 100644 index 00000000000..c6e65ed0a1d --- /dev/null +++ b/apps/namada-interface/src/App/Staking/StakingOverview/index.ts @@ -0,0 +1 @@ +export { StakingOverview } from "./StakingOverview"; diff --git a/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.components.ts b/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.components.ts new file mode 100644 index 00000000000..c44ee245c65 --- /dev/null +++ b/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.components.ts @@ -0,0 +1,13 @@ +import styled from "styled-components/macro"; + +export const ValidatorDetailsContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + margin: 16px 0 16px; + overflow-y: scroll; + color: ${(props) => props.theme.colors.utility2.main}; + background-color: ${(props) => props.theme.colors.utility1.main80}; +`; diff --git a/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.tsx b/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.tsx new file mode 100644 index 00000000000..9b16fe730d0 --- /dev/null +++ b/apps/namada-interface/src/App/Staking/ValidatorDetails/ValidatorDetails.tsx @@ -0,0 +1,17 @@ +import { useState } from "react"; +import { MainContainerNavigation } from "App/StakingAndGovernance/MainContainerNavigation"; +import { ValidatorDetailsContainer } from "./ValidatorDetails.components"; +import { Table } from "components/Table"; + +type Props = { + validator?: string; +}; + +export const ValidatorDetails = (props: Props): JSX.Element => { + const { validator } = props; + return ( + + Validator Details for {validator} + + ); +}; diff --git a/apps/namada-interface/src/App/Staking/ValidatorDetails/index.ts b/apps/namada-interface/src/App/Staking/ValidatorDetails/index.ts new file mode 100644 index 00000000000..e2600bdad29 --- /dev/null +++ b/apps/namada-interface/src/App/Staking/ValidatorDetails/index.ts @@ -0,0 +1 @@ +export { ValidatorDetails } from "./ValidatorDetails"; diff --git a/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.components.ts b/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.components.ts index 69f15666ccc..8df4e880188 100644 --- a/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.components.ts +++ b/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.components.ts @@ -6,5 +6,5 @@ export const StakingAndGovernanceContainer = styled.div` justify-content: start; align-items: center; width: 100%; - min-height: 620px; + height: 620px; `; diff --git a/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.tsx b/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.tsx index c49db192b9c..25329c03abb 100644 --- a/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.tsx +++ b/apps/namada-interface/src/App/StakingAndGovernance/StakingAndGovernance.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { useAppDispatch, useAppSelector, RootState } from "store"; import { Routes, Route, useLocation, useNavigate } from "react-router-dom"; import { Staking } from "App/Staking"; @@ -11,35 +12,62 @@ import { locationToStakingAndGovernanceSubRoute, } from "App/types"; +import { + fetchValidators, + fetchValidatorDetails, +} from "slices/StakingAndGovernance"; + // This is just rendering the actual Staking/Governance/PGF screens // mostly the purpose of this is to define the default behavior when // the user clicks the top level Staking & Governance menu export const StakingAndGovernance = (): JSX.Element => { const location = useLocation(); const navigate = useNavigate(); - + const dispatch = useAppDispatch(); + const stakingAndGovernance = useAppSelector( + (state: RootState) => state.stakingAndGovernance + ); + const { validators, selectedValidatorId } = stakingAndGovernance; // we need one of the sub routes, staking alone has nothing const stakingAndGovernanceSubRoute = locationToStakingAndGovernanceSubRoute(location); + // from outside this view we just navigate here + // this view decides what is the default view useEffect(() => { if (!!!stakingAndGovernanceSubRoute) { navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.Staking}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}${StakingAndGovernanceSubRoute.StakingOverview}` ); } }); + // triggered by the initial load of + const fetchValidatorsCallback = (): void => { + dispatch(fetchValidators()); + }; + + // triggered by the url load or user click in + const fetchValidatorDetailsCallback = (validatorId: string): void => { + dispatch(fetchValidatorDetails(validatorId)); + }; + return ( } + path={`${StakingAndGovernanceSubRoute.Staking}/*`} + element={ + + } /> } /> { - navigate(`${TopLevelRoute.Staking}`); + navigate(`${TopLevelRoute.StakingAndGovernance}`); }} - isSelected={topLevelPath === TopLevelRoute.Staking} + isSelected={topLevelPath === TopLevelRoute.StakingAndGovernance} > Staking @@ -117,11 +117,12 @@ const SecondMenuRow = (props: SecondMenuRowProps): React.ReactElement => { const topLevelRoute = locationToTopLevelRoute(location); const stakingAndGovernanceSubRoute = locationToStakingAndGovernanceSubRoute(location); - const isSubMenuContentVisible = topLevelRoute === TopLevelRoute.Staking; + const isSubMenuContentVisible = + topLevelRoute === TopLevelRoute.StakingAndGovernance; const { chainId } = useAppSelector((state) => state.settings); useEffect(() => { - if (topLevelRoute === TopLevelRoute.Staking) { + if (topLevelRoute === TopLevelRoute.StakingAndGovernance) { setIsLightMode(false); } }); @@ -150,7 +151,7 @@ const SecondMenuRow = (props: SecondMenuRowProps): React.ReactElement => { { navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.Staking}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}` ); }} isSelected={ @@ -163,7 +164,7 @@ const SecondMenuRow = (props: SecondMenuRowProps): React.ReactElement => { { navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.Governance}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Governance}` ); }} isSelected={ @@ -176,7 +177,7 @@ const SecondMenuRow = (props: SecondMenuRowProps): React.ReactElement => { { navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.PublicGoodsFunding}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.PublicGoodsFunding}` ); }} isSelected={ @@ -302,7 +303,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { /> - {topLevelRoute !== TopLevelRoute.Staking && ( + {topLevelRoute !== TopLevelRoute.StakingAndGovernance && ( { @@ -347,7 +348,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { {isLoggedIn && ( - {topLevelRoute !== TopLevelRoute.Staking && ( + {topLevelRoute !== TopLevelRoute.StakingAndGovernance && ( { @@ -383,7 +384,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { /> - {topLevelRoute !== TopLevelRoute.Staking && ( + {topLevelRoute !== TopLevelRoute.StakingAndGovernance && ( { @@ -415,7 +416,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { /> - {topLevelRoute !== TopLevelRoute.Staking && ( + {topLevelRoute !== TopLevelRoute.StakingAndGovernance && ( { @@ -455,7 +456,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { setShowMenu(false); setIsLightMode(false); navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.Staking}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Staking}` ); }} isSelected={ @@ -470,7 +471,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { setShowMenu(false); setIsLightMode(false); navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.Governance}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.Governance}` ); }} isSelected={ @@ -485,7 +486,7 @@ function TopNavigation(props: TopNavigationProps): JSX.Element { setShowMenu(false); setIsLightMode(false); navigate( - `${TopLevelRoute.Staking}${StakingAndGovernanceSubRoute.PublicGoodsFunding}` + `${TopLevelRoute.StakingAndGovernance}${StakingAndGovernanceSubRoute.PublicGoodsFunding}` ); }} isSelected={ diff --git a/apps/namada-interface/src/App/TopNavigation/topNavigationLoggedIn.tsx b/apps/namada-interface/src/App/TopNavigation/topNavigationLoggedIn.tsx index eba39e95662..3ee59cfab64 100644 --- a/apps/namada-interface/src/App/TopNavigation/topNavigationLoggedIn.tsx +++ b/apps/namada-interface/src/App/TopNavigation/topNavigationLoggedIn.tsx @@ -32,7 +32,7 @@ const TopNavigationLoggedIn = (props: Props): JSX.Element => { - {topLevelRoute !== TopLevelRoute.Staking && ( + {topLevelRoute !== TopLevelRoute.StakingAndGovernance && ( { diff --git a/apps/namada-interface/src/App/types.ts b/apps/namada-interface/src/App/types.ts index 95fce33d4c5..98ed9e7efee 100644 --- a/apps/namada-interface/src/App/types.ts +++ b/apps/namada-interface/src/App/types.ts @@ -30,7 +30,7 @@ export enum TopLevelRoute { TokenIbcTransfer = "/token/:id/ibc-transfer", /* STAKING AND GOVERNANCE */ - Staking = "/staking", + StakingAndGovernance = "/staking-and-governance", Governance = "/governance", /* SETTINGS */ @@ -42,6 +42,8 @@ export enum TopLevelRoute { export enum StakingAndGovernanceSubRoute { Staking = "/staking", + StakingOverview = "/staking-overview", + ValidatorDetails = "/validator-details", Governance = "/governance", PublicGoodsFunding = "/public-goods-funding", } diff --git a/apps/namada-interface/src/components/Table/Table.components.ts b/apps/namada-interface/src/components/Table/Table.components.ts new file mode 100644 index 00000000000..5f6e9769e4a --- /dev/null +++ b/apps/namada-interface/src/components/Table/Table.components.ts @@ -0,0 +1,24 @@ +import styled from "styled-components/macro"; + +export const TableContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + width: 100%; + margin-bottom: 32px; + color: ${(props) => props.theme.colors.utility2.main}; + background-color: ${(props) => props.theme.colors.utility1.main80}; +`; + +export const TableElement = styled.table` + max-height: 1px; +`; + +export const TableLink = styled.span` + cursor: pointer; + color: ${(props) => props.theme.colors.secondary.main}; +`; + +export const TableDimmedCell = styled.span` + color: ${(props) => props.theme.colors.utility2.main40}; +`; diff --git a/apps/namada-interface/src/components/Table/Table.tsx b/apps/namada-interface/src/components/Table/Table.tsx new file mode 100644 index 00000000000..61212739ed3 --- /dev/null +++ b/apps/namada-interface/src/components/Table/Table.tsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { MainContainerNavigation } from "App/StakingAndGovernance/MainContainerNavigation"; +import { TableContainer, TableElement } from "./Table.components"; +import { TableConfigurations, RowBase, ColumnDefinition } from "./types"; + +type Props = { + title: string; + data: RowType[]; + tableConfigurations: TableConfigurations; +}; + +const getRenderedHeaderRow = ( + columnDefinitions: ColumnDefinition[] +): JSX.Element => { + let shouldHaveBottomBorder = false; + const columns = columnDefinitions.map((columnDefinition) => { + // if at least one column header has text we render the bottom border + if (columnDefinition.columnLabel && columnDefinition.columnLabel !== "") { + shouldHaveBottomBorder = true; + } + + return ( + + ); + }); + + const headerRowStyle = shouldHaveBottomBorder + ? { borderBottom: "solid 1px grey" } + : {}; + const header = ( + + {columns} + + ); + + return header; +}; + +const getRenderedDataRows = ( + rows: RowType[], + rowRenderer: (row: RowType, callbacks?: Callbacks) => JSX.Element, + callbacks?: Callbacks +): JSX.Element[] => { + const renderedRows = rows.map((row) => { + return {rowRenderer(row, callbacks)}; + }); + return renderedRows; +}; + +export const Table = ( + props: Props +): JSX.Element => { + const { data, tableConfigurations } = props; + const { title, columns, rowRenderer, callbacks } = + tableConfigurations && tableConfigurations; + + const renderedHeaderRow = getRenderedHeaderRow(columns); + const renderedDataRows = getRenderedDataRows( + data, + rowRenderer, + callbacks + ); + + const renderedRows = [renderedHeaderRow, ...renderedDataRows]; + return ( + +

{title}

+ +
{renderedRows} + + + ); +}; diff --git a/apps/namada-interface/src/components/Table/index.ts b/apps/namada-interface/src/components/Table/index.ts new file mode 100644 index 00000000000..ae26badf1d5 --- /dev/null +++ b/apps/namada-interface/src/components/Table/index.ts @@ -0,0 +1,3 @@ +export { Table } from "./Table"; +export { TableLink, TableDimmedCell } from "./Table.components"; +export type { TableConfigurations, RowBase, ColumnDefinition } from "./types"; diff --git a/apps/namada-interface/src/components/Table/types.ts b/apps/namada-interface/src/components/Table/types.ts new file mode 100644 index 00000000000..ad57f01e86a --- /dev/null +++ b/apps/namada-interface/src/components/Table/types.ts @@ -0,0 +1,16 @@ +export type ColumnDefinition = { + uuid: string; + columnLabel: string; + width: string; +}; + +export type TableConfigurations = { + title: string; + rowRenderer: (rowData: RowType, callbacks?: Callbacks) => JSX.Element; + columns: ColumnDefinition[]; + callbacks?: Callbacks; +}; + +export type RowBase = { + uuid: string; +}; diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/README.md b/apps/namada-interface/src/slices/StakingAndGovernance/README.md new file mode 100644 index 00000000000..b2d454301de --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/README.md @@ -0,0 +1,3 @@ +# Staking And Governance +This part of hte state deals with the data that is mostly under Staking & Governance in the UI. It allows the user to perform all the actions regarding Staking, Governance and Public Goods Funding. + diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/actions.ts b/apps/namada-interface/src/slices/StakingAndGovernance/actions.ts new file mode 100644 index 00000000000..db3a48f5102 --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/actions.ts @@ -0,0 +1,39 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { FETCH_VALIDATORS, FETCH_VALIDATOR_DETAILS, Validator } from "./types"; +import { + myBalancesData as _myBalancesData, + allValidatorsData, + myValidatorData as _myStakingPositions, +} from "./fakeData"; +export type ValidatorsPayload = { + chainId: string; + shieldedBalances: { + [accountId: string]: number; + }; +}; + +export type ValidatorDetailsPayload = { + name: string; + websiteUrl: string; +}; + +export const fetchValidators = createAsyncThunk< + { allValidators: Validator[] }, + void +>(FETCH_VALIDATORS, async () => { + return Promise.resolve({ allValidators: allValidatorsData }); +}); + +export const fetchValidatorDetails = createAsyncThunk< + ValidatorDetailsPayload | undefined, + string +>(FETCH_VALIDATOR_DETAILS, async (validatorId: string) => { + try { + return Promise.resolve({ + name: "polychain", + websiteUrl: "polychain.com", + }); + } catch { + return Promise.reject(); + } +}); diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/fakeData.ts b/apps/namada-interface/src/slices/StakingAndGovernance/fakeData.ts new file mode 100644 index 00000000000..9e060e1831e --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/fakeData.ts @@ -0,0 +1,124 @@ +import { Validator, MyStaking, MyBalanceRow } from "./types"; +export const myBalancesData: MyBalanceRow[] = [ + { + uuid: "1", + key: "Total Balance", + baseCurrency: "NAM 33.00", + fiatCurrency: "EUR 33.00", + }, + { + uuid: "2", + key: "Total Bonded", + baseCurrency: "NAM 10.00", + fiatCurrency: "EUR 10.00", + }, + { + uuid: "3", + key: "Pending Rewards", + baseCurrency: "NAM 23.00", + fiatCurrency: "EUR 23.00", + }, + { + uuid: "4", + key: "Available for Bonding", + baseCurrency: "NAM 10.00", + fiatCurrency: "EUR 10.00", + }, +]; + +export const myValidatorData: MyStaking[] = [ + { + uuid: "1", + name: "Polychain capital", + homepageUrl: "poly", + stakingStatus: "Bonded", + stakedAmount: "10.00", + }, + { + uuid: "2", + name: "Figment", + homepageUrl: "poly", + stakingStatus: "Bonded Pending", + stakedAmount: "3.00", + }, + { + uuid: "3", + name: "P2P", + homepageUrl: "poly", + stakingStatus: "Unboding (22 days left)", + stakedAmount: "20.00", + }, +]; + +export const allValidatorsData: Validator[] = [ + { + uuid: "1", + name: "Polychain capital", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "22%", + }, + { + uuid: "2", + name: "Figment", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "3", + name: "P2P", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "4", + name: "Coinbase Custody", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "5", + name: "Chorus One", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "6", + name: "Binance Staking", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "7", + name: "DokiaCapital", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "8", + name: "Kraken", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "9", + name: "Zero Knowledge Validator (ZKV)", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, + { + uuid: "10", + name: "Paradigm", + homepageUrl: "https://polychain.capital", + votingPower: "NAM 100 000", + commission: "20%", + }, +]; diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/index.ts b/apps/namada-interface/src/slices/StakingAndGovernance/index.ts new file mode 100644 index 00000000000..4b7f7232a75 --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/index.ts @@ -0,0 +1,4 @@ +export { fetchValidators, fetchValidatorDetails } from "./actions"; +export { reducer as stakingAndGovernanceReducers } from "./reducers"; +export type { StakingAndGovernanceState } from "./reducers"; +export type { MyBalanceRow, Validator, MyStaking } from "./types"; diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/reducers.ts b/apps/namada-interface/src/slices/StakingAndGovernance/reducers.ts new file mode 100644 index 00000000000..3b65d51102a --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/reducers.ts @@ -0,0 +1,46 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { fetchValidators, fetchValidatorDetails } from "./actions"; +import { STAKING_AND_GOVERNANCE } from "./types"; +import { Validator, ValidatorId } from "./types"; + +export type StakingAndGovernanceState = { + validators: Validator[]; + selectedValidatorId?: ValidatorId; +}; + +const initialState: StakingAndGovernanceState = { + validators: [], +}; + +export const stakingAndGovernanceSlice = createSlice({ + name: STAKING_AND_GOVERNANCE, + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchValidators.pending, (_state, _action) => { + // start the loader + }) + .addCase(fetchValidators.fulfilled, (state, action) => { + // stop the loader + state.validators = action.payload.allValidators; + }) + .addCase(fetchValidators.rejected, (_state, _action) => { + // stop the loader + }) + .addCase(fetchValidatorDetails.pending, (state, action) => { + // start the loader + state.selectedValidatorId = undefined; + }) + .addCase(fetchValidatorDetails.fulfilled, (state, action) => { + // stop the loader + state.selectedValidatorId = action.payload?.name; + }) + .addCase(fetchValidatorDetails.rejected, (_state, _action) => { + // stop the loader + }); + }, +}); + +const { reducer } = stakingAndGovernanceSlice; +export { reducer }; diff --git a/apps/namada-interface/src/slices/StakingAndGovernance/types.ts b/apps/namada-interface/src/slices/StakingAndGovernance/types.ts new file mode 100644 index 00000000000..af26a0d9056 --- /dev/null +++ b/apps/namada-interface/src/slices/StakingAndGovernance/types.ts @@ -0,0 +1,44 @@ +export const STAKING_AND_GOVERNANCE = "stakingAndGovernance"; +export const FETCH_VALIDATORS = `${STAKING_AND_GOVERNANCE}/FETCH_VALIDATORS`; +export const FETCH_VALIDATOR_DETAILS = `${STAKING_AND_GOVERNANCE}/FETCH_VALIDATOR_DETAILS`; + +export enum StakingAndGovernanceErrors { + StakingAndGovernanceErrors = "StakingAndGovernanceError.GenericError", +} + +// TODO check this out, what format, do we have constrains +export type ValidatorId = string; + +type Unique = { + uuid: string; +}; + +export type Validator = Unique & { + name: string; + votingPower: string; + homepageUrl: string; + commission: string; +}; + +// USE THIS +// export type MyStaking = Unique & { +// stakingStatus: string; +// stakedAmount: string; +// validator: Validator; +// }; + +// PLACEHOLDERS +export type MyStaking = { + uuid: string; + name: string; + homepageUrl: string; + stakingStatus: string; + stakedAmount: string; +}; + +export type MyBalanceRow = { + uuid: string; + key: string; + baseCurrency: string; + fiatCurrency: string; +}; diff --git a/apps/namada-interface/src/slices/index.ts b/apps/namada-interface/src/slices/index.ts index 5423f4a4cce..2f6ae629153 100644 --- a/apps/namada-interface/src/slices/index.ts +++ b/apps/namada-interface/src/slices/index.ts @@ -11,3 +11,4 @@ export { default as transfersReducer } from "./transfers"; export { default as channelsReducer } from "./channels"; export { default as settingsReducer } from "./settings"; export { default as coinsReducer } from "./coins"; +export { stakingAndGovernanceReducers } from "./StakingAndGovernance"; diff --git a/apps/namada-interface/src/store/index.ts b/apps/namada-interface/src/store/index.ts index 9c4e8f22a47..0ff024e211e 100644 --- a/apps/namada-interface/src/store/index.ts +++ b/apps/namada-interface/src/store/index.ts @@ -1,2 +1,3 @@ export { default as makeStore } from "./store"; +export type { RootState } from "./store"; export { useAppDispatch, useAppSelector } from "./hooks"; diff --git a/apps/namada-interface/src/store/mocks.ts b/apps/namada-interface/src/store/mocks.ts index 4bf191274b7..30b489c7b4b 100644 --- a/apps/namada-interface/src/store/mocks.ts +++ b/apps/namada-interface/src/store/mocks.ts @@ -218,4 +218,7 @@ export const mockAppState: RootState = { coins: { rates: {}, }, + stakingAndGovernance: { + validators: [], + }, }; diff --git a/apps/namada-interface/src/store/store.ts b/apps/namada-interface/src/store/store.ts index cb746a378f7..331b25a77e7 100644 --- a/apps/namada-interface/src/store/store.ts +++ b/apps/namada-interface/src/store/store.ts @@ -16,6 +16,7 @@ import { settingsReducer, channelsReducer, coinsReducer, + stakingAndGovernanceReducers, } from "slices"; import { LocalStorageKeys } from "App/types"; import { hashPassword } from "@anoma/utils"; @@ -27,6 +28,7 @@ const reducers = combineReducers({ channels: channelsReducer, settings: settingsReducer, coins: coinsReducer, + stakingAndGovernance: stakingAndGovernanceReducers, }); type StoreFactory = (secretKey: string) => EnhancedStore; @@ -49,6 +51,7 @@ const makeStore: StoreFactory = (secret) => { "settings", "channels", "coins", + "stakingAndGovernance", ], transforms: [ encryptTransform({
+ {columnDefinition.columnLabel} +