diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx new file mode 100644 index 0000000000..0215f53d48 --- /dev/null +++ b/src/canvas/JoinPool/Header.tsx @@ -0,0 +1,119 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faArrowsRotate, + faHashtag, + faTimes, +} from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { ButtonPrimaryInvert } from 'kits/Buttons/ButtonPrimaryInvert'; +import { TitleWrapper } from './Wrappers'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { determinePoolDisplay, remToUnit } from '@w3ux/utils'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import type { JoinPoolHeaderProps } from './types'; + +export const Header = ({ + activeTab, + bondedPool, + filteredBondedPools, + metadata, + autoSelected, + setActiveTab, + setSelectedPoolId, + setSelectedPoolCount, +}: JoinPoolHeaderProps) => { + const { t } = useTranslation(); + const { closeCanvas } = useOverlay().canvas; + + // Randomly select a new pool to display. + const handleChooseNewPool = () => { + // Trigger refresh of memoied selected bonded pool. + setSelectedPoolCount((prev: number) => prev + 1); + + // Randomly select a filtered bonded pool and set it as the selected pool. + const index = Math.ceil(Math.random() * filteredBondedPools.length - 1); + setSelectedPoolId(filteredBondedPools[index].id); + }; + + return ( + <> +
+ handleChooseNewPool()} + lg + /> + closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ +
+
+ +
+
+
+

+ {determinePoolDisplay( + bondedPool?.addresses.stash || '', + metadata + )} +

+
+
+

+ {t('pool', { ns: 'library' })}{' '} + + {bondedPool.id} + {['Blocked', 'Destroying'].includes(bondedPool.state) && ( + + {t(bondedPool.state.toLowerCase(), { ns: 'library' })} + + )} +

+ + {autoSelected && ( +

+ {t('autoSelected', { ns: 'library' })} +

+ )} +
+
+
+ + setActiveTab(0), + }, + { + title: t('nominate.nominations', { ns: 'pages' }), + active: activeTab === 1, + onClick: () => setActiveTab(1), + }, + ]} + tabClassName="canvas" + inline={true} + /> +
+ + ); +}; diff --git a/src/canvas/JoinPool/Nominations/index.tsx b/src/canvas/JoinPool/Nominations/index.tsx new file mode 100644 index 0000000000..276d67e96a --- /dev/null +++ b/src/canvas/JoinPool/Nominations/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ValidatorList } from 'library/ValidatorList'; +import { ListWrapper } from 'modals/PoolNominations/Wrappers'; +import { useTranslation } from 'react-i18next'; +import { HeadingWrapper, NominationsWrapper } from '../Wrappers'; +import type { NominationsProps } from '../types'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; + +export const Nominations = ({ stash, poolId }: NominationsProps) => { + const { t } = useTranslation(); + const { validators } = useValidators(); + const { poolsNominations } = useBondedPools(); + + // Extract validator entries from pool targets. + const targets = poolsNominations[poolId]?.targets || []; + const filteredTargets = validators.filter(({ address }) => + targets.includes(address) + ); + + return ( + + +

+ {targets.length}{' '} + {!targets.length + ? t('nominate.noNominationsSet', { ns: 'pages' }) + : ``}{' '} + {t('nominations', { ns: 'library', count: targets.length })} +

+
+ + {targets.length > 0 ? ( + + ) : ( +

{t('poolIsNotNominating', { ns: 'modals' })}

+ )} +
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/AddressSection.tsx b/src/canvas/JoinPool/Overview/AddressSection.tsx new file mode 100644 index 0000000000..ac569d23b1 --- /dev/null +++ b/src/canvas/JoinPool/Overview/AddressSection.tsx @@ -0,0 +1,45 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useHelp } from 'contexts/Help'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { HeadingWrapper } from '../Wrappers'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { CopyAddress } from 'library/ListItem/Labels/CopyAddress'; +import { ellipsisFn, remToUnit } from '@w3ux/utils'; +import type { AddressSectionProps } from '../types'; + +export const AddressSection = ({ + address, + label, + helpKey, +}: AddressSectionProps) => { + const { openHelp } = useHelp(); + + return ( +
+ +

+ {label} + {!!helpKey && ( + openHelp(helpKey)} /> + )} +

+
+ +
+ + + +

+ {ellipsisFn(address, 6)} + +

+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Addresses.tsx b/src/canvas/JoinPool/Overview/Addresses.tsx new file mode 100644 index 0000000000..8a8cc93a27 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Addresses.tsx @@ -0,0 +1,27 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Addresses = ({ + bondedPool: { addresses }, +}: OverviewSectionProps) => { + const { t } = useTranslation('library'); + + return ( + + +

{t('addresses')}

+
+ + + + + +
+ ); +}; diff --git a/src/modals/JoinPool/index.tsx b/src/canvas/JoinPool/Overview/JoinForm.tsx similarity index 52% rename from src/modals/JoinPool/index.tsx rename to src/canvas/JoinPool/Overview/JoinForm.tsx index c53268a453..82fdb79f85 100644 --- a/src/modals/JoinPool/index.tsx +++ b/src/canvas/JoinPool/Overview/JoinForm.tsx @@ -2,99 +2,88 @@ // SPDX-License-Identifier: GPL-3.0-only import { planckToUnit, unitToPlanck } from '@w3ux/utils'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useApi } from 'contexts/Api'; -import { usePoolMembers } from 'contexts/Pools/PoolMembers'; -import { useSetup } from 'contexts/Setup'; -import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import type BigNumber from 'bignumber.js'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNetwork } from 'contexts/Network'; +import type { ClaimPermission } from 'contexts/Pools/types'; import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxMeta } from 'contexts/TxMeta'; -import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { useState } from 'react'; +import { JoinFormWrapper } from '../Wrappers'; import { ClaimPermissionInput } from 'library/Form/ClaimPermissionInput'; -import { useBatchCall } from 'hooks/useBatchCall'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; import { useBondGreatestFee } from 'hooks/useBondGreatestFee'; -import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { useApi } from 'contexts/Api'; +import { useBatchCall } from 'hooks/useBatchCall'; import { useSubmitExtrinsic } from 'hooks/useSubmitExtrinsic'; -import { Close } from 'library/Modal/Close'; -import { SubmitTx } from 'library/SubmitTx'; import { useOverlay } from 'kits/Overlay/Provider'; -import { useNetwork } from 'contexts/Network'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import type { ClaimPermission } from 'contexts/Pools/types'; -import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; +import { useSetup } from 'contexts/Setup'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { SubmitTx } from 'library/SubmitTx'; +import type { OverviewSectionProps } from '../types'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; +import { useTranslation } from 'react-i18next'; -export const JoinPool = () => { - const { t } = useTranslation('modals'); +export const JoinForm = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation(); const { api } = useApi(); const { - networkData: { units }, + networkData: { units, unit }, } = useNetwork(); - const { activeAccount } = useActiveAccounts(); + const { + closeCanvas, + config: { options }, + } = useOverlay().canvas; const { newBatchCall } = useBatchCall(); const { setActiveAccountSetup } = useSetup(); - const { txFees, notEnoughFunds } = useTxMeta(); + const { activeAccount } = useActiveAccounts(); const { getSignerWarnings } = useSignerWarnings(); const { getTransferOptions } = useTransferOptions(); + const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); const { queryPoolMember, addToPoolMembers } = usePoolMembers(); - const { - setModalStatus, - config: { options }, - setModalResize, - } = useOverlay().modal; - - const { id: poolId, setActiveTab } = options; const { pool: { totalPossibleBond }, - transferrableBalance, } = getTransferOptions(activeAccount); - const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); - - // if we are bonding, subtract tx fees from bond amount - const freeBondAmount = BigNumber.max(transferrableBalance.minus(txFees), 0); + // Pool claim permission value. + const [claimPermission, setClaimPermission] = useState( + defaultClaimPermission + ); - // local bond value + // Bond amount to join pool with. const [bond, setBond] = useState<{ bond: string }>({ bond: planckToUnit(totalPossibleBond, units).toString(), }); - // handler to set bond as a string - const handleSetBond = (newBond: { bond: BigNumber }) => { - setBond({ bond: newBond.bond.toString() }); - }; - - // Updated claim permission value - const [claimPermission, setClaimPermission] = useState< - ClaimPermission | undefined - >('Permissioned'); - - // bond valid + // Whether the bond amount is valid. const [bondValid, setBondValid] = useState(false); // feedback errors to trigger modal resize const [feedbackErrors, setFeedbackErrors] = useState([]); - // modal resize on form update - useEffect( - () => setModalResize(), - [bond, notEnoughFunds, feedbackErrors.length] - ); + // Handler to set bond on input change. + const handleSetBond = (value: { bond: BigNumber }) => { + setBond({ bond: value.bond.toString() }); + }; - // tx to submit + // Whether the form is ready to submit. + const formValid = bondValid && feedbackErrors.length === 0; + + // Get transaction for submission. const getTx = () => { const tx = null; - if (!api) { + if (!api || !claimPermission || !formValid) { return tx; } const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); - const txs = [api.tx.nominationPools.join(bondAsString, poolId)]; + const txs = [api.tx.nominationPools.join(bondAsString, bondedPool.id)]; - if (![undefined, 'Permissioned'].includes(claimPermission)) { + // If claim permission is not the default, add it to tx. + if (claimPermission !== defaultClaimPermission) { txs.push(api.tx.nominationPools.setClaimPermission(claimPermission)); } @@ -110,17 +99,23 @@ export const JoinPool = () => { from: activeAccount, shouldSubmit: bondValid, callbackSubmit: () => { - setModalStatus('closing'); - setActiveTab(0); + closeCanvas(); + + // Optional callback function on join success. + const onJoinCallback = options?.onJoinCallback; + + if (typeof onJoinCallback === 'function') { + onJoinCallback(); + } }, callbackInBlock: async () => { - // query and add account to poolMembers list + // Query and add account to poolMembers list const member = await queryPoolMember(activeAccount); if (member) { addToPoolMembers(member); } - // reset localStorage setup progress + // Reset local storage setup progress setActiveAccountSetup('pool', defaultPoolProgress); }, }); @@ -132,33 +127,49 @@ export const JoinPool = () => { ); return ( - <> - - -

{t('joinPool')}

- { - setBondValid(valid); - setFeedbackErrors(errors); - }} - defaultBond={null} - setters={[handleSetBond]} - parentErrors={warnings} - txFees={largestTxFee} - /> - { - setClaimPermission(val); - }} - disabled={freeBondAmount.isZero()} + +

{t('pools.joinPool', { ns: 'pages' })}

+

+ {t('bond', { ns: 'library' })} {unit} +

+ +
+
+ { + setBondValid(valid); + setFeedbackErrors(errors); + }} + defaultBond={null} + setters={[handleSetBond]} + parentErrors={warnings} + txFees={largestTxFee} + /> +
+
+ +

{t('claimSetting', { ns: 'library' })}

+ + { + setClaimPermission(val); + }} + /> + +
+ - - - +
+
); }; diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx new file mode 100644 index 0000000000..8e551ed3d0 --- /dev/null +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -0,0 +1,180 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { useNetwork } from 'contexts/Network'; +import { GraphWrapper, HeadingWrapper } from '../Wrappers'; +import { Bar } from 'react-chartjs-2'; +import BigNumber from 'bignumber.js'; +import type { AnyJson } from 'types'; +import { graphColors } from 'theme/graphs'; +import { useTheme } from 'contexts/Themes'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { useRef } from 'react'; +import { formatSize } from 'library/Graphs/Utils'; +import { useSize } from 'hooks/useSize'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend +); + +export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation(); + const { mode } = useTheme(); + const { openHelp } = useHelp(); + const { colors } = useNetwork().networkData; + const { poolRewardPoints } = usePoolPerformance(); + const rawEraRewardPoints = poolRewardPoints[bondedPool.addresses.stash] || {}; + + // Ref to the graph container. + const graphInnerRef = useRef(null); + + // Get the size of the graph container. + const size = useSize(graphInnerRef?.current || undefined); + const { width, height } = formatSize(size, 150); + + // Format reward points as an array of strings. + const dataset = Object.values( + Object.fromEntries( + Object.entries(rawEraRewardPoints).map(([k, v]: AnyJson) => [ + k, + new BigNumber(v).toString(), + ]) + ) + ); + + // Format labels, only displaying the first and last era. + const labels = Object.keys(rawEraRewardPoints).map(() => ''); + + const firstEra = Object.keys(rawEraRewardPoints)[0]; + labels[0] = firstEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[0]}` + : ''; + + const lastEra = Object.keys(rawEraRewardPoints)[labels.length - 1]; + labels[labels.length - 1] = lastEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[labels.length - 1]}` + : ''; + + // Use primary color for bars. + const color = colors.primary[mode]; + + const options = { + responsive: true, + maintainAspectRatio: false, + barPercentage: 0.3, + maxBarThickness: 13, + scales: { + x: { + stacked: true, + grid: { + display: false, + }, + ticks: { + font: { + size: 10, + }, + autoSkip: true, + }, + }, + y: { + stacked: true, + ticks: { + font: { + size: 10, + }, + }, + border: { + display: false, + }, + grid: { + color: graphColors.grid[mode], + }, + }, + }, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + tooltip: { + displayColors: false, + backgroundColor: graphColors.tooltip[mode], + titleColor: graphColors.label[mode], + bodyColor: graphColors.label[mode], + bodyFont: { + weight: 600, + }, + callbacks: { + title: () => [], + label: (context: AnyJson) => + `${new BigNumber(context.parsed.y).decimalPlaces(0).toFormat()} ${t('eraPoints', { ns: 'library' })}`, + }, + }, + }, + }; + + const data = { + labels, + datasets: [ + { + label: t('era', { ns: 'library' }), + data: dataset, + borderColor: color, + backgroundColor: color, + pointRadius: 0, + borderRadius: 3, + }, + ], + }; + + return ( +
+ +

+ {t('recentPerformance', { ns: 'library' })} + openHelp('Era Points')} + /> +

+
+ + +
+ +
+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Roles.tsx b/src/canvas/JoinPool/Overview/Roles.tsx new file mode 100644 index 0000000000..cc3f2a89a6 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Roles.tsx @@ -0,0 +1,55 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Roles = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation('pages'); + const { openHelp } = useHelp(); + + return ( +
+ + +

+ {t('pools.roles')} + openHelp('Pool Roles')} /> +

+
+ + + {bondedPool.roles.root && ( + + )} + {bondedPool.roles.nominator && ( + + )} + {bondedPool.roles.bouncer && ( + + )} + {bondedPool.roles.depositor && ( + + )} + +
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Stats.tsx b/src/canvas/JoinPool/Overview/Stats.tsx new file mode 100644 index 0000000000..5281c2debd --- /dev/null +++ b/src/canvas/JoinPool/Overview/Stats.tsx @@ -0,0 +1,67 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useNetwork } from 'contexts/Network'; +import { HeadingWrapper } from '../Wrappers'; +import { planckToUnit, rmCommas } from '@w3ux/utils'; +import { useApi } from 'contexts/Api'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Stats = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation('library'); + const { + networkData: { + units, + unit, + brand: { token: Token }, + }, + } = useNetwork(); + const { isReady, api } = useApi(); + + // Store the pool balance. + const [poolBalance, setPoolBalance] = useState(null); + + // Fetches the balance of the bonded pool. + const getPoolBalance = async () => { + if (!api) { + return; + } + + const balance = ( + await api.call.nominationPoolsApi.pointsToBalance( + bondedPool.id, + rmCommas(bondedPool.points) + ) + ).toString(); + + if (balance) { + setPoolBalance(new BigNumber(balance)); + } + }; + + // Fetch the balance when pool or points change. + useEffect(() => { + if (isReady) { + getPoolBalance(); + } + }, [bondedPool.id, bondedPool.points, isReady]); + + return ( + +

+ {t('activelyNominating')} + + + + {!poolBalance + ? `...` + : planckToUnit(poolBalance, units).decimalPlaces(3).toFormat()}{' '} + {unit} {t('bonded')} + +

+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/index.tsx b/src/canvas/JoinPool/Overview/index.tsx new file mode 100644 index 0000000000..107a5b7ed9 --- /dev/null +++ b/src/canvas/JoinPool/Overview/index.tsx @@ -0,0 +1,29 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { JoinForm } from './JoinForm'; + +import { PerformanceGraph } from './PerformanceGraph'; +import { Stats } from './Stats'; +import { Addresses } from './Addresses'; +import { Roles } from './Roles'; +import { GraphLayoutWrapper } from '../Wrappers'; +import type { OverviewSectionProps } from '../types'; + +export const Overview = (props: OverviewSectionProps) => ( + <> +
+ + + + + + +
+
+
+ +
+
+ +); diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts new file mode 100644 index 0000000000..d1d76432d3 --- /dev/null +++ b/src/canvas/JoinPool/Wrappers.ts @@ -0,0 +1,382 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const JoinPoolInterfaceWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + > .header { + display: flex; + margin-bottom: 2rem; + } + + > .content { + display: flex; + flex-grow: 1; + + @media (max-width: 1000px) { + flex-flow: row wrap; + } + + > div { + display: flex; + + &.main { + flex-grow: 1; + display: flex; + flex-direction: column; + padding-right: 4rem; + + @media (max-width: 1000px) { + flex-basis: 100%; + padding-right: 0; + } + } + + &.side { + min-width: 450px; + + @media (max-width: 1000px) { + flex-grow: 1; + flex-basis: 100%; + margin-top: 0.5rem; + } + + > div { + width: 100%; + } + } + } + } +`; + +export const TitleWrapper = styled.div` + border-bottom: 1px solid var(--border-secondary-color); + flex: 1; + display: flex; + flex-direction: column; + margin: 2rem 0 1.55rem 0; + padding-bottom: 0.1rem; + + > .inner { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + flex: 1; + + > div { + display: flex; + flex: 1; + + &:nth-child(1) { + max-width: 4rem; + } + + &:nth-child(2) { + padding-left: 1rem; + flex-direction: column; + + > .title { + position: relative; + padding-top: 2rem; + flex: 1; + + h1 { + position: absolute; + top: 0; + left: 0; + margin: 0; + line-height: 2.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + } + + > .labels { + display: flex; + margin-top: 1.1rem; + + > h3 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + + > svg { + margin: 0 0 0 0.2rem; + } + + > span { + border: 1px solid var(--border-secondary-color); + border-radius: 0.5rem; + padding: 0.4rem 0.6rem; + margin-left: 1rem; + font-size: 1.1rem; + + &.blocked { + color: var(--status-warning-color); + border-color: var(--status-warning-color); + } + + &.destroying { + color: var(--status-danger-color); + border-color: var(--status-danger-color); + } + } + } + } + } + } + } +`; + +export const JoinFormWrapper = styled.div` + background: var(--background-canvas-card); + border: 0.75px solid var(--border-primary-color); + box-shadow: var(--card-shadow); + border-radius: 1.5rem; + padding: 1.5rem; + width: 100%; + + @media (max-width: 1000px) { + margin-top: 1rem; + } + + h4 { + display: flex; + align-items: center; + &.note { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + } + } + + > h2 { + color: var(--text-color-secondary); + margin: 0.25rem 0; + } + + > h4 { + margin: 1.5rem 0 0.5rem 0; + color: var(--text-color-tertiary); + + &.underline { + border-bottom: 1px solid var(--border-primary-color); + padding-bottom: 0.5rem; + margin: 2rem 0 1rem 0; + } + } + + > .input { + border-bottom: 1px solid var(--border-primary-color); + padding: 0 0.25rem; + display: flex; + align-items: flex-end; + padding-bottom: 1.25rem; + + > div { + flex-grow: 1; + display: flex; + flex-direction: column; + + > div { + margin: 0; + } + } + } + + > .available { + margin-top: 0.5rem; + margin-bottom: 1.5rem; + display: flex; + } + + > .submit { + margin-top: 2.5rem; + } +`; + +export const HeadingWrapper = styled.div` + margin: 0.5rem 0.5rem 0.5rem 0rem; + + @media (max-width: 600px) { + margin-right: 0; + } + + h3, + p { + padding: 0 0.5rem; + } + + h4 { + font-size: 1.15rem; + } + + p { + color: var(--text-color-tertiary); + margin: 0.35rem 0 0 0; + } + + > h3, + h4 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-wrap: wrap; + } + + > span { + background-color: var(--background-canvas-card-secondary); + color: var(--text-color-secondary); + font-family: InterBold, sans-serif; + border-radius: 1.5rem; + padding: 0rem 1.25rem; + margin-right: 1rem; + height: 2.6rem; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-grow: 1; + justify-content: center; + margin-bottom: 1rem; + margin-right: 0; + height: 2.9rem; + width: 100%; + + &:last-child { + margin-bottom: 0; + } + } + + &.balance { + padding-left: 0.5rem; + } + + > .icon { + width: 2.1rem; + height: 2.1rem; + margin-right: 0.3rem; + } + &.inactive { + color: var(--text-color-tertiary); + border: 1px solid var(--border-secondary-color); + } + } + } +`; + +export const AddressesWrapper = styled.div` + flex: 1; + display: flex; + padding: 0rem 0.25rem; + flex-wrap: wrap; + + > section { + display: flex; + flex-direction: column; + flex-basis: 50%; + margin: 0.9rem 0 0.7rem 0; + + @media (max-width: 600px) { + flex-basis: 100%; + } + + > div { + display: flex; + flex-direction: row; + align-items: center; + + > span { + margin-right: 0.75rem; + } + + > h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + display: flex; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; + flex: 1; + + &.heading { + font-family: InterBold, sans-serif; + } + + > .label { + margin-left: 0.75rem; + + > button { + color: var(--text-color-tertiary); + } + } + } + } + } +`; + +// Wrapper that houses the chart, allowing it to be responsive. +export const GraphWrapper = styled.div` + flex: 1; + position: relative; + padding: 0 4rem 0 1rem; + margin-top: 2rem; + + @media (max-width: 1000px) { + padding: 0 0 0 1rem; + } + + > .inner { + position: absolute; + width: 100%; + height: 100%; + padding-left: 1rem; + padding-right: 4rem; + + @media (max-width: 1000px) { + padding-right: 1.5rem; + } + } +`; + +// Element used to wrap graph and pool stats, allowing flex ordering on smaller screens. +export const GraphLayoutWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + + @media (min-width: 1001px) { + > div:last-child { + margin-top: 1.25rem; + } + } + + @media (max-width: 1000px) { + > div { + &:first-child { + order: 2; + margin-top: 1.5rem; + margin-bottom: 0; + } + &:last-child { + order: 1; + } + } + } +`; + +export const NominationsWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; +`; diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx new file mode 100644 index 0000000000..7f31fbb023 --- /dev/null +++ b/src/canvas/JoinPool/index.tsx @@ -0,0 +1,107 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CanvasFullScreenWrapper } from 'canvas/Wrappers'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { JoinPoolInterfaceWrapper } from './Wrappers'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useMemo, useState } from 'react'; +import { Header } from './Header'; +import { Overview } from './Overview'; +import { Nominations } from './Nominations'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useStaking } from 'contexts/Staking'; + +export const JoinPool = () => { + const { + closeCanvas, + config: { options }, + } = useOverlay().canvas; + const { eraStakers } = useStaking(); + const { poolRewardPoints } = usePoolPerformance(); + const { poolsMetaData, bondedPools } = useBondedPools(); + + // The active canvas tab. + const [activeTab, setActiveTab] = useState(0); + + // Trigger re-render when chosen selected pool is incremented. + const [selectedPoolCount, setSelectedPoolCount] = useState(0); + + // Filter bonded pools to only those that are open and that have active daily rewards for the last + // `MaxEraRewardPointsEras` eras. The second filter checks if the pool is in `eraStakers` for the + // active era. + const filteredBondedPools = useMemo( + () => + bondedPools + .filter((pool) => { + // Fetch reward point data for the pool. + const rawEraRewardPoints = + poolRewardPoints[pool.addresses.stash] || {}; + const rewardPoints = Object.values(rawEraRewardPoints); + + // Ensure pool has been active for every era in performance data. + const activeDaily = + rewardPoints.every((points) => Number(points) > 0) && + rewardPoints.length === MaxEraRewardPointsEras; + + return pool.state === 'Open' && activeDaily; + }) + // Ensure the pool is currently in the active set of backers. + .filter((pool) => + eraStakers.stakers.find((staker) => + staker.others.find(({ who }) => who !== pool.addresses.stash) + ) + ), + [bondedPools, poolRewardPoints] + ); + + // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered + // pool otherwise. Re-fetches when the selected pool count is incremented. + const bondedPool = useMemo( + () => + options?.poolId + ? bondedPools.find(({ id }) => id === options.poolId) + : filteredBondedPools[ + (filteredBondedPools.length * Math.random()) << 0 + ], + [selectedPoolCount] + ); + + // The selected bonded pool id. + const [selectedPoolId, setSelectedPoolId] = useState( + bondedPool?.id || 0 + ); + + if (!bondedPool) { + closeCanvas(); + return null; + } + + return ( + +
+ + +
+ {activeTab === 0 && } + {activeTab === 1 && ( + + )} +
+
+ + ); +}; diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts new file mode 100644 index 0000000000..b7d2dcedac --- /dev/null +++ b/src/canvas/JoinPool/types.ts @@ -0,0 +1,31 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; +import type { Dispatch, SetStateAction } from 'react'; + +export interface JoinPoolHeaderProps { + activeTab: number; + bondedPool: BondedPool; + filteredBondedPools: BondedPool[]; + metadata: string; + autoSelected: boolean; + setActiveTab: (tab: number) => void; + setSelectedPoolId: Dispatch>; + setSelectedPoolCount: Dispatch>; +} + +export interface NominationsProps { + stash: string; + poolId: number; +} + +export interface AddressSectionProps { + address: string; + label: string; + helpKey?: string; +} + +export interface OverviewSectionProps { + bondedPool: BondedPool; +} diff --git a/src/canvas/Wrappers.tsx b/src/canvas/Wrappers.ts similarity index 100% rename from src/canvas/Wrappers.tsx rename to src/canvas/Wrappers.ts diff --git a/src/contexts/Api/index.tsx b/src/contexts/Api/index.tsx index 46b1bea262..031bd3149e 100644 --- a/src/contexts/Api/index.tsx +++ b/src/contexts/Api/index.tsx @@ -331,6 +331,11 @@ export const APIProvider = ({ children, network }: APIProviderProps) => { const reInitialiseApi = async (type: ConnectionType) => { setApiStatus('disconnected'); + + // Dispatch all default syncIds as syncing. + SyncController.dispatchAllDefault(); + + // Instanaite new API instance. await ApiController.instantiate(network, type, rpcEndpoint); }; diff --git a/src/contexts/Pools/ActivePool/index.tsx b/src/contexts/Pools/ActivePool/index.tsx index b329d86608..b8904850af 100644 --- a/src/contexts/Pools/ActivePool/index.tsx +++ b/src/contexts/Pools/ActivePool/index.tsx @@ -76,8 +76,9 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { // Only listen to the currently selected active pool, otherwise return an empty array. const poolIds = activePoolIdRef.current ? [activePoolIdRef.current] : []; - // Listen for active pools. - const { activePools, poolNominations } = useActivePools({ + // Listen for active pools. NOTE: `activePoolsRef` is needed to check if the pool has changed + // after the async call of fetching pending rewards. + const { activePools, activePoolsRef, poolNominations } = useActivePools({ poolIds, onCallback: async () => { // Sync: active pools synced once all account pools have been reported. @@ -116,6 +117,9 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { addresses: { ...createPoolAccounts(Number(pool)) }, })); ActivePoolsController.syncPools(api, newActivePools); + } else { + // No active pools to sync. Mark as complete. + SyncController.dispatch('active-pools', 'complete'); } }; @@ -197,9 +201,20 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { if ( activePool && membership?.poolId && + membership?.address && String(activePool.id) === String(membership.poolId) ) { - setPendingPoolRewards(await fetchPendingRewards(membership?.address)); + const pendingRewards = await fetchPendingRewards(membership.address); + + // Check if active pool has changed in the time the pending rewards were being fetched. If it + // has, do not update. + if ( + activePoolId && + activePoolsRef.current[activePoolId]?.id === + Number(membership.poolId || -1) + ) { + setPendingPoolRewards(pendingRewards); + } } else { setPendingPoolRewards(new BigNumber(0)); } diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx index d3d22677f8..c070f79a8f 100644 --- a/src/contexts/Pools/BondedPools/index.tsx +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -20,6 +20,7 @@ import { useNetwork } from 'contexts/Network'; import { useApi } from '../../Api'; import { defaultBondedPoolsContext } from './defaults'; import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; +import { SyncController } from 'controllers/SyncController'; export const BondedPoolsContext = createContext( defaultBondedPoolsContext @@ -85,6 +86,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { ); bondedPoolsSynced.current = 'synced'; + SyncController.dispatch('bonded-pools', 'complete'); }; // Fetches pool nominations and updates state. @@ -197,7 +199,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { }); const getBondedPool = (poolId: MaybePool) => - bondedPools.find((p) => p.id === poolId) ?? null; + bondedPools.find((p) => String(p.id) === String(poolId)) ?? null; /* * poolSearchFilter Iterates through the supplied list and refers to the meta batch of the list to @@ -386,6 +388,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { // Clear existing state for network refresh. useEffectIgnoreInitial(() => { bondedPoolsSynced.current = 'unsynced'; + SyncController.dispatch('bonded-pools', 'syncing'); setStateWithRef([], setBondedPools, bondedPoolsRef); setPoolsMetadata({}); setPoolsNominations({}); diff --git a/src/controllers/ActivePoolsController/defaults.ts b/src/controllers/ActivePoolsController/defaults.ts new file mode 100644 index 0000000000..5280165f72 --- /dev/null +++ b/src/controllers/ActivePoolsController/defaults.ts @@ -0,0 +1,4 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const defaultClaimPermission = 'PermissionlessWithdraw'; diff --git a/src/controllers/SyncController/defaults.ts b/src/controllers/SyncController/defaults.ts new file mode 100644 index 0000000000..e04d5254b5 --- /dev/null +++ b/src/controllers/SyncController/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { SyncID } from './types'; + +export const defaultSyncIds: SyncID[] = [ + 'initialization', + 'balances', + 'era-stakers', + 'bonded-pools', + 'active-pools', +]; diff --git a/src/controllers/SyncController/index.ts b/src/controllers/SyncController/index.ts index 67baa82887..7ad062c693 100644 --- a/src/controllers/SyncController/index.ts +++ b/src/controllers/SyncController/index.ts @@ -1,18 +1,27 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import { defaultSyncIds } from './defaults'; import type { SyncEvent, SyncID, SyncIDConfig, SyncStatus } from './types'; export class SyncController { // ------------------------------------------------------ // Class members // ------------------------------------------------------ - static syncIds: SyncID[] = []; + + // List of all syncIds currently syncing. NOTE: `initialization` is added by default as the + // network always initializes from initial state. + static syncIds: SyncID[] = defaultSyncIds; // ------------------------------------------------------ // Dispatch sync events // ------------------------------------------------------ + // Dispatch all default syncId events as syncing. + static dispatchAllDefault = () => { + this.syncIds.forEach((id) => this.dispatch(id, 'syncing')); + }; + // Dispatches a new sync event to the document. static dispatch = (id: SyncID, status: SyncStatus) => { const detail: SyncEvent = { @@ -20,20 +29,31 @@ export class SyncController { status, }; + // Whether to dispatch the event. + let dispatch = true; + // Keep class syncIds up to date. - if (status === 'syncing' && !this.syncIds.includes(id)) { - this.syncIds.push(id); + if (status === 'syncing') { + if (this.syncIds.includes(id)) { + // Cancel event if already syncing. + dispatch = false; + } else { + this.syncIds.push(id); + } } - if (status === 'complete' && this.syncIds.includes(id)) { + + if (status === 'complete') { this.syncIds = this.syncIds.filter((syncId) => syncId !== id); } // Dispatch event to UI. - document.dispatchEvent( - new CustomEvent('new-sync-status', { - detail, - }) - ); + if (dispatch) { + document.dispatchEvent( + new CustomEvent('new-sync-status', { + detail, + }) + ); + } }; // Checks if event detailis a valid `new-sync-status` event. diff --git a/src/controllers/SyncController/types.ts b/src/controllers/SyncController/types.ts index ced7533cba..75c3f97b81 100644 --- a/src/controllers/SyncController/types.ts +++ b/src/controllers/SyncController/types.ts @@ -5,6 +5,7 @@ export type SyncID = | 'initialization' | 'balances' | 'era-stakers' + | 'bonded-pools' | 'active-pools'; export interface SyncEvent { diff --git a/src/hooks/useActivePools/index.tsx b/src/hooks/useActivePools/index.tsx index f658c9da59..02a80374ce 100644 --- a/src/hooks/useActivePools/index.tsx +++ b/src/hooks/useActivePools/index.tsx @@ -33,11 +33,6 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { const { pool, nominations } = e.detail; const { id } = pool; - // Call custom `onCallback` function if provided. - if (typeof onCallback === 'function') { - await onCallback(e.detail); - } - // Persist to active pools state if this pool is specified in `poolIds`. if ( poolIds === '*' || @@ -55,11 +50,14 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { poolNominationsRef ); } + + // Call custom `onCallback` function if provided. + if (typeof onCallback === 'function') { + await onCallback(e.detail); + } } }; - const documentRef = useRef(document); - // Bootstrap state on initial render. useEffect(() => { const initialActivePools = @@ -94,7 +92,8 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { }, [network, activeAccount]); // Listen for new active pool events. + const documentRef = useRef(document); useEventListener('new-active-pool', newActivePoolCallback, documentRef); - return { activePools, poolNominations }; + return { activePools, activePoolsRef, poolNominations }; }; diff --git a/src/hooks/useSyncing/index.tsx b/src/hooks/useSyncing/index.tsx index b1a6c843a2..f1a19b1fae 100644 --- a/src/hooks/useSyncing/index.tsx +++ b/src/hooks/useSyncing/index.tsx @@ -8,12 +8,12 @@ import type { SyncID, SyncIDConfig } from 'controllers/SyncController/types'; import { isCustomEvent } from 'controllers/utils'; import { useEventListener } from 'usehooks-ts'; -export const useSyncing = (config: SyncIDConfig) => { +export const useSyncing = (config: SyncIDConfig = '*') => { // Retrieve the ids from the config provided. const ids = SyncController.getIdsFromSyncConfig(config); // Keep a record of active sync statuses. - const [syncIds, setSyncIds] = useState([]); + const [syncIds, setSyncIds] = useState(SyncController.syncIds); const syncIdsRef = useRef(syncIds); // Handle new syncing status events. @@ -40,7 +40,16 @@ export const useSyncing = (config: SyncIDConfig) => { } }; - const documentRef = useRef(document); + // Helper to determine if pool membership is syncing. + const poolMembersipSyncing = (): boolean => { + const POOL_SYNC_IDS: SyncID[] = [ + 'initialization', + 'balances', + 'bonded-pools', + 'active-pools', + ]; + return syncIds.some(() => POOL_SYNC_IDS.find((id) => syncIds.includes(id))); + }; // Bootstrap existing sync statuses of interest when hook is mounted. useEffect(() => { @@ -54,7 +63,8 @@ export const useSyncing = (config: SyncIDConfig) => { }, []); // Listen for new sync events. + const documentRef = useRef(document); useEventListener('new-sync-status', newSyncStatusCallback, documentRef); - return { syncing: syncIds.length > 0 }; + return { syncing: syncIds.length > 0, poolMembersipSyncing }; }; diff --git a/src/kits/Buttons/index.scss b/src/kits/Buttons/index.scss index 2ecc25d1e1..f80d20a92e 100644 --- a/src/kits/Buttons/index.scss +++ b/src/kits/Buttons/index.scss @@ -440,6 +440,15 @@ border: 1px solid var(--border-secondary-color); } } + + &.canvas { + color: var(--text-color-tertiary); + + &.active { + color: var(--text-color-primary); + background: var(--button-tab-canvas-background); + } + } } .btn-tertiary { diff --git a/src/kits/Structure/PageTitle/types.ts b/src/kits/Structure/PageTitle/types.ts index bedc24c1d9..c71624a590 100644 --- a/src/kits/Structure/PageTitle/types.ts +++ b/src/kits/Structure/PageTitle/types.ts @@ -4,6 +4,10 @@ import type { PageTitleTabsProps } from '../PageTitleTabs/types'; export type PageTitleProps = PageTitleTabsProps & { + // tab button class. + tabClassName?: string; + // whether tabs are inline. + inline?: boolean; // title of the page. title?: string; // a button right next to the page title. diff --git a/src/kits/Structure/PageTitleTabs/Wrapper.ts b/src/kits/Structure/PageTitleTabs/Wrapper.ts index d9dcdf0bc1..7137b57580 100644 --- a/src/kits/Structure/PageTitleTabs/Wrapper.ts +++ b/src/kits/Structure/PageTitleTabs/Wrapper.ts @@ -11,6 +11,10 @@ export const Wrapper = styled.section` margin-top: 0.9rem; max-width: 100%; + &.inline { + border-bottom: none; + } + @media (max-width: ${PageWidthMediumThreshold}px) { margin-top: 0.5rem; } diff --git a/src/kits/Structure/PageTitleTabs/index.tsx b/src/kits/Structure/PageTitleTabs/index.tsx index 180de7132a..5855b96cb5 100644 --- a/src/kits/Structure/PageTitleTabs/index.tsx +++ b/src/kits/Structure/PageTitleTabs/index.tsx @@ -11,13 +11,21 @@ import { Wrapper } from './Wrapper'; * @name PageTitleTabs * @summary The element in a page title. Inculding the ButtonTab. */ -export const PageTitleTabs = ({ sticky, tabs = [] }: PageTitleProps) => ( - +export const PageTitleTabs = ({ + sticky, + tabs = [], + inline = false, + tabClassName, +}: PageTitleProps) => ( +
{tabs.map( ({ active, onClick, title, badge }: PageTitleTabProps, i: number) => ( onClick()} diff --git a/src/kits/Structure/Tx/Signer.tsx b/src/kits/Structure/Tx/Signer.tsx new file mode 100644 index 0000000000..b763f2d700 --- /dev/null +++ b/src/kits/Structure/Tx/Signer.tsx @@ -0,0 +1,33 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import type { SignerProps } from './types'; +import { SignerWrapper } from './Wrapper'; + +export const Signer = ({ + dangerMessage, + notEnoughFunds, + name, + label, +}: SignerProps) => ( + + + + {label} + + {name} + {notEnoughFunds && ( + + /   + + {dangerMessage} + + )} + +); diff --git a/src/kits/Structure/Tx/Wrapper.ts b/src/kits/Structure/Tx/Wrapper.ts index 88573abb21..89ff719b76 100644 --- a/src/kits/Structure/Tx/Wrapper.ts +++ b/src/kits/Structure/Tx/Wrapper.ts @@ -23,6 +23,10 @@ export const Wrapper = styled.div` background: var(--background-canvas-card); } + &.card { + border-radius: 0.5rem; + } + > section { width: 100%; @@ -31,6 +35,26 @@ export const Wrapper = styled.div` flex-direction: row; align-items: center; + &.col { + flex-direction: column; + margin-top: 0.5rem; + + > div { + width: 100%; + margin-bottom: 0.4rem; + + > div, + > p { + width: 100%; + margin-bottom: 0.4rem; + } + + > div:last-child { + margin-bottom: 0; + } + } + } + > div { display: flex; @@ -81,35 +105,35 @@ export const Wrapper = styled.div` } } } +`; - .sign { - display: flex; - align-items: center; - font-size: 0.9rem; - padding-bottom: 0.5rem; - margin: 0; - - .badge { - border: 1px solid var(--border-secondary-color); - border-radius: 0.45rem; - padding: 0.2rem 0.5rem; - margin-right: 0.75rem; - - > svg { - margin-right: 0.5rem; - } +export const SignerWrapper = styled.p` + display: flex; + align-items: center; + font-size: 0.9rem; + padding-bottom: 0.5rem; + margin: 0; + + .badge { + border: 1px solid var(--border-secondary-color); + border-radius: 0.45rem; + padding: 0.2rem 0.5rem; + margin-right: 0.75rem; + + > svg { + margin-right: 0.5rem; } + } - .not-enough { - margin-left: 0.5rem; - } + .not-enough { + margin-left: 0.5rem; + } - .danger { - color: var(--status-danger-color); - } + .danger { + color: var(--status-danger-color); + } - > .icon { - margin-right: 0.3rem; - } + > .icon { + margin-right: 0.3rem; } `; diff --git a/src/kits/Structure/Tx/index.tsx b/src/kits/Structure/Tx/index.tsx index 8c93b9f4b2..5e312d441a 100644 --- a/src/kits/Structure/Tx/index.tsx +++ b/src/kits/Structure/Tx/index.tsx @@ -1,29 +1,10 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { ReactElement } from 'react'; -import type { DisplayFor } from 'types'; import { Wrapper } from './Wrapper'; import { appendOrEmpty } from '@w3ux/utils'; - -export interface TxProps { - // whether there is margin on top. - margin?: boolean; - // account type for the transaction signing. - label: string; - // account id - name: string; - // whether there is enough funds for the transaction. - notEnoughFunds: boolean; - // warning messgae. - dangerMessage: string; - // signing component. - SignerComponent: ReactElement; - // display for. - displayFor?: DisplayFor; -} +import type { TxProps } from './types'; +import { Signer } from './Signer'; /** * @name Tx @@ -39,25 +20,15 @@ export const Tx = ({ displayFor = 'default', }: TxProps) => ( -
-

- - - {label} - - {name} - {notEnoughFunds && ( - - /   - {' '} - {dangerMessage} - - )} -

+
+
{SignerComponent}
diff --git a/src/kits/Structure/Tx/types.ts b/src/kits/Structure/Tx/types.ts new file mode 100644 index 0000000000..01e5981dac --- /dev/null +++ b/src/kits/Structure/Tx/types.ts @@ -0,0 +1,18 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactElement } from 'react'; +import type { DisplayFor } from 'types'; + +export interface SignerProps { + label: string; + name: string; + notEnoughFunds: boolean; + dangerMessage: string; +} + +export interface TxProps extends SignerProps { + margin?: boolean; + SignerComponent: ReactElement; + displayFor?: DisplayFor; +} diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx new file mode 100644 index 0000000000..259285960a --- /dev/null +++ b/src/library/CallToAction/index.tsx @@ -0,0 +1,187 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const CallToActionWrapper = styled.div` + --button-border-radius: 2rem; + --button-vertical-space: 1.1rem; + + height: inherit; + width: 100%; + + > .inner { + flex: 1; + display: flex; + flex-direction: row; + margin-top: 0.38rem; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > section { + display: flex; + flex-direction: row; + height: inherit; + + @media (max-width: 650px) { + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + &:nth-child(1) { + flex-grow: 1; + + @media (min-width: 651px) { + border-right: 1px solid var(--border-primary-color); + padding-right: 1rem; + } + } + + &:nth-child(2) { + flex: 1; + + @media (min-width: 651px) { + padding-left: 1rem; + } + } + + &.standalone { + border: none; + padding: 0; + } + + h3 { + line-height: 1.4rem; + } + + .buttons { + border: 0.75px solid var(--border-primary-color); + border-radius: var(--button-border-radius); + display: flex; + flex-wrap: nowrap; + width: 100%; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > .button { + height: 3.75rem; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + white-space: nowrap; + overflow: hidden; + transition: filter 0.15s; + + &.primary { + background-color: var(--accent-color-primary); + border-top-left-radius: var(--button-border-radius); + border-bottom-left-radius: var(--button-border-radius); + color: white; + flex-grow: 1; + + &:hover { + filter: brightness(90%); + } + + &.disabled { + background-color: var(--accent-color-pending); + + &:hover { + filter: none; + } + } + + &.pulse { + box-shadow: 0 0 30px 0 var(--accent-color-pending); + transform: scale(1); + animation: pulse 4s infinite; + + @keyframes pulse { + 0% { + transform: scale(0.98); + box-shadow: 0 0 0 0 var(--accent-color-pending); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgb(0 0 0 / 0%); + } + + 100% { + transform: scale(0.98); + box-shadow: 0 0 0 0 rgb(0 0 0 / 0%); + } + } + } + } + + &.secondary { + background-color: var(--button-primary-background); + border-top-right-radius: var(--button-border-radius); + border-bottom-right-radius: var(--button-border-radius); + color: var(--text-color-primary); + + &:hover { + filter: brightness(95%); + } + + &.disabled { + opacity: 0.5; + + &:hover { + filter: none; + } + } + } + + &.standalone { + border-radius: var(--button-border-radius); + flex-grow: 1; + } + + @media (max-width: 650px) { + border-radius: var(--button-border-radius); + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + > button { + color: inherit; + height: inherit; + transition: transform 0.25s; + padding: 0 2rem; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + font-size: 1.3rem; + width: 100%; + + &:disabled { + cursor: default; + } + + > svg { + margin: 0 0.75rem; + } + } + } + } + } + } +`; diff --git a/src/library/Card/Wrappers.ts b/src/library/Card/Wrappers.ts index 87e04050d1..a63ebf9f65 100644 --- a/src/library/Card/Wrappers.ts +++ b/src/library/Card/Wrappers.ts @@ -72,6 +72,7 @@ export const CardHeaderWrapper = styled.div` * Used to separate the main modules throughout the app. */ export const CardWrapper = styled.div` + border: 1px solid transparent; box-shadow: var(--card-shadow); background: var(--background-primary); border-radius: 1.1rem; @@ -82,10 +83,23 @@ export const CardWrapper = styled.div` overflow: hidden; margin-top: 1.4rem; padding: 1.5rem; + transition: border 0.2s; &.canvas { background: var(--background-canvas-card); padding: 1.25rem; + + &.secondary { + padding: 1rem; + + @media (max-width: 1000px) { + background: var(--background-canvas-card); + } + + @media (min-width: 1001px) { + background: var(--background-canvas-card-secondary); + } + } } &.transparent { @@ -101,6 +115,10 @@ export const CardWrapper = styled.div` border: 1px solid var(--status-warning-color); } + &.prompt { + border: 1px solid var(--accent-color-pending); + } + @media (max-width: ${PageWidthMediumThreshold}px) { padding: 1rem 0.75rem; } diff --git a/src/library/Form/Bond/BondFeedback.tsx b/src/library/Form/Bond/BondFeedback.tsx index 06ccbfb027..944095dd51 100644 --- a/src/library/Form/Bond/BondFeedback.tsx +++ b/src/library/Form/Bond/BondFeedback.tsx @@ -27,6 +27,7 @@ export const BondFeedback = ({ txFees, maxWidth, syncing = false, + displayFirstWarningOnly = true, }: BondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -145,6 +146,10 @@ export const BondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ @@ -168,7 +173,7 @@ export const BondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/ClaimPermissionInput/index.tsx b/src/library/Form/ClaimPermissionInput/index.tsx index 9915509d6a..ac87b2c324 100644 --- a/src/library/Form/ClaimPermissionInput/index.tsx +++ b/src/library/Form/ClaimPermissionInput/index.tsx @@ -6,48 +6,35 @@ import { useTranslation } from 'react-i18next'; import { TabWrapper, TabsWrapper } from 'library/Filter/Wrappers'; import type { ClaimPermission } from 'contexts/Pools/types'; import type { ClaimPermissionConfig } from '../types'; -import { ActionItem } from 'library/ActionItem'; - -export interface ClaimPermissionInputProps { - current: ClaimPermission | undefined; - permissioned: boolean; - onChange: (value: ClaimPermission | undefined) => void; - disabled?: boolean; -} +import type { ClaimPermissionInputProps } from './types'; export const ClaimPermissionInput = ({ current, - permissioned, onChange, disabled = false, }: ClaimPermissionInputProps) => { const { t } = useTranslation('library'); const claimPermissionConfig: ClaimPermissionConfig[] = [ - { - label: t('allowCompound'), - value: 'PermissionlessCompound', - description: t('allowAnyoneCompound'), - }, { label: t('allowWithdraw'), value: 'PermissionlessWithdraw', description: t('allowAnyoneWithdraw'), }, { - label: t('allowAll'), - value: 'PermissionlessAll', - description: t('allowAnyoneCompoundWithdraw'), + label: t('allowCompound'), + value: 'PermissionlessCompound', + description: t('allowAnyoneCompound'), + }, + { + label: t('permissioned'), + value: 'Permissioned', + description: t('permissionedSubtitle'), }, ]; - // Updated claim permission value - const [selected, setSelected] = useState( - current - ); - - // Permissionless claim enabled. - const [enabled, setEnabled] = useState(permissioned); + // Updated claim permission value. + const [selected, setSelected] = useState(current); const activeTab = claimPermissionConfig.find( ({ value }) => value === selected @@ -60,43 +47,22 @@ export const ClaimPermissionInput = ({ return ( <> - { - // toggle enable claim permission. - setEnabled(val); - - const newClaimPermission = val - ? claimPermissionConfig[0].value - : current === undefined - ? undefined - : 'Permissioned'; - - setSelected(newClaimPermission); - onChange(newClaimPermission); - }} - disabled={disabled} - inactive={disabled} - /> {claimPermissionConfig.map(({ label, value }, i) => ( { setSelected(value); onChange(value); }} + style={{ flexGrow: 1 }} > {label} @@ -104,13 +70,17 @@ export const ClaimPermissionInput = ({
{activeTab ? ( -

{activeTab.description}

+

+ {activeTab.description} +

) : ( -

{t('permissionlessClaimingTurnedOff')}

+

+ {t('permissionlessClaimingTurnedOff')} +

)}
diff --git a/src/library/Form/ClaimPermissionInput/types.ts b/src/library/Form/ClaimPermissionInput/types.ts new file mode 100644 index 0000000000..b4d2de3367 --- /dev/null +++ b/src/library/Form/ClaimPermissionInput/types.ts @@ -0,0 +1,10 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ClaimPermission } from 'contexts/Pools/types'; + +export interface ClaimPermissionInputProps { + current: ClaimPermission; + onChange: (value: ClaimPermission) => void; + disabled?: boolean; +} diff --git a/src/library/Form/Unbond/UnbondFeedback.tsx b/src/library/Form/Unbond/UnbondFeedback.tsx index 6f82845d57..9521af7982 100644 --- a/src/library/Form/Unbond/UnbondFeedback.tsx +++ b/src/library/Form/Unbond/UnbondFeedback.tsx @@ -24,6 +24,7 @@ export const UnbondFeedback = ({ setLocalResize, parentErrors = [], txFees, + displayFirstWarningOnly = true, }: UnbondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -129,6 +130,10 @@ export const UnbondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ bond: defaultValue }); @@ -148,7 +153,7 @@ export const UnbondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/types.ts b/src/library/Form/types.ts index 3222f067f7..186f78672b 100644 --- a/src/library/Form/types.ts +++ b/src/library/Form/types.ts @@ -49,6 +49,7 @@ export interface BondFeedbackProps { setLocalResize?: () => void; txFees: BigNumber; maxWidth?: boolean; + displayFirstWarningOnly?: boolean; } export interface BondInputProps { @@ -70,6 +71,7 @@ export interface UnbondFeedbackProps { parentErrors?: string[]; setLocalResize?: () => void; txFees: BigNumber; + displayFirstWarningOnly?: boolean; } export interface UnbondInputProps { diff --git a/src/library/Graphs/GeoDonut.tsx b/src/library/Graphs/GeoDonut.tsx index 3dd8904e45..4ce00d3d13 100644 --- a/src/library/Graphs/GeoDonut.tsx +++ b/src/library/Graphs/GeoDonut.tsx @@ -76,8 +76,8 @@ export const GeoDonut = ({ { label: title, data, - // We make a gradient of N+2 colors from active to inactive, and we discard both ends - // N is the number of datapoints to plot + // We make a gradient of N+2 colors from active to inactive, and we discard both ends N is + // the number of datapoints to plot. backgroundColor: chroma .scale([backgroundColor, graphColors.inactive[mode]]) .colors(data.length + 1), diff --git a/src/library/Graphs/PayoutBar.tsx b/src/library/Graphs/PayoutBar.tsx index e6d17e6230..9d4fdbcd78 100644 --- a/src/library/Graphs/PayoutBar.tsx +++ b/src/library/Graphs/PayoutBar.tsx @@ -94,6 +94,7 @@ export const PayoutBar = ({ }); return `${dateObj}`; }), + datasets: [ { order: 1, diff --git a/src/library/Headers/Sync.tsx b/src/library/Headers/Sync.tsx index a6f4f63c1c..6c4188a047 100644 --- a/src/library/Headers/Sync.tsx +++ b/src/library/Headers/Sync.tsx @@ -13,8 +13,8 @@ import { useTxMeta } from 'contexts/TxMeta'; import { useSyncing } from 'hooks/useSyncing'; export const Sync = () => { + const { syncing } = useSyncing(); const { pathname } = useLocation(); - const { syncing } = useSyncing('*'); const { pendingNonces } = useTxMeta(); const { payoutsSynced } = usePayouts(); const { pluginEnabled } = usePlugins(); diff --git a/src/library/ListItem/Labels/EraStatus.tsx b/src/library/ListItem/Labels/EraStatus.tsx index b3256cf61d..d407860c9c 100644 --- a/src/library/ListItem/Labels/EraStatus.tsx +++ b/src/library/ListItem/Labels/EraStatus.tsx @@ -10,7 +10,7 @@ import { useSyncing } from 'hooks/useSyncing'; export const EraStatus = ({ noMargin, status, totalStake }: EraStatusProps) => { const { t } = useTranslation('library'); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { unit, units } = useNetwork().networkData; // Fallback to `waiting` status if still syncing. diff --git a/src/library/ListItem/Labels/JoinPool.tsx b/src/library/ListItem/Labels/JoinPool.tsx index 0bb9e9e6ff..b6e98cb25b 100644 --- a/src/library/ListItem/Labels/JoinPool.tsx +++ b/src/library/ListItem/Labels/JoinPool.tsx @@ -14,20 +14,20 @@ export const JoinPool = ({ setActiveTab: (t: number) => void; }) => { const { t } = useTranslation('library'); - const { openModal } = useOverlay().modal; + const { openCanvas } = useOverlay().canvas; return (
+
+
+ +
+ +); diff --git a/src/library/SubmitTx/Default.tsx b/src/library/SubmitTx/Default.tsx index e475696a4b..5305e89ec0 100644 --- a/src/library/SubmitTx/Default.tsx +++ b/src/library/SubmitTx/Default.tsx @@ -8,6 +8,8 @@ import { EstimatedTxFee } from 'library/EstimatedTxFee'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from './types'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from './ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Default = ({ onSubmit, @@ -25,22 +27,35 @@ export const Default = ({ submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; return ( -
-
- + <> +
+
+ +
+
+ {buttons} + {displayFor !== 'card' && ( + onSubmit()} + disabled={disabled} + pulse={!disabled} + /> + )} +
-
- {buttons} - onSubmit()} + {displayFor === 'card' && ( + -
-
+ )} + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx index 04a9fb1500..4f95c80416 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx @@ -11,6 +11,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import { useNetwork } from 'contexts/Network'; import { useTxMeta } from 'contexts/TxMeta'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; import type { LedgerSubmitProps } from 'library/SubmitTx/types'; import { useTranslation } from 'react-i18next'; @@ -73,7 +74,7 @@ export const Submit = ({ // Button icon. const icon = !integrityChecked ? faUsb : faSquarePen; - return ( + return displayFor !== 'card' ? ( + ) : ( + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/index.tsx b/src/library/SubmitTx/ManualSign/Ledger/index.tsx index 3011f06e19..1b2701cb0f 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/index.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/index.tsx @@ -19,6 +19,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import type { SubmitProps } from '../../types'; import { Submit } from './Submit'; import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { appendOrEmpty } from '@w3ux/utils'; export const Ledger = ({ uid, @@ -133,7 +134,9 @@ export const Ledger = ({
)} -
+
{valid ? (

diff --git a/src/library/SubmitTx/ManualSign/Vault/index.tsx b/src/library/SubmitTx/ManualSign/Vault/index.tsx index 5160640ba2..94a5a1d4be 100644 --- a/src/library/SubmitTx/ManualSign/Vault/index.tsx +++ b/src/library/SubmitTx/ManualSign/Vault/index.tsx @@ -11,6 +11,8 @@ import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from '../../types'; import { SignPrompt } from './SignPrompt'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Vault = ({ onSubmit, @@ -30,38 +32,51 @@ export const Vault = ({ const disabled = submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + // Format submit button based on whether signature currently exists or submission is ongoing. + let buttonText: string; + let buttonOnClick: () => void; + let buttonDisabled: boolean; + let buttonPulse: boolean; + + if (getTxSignature() !== null || submitting) { + buttonText = submitText || ''; + buttonOnClick = onSubmit; + buttonDisabled = disabled; + buttonPulse = !(!valid || promptStatus !== 0); + } else { + buttonText = promptStatus === 0 ? t('sign') : t('signing'); + buttonOnClick = async () => { + openPromptWith(, 'small'); + }; + buttonDisabled = disabled || promptStatus !== 0; + buttonPulse = !disabled || promptStatus === 0; + } + return ( -

+
{valid ?

{t('submitTransaction')}

:

...

}
{buttons} - {getTxSignature() !== null || submitting ? ( + {displayFor !== 'card' ? ( onSubmit()} - disabled={disabled} - pulse={!(!valid || promptStatus !== 0)} + onClick={() => buttonOnClick()} + pulse={buttonPulse} /> ) : ( - { - openPromptWith( - , - 'small' - ); - }} - disabled={disabled || promptStatus !== 0} - pulse={!disabled || promptStatus === 0} + )}
diff --git a/src/library/SubmitTx/types.ts b/src/library/SubmitTx/types.ts index 3e6072d3bf..4f1416550d 100644 --- a/src/library/SubmitTx/types.ts +++ b/src/library/SubmitTx/types.ts @@ -1,6 +1,7 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; import type { ReactNode } from 'react'; import type { DisplayFor, MaybeAddress } from 'types'; @@ -33,3 +34,12 @@ export interface LedgerSubmitProps { disabled: boolean; submitText?: string; } + +export interface ButtonSubmitLargeProps { + disabled: boolean; + onSubmit: () => void; + submitText: string; + icon?: IconProp; + iconTransform?: string; + pulse: boolean; +} diff --git a/src/library/ValidatorList/ValidatorItem/Nomination.tsx b/src/library/ValidatorList/ValidatorItem/Nomination.tsx index 7ca4be2327..0d0236637e 100644 --- a/src/library/ValidatorList/ValidatorItem/Nomination.tsx +++ b/src/library/ValidatorList/ValidatorItem/Nomination.tsx @@ -43,13 +43,15 @@ export const Nomination = ({ {toggleFavorites && } - + {displayFor !== 'canvas' && ( + + )}
diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx index 9c967dca10..691989594c 100644 --- a/src/library/ValidatorList/index.tsx +++ b/src/library/ValidatorList/index.tsx @@ -78,7 +78,7 @@ export const ValidatorListInner = ({ } = useFilters(); const { mode } = useTheme(); const listProvider = useList(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { isReady, activeEra } = useApi(); const { activeAccount } = useActiveAccounts(); const { setModalResize } = useOverlay().modal; diff --git a/src/library/WithdrawPrompt/index.tsx b/src/library/WithdrawPrompt/index.tsx index 3696f05e1c..ad1021a051 100644 --- a/src/library/WithdrawPrompt/index.tsx +++ b/src/library/WithdrawPrompt/index.tsx @@ -18,17 +18,21 @@ import { useErasToTimeLeft } from 'hooks/useErasToTimeLeft'; import { useApi } from 'contexts/Api'; import { useTranslation } from 'react-i18next'; import type { BondFor } from 'types'; +import { useActivePool } from 'contexts/Pools/ActivePool'; export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const { t } = useTranslation('modals'); const { mode } = useTheme(); const { consts } = useApi(); + const { activePool } = useActivePool(); const { openModal } = useOverlay().modal; const { colors } = useNetwork().networkData; + const { syncing } = useSyncing(['balances']); const { activeAccount } = useActiveAccounts(); const { erasToSeconds } = useErasToTimeLeft(); const { getTransferOptions } = useTransferOptions(); + const { state } = activePool?.bondedPool || {}; const { bondDuration } = consts; const allTransferOptions = getTransferOptions(activeAccount); @@ -49,6 +53,9 @@ export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const displayPrompt = totalUnlockChunks > 0; return ( + /* NOTE: ClosurePrompts is a component that displays a prompt to the user when a pool is being + destroyed. */ + state !== 'Destroying' && displayPrompt && ( diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 894afb3979..3802a7e018 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -9,10 +9,12 @@ "activePools": "活跃提名池", "activeValidator": "活跃验证人", "activeValidators": "活跃验证人", + "activelyNominating": "活跃提名中", "add": "添加", "addFromFavorites": "从收藏夹添加", "address": "地址", "addressCopiedToClipboard": "复制到剪贴板的地址", + "addresses": "地址", "all": "全部", "allowAll": "允许所有", "allowAnyoneCompound": "允许任何人代表您复利收益", @@ -24,19 +26,24 @@ "asAPoolMember": "作为提名池成员", "asThePoolDepositor": "作为提名池存款人", "atLeast": "质押金最低为", + "autoSelected": "己自动选定", "available": "可用", "backToMethods": "返回方案选择", "backToScan": "回到扫描", + "blocked": "己关闭", "blockedNominations": "己冻结提名", "blockingNominations": "冻结提名中", "bond": "质押", "bondAmountDecimals": "质押金额最多只能有 {{units}}个小数位", "bondDecimalsError": "质押金额能最多有 {{units}} 位点数", "bonded": "己质押", + "browseValidators": "浏览验证人", "cancel": "取消", "cancelled": "已取消", + "chooseAnotherPool": "选择另一个池", "chooseValidators": "最多能选择 {{maxNominations}} 个验证人。", "chooseValidators2": "自动生成提名或手动加入提名", + "claimSetting": "申领设置", "clear": "清除", "clearSelection": "清除选择", "clickToReload": "重新加载", @@ -64,6 +71,7 @@ "displayingValidators": "正在显示 {{count}} 个验证人", "done": "完成", "enablePermissionlessClaiming": "启用己许可申领", + "era": "Era", "eraPoints": "Era 点数", "errorUnknown": "抱歉,页面出现点小问题哦", "errorWithTransaction": "交易出错", @@ -121,6 +129,7 @@ "nominate": "提名", "nominateActive": "激活", "nominateInactive": "未激活", + "nominations": "提名", "nominationsReverted": "已恢复原来提名", "nominator": "提名人", "notEnough": "不足", @@ -142,6 +151,8 @@ "payoutAccount": "收益到账账户", "payoutAddress": "收益到账地址", "pending": "待定中", + "permissioned": "已获许可", + "permissionedSubtitle": "仅本人可申领奖励", "permissionlessClaimingTurnedOff": "己许可申领己关闭", "points": "点数", "pool": "提名池", @@ -154,6 +165,7 @@ "proxy": "代理账户", "randomValidator": "随机验证人", "reGenerate": "重新生成", + "recentPerformance": "最近表现", "remove": "删除", "removeSelected": "移除选定项", "reset": "重设", @@ -170,6 +182,7 @@ "signing": "签署中", "submitTransaction": "准备提交交易", "syncing": "正在同步", + "syncingPoolData": "同步提名池数据中", "syncingPoolList": "同步提名池列表", "tooSmall": "质押金额太少", "top": "首", diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index cca2e74eba..b0934d3feb 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -137,10 +137,10 @@ "bondedFunds": "己质押金额", "bouncer": "守护人", "browseMembers": "浏览成员", + "browsePools": "浏览提名池", "cancel": "取消", "closePool": "可提取己解锁金额并关闭池", "compound": "复利", - "create": "创建", "createAPool": "创建提名池", "createPool": "创建提名池", "depositor": "存款人", @@ -154,7 +154,7 @@ "generateNominations": "生成提名", "inPool": "提名池中", "inactivePoolNotNominating": "非活跃:提名池未提名任何验证人", - "join": "加入", + "joinPool": "加入提名池", "leave": "离开", "leavingPool": "离开提名池中", "leftThePool": "所有成员已离开", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index c77b0a37c7..084462da4d 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -9,34 +9,41 @@ "activePools": "Active Pools", "activeValidator": "Active Validator", "activeValidators": "Active Validators", + "activelyNominating": "Actively Nominating", "add": "Add", "addFromFavorites": "Add From Favorites", "address": "Address", "addressCopiedToClipboard": "Address Copied to Clipboard", + "addresses": "Addresses", "all": "All", "allowAll": "Allow All", - "allowAnyoneCompound": "Allow anyone to compound rewards on your behalf.", + "allowAnyoneCompound": "Allow anyone to compound your rewards.", "allowAnyoneCompoundWithdraw": "Allow anyone to compound or withdraw rewards on your behalf.", - "allowAnyoneWithdraw": "Allow anyone to withdraw rewards on your behalf.", + "allowAnyoneWithdraw": "Allow anyone to withdraw your rewards to your account.", "allowCompound": "Allow Compound", "allowWithdraw": "Allow Withdraw", "alreadyImported": "Address Already Imported", "asAPoolMember": "as a pool member.", "asThePoolDepositor": "as the pool depositor.", "atLeast": "Bond amount must be at least", + "autoSelected": "Auto Selected", "available": "Available", "backToMethods": "Back to Methods", "backToScan": "Back to Scan", + "blocked": "Blocked", "blockedNominations": "Blocked Nominations", "blockingNominations": "Blocking Nominations", "bond": "Bond", "bondAmountDecimals": "Bond amount can only have at most {{units}} decimals.", "bondDecimalsError": "Bond amount can have at most {{units}} decimals.", "bonded": "Bonded", + "browseValidators": "Browse Validators", "cancel": "Cancel", "cancelled": "Cancelled", + "chooseAnotherPool": "Choose Another Pool", "chooseValidators": "Choose up to {{maxNominations}} validators to nominate.", "chooseValidators2": "Generate your nominations automatically or manually insert them.", + "claimSetting": "Claim Setting", "clear": "Clear", "clearSelection": "clear selection", "clickToReload": "Click to reload", @@ -65,6 +72,7 @@ "displayingValidators_other": "Displaying {{count}} Validators", "done": "Done", "enablePermissionlessClaiming": "Enable Permissionless Claiming", + "era": "Era", "eraPoints": "Era Points", "errorUnknown": "Oops, Something Went Wrong", "errorWithTransaction": "Error with transaction", @@ -124,6 +132,8 @@ "nominateActive": "Active", "nominateInactive": "Inactive", "nominationsReverted": "Nominations Reverted", + "nominations_one": "Nomination", + "nominations_other": "Nominations", "nominator": "Nominator", "notEnough": "Not Enough", "notEnoughAfter": "Not enough {{unit}} to bond after transaction fees.", @@ -144,6 +154,8 @@ "payoutAccount": "Payout Account", "payoutAddress": "Payout Adddress", "pending": "Pending", + "permissioned": "Permissioned", + "permissionedSubtitle": "Only you can claim rewards.", "permissionlessClaimingTurnedOff": "Permissionless claiming is turned off.", "points": "Points", "pool": "Pool", @@ -156,6 +168,7 @@ "proxy": "Proxy", "randomValidator": "Random Validator", "reGenerate": "Re-Generate", + "recentPerformance": "Recent Performance", "remove": "Remove", "removeSelected": "Remove Selected", "reset": "Reset", @@ -172,6 +185,7 @@ "signing": "Signing", "submitTransaction": "Ready to submit transaction.", "syncing": "Syncing", + "syncingPoolData": "Syncing Pool Data", "syncingPoolList": "Syncing Pool list", "tooSmall": "Bond amount is too small.", "top": "Top", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index 9c06757d17..b833194a2b 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -139,10 +139,10 @@ "bondedFunds": "Bonded Funds", "bouncer": "Bouncer", "browseMembers": "Browse Members", + "browsePools": "Browse Pools", "cancel": "Cancel", "closePool": "You can now withdraw and close the pool.", "compound": "Compound", - "create": "Create", "createAPool": "Create a Pool", "createPool": "Create Pool", "depositor": "Depositor", @@ -156,7 +156,7 @@ "generateNominations": "Generate Nominations", "inPool": "In Pool", "inactivePoolNotNominating": "Inactive: Pool Not Nominating", - "join": "Join", + "joinPool": "Join Pool", "leave": "Leave", "leavingPool": "Leaving Pool", "leftThePool": "All members have now left the pool", diff --git a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx index 919d3b9198..7ce53ec93b 100644 --- a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx +++ b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx @@ -19,6 +19,7 @@ import { useBalances } from 'contexts/Balances'; import { ButtonSubmitInvert } from 'kits/Buttons/ButtonSubmitInvert'; import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; import { ModalWarnings } from 'kits/Overlay/structure/ModalWarnings'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; export const SetClaimPermission = ({ setSection, @@ -92,10 +93,7 @@ export const SetClaimPermission = ({ ) : null} { setClaimPermission(val); }} diff --git a/src/modals/PoolNominations/index.tsx b/src/modals/PoolNominations/index.tsx index b29b23fdfd..6e6db3e987 100644 --- a/src/modals/PoolNominations/index.tsx +++ b/src/modals/PoolNominations/index.tsx @@ -9,11 +9,11 @@ import { ListWrapper } from './Wrappers'; import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; export const PoolNominations = () => { + const { t } = useTranslation('modals'); const { config: { options }, } = useOverlay().modal; const { nominator, targets } = options; - const { t } = useTranslation('modals'); return ( <> diff --git a/src/model/Api/index.ts b/src/model/Api/index.ts index bfb40e30e8..ec1fa28b5f 100644 --- a/src/model/Api/index.ts +++ b/src/model/Api/index.ts @@ -92,7 +92,8 @@ export class Api { // Class initialization. Sets the `provider` and `api` class members. async initialize(type: ConnectionType, rpcEndpoint: string) { - // Add initial syncing items. + // Add initial syncing items. Even though `initialization` is added by default, it is called + // again here in case a new API is initialized. SyncController.dispatch('initialization', 'syncing'); // Persist the network to local storage. diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 7b76f3821f..287b125755 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -16,7 +16,6 @@ import { Connect } from '../modals/Connect'; import { GoToFeedback } from '../modals/GoToFeedback'; import { ImportLedger } from '../modals/ImportLedger'; import { ImportVault } from '../modals/ImportVault'; -import { JoinPool } from '../modals/JoinPool'; import { ManageFastUnstake } from '../modals/ManageFastUnstake'; import { ManagePool } from '../modals/ManagePool'; import { Networks } from '../modals/Networks'; @@ -31,6 +30,7 @@ import { ValidatorMetrics } from '../modals/ValidatorMetrics'; import { ValidatorGeo } from '../modals/ValidatorGeo'; import { ManageNominations } from '../canvas/ManageNominations'; import { PoolMembers } from 'canvas/PoolMembers'; +import { JoinPool } from 'canvas/JoinPool'; import { Overlay } from 'kits/Overlay'; export const Overlays = () => { @@ -51,7 +51,6 @@ export const Overlays = () => { Connect, Accounts, GoToFeedback, - JoinPool, ImportLedger, ImportVault, ManagePool, @@ -70,6 +69,7 @@ export const Overlays = () => { canvas={{ ManageNominations, PoolMembers, + JoinPool, }} /> ); diff --git a/src/pages/Nominate/Active/ManageBond.tsx b/src/pages/Nominate/Active/ManageBond.tsx index 76999e7e83..d0967f16cc 100644 --- a/src/pages/Nominate/Active/ManageBond.tsx +++ b/src/pages/Nominate/Active/ManageBond.tsx @@ -31,8 +31,8 @@ export const ManageBond = () => { }, } = useNetwork(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getLedger } = useBalances(); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); diff --git a/src/pages/Nominate/Active/Status/NewNominator.tsx b/src/pages/Nominate/Active/Status/NewNominator.tsx new file mode 100644 index 0000000000..90384c193f --- /dev/null +++ b/src/pages/Nominate/Active/Status/NewNominator.tsx @@ -0,0 +1,61 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from 'library/CallToAction'; +import { + faChevronCircleRight, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNavigate } from 'react-router-dom'; +import { useApi } from 'contexts/Api'; +import type { NewNominatorProps } from '../types'; +import { CallToActionLoader } from 'library/Loader/CallToAction'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const NewNominator = ({ syncing }: NewNominatorProps) => { + const { t } = useTranslation(); + const { isReady } = useApi(); + const navigate = useNavigate(); + const { setOnNominatorSetup } = useSetup(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const nominateButtonDisabled = + !isReady || !activeAccount || isReadOnlyAccount(activeAccount); + + return ( + +
+ {syncing ? ( + + ) : ( +
+
+
+ +
+
+ +
+
+
+ )} +
+
+ ); +}; diff --git a/src/pages/Nominate/Active/Status/NominationStatus.tsx b/src/pages/Nominate/Active/Status/NominationStatus.tsx index 495a5d212e..5ee363c027 100644 --- a/src/pages/Nominate/Active/Status/NominationStatus.tsx +++ b/src/pages/Nominate/Active/Status/NominationStatus.tsx @@ -1,16 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { - faBolt, - faChevronCircleRight, - faSignOutAlt, -} from '@fortawesome/free-solid-svg-icons'; +import { faBolt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useBonded } from 'contexts/Bonded'; import { useFastUnstake } from 'contexts/FastUnstake'; -import { useSetup } from 'contexts/Setup'; import { useStaking } from 'contexts/Staking'; import { useNominationStatus } from 'hooks/useNominationStatus'; import { useUnstaking } from 'hooks/useUnstaking'; @@ -41,7 +36,6 @@ export const NominationStatus = ({ const { isReadOnlyAccount } = useImportedAccounts(); const { getNominationStatus } = useNominationStatus(); const { getFastUnstakeText, isUnstaking } = useUnstaking(); - const { setOnNominatorSetup, getNominatorSetupPercent } = useSetup(); const fastUnstakeText = getFastUnstakeText(); const controller = getBondedAccount(activeAccount); @@ -67,15 +61,6 @@ export const NominationStatus = ({ onClick: () => openModal({ key: 'Unstake', size: 'sm' }), }; - // Display progress alongside start title if exists and in setup. - let startTitle = t('nominate.startNominating'); - if (inSetup()) { - const progress = getNominatorSetupPercent(activeAccount); - if (progress > 0) { - startTitle += `: ${progress}%`; - } - } - return ( setOnNominatorSetup(true), - }, - ] + : [] } buttonType={buttonType} /> diff --git a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx index 39cc95b035..8334686cfc 100644 --- a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx +++ b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx @@ -16,8 +16,8 @@ import { useSyncing } from 'hooks/useSyncing'; export const PayoutDestinationStatus = () => { const { t } = useTranslation('pages'); const { getPayee } = useBalances(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); const { getPayeeItems } = usePayeeConfig(); diff --git a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx index cca2d2cc3c..46805ace60 100644 --- a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx +++ b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx @@ -13,7 +13,7 @@ import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const UnclaimedPayoutsStatus = () => { +export const UnclaimedPayoutsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation(); const { networkData: { units }, @@ -43,6 +43,7 @@ export const UnclaimedPayoutsStatus = () => { 2 ), }} + dimmed={dimmed} buttons={ Object.keys(unclaimedPayouts || {}).length > 0 && !totalUnclaimed.isZero() diff --git a/src/pages/Nominate/Active/Status/index.tsx b/src/pages/Nominate/Active/Status/index.tsx index 2ed3ce0c39..f37a61ec12 100644 --- a/src/pages/Nominate/Active/Status/index.tsx +++ b/src/pages/Nominate/Active/Status/index.tsx @@ -6,13 +6,41 @@ import { UnclaimedPayoutsStatus } from './UnclaimedPayoutsStatus'; import { NominationStatus } from './NominationStatus'; import { PayoutDestinationStatus } from './PayoutDestinationStatus'; import { Separator } from 'kits/Structure/Separator'; +import { useSyncing } from 'hooks/useSyncing'; +import { useStaking } from 'contexts/Staking'; +import { NewNominator } from './NewNominator'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => ( - - - - - - - -); +export const Status = ({ height }: { height: number }) => { + const { syncing } = useSyncing(); + const { inSetup } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + return ( + + + + + + {!syncing ? ( + !inSetup() ? ( + <> + + + + ) : ( + !isReadOnlyAccount(activeAccount) && ( + + ) + ) + ) : ( + + )} + + ); +}; diff --git a/src/pages/Nominate/Active/UnstakePrompts.tsx b/src/pages/Nominate/Active/UnstakePrompts.tsx index 122eb5ec0a..f9a3ea106a 100644 --- a/src/pages/Nominate/Active/UnstakePrompts.tsx +++ b/src/pages/Nominate/Active/UnstakePrompts.tsx @@ -19,7 +19,7 @@ import { ButtonRow } from 'kits/Structure/ButtonRow'; export const UnstakePrompts = () => { const { t } = useTranslation('pages'); const { mode } = useTheme(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { openModal } = useOverlay().modal; const { activeAccount } = useActiveAccounts(); const { unit, colors } = useNetwork().networkData; diff --git a/src/pages/Nominate/Active/index.tsx b/src/pages/Nominate/Active/index.tsx index 58e7ef1d59..cf05491721 100644 --- a/src/pages/Nominate/Active/index.tsx +++ b/src/pages/Nominate/Active/index.tsx @@ -31,8 +31,8 @@ import { WithdrawPrompt } from 'library/WithdrawPrompt'; export const Active = () => { const { t } = useTranslation(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getNominations } = useBalances(); const { openCanvas } = useOverlay().canvas; const { isFastUnstaking } = useUnstaking(); @@ -55,14 +55,14 @@ export const Active = () => { - - - - + + + + diff --git a/src/pages/Nominate/Active/types.ts b/src/pages/Nominate/Active/types.ts index d3c14eeec1..db68ada844 100644 --- a/src/pages/Nominate/Active/types.ts +++ b/src/pages/Nominate/Active/types.ts @@ -10,3 +10,7 @@ export interface BondedChartProps { unlocked: BigNumber; inactive: boolean; } + +export interface NewNominatorProps { + syncing: boolean; +} diff --git a/src/pages/Nominate/Setup/index.tsx b/src/pages/Nominate/Setup/index.tsx index a67fc654a9..df7384c110 100644 --- a/src/pages/Nominate/Setup/index.tsx +++ b/src/pages/Nominate/Setup/index.tsx @@ -42,6 +42,7 @@ export const Setup = () => { setOnNominatorSetup(false); } }} + lg /> @@ -53,6 +54,7 @@ export const Setup = () => { setOnNominatorSetup(false); removeSetupProgress('nominator', activeAccount); }} + lg />
diff --git a/src/pages/Overview/Payouts.tsx b/src/pages/Overview/Payouts.tsx index b9b9d8b2a3..5c4750d1fd 100644 --- a/src/pages/Overview/Payouts.tsx +++ b/src/pages/Overview/Payouts.tsx @@ -30,7 +30,7 @@ export const Payouts = () => { }, } = useNetwork(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { plugins } = usePlugins(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', diff --git a/src/pages/Payouts/index.tsx b/src/pages/Payouts/index.tsx index eaeb36ea8d..ea1a94a200 100644 --- a/src/pages/Payouts/index.tsx +++ b/src/pages/Payouts/index.tsx @@ -32,7 +32,7 @@ export const Payouts = ({ page: { key } }: PageProps) => { const { openHelp } = useHelp(); const { plugins } = usePlugins(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', 'unclaimedPayouts', diff --git a/src/pages/Pools/Create/index.tsx b/src/pages/Pools/Create/index.tsx index af4d3c5f71..75c6f4c576 100644 --- a/src/pages/Pools/Create/index.tsx +++ b/src/pages/Pools/Create/index.tsx @@ -33,6 +33,7 @@ export const Create = () => { iconLeft={faChevronLeft} iconTransform="shrink-3" onClick={() => setOnPoolSetup(false)} + lg /> @@ -42,6 +43,7 @@ export const Create = () => { setOnPoolSetup(false); removeSetupProgress('pool', activeAccount); }} + lg />
diff --git a/src/pages/Pools/Home/ClosurePrompts.tsx b/src/pages/Pools/Home/ClosurePrompts.tsx index 473506c0bd..0adf25705c 100644 --- a/src/pages/Pools/Home/ClosurePrompts.tsx +++ b/src/pages/Pools/Home/ClosurePrompts.tsx @@ -46,6 +46,7 @@ export const ClosurePrompts = () => { active.toNumber() === 0 && totalUnlockChunks === 0 && !targets.length; return ( + state === 'Destroying' && depositorCanClose && ( diff --git a/src/pages/Pools/Home/Status/MembershipStatus.tsx b/src/pages/Pools/Home/Status/MembershipStatus.tsx index b76848427a..59b40f75f6 100644 --- a/src/pages/Pools/Home/Status/MembershipStatus.tsx +++ b/src/pages/Pools/Home/Status/MembershipStatus.tsx @@ -13,22 +13,18 @@ import { useOverlay } from 'kits/Overlay/Provider'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useStatusButtons } from './useStatusButtons'; -import { useSyncing } from 'hooks/useSyncing'; +import type { MembershipStatusProps } from './types'; export const MembershipStatus = ({ showButtons = true, buttonType = 'primary', -}: { - showButtons?: boolean; - buttonType?: string; -}) => { +}: MembershipStatusProps) => { const { t } = useTranslation('pages'); const { isReady } = useApi(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { poolsMetaData } = useBondedPools(); const { activeAccount } = useActiveAccounts(); - const { label, buttons } = useStatusButtons(); + const { label } = useStatusButtons(); const { isReadOnlyAccount } = useImportedAccounts(); const { getTransferOptions } = useTransferOptions(); const { activePool, isOwner, isBouncer, isMember } = useActivePool(); @@ -52,18 +48,21 @@ export const MembershipStatus = ({ (poolState !== 'Destroying' && (isOwner() || isBouncer())) || (isMember() && active?.isGreaterThan(0)) ) { - membershipButtons.push({ - title: t('pools.manage'), - icon: faCog, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ManagePool', - options: { disableWindowResize: true, disableScroll: true }, - size: 'sm', - }), - }); + // Display manage button if active account is not a read-only account. + if (!isReadOnlyAccount(activeAccount)) { + membershipButtons.push({ + title: t('pools.manage'), + icon: faCog, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ManagePool', + options: { disableWindowResize: true, disableScroll: true }, + size: 'sm', + }), + }); + } } } @@ -83,7 +82,6 @@ export const MembershipStatus = ({ label={t('pools.poolMembership')} helpKey="Pool Membership" stat={t('pools.notInPool')} - buttons={!showButtons || syncing ? [] : buttons} buttonType={buttonType} /> ); diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx new file mode 100644 index 0000000000..042cb55368 --- /dev/null +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -0,0 +1,96 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from '../../../../library/CallToAction'; +import { faChevronRight, faUserGroup } from '@fortawesome/free-solid-svg-icons'; +import { useSetup } from 'contexts/Setup'; +import { usePoolsTabs } from '../context'; +import { useStatusButtons } from './useStatusButtons'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import type { NewMemberProps } from './types'; +import { CallToActionLoader } from 'library/Loader/CallToAction'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; + +export const NewMember = ({ syncing }: NewMemberProps) => { + const { t } = useTranslation(); + const { setOnPoolSetup } = useSetup(); + const { setActiveTab } = usePoolsTabs(); + const { openCanvas } = useOverlay().canvas; + const { poolRewardPointsFetched } = usePoolPerformance(); + const { disableJoin, disableCreate } = useStatusButtons(); + + const joinButtonDisabled = + disableJoin() || poolRewardPointsFetched !== 'synced'; + + const createButtonDisabled = disableCreate(); + + return ( + +
+ {syncing ? ( + + ) : ( + <> +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+
+
+ + )} +
+
+ ); +}; diff --git a/src/pages/Pools/Home/Status/RewardsStatus.tsx b/src/pages/Pools/Home/Status/RewardsStatus.tsx index d9f5df9885..c2d2e25862 100644 --- a/src/pages/Pools/Home/Status/RewardsStatus.tsx +++ b/src/pages/Pools/Home/Status/RewardsStatus.tsx @@ -14,7 +14,7 @@ import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useSyncing } from 'hooks/useSyncing'; -export const RewardsStatus = () => { +export const RewardsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation('pages'); const { networkData: { units }, @@ -34,37 +34,37 @@ export const RewardsStatus = () => { : '0'; // Display Reward buttons if unclaimed rewards is a non-zero value. - const buttonsRewards = pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) - ? [ - { - title: t('pools.withdraw'), - icon: faCircleDown, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'withdraw' }, - size: 'sm', - }), - }, - { - title: t('pools.compound'), - icon: faPlus, - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - activePool?.bondedPool?.state === 'Destroying', - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'bond' }, - size: 'sm', - }), - }, - ] - : undefined; + const buttonsRewards = isReadOnlyAccount(activeAccount) + ? [] + : pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) + ? [ + { + title: t('pools.withdraw'), + icon: faCircleDown, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'withdraw' }, + size: 'sm', + }), + }, + { + title: t('pools.compound'), + icon: faPlus, + disabled: + !isReady || activePool?.bondedPool?.state === 'Destroying', + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'bond' }, + size: 'sm', + }), + }, + ] + : undefined; return ( { helpKey="Pool Rewards" type="odometer" stat={{ value: labelRewards }} + dimmed={dimmed} buttons={syncing ? [] : buttonsRewards} /> ); diff --git a/src/pages/Pools/Home/Status/index.tsx b/src/pages/Pools/Home/Status/index.tsx index 119e7eb46f..145d1303fa 100644 --- a/src/pages/Pools/Home/Status/index.tsx +++ b/src/pages/Pools/Home/Status/index.tsx @@ -7,20 +7,43 @@ import { MembershipStatus } from './MembershipStatus'; import { PoolStatus } from './PoolStatus'; import { RewardsStatus } from './RewardsStatus'; import { Separator } from 'kits/Structure/Separator'; +import type { StatusProps } from './types'; +import { NewMember } from './NewMember'; +import { useSyncing } from 'hooks/useSyncing'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useBalances } from 'contexts/Balances'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => { +export const Status = ({ height }: StatusProps) => { const { activePool } = useActivePool(); + const { getPoolMembership } = useBalances(); + const { poolMembersipSyncing } = useSyncing(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const membership = getPoolMembership(activeAccount); + const syncing = poolMembersipSyncing(); return ( - + - - {activePool && ( - <> - - - + + {!syncing ? ( + activePool && !!membership ? ( + <> + + + + ) : ( + membership === null && + !isReadOnlyAccount(activeAccount) && + ) + ) : ( + )} ); diff --git a/src/pages/Pools/Home/Status/types.ts b/src/pages/Pools/Home/Status/types.ts new file mode 100644 index 0000000000..dc15b67805 --- /dev/null +++ b/src/pages/Pools/Home/Status/types.ts @@ -0,0 +1,15 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface StatusProps { + height: number; +} + +export interface MembershipStatusProps { + showButtons?: boolean; + buttonType?: string; +} + +export interface NewMemberProps { + syncing: boolean; +} diff --git a/src/pages/Pools/Home/Status/useStatusButtons.tsx b/src/pages/Pools/Home/Status/useStatusButtons.tsx index 9ddc1c7adb..53b6ee85d1 100644 --- a/src/pages/Pools/Home/Status/useStatusButtons.tsx +++ b/src/pages/Pools/Home/Status/useStatusButtons.tsx @@ -1,16 +1,13 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPlusCircle, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useSetup } from 'contexts/Setup'; import { useTransferOptions } from 'contexts/TransferOptions'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { usePoolsTabs } from '../context'; import { useBalances } from 'contexts/Balances'; export const useStatusButtons = () => { @@ -20,17 +17,14 @@ export const useStatusButtons = () => { poolsConfig: { maxPools }, } = useApi(); const { isOwner } = useActivePool(); - const { setActiveTab } = usePoolsTabs(); const { bondedPools } = useBondedPools(); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); const { getTransferOptions } = useTransferOptions(); const { isReadOnlyAccount } = useImportedAccounts(); - const { setOnPoolSetup, getPoolSetupPercent } = useSetup(); const membership = getPoolMembership(activeAccount); const { active } = getTransferOptions(activeAccount).pool; - const poolSetupPercent = getPoolSetupPercent(activeAccount); const disableCreate = () => { if (!isReady || isReadOnlyAccount(activeAccount) || !activeAccount) { @@ -46,34 +40,15 @@ export const useStatusButtons = () => { }; let label; - let buttons; - const createBtn = { - title: `${t('pools.create')}${ - poolSetupPercent > 0 ? `: ${poolSetupPercent}%` : `` - }`, - icon: faPlusCircle, - large: false, - transform: 'grow-1', - disabled: disableCreate(), - onClick: () => setOnPoolSetup(true), - }; - const joinPoolBtn = { - title: `${t('pools.join')}`, - icon: faUserPlus, - large: false, - transform: 'grow-1', - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - !activeAccount || - !bondedPools.length, - onClick: () => setActiveTab(1), - }; + const disableJoin = () => + !isReady || + isReadOnlyAccount(activeAccount) || + !activeAccount || + !bondedPools.length; if (!membership) { label = t('pools.poolMembership'); - buttons = [createBtn, joinPoolBtn]; } else if (isOwner()) { label = `${t('pools.ownerOfPool')} ${membership.poolId}`; } else if (active?.isGreaterThan(0)) { @@ -81,5 +56,5 @@ export const useStatusButtons = () => { } else { label = `${t('pools.leavingPool')} ${membership.poolId}`; } - return { label, buttons }; + return { label, disableJoin, disableCreate }; }; diff --git a/src/pages/Pools/Home/index.tsx b/src/pages/Pools/Home/index.tsx index 7a347df83f..590a49af35 100644 --- a/src/pages/Pools/Home/index.tsx +++ b/src/pages/Pools/Home/index.tsx @@ -31,9 +31,11 @@ import type { PageTitleTabProps } from 'kits/Structure/PageTitleTabs/types'; import { PageRow } from 'kits/Structure/PageRow'; import { RowSection } from 'kits/Structure/RowSection'; import { WithdrawPrompt } from 'library/WithdrawPrompt'; +import { useSyncing } from 'hooks/useSyncing'; export const HomeInner = () => { const { t } = useTranslation('pages'); + const { poolMembersipSyncing } = useSyncing(); const { favorites } = useFavoritePools(); const { openModal } = useOverlay().modal; const { bondedPools } = useBondedPools(); @@ -42,16 +44,16 @@ export const HomeInner = () => { const { activeTab, setActiveTab } = usePoolsTabs(); const { getPoolRoles, activePool } = useActivePool(); const { counterForBondedPools } = useApi().poolsConfig; - const membership = getPoolMembership(activeAccount); - const { state } = activePool?.bondedPool || {}; const { activePools } = useActivePools({ poolIds: '*', }); - const activePoolsNoMembership = { ...activePools }; - delete activePoolsNoMembership[membership?.poolId || -1]; + // Calculate the number of _other_ pools the user has a role in. + const poolRoleCount = Object.keys(activePools).filter( + (poolId) => poolId !== String(membership?.poolId) + ).length; let tabs: PageTitleTabProps[] = [ { @@ -76,6 +78,8 @@ export const HomeInner = () => { } ); + const ROW_HEIGHT = 220; + // Back to tab 0 if not in a pool & on members tab. useEffect(() => { if (!activePool) { @@ -83,15 +87,13 @@ export const HomeInner = () => { } }, [activePool]); - const ROW_HEIGHT = 220; - return ( <> 0 + !poolMembersipSyncing() && poolRoleCount > 0 ? { title: t('pools.allRoles'), onClick: () => @@ -111,21 +113,18 @@ export const HomeInner = () => { - {state === 'Destroying' ? ( - - ) : ( - - )} + + - - - - + + + + {activePool !== null && ( <> diff --git a/src/theme/theme.scss b/src/theme/theme.scss index aab7daf572..4e46e8af83 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -9,6 +9,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(238 238 238 / 100%); --background-modal-card: rgb(237 237 237 / 75%); --background-canvas-card: rgb(245 245 245 / 90%); + --background-canvas-card-secondary: rgb(255 255 255 / 35%); --background-floating-card: rgb(255 255 255 / 90%); --background-app-footer: rgb(244 225 225 / 75%); --background-warning: #fdf9eb; @@ -23,6 +24,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: #e7e5e5; --button-tertiary-background: #ececec; --button-tab-background: #e4e2e2; + --button-tab-canvas-background: #d9d9d9; --button-hover-background: #e8e6e6; --card-shadow-color: rgb(158 158 158 / 20%); --card-deep-shadow-color: rgb(158 158 158 / 50%); @@ -44,7 +46,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --status-danger-color: #ae2324; --status-danger-color-transparent: rgb(255 0 0 / 25%); --text-color-primary: #3f3f3f; - --text-color-secondary: #555; + --text-color-secondary: #585858; --text-color-tertiary: #888; --text-color-invert: #fafafa; --gradient-background: linear-gradient( @@ -86,6 +88,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(38 33 38 / 100%); --background-modal-card: rgb(32 26 32 / 50%); --background-canvas-card: rgb(44 40 44 / 90%); + --background-canvas-card-secondary: rgb(44 40 44 / 35%); --background-floating-card: rgb(43 38 43 / 95%); --background-app-footer: #262327; --background-warning: #33332a; @@ -100,6 +103,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: rgb(55 50 55); --button-tertiary-background: rgb(54 49 54); --button-tab-background: rgb(56 51 56); + --button-tab-canvas-background: rgb(60 59 60); --button-hover-background: rgb(66 61 68); --card-shadow-color: rgb(28 24 28 / 25%); --card-deep-shadow-color: rgb(28 24 28 / 50%); diff --git a/src/types.ts b/src/types.ts index 9289752501..77fb00fdfd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,7 +140,7 @@ export type Sync = 'unsynced' | 'syncing' | 'synced'; export type BondFor = 'pool' | 'nominator'; // which medium components are being displayed on. -export type DisplayFor = 'default' | 'modal' | 'canvas'; +export type DisplayFor = 'default' | 'modal' | 'canvas' | 'card'; // generic function with no args or return type. export type Fn = () => void;