From f671e21da017bfb2d985ac56c606436178cb5393 Mon Sep 17 00:00:00 2001 From: Yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 5 Jan 2025 20:48:43 -0500 Subject: [PATCH] refactor(tangle-dapp): Update restaking page organization (#2711) --- apps/tangle-cloud/app/blueprints/page.tsx | 13 +- apps/tangle-cloud/app/providers.tsx | 2 +- apps/tangle-dapp/src/app/app.tsx | 54 +---- apps/tangle-dapp/src/app/providers.tsx | 7 +- .../components/LiquidStaking/LstListItem.tsx | 62 +++++ .../stakeAndUnstake/LsSelectLstModal.tsx | 215 ------------------ .../stakeAndUnstake/LsStakeCard.tsx | 35 ++- .../stakeAndUnstake/LsTokenChip.tsx | 4 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 36 ++- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 7 +- .../tangle-dapp/src/components/ListStatus.tsx | 40 ---- .../src/components/Lists/AssetList.tsx | 2 +- .../src/components/Lists/LogoListItem.tsx | 75 ++++++ .../src/components/Lists/OperatorList.tsx | 139 ----------- .../src/components/Lists/OperatorListItem.tsx | 40 ++++ .../ValidatorSelectionTable.tsx | 2 +- .../src/components/tables/Vaults/index.tsx | 2 +- apps/tangle-dapp/src/constants/index.ts | 8 + .../src/containers/AssetsAndBalancesTable.tsx | 27 ++- .../NominationsPayoutsContainer.tsx | 4 - apps/tangle-dapp/src/pages/account.tsx | 2 +- .../src/pages/blueprints/index.tsx | 10 +- .../components/BridgeConfirmationModal.tsx | 2 +- apps/tangle-dapp/src/pages/notFound.tsx | 28 +++ .../src/pages/restake/ActionButtonBase.tsx | 23 -- .../src/pages/restake/AnimatedTable.tsx | 5 +- .../src/pages/restake/AssetList.tsx | 35 --- .../src/pages/restake/AssetPlaceholder.tsx | 18 +- .../src/pages/restake/ModalContent.tsx | 36 --- .../src/pages/restake/RestakeTabs.tsx | 31 ++- .../src/pages/restake/SelectorPlaceholder.tsx | 22 -- .../src/pages/restake/SupportedChainModal.tsx | 3 +- .../src/pages/restake/deposit/DepositForm.tsx | 157 +++++++------ .../restake/deposit/SourceChainInput.tsx | 7 +- .../src/pages/restake/deposit/index.tsx | 12 - apps/tangle-dapp/src/pages/restake/layout.tsx | 13 -- ...{TableTabs.tsx => RestakeOverviewTabs.tsx} | 50 +++- .../src/pages/restake/overview/index.tsx | 114 ++-------- .../src/pages/restake/providers.tsx | 6 - .../src/pages/restake/stake/StakeInput.tsx | 75 +++--- .../src/pages/restake/stake/index.tsx | 177 +++++++------- .../restake/unstake/SelectOperatorModal.tsx | 101 ++++++++ .../pages/restake/unstake/UnstakeModal.tsx | 144 ------------ .../restake/unstake/UnstakeRequestTable.tsx | 14 +- .../src/pages/restake/unstake/index.tsx | 126 +++++----- .../src/pages/restake/withdraw/TxInfo.tsx | 3 +- .../pages/restake/withdraw/WithdrawModal.tsx | 142 ++++-------- .../src/pages/restake/withdraw/index.tsx | 128 +++++------ apps/tangle-dapp/src/types/index.ts | 6 +- .../src/utils/createSearchFilterFn.ts | 26 +++ apps/tangle-dapp/src/utils/isEnumValue.ts | 21 ++ apps/tangle-dapp/src/utils/searchBy.ts | 22 ++ apps/tangle-dapp/tailwind.config.js | 2 +- .../chains/evm/customChains/tangleLocalEvm.ts | 2 +- .../evm/customChains/tangleTestnetEVM.ts | 2 +- libs/dapp-config/src/chains/evm/index.tsx | 3 +- .../src/chains/substrate/index.tsx | 4 +- libs/icons/src/TokenIcon.tsx | 6 +- libs/icons/src/utils.ts | 23 +- .../ConnectWalletButton/WalletDropdown.tsx | 1 - .../src/components/ListModal.tsx | 179 +++++++++++++++ .../NetworkSelectorDropdown/index.tsx | 2 +- .../src/components/SkeletonRows.tsx | 0 .../components/blueprints/RestakeBanner.tsx | 56 +++++ .../src/components/blueprints/TopBanner.tsx | 37 --- .../RestakeContext/RestakeContextProvider.tsx | 1 - libs/tangle-shared-ui/src/types/restake.ts | 4 +- .../src/components/Card/Card.tsx | 7 +- .../src/components/ListCard/TokenListCard.tsx | 188 --------------- .../src/components/ListCard/index.ts | 1 - .../src/components/ListCard/types.ts | 67 +----- .../src/components/ListStatus.tsx | 48 ++++ .../src/components/SideBar/SideBar.tsx | 6 +- .../TokenSelector/TokenSelector.tsx | 67 +++--- .../TransactionInputCard.tsx | 3 +- .../src/components/buttons/ButtonSpinner.tsx | 2 +- .../components/buttons/ChainOrTokenButton.tsx | 2 +- .../src/components/buttons/utils.ts | 2 +- .../src/components/index.ts | 1 + .../src/constants/tangleDocs.ts | 5 +- .../containers/DepositCard/DepositCard.tsx | 44 ++-- .../GovernanceContractDetailCard/types.ts | 2 +- .../containers/TransferCard/TransferCard.tsx | 21 +- .../containers/WebbUIErrorBoudary/index.ts | 1 - .../WebbUIErrorBoundary.tsx} | 10 +- .../containers/WebbUIErrorBoundary/index.ts | 1 + .../types.ts | 4 +- .../containers/WithdrawCard/WithdrawCard.tsx | 30 +-- .../src/containers/index.ts | 2 +- libs/webb-ui-components/src/hooks/index.ts | 2 +- .../src/hooks/useCopyable.ts | 29 +-- .../src/hooks/useDarkMode.ts | 3 - .../{useCheckMobile.ts => useIsMobile.ts} | 14 +- .../webb-ui-components/src/provider/index.tsx | 8 +- libs/webb-ui-components/src/provider/types.ts | 8 +- 95 files changed, 1439 insertions(+), 1794 deletions(-) create mode 100644 apps/tangle-dapp/src/components/LiquidStaking/LstListItem.tsx delete mode 100644 apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsSelectLstModal.tsx delete mode 100644 apps/tangle-dapp/src/components/ListStatus.tsx create mode 100644 apps/tangle-dapp/src/components/Lists/LogoListItem.tsx delete mode 100644 apps/tangle-dapp/src/components/Lists/OperatorList.tsx create mode 100644 apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx create mode 100644 apps/tangle-dapp/src/pages/notFound.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/AssetList.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/ModalContent.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/SelectorPlaceholder.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/deposit/index.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/layout.tsx rename apps/tangle-dapp/src/pages/restake/overview/{TableTabs.tsx => RestakeOverviewTabs.tsx} (77%) delete mode 100644 apps/tangle-dapp/src/pages/restake/providers.tsx create mode 100644 apps/tangle-dapp/src/pages/restake/unstake/SelectOperatorModal.tsx delete mode 100644 apps/tangle-dapp/src/pages/restake/unstake/UnstakeModal.tsx create mode 100644 apps/tangle-dapp/src/utils/createSearchFilterFn.ts create mode 100644 apps/tangle-dapp/src/utils/isEnumValue.ts create mode 100644 apps/tangle-dapp/src/utils/searchBy.ts create mode 100644 libs/tangle-shared-ui/src/components/ListModal.tsx rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/components/SkeletonRows.tsx (100%) create mode 100644 libs/tangle-shared-ui/src/components/blueprints/RestakeBanner.tsx delete mode 100644 libs/tangle-shared-ui/src/components/blueprints/TopBanner.tsx delete mode 100644 libs/webb-ui-components/src/components/ListCard/TokenListCard.tsx create mode 100644 libs/webb-ui-components/src/components/ListStatus.tsx delete mode 100644 libs/webb-ui-components/src/containers/WebbUIErrorBoudary/index.ts rename libs/webb-ui-components/src/containers/{WebbUIErrorBoudary/WebbUIErrorBoudary.tsx => WebbUIErrorBoundary/WebbUIErrorBoundary.tsx} (79%) create mode 100644 libs/webb-ui-components/src/containers/WebbUIErrorBoundary/index.ts rename libs/webb-ui-components/src/containers/{WebbUIErrorBoudary => WebbUIErrorBoundary}/types.ts (74%) rename libs/webb-ui-components/src/hooks/{useCheckMobile.ts => useIsMobile.ts} (68%) diff --git a/apps/tangle-cloud/app/blueprints/page.tsx b/apps/tangle-cloud/app/blueprints/page.tsx index a9706bf990..092096b52b 100644 --- a/apps/tangle-cloud/app/blueprints/page.tsx +++ b/apps/tangle-cloud/app/blueprints/page.tsx @@ -1,12 +1,19 @@ -import TopBanner from '@webb-tools/tangle-shared-ui/components/blueprints/TopBanner'; +import RestakeBanner from '@webb-tools/tangle-shared-ui/components/blueprints/RestakeBanner'; import BlueprintListing from './BlueprintListing'; +import { BLUEPRINT_DOCS_LINK } from '@webb-tools/webb-ui-components/constants/tangleDocs'; +import { FC } from 'react'; export const dynamic = 'force-static'; -const Page = () => { +const Page: FC = () => { return (
- +
diff --git a/apps/tangle-cloud/app/providers.tsx b/apps/tangle-cloud/app/providers.tsx index 9b5a76e41a..be142f23a0 100644 --- a/apps/tangle-cloud/app/providers.tsx +++ b/apps/tangle-cloud/app/providers.tsx @@ -21,7 +21,7 @@ const Providers = ({ }: PropsWithChildren): ReactNode => { return ( - + - {/* START: routes */} } /> @@ -61,6 +55,7 @@ function App() { }> } /> + } @@ -72,44 +67,15 @@ function App() { element={} /> - }> - } - /> - - } - /> - - } - /> - - } - /> - - } - /> + } /> - } - /> + } + /> - } - /> - + } /> - {/* END: routes */} diff --git a/apps/tangle-dapp/src/app/providers.tsx b/apps/tangle-dapp/src/app/providers.tsx index 871e24c6c9..fbb6c86ed4 100644 --- a/apps/tangle-dapp/src/app/providers.tsx +++ b/apps/tangle-dapp/src/app/providers.tsx @@ -11,6 +11,7 @@ import { z } from 'zod'; import HyperlaneWarpContext from '../pages/bridge/context/HyperlaneWarpContext'; import BridgeTxQueueProvider from '../pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueProvider'; import PolkadotApiProvider from '@webb-tools/tangle-shared-ui/context/PolkadotApiProvider'; +import { RestakeContextProvider } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; const appEvent = new AppEvent(); @@ -37,7 +38,7 @@ const Providers = ({ } = envSchema.parse(process.env); return ( - + - {children} + + {children} + diff --git a/apps/tangle-dapp/src/components/LiquidStaking/LstListItem.tsx b/apps/tangle-dapp/src/components/LiquidStaking/LstListItem.tsx new file mode 100644 index 0000000000..c174e5af93 --- /dev/null +++ b/apps/tangle-dapp/src/components/LiquidStaking/LstListItem.tsx @@ -0,0 +1,62 @@ +import { FC } from 'react'; +import LstIcon from './LstIcon'; +import { + AmountFormatStyle, + EMPTY_VALUE_PLACEHOLDER, + formatDisplayAmount, + Typography, +} from '@webb-tools/webb-ui-components'; +import { LsPool } from '../../constants/liquidStaking/types'; +import { LstIconSize } from './types'; +import formatFractional from '@webb-tools/webb-ui-components/utils/formatFractional'; +import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; +import LogoListItem from '../Lists/LogoListItem'; + +type Props = { + pool: LsPool; + isSelfStaked: boolean; +}; + +const LstListItem: FC = ({ pool, isSelfStaked }) => { + const lsProtocol = getLsProtocolDef(pool.protocolId); + + const fmtStakeAmount = formatDisplayAmount( + pool.totalStaked, + lsProtocol.decimals, + AmountFormatStyle.SI, + ); + + const fmtCommission = + pool.commissionFractional === undefined + ? undefined + : `${formatFractional(pool.commissionFractional)} commission`; + + const stakeText = `${fmtStakeAmount} ${lsProtocol.token}`; + + return ( + + {pool.name?.toUpperCase()} + #{pool.id} + + } + leftBottomContent={`${stakeText} ${isSelfStaked ? 'self ' : ''}staked`} + rightUpperText={`${pool.apyPercentage ?? EMPTY_VALUE_PLACEHOLDER}% APY`} + rightBottomText={fmtCommission} + logo={ + + } + /> + ); +}; + +export default LstListItem; diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsSelectLstModal.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsSelectLstModal.tsx deleted file mode 100644 index b64a1adb6b..0000000000 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsSelectLstModal.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { Search } from '@webb-tools/icons'; -import { - AmountFormatStyle, - formatDisplayAmount, - Input, - ListItem, - Modal, - ModalContent, - Typography, -} from '@webb-tools/webb-ui-components'; -import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constants'; -import formatFractional from '@webb-tools/webb-ui-components/utils/formatFractional'; -import { FC, useMemo, useState } from 'react'; -import { twMerge } from 'tailwind-merge'; - -import { - LsPool, - LsPoolDisplayName, -} from '../../../constants/liquidStaking/types'; -import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; -import { ListCardWrapper } from '../../Lists/ListCardWrapper'; -import ListStatus from '../../ListStatus'; -import SkeletonRows from '../../SkeletonRows'; -import LstIcon from '../LstIcon'; -import { LstIconSize } from '../types'; - -export type LsSelectLstModalProps = { - pools: LsPool[] | Error | null; - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; - onSelect: (lsPoolId: number) => void; - isSelfStaked?: boolean; -}; - -const LsSelectLstModal: FC = ({ - pools, - isOpen, - onSelect, - setIsOpen, - isSelfStaked = false, -}) => { - const [searchQuery, setSearchQuery] = useState(''); - - const filteredPools = useMemo(() => { - if (!Array.isArray(pools)) { - return pools; - } - - return pools.filter((pool) => { - const displayName: LsPoolDisplayName = `${pool.name}#${pool.id}`; - - return displayName.toLowerCase().includes(searchQuery.toLowerCase()); - }); - }, [pools, searchQuery]); - - // Sort pools by highest TVL in descending order. - const sortedPools = useMemo(() => { - if (!Array.isArray(filteredPools)) { - return filteredPools; - } - - return filteredPools.toSorted((a, b) => { - return b.totalStaked.sub(a.totalStaked).isNeg() ? -1 : 1; - }); - }, [filteredPools]); - - return ( - - setIsOpen(false)} - size="md" - className={twMerge( - 'max-h-[600px]', - Array.isArray(pools) && pools.length > 0 && 'h-full', - )} - > - setIsOpen(false)} - className="w-full max-w-none" - > -
- } - placeholder="Search liquid staking tokens by name or ID..." - value={searchQuery} - onChange={setSearchQuery} - inputClassName="placeholder:text-mono-80 dark:placeholder:text-mono-120" - /> -
- - -
    - {sortedPools instanceof Error ? ( - - ) : Array.isArray(sortedPools) && - sortedPools.length === 0 && - searchQuery.length > 0 ? ( - - ) : Array.isArray(sortedPools) && sortedPools.length === 0 ? ( - - ) : ( - { - onSelect(poolId); - setIsOpen(false); - }} - /> - )} -
-
-
-
-
- ); -}; - -type ListItemsProps = { - pools: LsPool[] | null; - isSelfStaked: boolean; - onSelect: (lsPoolId: number) => void; -}; - -/** @internal */ -const ListItems: FC = ({ pools, onSelect, isSelfStaked }) => { - return pools === null ? ( -
- -
- ) : ( - pools.map((pool, idx) => { - const commissionText = - pool.commissionFractional === undefined - ? undefined - : `${formatFractional(pool.commissionFractional)} commission`; - - const lsProtocol = getLsProtocolDef(pool.protocolId); - - const stakeAmountString = formatDisplayAmount( - pool.totalStaked, - lsProtocol.decimals, - AmountFormatStyle.SI, - ); - - const stakeText = `${stakeAmountString} ${lsProtocol.token}`; - - return ( - onSelect(pool.id)} - className="w-full flex items-center gap-4 justify-between max-w-full min-h-[60px] py-3 cursor-pointer" - > -
- - -
- - {pool.name?.toUpperCase()} - - #{pool.id} - - - - - {stakeText} {isSelfStaked && 'self '}staked - -
-
- -
- - {pool.apyPercentage ?? EMPTY_VALUE_PLACEHOLDER}% APY - - - {commissionText !== undefined && ( - - {commissionText} - - )} -
-
- ); - }) - ); -}; - -export default LsSelectLstModal; diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 5302c8246a..731060ccbf 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -9,7 +9,10 @@ import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constants'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { + LsNetworkId, + LsPoolDisplayName, +} from '../../../constants/liquidStaking/types'; import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx'; import useLsExchangeRate, { @@ -27,11 +30,13 @@ import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import FeeDetailItem from './FeeDetailItem'; import LsAgnosticBalance from './LsAgnosticBalance'; import LsInput from './LsInput'; -import LsSelectLstModal from './LsSelectLstModal'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; +import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; +import LstListItem from '../LstListItem'; +import searchBy from '../../../utils/searchBy'; const LsStakeCard: FC = () => { const lsPools = useLsPools(); @@ -184,6 +189,7 @@ const LsStakeCard: FC = () => { return ( { {actionText} - pool.id.toString()} + renderItem={(pool) => } + onSelect={(pool) => { + setLsPoolId(pool.id); + setIsSelectTokenModalOpen(false); + }} + sorting={(a, b) => { + // Sort pools by highest TVL in descending order. + return b.totalStaked.sub(a.totalStaked).isNeg() ? -1 : 1; + }} + filterItem={(pool, query) => { + const displayName: LsPoolDisplayName = `${pool.name}#${pool.id}`; + + return searchBy(query, [displayName]); + }} /> ); diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsTokenChip.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsTokenChip.tsx index a0b89ef72e..2630c101d3 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsTokenChip.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsTokenChip.tsx @@ -22,7 +22,9 @@ const LsTokenChip: FC = ({
{ const isAccountConnected = useIsAccountConnected(); @@ -169,6 +174,7 @@ const LsUnstakeCard: FC = () => { <> {/* TODO: Have a way to trigger a refresh of the amount once the wallet balance (max) button is clicked. Need to signal to the liquid staking input to update its display amount based on the `fromAmount` prop. */} @@ -250,12 +256,30 @@ const LsUnstakeCard: FC = () => { - pool.id.toString()} + renderItem={(pool) => } + onSelect={(pool) => { + setLsPoolId(pool.id); + setIsSelectTokenModalOpen(false); + }} + sorting={(a, b) => { + // Sort pools by highest TVL in descending order. + return b.totalStaked.sub(a.totalStaked).isNeg() ? -1 : 1; + }} + filterItem={(pool, query) => { + const displayName: LsPoolDisplayName = `${pool.name}#${pool.id}`; + + return displayName.toLowerCase().includes(query.toLowerCase()); + }} /> ); diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx index 3800743555..651594bf88 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx @@ -19,10 +19,11 @@ const SelectedPoolIndicator: FC = ({ onClick }) => {
{activePool !== null && ( @@ -43,7 +44,7 @@ const SelectedPoolIndicator: FC = ({ onClick }) => { {onClick !== undefined && ( - + )}
); diff --git a/apps/tangle-dapp/src/components/ListStatus.tsx b/apps/tangle-dapp/src/components/ListStatus.tsx deleted file mode 100644 index 58acd4c191..0000000000 --- a/apps/tangle-dapp/src/components/ListStatus.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; -import { twMerge } from 'tailwind-merge'; - -export type ListStatusProps = { - className?: string; - title: string; - description: string; -}; - -const ListStatus: FC = ({ title, description, className }) => { - return ( -
- - 🔍 - - - {title} - - - {description} - -
- ); -}; - -export default ListStatus; diff --git a/apps/tangle-dapp/src/components/Lists/AssetList.tsx b/apps/tangle-dapp/src/components/Lists/AssetList.tsx index d63186cb39..a22effdaaf 100644 --- a/apps/tangle-dapp/src/components/Lists/AssetList.tsx +++ b/apps/tangle-dapp/src/components/Lists/AssetList.tsx @@ -82,7 +82,7 @@ export const AssetList = ({ size="xl" name={asset.symbol} className="mr-2" - spinnersize="lg" + spinnerSize="lg" />
diff --git a/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx b/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx new file mode 100644 index 0000000000..b9de54f85d --- /dev/null +++ b/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx @@ -0,0 +1,75 @@ +import { + EMPTY_VALUE_PLACEHOLDER, + Typography, +} from '@webb-tools/webb-ui-components'; +import { FC, ReactNode } from 'react'; + +type Props = { + logo: ReactNode; + leftUpperContent: ReactNode | string; + leftBottomContent?: ReactNode | string; + rightUpperText?: string; + rightBottomText?: string; +}; + +const LogoListItem: FC = ({ + logo, + leftUpperContent, + leftBottomContent, + rightUpperText, + rightBottomText, +}) => { + return ( + <> +
+ {logo} + +
+ {typeof leftUpperContent === 'string' ? ( + + {leftUpperContent} + + ) : ( + leftUpperContent + )} + + {leftBottomContent !== undefined ? ( + typeof leftBottomContent === 'string' ? ( + + {leftBottomContent} + + ) : ( + leftBottomContent + ) + ) : null} +
+
+ + {rightUpperText !== undefined && rightBottomText !== undefined && ( +
+ + {rightUpperText ?? EMPTY_VALUE_PLACEHOLDER} + + + {rightBottomText !== undefined && ( + + {rightBottomText} + + )} +
+ )} + + ); +}; + +export default LogoListItem; diff --git a/apps/tangle-dapp/src/components/Lists/OperatorList.tsx b/apps/tangle-dapp/src/components/Lists/OperatorList.tsx deleted file mode 100644 index e6698302bf..0000000000 --- a/apps/tangle-dapp/src/components/Lists/OperatorList.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Search } from '@webb-tools/icons'; -import type { OperatorMap } from '@webb-tools/tangle-shared-ui/types/restake'; -import type { IdentityType } from '@webb-tools/tangle-shared-ui/utils/polkadot/identity'; -import { - Input, - KeyValueWithButton, - ListItem, - Typography, -} from '@webb-tools/webb-ui-components'; -import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import { keys, omitBy, pick } from 'lodash'; -import { ComponentProps, useMemo, useState } from 'react'; -import { twMerge } from 'tailwind-merge'; - -import AvatarWithText from '../AvatarWithText'; -import { ListCardWrapper } from './ListCardWrapper'; - -export type OperatorConfig = { - accountId: string; - name: string; - status: string; -}; - -type OperatorListProps = { - title?: string; - onClose: () => void; - operators: OperatorConfig[]; - operatorMap?: OperatorMap | null; - operatorIdentities?: Record | null; - onSelectOperator: (operator: OperatorConfig) => void; - overrideScrollAreaProps?: ComponentProps; -}; - -export const OperatorList = ({ - onClose, - title = 'Select Operator', - overrideScrollAreaProps, - onSelectOperator, - operatorMap: operatorMapProp = {}, - operatorIdentities, -}: OperatorListProps) => { - const [searchText, setSearchText] = useState(''); - - // Only show active operators - const activeOperator = useMemo( - () => omitBy(operatorMapProp, (operator) => operator.status !== 'Active'), - [operatorMapProp], - ); - - const isEmpty = Object.keys(activeOperator).length === 0; - - const filteredOperator = useMemo(() => { - if (searchText === '') return activeOperator; - - const pickedOperators = keys(activeOperator).filter((operator) => { - const identity = operatorIdentities?.[operator]?.name; - if (!identity) return operator.includes(searchText); - - return ( - identity.toLowerCase().includes(searchText.toLowerCase()) || - operator.includes(searchText) - ); - }); - - return pick(activeOperator, pickedOperators); - }, [activeOperator, operatorIdentities, searchText]); - - return ( - - {!isEmpty && ( - <> -
- } - placeholder="Search operators" - value={searchText} - onChange={(val) => setSearchText(val.toString())} - inputClassName="placeholder:text-mono-80 dark:placeholder:text-mono-120" - /> -
- - -
    - {keys(filteredOperator).map((operator, idx) => ( - - onSelectOperator({ - accountId: operator, - name: operatorIdentities?.[operator]?.name || '', - status: filteredOperator[operator].status.toString(), - }) - } - className="cursor-pointer w-full flex items-center gap-4 justify-between max-w-full min-h-[60px] py-[12px]" - > - ' - } - description={ - - } - /> - - ))} -
-
- - )} - - {isEmpty && ( -
- - No Operator Found. - - - - You can comeback later or add apply to become a operator. - -
- )} -
- ); -}; diff --git a/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx b/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx new file mode 100644 index 0000000000..9e9206828b --- /dev/null +++ b/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx @@ -0,0 +1,40 @@ +import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; +import { + Avatar, + EMPTY_VALUE_PLACEHOLDER, + KeyValueWithButton, + shortenString, +} from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; +import LogoListItem from './LogoListItem'; + +type Props = { + accountAddress: SubstrateAddress; + identity?: string; + rightUpperText?: string; + rightBottomText?: string; +}; + +const OperatorListItem: FC = ({ + accountAddress, + identity, + rightUpperText, + rightBottomText, +}) => { + const shortAccountAddress = shortenString(accountAddress); + const leftUpperContent = identity ?? shortAccountAddress; + + return ( + } + leftUpperContent={leftUpperContent} + leftBottomContent={ + + } + rightUpperText={rightUpperText ?? EMPTY_VALUE_PLACEHOLDER} + rightBottomText={rightBottomText} + /> + ); +}; + +export default OperatorListItem; diff --git a/apps/tangle-dapp/src/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx b/apps/tangle-dapp/src/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx index 83b4779c63..8f773d1371 100644 --- a/apps/tangle-dapp/src/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx +++ b/apps/tangle-dapp/src/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx @@ -47,8 +47,8 @@ import calculateCommission from '../../utils/calculateCommission'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import { ValidatorSelectionTableProps } from './types'; -import SkeletonRows from '../SkeletonRows'; import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; +import SkeletonRows from '@webb-tools/tangle-shared-ui/components/SkeletonRows'; const columnHelper = createColumnHelper(); diff --git a/apps/tangle-dapp/src/components/tables/Vaults/index.tsx b/apps/tangle-dapp/src/components/tables/Vaults/index.tsx index a3493de663..4453dd17aa 100644 --- a/apps/tangle-dapp/src/components/tables/Vaults/index.tsx +++ b/apps/tangle-dapp/src/components/tables/Vaults/index.tsx @@ -99,7 +99,7 @@ const columns = [
- Assets & Balances + Restake Assets diff --git a/apps/tangle-dapp/src/pages/blueprints/index.tsx b/apps/tangle-dapp/src/pages/blueprints/index.tsx index 39e32babbc..426c9e2b3f 100644 --- a/apps/tangle-dapp/src/pages/blueprints/index.tsx +++ b/apps/tangle-dapp/src/pages/blueprints/index.tsx @@ -1,12 +1,18 @@ -import TopBanner from '@webb-tools/tangle-shared-ui/components/blueprints/TopBanner'; +import RestakeBanner from '@webb-tools/tangle-shared-ui/components/blueprints/RestakeBanner'; import { FC } from 'react'; import BlueprintListing from './BlueprintListing'; +import { BLUEPRINT_DOCS_LINK } from '@webb-tools/webb-ui-components/constants/tangleDocs'; const BlueprintsPage: FC = () => { return (
- +
diff --git a/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx b/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx index 0fc6dd6ab8..572be8bfcd 100644 --- a/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx +++ b/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx @@ -339,7 +339,7 @@ const ConfirmationItem: FC<{
diff --git a/apps/tangle-dapp/src/pages/notFound.tsx b/apps/tangle-dapp/src/pages/notFound.tsx new file mode 100644 index 0000000000..c35a0fb3b5 --- /dev/null +++ b/apps/tangle-dapp/src/pages/notFound.tsx @@ -0,0 +1,28 @@ +import { Button, Typography } from '@webb-tools/webb-ui-components'; +import { PagePath } from '../types'; +import { FC } from 'react'; +import { ArrowRight } from '@webb-tools/icons'; +import { Link } from 'react-router'; + +const NotFoundPage: FC = () => { + return ( +
+
+ + Page Not Found + + + + Hmm... We've looked far and wide but could not find the requested + page. Please check the URL path for typos. + +
+ + + + +
+ ); +}; + +export default NotFoundPage; diff --git a/apps/tangle-dapp/src/pages/restake/ActionButtonBase.tsx b/apps/tangle-dapp/src/pages/restake/ActionButtonBase.tsx index 9c6e48d557..0ea19873d0 100644 --- a/apps/tangle-dapp/src/pages/restake/ActionButtonBase.tsx +++ b/apps/tangle-dapp/src/pages/restake/ActionButtonBase.tsx @@ -1,12 +1,7 @@ import { useConnectWallet } from '@webb-tools/api-provider-environment/ConnectWallet'; import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; -import { ConnectWalletMobileButton } from '@webb-tools/webb-ui-components/components/ConnectWalletMobileButton'; -import { useCheckMobile } from '@webb-tools/webb-ui-components/hooks/useCheckMobile'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import { Link } from 'react-router'; import { type ReactNode, useMemo } from 'react'; -import { PagePath } from '../../types'; type Props = { targetTypedChainId?: number; @@ -17,7 +12,6 @@ export default function ActionButtonBase({ targetTypedChainId, children, }: Props) { - const { isMobile } = useCheckMobile(); const { loading, isConnecting, activeWallet } = useWebContext(); const { toggleModal } = useConnectWallet(); @@ -38,23 +32,6 @@ export default function ActionButtonBase({ }; }, [isConnecting, loading]); - if (isMobile) { - return ( - - - A complete mobile experience for Hubble Bridge is in the works. For - now, enjoy all features on a desktop device. - - - Visit the link on desktop below to start transacting privately! - - - - - - ); - } - // If the user is not connected to a wallet, show the connect wallet button if (activeWallet === undefined) { return ( diff --git a/apps/tangle-dapp/src/pages/restake/AnimatedTable.tsx b/apps/tangle-dapp/src/pages/restake/AnimatedTable.tsx index 08eeeace7a..c55944f708 100644 --- a/apps/tangle-dapp/src/pages/restake/AnimatedTable.tsx +++ b/apps/tangle-dapp/src/pages/restake/AnimatedTable.tsx @@ -17,10 +17,7 @@ export function AnimatedTable({ {(!isMediumScreen || isTableOpen) && ( ) => { - return ( - - ); -}; - -export default AssetList; diff --git a/apps/tangle-dapp/src/pages/restake/AssetPlaceholder.tsx b/apps/tangle-dapp/src/pages/restake/AssetPlaceholder.tsx index 673fb93a95..42397bcb4d 100644 --- a/apps/tangle-dapp/src/pages/restake/AssetPlaceholder.tsx +++ b/apps/tangle-dapp/src/pages/restake/AssetPlaceholder.tsx @@ -1,17 +1,11 @@ -import { TokenIcon } from '@webb-tools/icons/TokenIcon'; -import type { ComponentProps } from 'react'; +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; -import SelectorPlaceholder from './SelectorPlaceholder'; - -const AssetPlaceholder = ({ - children = 'Asset', - Icon = , - ...props -}: Partial>) => { +const AssetPlaceholder: FC = () => { return ( - - {children} - + + Select Asset + ); }; diff --git a/apps/tangle-dapp/src/pages/restake/ModalContent.tsx b/apps/tangle-dapp/src/pages/restake/ModalContent.tsx deleted file mode 100644 index 91d647e619..0000000000 --- a/apps/tangle-dapp/src/pages/restake/ModalContent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { - ModalContent as ModalContentCmp, - ModalDescription, - ModalTitle, -} from '@webb-tools/webb-ui-components/components/Modal'; -import { ComponentProps } from 'react'; -import { twMerge } from 'tailwind-merge'; - -const ModalContent = ({ - children, - className, - title, - description, - ...props -}: ComponentProps & { - title: string; - description: string; -}) => { - return ( - - {title} - - {description} - - {children} - - ); -}; - -export default ModalContent; diff --git a/apps/tangle-dapp/src/pages/restake/RestakeTabs.tsx b/apps/tangle-dapp/src/pages/restake/RestakeTabs.tsx index a2932bf315..730ad6823a 100644 --- a/apps/tangle-dapp/src/pages/restake/RestakeTabs.tsx +++ b/apps/tangle-dapp/src/pages/restake/RestakeTabs.tsx @@ -6,31 +6,44 @@ import { twMerge } from 'tailwind-merge'; import { PagePath } from '../../types'; import TabListItem from './TabListItem'; import TabsList from './TabsList'; +import { RestakeAction } from '../../constants'; export type TabsListProps = PropsOf<'ul'>; -const tabs = ['deposit', 'stake', 'unstake', 'withdraw'] as const; +const getTabRoute = (tab: RestakeAction): PagePath => { + switch (tab) { + case RestakeAction.DEPOSIT: + return PagePath.RESTAKE; + case RestakeAction.DELEGATE: + return PagePath.RESTAKE_DELEGATE; + case RestakeAction.UNDELEGATE: + return PagePath.RESTAKE_UNDELEGATE; + case RestakeAction.WITHDRAW: + return PagePath.RESTAKE_WITHDRAW; + } +}; const RestakeTabs = (props: TabsListProps) => { const location = useLocation(); const activeTab = useMemo(() => { - const paths = location.pathname.split('/'); - - const activeTab = tabs.find((tab) => paths.some((path) => path === tab)); - - return activeTab; + return Object.values(RestakeAction).find( + (tab) => location.pathname === getTabRoute(tab), + ); }, [location.pathname]); return ( - {tabs.map((tab, idx) => ( + {Object.values(RestakeAction).map((tab, idx) => ( {`${tab[0].toUpperCase()}${tab.substring(1)}`} diff --git a/apps/tangle-dapp/src/pages/restake/SelectorPlaceholder.tsx b/apps/tangle-dapp/src/pages/restake/SelectorPlaceholder.tsx deleted file mode 100644 index efb90dd1d8..0000000000 --- a/apps/tangle-dapp/src/pages/restake/SelectorPlaceholder.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import type { PropsWithChildren, ReactElement } from 'react'; - -type Props = { - Icon: ReactElement; -}; - -const SelectorPlaceholder = ({ Icon, children }: PropsWithChildren) => { - return ( - - {Icon} - {children} - - ); -}; - -export default SelectorPlaceholder; diff --git a/apps/tangle-dapp/src/pages/restake/SupportedChainModal.tsx b/apps/tangle-dapp/src/pages/restake/SupportedChainModal.tsx index 33c7e51789..db60d64d76 100644 --- a/apps/tangle-dapp/src/pages/restake/SupportedChainModal.tsx +++ b/apps/tangle-dapp/src/pages/restake/SupportedChainModal.tsx @@ -3,7 +3,7 @@ import type { ComponentProps, FC } from 'react'; import { ChainList } from '../../components/Lists/ChainList'; import { SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS } from '../../constants/restake'; -import ModalContent from './ModalContent'; +import { ModalContent } from '@webb-tools/webb-ui-components'; type Props = { isOpen: boolean; @@ -16,7 +16,6 @@ const SupportedChainModal: FC = ({ isOpen, onChainChange, onClose }) => { { open: openChainModal, } = useModal(); - const { - status: tokenModalOpen, - close: closeTokenModal, - open: openTokenModal, - } = useModal(); + const [isTokenModalOpen, setIsTokenModalOpen] = useState(false); const selectableTokens = useMemo( () => @@ -231,9 +235,9 @@ const DepositForm = ({ ...props }: DepositFormProps) => { const handleTokenChange = useCallback( (token: TokenListCardProps['selectTokens'][number]) => { setValue('depositAssetId', token.id); - closeTokenModal(); + setIsTokenModalOpen(false); }, - [closeTokenModal, setValue], + [setIsTokenModalOpen, setValue], ); const onSubmit = useCallback>( @@ -276,64 +280,85 @@ const DepositForm = ({ ...props }: DepositFormProps) => { ); return ( - -
-
-
- +
+ + + + +
+
+ setIsTokenModalOpen(true)} + register={register} + setValue={setValue} + watch={watch} + /> +
+ +
+ + + +
-
- - - -
-
- - - - - - - + + + + + + - - - - - + isOpen={isTokenModalOpen} + setIsOpen={setIsTokenModalOpen} + filterItem={(asset, query) => + searchBy(query, [asset.id, asset.name, asset.symbol]) + } + searchInputId="restake-deposit-assets-search" + searchPlaceholder="Search assets..." + titleWhenEmpty="No Assets Found" + descriptionWhenEmpty="It seems that there are no available assets in this network yet. Please try again later." + items={selectableTokens} + renderItem={(asset) => { + const fmtBalance = + asset.assetBalanceProps?.balance !== undefined + ? `${addCommasToNumber(asset.assetBalanceProps.balance)} ${asset.symbol}` + : EMPTY_VALUE_PLACEHOLDER; + + return ( + } + leftUpperContent={`${asset.name} (${asset.symbol})`} + leftBottomContent={`Asset ID: ${asset.id}`} + rightBottomText="Balance" + rightUpperText={fmtBalance} + /> + ); + }} + onSelect={handleTokenChange} + /> + + +
); }; diff --git a/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx b/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx index d55c762b37..8cc0719cc1 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx @@ -115,11 +115,15 @@ const SourceChainInput = ({ ); return ( - // Pass token symbol to root here to share between max amount & token selection button
+ {/** + * Pass token symbol to root here to share between max amount + * & token selection button. + */} - {amountError}
); diff --git a/apps/tangle-dapp/src/pages/restake/deposit/index.tsx b/apps/tangle-dapp/src/pages/restake/deposit/index.tsx deleted file mode 100644 index a62d3fdf75..0000000000 --- a/apps/tangle-dapp/src/pages/restake/deposit/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import RestakeTabs from '../RestakeTabs'; -import StyleContainer from '../StyleContainer'; -import DepositForm from './DepositForm'; - -export default function DepositPage() { - return ( - - - - - ); -} diff --git a/apps/tangle-dapp/src/pages/restake/layout.tsx b/apps/tangle-dapp/src/pages/restake/layout.tsx deleted file mode 100644 index 4d6fdb30ae..0000000000 --- a/apps/tangle-dapp/src/pages/restake/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { FC } from 'react'; -import { Outlet } from 'react-router'; -import Providers from './providers'; - -const RestakeLayout: FC = () => { - return ( - - - - ); -}; - -export default RestakeLayout; diff --git a/apps/tangle-dapp/src/pages/restake/overview/TableTabs.tsx b/apps/tangle-dapp/src/pages/restake/overview/RestakeOverviewTabs.tsx similarity index 77% rename from apps/tangle-dapp/src/pages/restake/overview/TableTabs.tsx rename to apps/tangle-dapp/src/pages/restake/overview/RestakeOverviewTabs.tsx index 5c947be72a..94f3ac3899 100644 --- a/apps/tangle-dapp/src/pages/restake/overview/TableTabs.tsx +++ b/apps/tangle-dapp/src/pages/restake/overview/RestakeOverviewTabs.tsx @@ -6,15 +6,22 @@ import type { } from '@webb-tools/tangle-shared-ui/types/restake'; import { TableAndChartTabs } from '@webb-tools/webb-ui-components/components/TableAndChartTabs'; import { TabContent } from '@webb-tools/webb-ui-components/components/Tabs/TabContent'; -import { type ComponentProps, type FC, useMemo } from 'react'; +import { type ComponentProps, type FC, ReactNode, useMemo } from 'react'; import VaultAssetsTable from '../../../components/tables/VaultAssets'; import VaultsTable from '../../../components/tables/Vaults'; import useRestakeRewardConfig from '../../../data/restake/useRestakeRewardConfig'; import OperatorsTable from './OperatorsTable'; - -const RESTAKE_VAULTS_TAB = 'Restake Vaults'; - -const OPERATORS_TAB = 'Operators'; +import DepositForm from '../deposit/DepositForm'; +import { RestakeAction } from '../../../constants'; +import RestakeWithdrawPage from '../withdraw'; +import RestakeStakePage from '../stake'; +import RestakeUnstakePage from '../unstake'; + +enum RestakeTab { + RESTAKE = 'Restake', + VAULTS = 'Vaults', + OPERATORS = 'Operators', +} type VaultUI = NonNullable['data']>[number]; @@ -29,15 +36,30 @@ type Props = { operatorMap: OperatorMap; operatorTVL?: Record; vaultTVL?: Record; + action: RestakeAction; +}; + +const getFormOfRestakeAction = (action: RestakeAction): ReactNode => { + switch (action) { + case RestakeAction.DEPOSIT: + return ; + case RestakeAction.WITHDRAW: + return ; + case RestakeAction.DELEGATE: + return ; + case RestakeAction.UNDELEGATE: + return ; + } }; -const TableTabs: FC = ({ +const RestakeOverviewTabs: FC = ({ delegatorInfo, delegatorTVL, operatorConcentration, operatorMap, operatorTVL, vaultTVL, + action, }) => { const { assetMap } = useRestakeContext(); const { rewardConfig } = useRestakeRewardConfig(); @@ -97,6 +119,7 @@ const TableTabs: FC = ({ }, getExpandedRowContent(row) { const vaultId = row.original.id; + const vaultAssets = Object.values(assetMap) .filter((asset) => asset.vaultId === vaultId) .map((asset) => { @@ -124,14 +147,21 @@ const TableTabs: FC = ({ return ( - + + {getFormOfRestakeAction(action)} + + + - + = ({ ); }; -export default TableTabs; +export default RestakeOverviewTabs; diff --git a/apps/tangle-dapp/src/pages/restake/overview/index.tsx b/apps/tangle-dapp/src/pages/restake/overview/index.tsx index 5897b59b87..7204b1c2e2 100644 --- a/apps/tangle-dapp/src/pages/restake/overview/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/overview/index.tsx @@ -1,104 +1,34 @@ import useRestakeDelegatorInfo from '@webb-tools/tangle-shared-ui/data/restake/useRestakeDelegatorInfo'; import useRestakeOperatorMap from '@webb-tools/tangle-shared-ui/data/restake/useRestakeOperatorMap'; import useRestakeTVL from '@webb-tools/tangle-shared-ui/data/restake/useRestakeTVL'; -import getTVLToDisplay from '@webb-tools/tangle-shared-ui/utils/getTVLToDisplay'; -import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; -import { - Card, - CardVariant, -} from '@webb-tools/webb-ui-components/components/Card'; -import { TANGLE_DOCS_RESTAKING_URL } from '@webb-tools/webb-ui-components/constants'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import { twMerge } from 'tailwind-merge'; -import StatItem from '../../../components/StatItem'; -import { CONTENT } from './shared'; -import TableTabs from './TableTabs'; -import { ArrowRightUp, ExternalLinkLine } from '@webb-tools/icons'; +import RestakeOverviewTabs from './RestakeOverviewTabs'; +import { useParams } from 'react-router'; +import { RestakeAction } from '../../../constants'; +import NotFoundPage from '../../notFound'; +import isEnumValue from '../../../utils/isEnumValue'; export default function RestakePage() { + const { action } = useParams(); const { delegatorInfo } = useRestakeDelegatorInfo(); const { operatorMap } = useRestakeOperatorMap(); - const { - delegatorTVL, - operatorConcentration, - operatorTVL, - vaultTVL, - totalDelegatorTVL, - totalNetworkTVL, - } = useRestakeTVL(operatorMap, delegatorInfo); + const { delegatorTVL, operatorConcentration, operatorTVL, vaultTVL } = + useRestakeTVL(operatorMap, delegatorInfo); - return ( -
-
- - - {CONTENT.OVERVIEW} - - -
- - - -
-
- - -
- - How It Works - + // If provided, make sure that the action parameter is valid. + if (action !== undefined && !isEnumValue(action, RestakeAction)) { + return ; + } - {CONTENT.HOW_IT_WORKS} -
- - -
-
- - -
+ return ( + ); } diff --git a/apps/tangle-dapp/src/pages/restake/providers.tsx b/apps/tangle-dapp/src/pages/restake/providers.tsx deleted file mode 100644 index 2394741fb5..0000000000 --- a/apps/tangle-dapp/src/pages/restake/providers.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { RestakeContextProvider } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; -import { type PropsWithChildren } from 'react'; - -export default function Providers({ children }: PropsWithChildren) { - return {children}; -} diff --git a/apps/tangle-dapp/src/pages/restake/stake/StakeInput.tsx b/apps/tangle-dapp/src/pages/restake/stake/StakeInput.tsx index 97202a78ce..15a8888dc6 100644 --- a/apps/tangle-dapp/src/pages/restake/stake/StakeInput.tsx +++ b/apps/tangle-dapp/src/pages/restake/stake/StakeInput.tsx @@ -111,44 +111,47 @@ export default function StakeInput({ ); return ( - - - ( - - ), - } - : {})} - /> - - +
+ + + ( + + ), + } + : {})} + /> + + - , - }} - customAmountProps={customAmountProps} - /> + , + }} + customAmountProps={customAmountProps} + /> + {amountError} - +
); } diff --git a/apps/tangle-dapp/src/pages/restake/stake/index.tsx b/apps/tangle-dapp/src/pages/restake/stake/index.tsx index d13799bd3c..51c1f738bb 100644 --- a/apps/tangle-dapp/src/pages/restake/stake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/stake/index.tsx @@ -5,23 +5,20 @@ import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeC import useRestakeDelegatorInfo from '@webb-tools/tangle-shared-ui/data/restake/useRestakeDelegatorInfo'; import useRestakeOperatorMap from '@webb-tools/tangle-shared-ui/data/restake/useRestakeOperatorMap'; import { useRpcSubscription } from '@webb-tools/tangle-shared-ui/hooks/usePolkadotApi'; -import { Card, isSubstrateAddress } from '@webb-tools/webb-ui-components'; -import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; +import { + assertSubstrateAddress, + Card, + isSubstrateAddress, +} from '@webb-tools/webb-ui-components'; import type { TokenListCardProps } from '@webb-tools/webb-ui-components/components/ListCard/types'; import { Modal } from '@webb-tools/webb-ui-components/components/Modal'; import { useModal } from '@webb-tools/webb-ui-components/hooks/useModal'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; import entries from 'lodash/entries'; import keys from 'lodash/keys'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { Link } from 'react-router'; import { formatUnits, parseUnits } from 'viem'; import AvatarWithText from '../../../components/AvatarWithText'; -import { - OperatorConfig, - OperatorList, -} from '../../../components/Lists/OperatorList'; import { DelegatorStakeContext, TxEvent, @@ -34,11 +31,9 @@ import ViewTxOnExplorer from '../../../data/restake/ViewTxOnExplorer'; import useIdentities from '../../../data/useIdentities'; import useActiveTypedChainId from '../../../hooks/useActiveTypedChainId'; import useQueryState from '../../../hooks/useQueryState'; -import { PagePath, QueryParamKey } from '../../../types'; +import { QueryParamKey } from '../../../types'; import type { DelegationFormFields } from '../../../types/restake'; -import AssetList from '../AssetList'; import Form from '../Form'; -import ModalContent from '../ModalContent'; import RestakeTabs from '../RestakeTabs'; import StyleContainer from '../StyleContainer'; import SupportedChainModal from '../SupportedChainModal'; @@ -46,8 +41,21 @@ import useSwitchChain from '../useSwitchChain'; import ActionButton from './ActionButton'; import Info from './Info'; import StakeInput from './StakeInput'; - -export default function Page() { +import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; +import OperatorListItem from '../../../components/Lists/OperatorListItem'; +import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; +import LogoListItem from '../../../components/Lists/LogoListItem'; +import { TokenIcon } from '@webb-tools/icons'; +import searchBy from '../../../utils/searchBy'; +import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; + +type RestakeOperator = { + accountId: SubstrateAddress; + identityName?: string; + isActive: boolean; +}; + +export default function RestakeStakePage() { const { register, setValue: setFormValue, @@ -137,17 +145,8 @@ export default function Page() { close: closeChainModal, } = useModal(false); - const { - status: isAssetModalOpen, - open: openAssetModal, - close: closeAssetModal, - } = useModal(false); - - const { - status: isOperatorModalOpen, - open: openOperatorModal, - close: closeOperatorModal, - } = useModal(false); + const [isOperatorModalOpen, setIsOperatorModalOpen] = useState(false); + const [isAssetModalOpen, setIsAssetModalOpen] = useState(false); const selectableTokens = useMemo(() => { if (!isDefined(delegatorInfo)) { @@ -163,6 +162,7 @@ export default function Page() { id: asset.id, name: asset.name, symbol: asset.symbol, + decimals: asset.decimals, assetBalanceProps: { balance: +formatUnits(amount, asset.decimals), ...(asset.vaultId @@ -171,16 +171,16 @@ export default function Page() { } : {}), }, - } satisfies TokenListCardProps['selectTokens'][number]; + }; }); }, [assetMap, delegatorInfo]); const handleAssetChange = useCallback( (asset: TokenListCardProps['selectTokens'][number]) => { setValue('assetId', asset.id); - closeAssetModal(); + setIsAssetModalOpen(false); }, - [closeAssetModal, setValue], + [setIsAssetModalOpen, setValue], ); const handleChainChange = useCallback( @@ -239,34 +239,39 @@ export default function Page() { [assetMap, delegate, txEventHandlers], ); - const operators = useMemo(() => { - return Object.entries(operatorMap).map(([accountId, _operator]) => ({ - accountId, - name: operatorIdentities?.[accountId]?.name || '', - status: 'active', - })); + const operators = useMemo(() => { + return ( + Object.entries(operatorMap) + // Include only active operators. + .filter(([, metadata]) => metadata.status === 'Active') + .map(([accountId]) => ({ + accountId: assertSubstrateAddress(accountId), + identityName: operatorIdentities?.[accountId]?.name ?? undefined, + isActive: true, + })) + ); }, [operatorMap, operatorIdentities]); const handleOnSelectOperator = useCallback( - (operator: OperatorConfig) => { + (operator: RestakeOperator) => { setValue('operatorAccountId', operator.accountId); - closeOperatorModal(); + setIsOperatorModalOpen(false); }, - [closeOperatorModal, setValue], + [setIsOperatorModalOpen, setValue], ); return ( - + - +
setIsAssetModalOpen(true)} + openOperatorModal={() => setIsOperatorModalOpen(true)} register={register} setValue={setValue} watch={watch} @@ -285,36 +290,55 @@ export default function Page() {
- - - item.id} + onSelect={handleAssetChange} + renderItem={(asset) => { + const fmtBalance = `${addCommasToNumber(asset.assetBalanceProps.balance)} ${asset.symbol}`; + + return ( + } + leftUpperContent={`${asset.name} (${asset.symbol})`} + leftBottomContent={`Asset ID: ${asset.id}`} + rightUpperText={fmtBalance} + rightBottomText="Balance" + /> + ); + }} + /> + + item.accountId} + onSelect={handleOnSelectOperator} + filterItem={(item, query) => + searchBy(query, [item.accountId, item.identityName]) + } + renderItem={({ accountId, identityName }) => ( + - - - - - + )} + /> + ); } - -/** @internal */ -const EmptyAsset = () => ( -
- - No assets available - - - - - -
-); diff --git a/apps/tangle-dapp/src/pages/restake/unstake/SelectOperatorModal.tsx b/apps/tangle-dapp/src/pages/restake/unstake/SelectOperatorModal.tsx new file mode 100644 index 0000000000..6aa735baea --- /dev/null +++ b/apps/tangle-dapp/src/pages/restake/unstake/SelectOperatorModal.tsx @@ -0,0 +1,101 @@ +import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; +import { DelegatorInfo } from '@webb-tools/tangle-shared-ui/types/restake'; +import type { IdentityType } from '@webb-tools/tangle-shared-ui/utils/polkadot/identity'; +import { useMemo } from 'react'; +import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; +import { DEFAULT_DECIMALS } from '@webb-tools/dapp-config'; +import { formatUnits } from 'viem'; +import searchBy from '../../../utils/searchBy'; +import OperatorListItem from '../../../components/Lists/OperatorListItem'; + +type Props = { + delegatorInfo: DelegatorInfo | null; + operatorIdentities?: Record | null; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + + onItemSelected: ( + item: DelegatorInfo['delegations'][number] & { + formattedAmount: string; + }, + ) => void; +}; + +const SelectOperatorModal = ({ + delegatorInfo, + isOpen, + setIsOpen, + onItemSelected, + operatorIdentities, +}: Props) => { + const { assetMap } = useRestakeContext(); + + // Aggregate the delegations based on the operator account ID and asset ID. + const delegations = useMemo(() => { + if (!Array.isArray(delegatorInfo?.delegations)) { + return []; + } + + return delegatorInfo.delegations; + }, [delegatorInfo]); + + return ( + { + const asset = assetMap[item.assetId]; + const decimals = asset?.decimals || DEFAULT_DECIMALS; + const fmtAmount = formatUnits(item.amountBonded, decimals); + + onItemSelected({ + ...item, + formattedAmount: fmtAmount, + }); + }} + filterItem={(item, query) => { + const asset = assetMap[item.assetId]; + + if (asset === undefined) { + return false; + } + + const assetSymbol = asset?.symbol; + const identityName = operatorIdentities?.[item.operatorAccountId]?.name; + + return searchBy(query, [ + item.operatorAccountId, + assetSymbol, + identityName, + ]); + }} + renderItem={({ amountBonded, assetId, operatorAccountId }) => { + const asset = assetMap[assetId]; + + if (asset === undefined) { + return null; + } + + const fmtAmount = formatUnits(amountBonded, asset.decimals); + const identityName = operatorIdentities?.[operatorAccountId]?.name; + + return ( + + ); + }} + /> + ); +}; + +export default SelectOperatorModal; diff --git a/apps/tangle-dapp/src/pages/restake/unstake/UnstakeModal.tsx b/apps/tangle-dapp/src/pages/restake/unstake/UnstakeModal.tsx deleted file mode 100644 index a333a144db..0000000000 --- a/apps/tangle-dapp/src/pages/restake/unstake/UnstakeModal.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { DEFAULT_DECIMALS } from '@webb-tools/dapp-config/constants'; -import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; -import { DelegatorInfo } from '@webb-tools/tangle-shared-ui/types/restake'; -import type { IdentityType } from '@webb-tools/tangle-shared-ui/utils/polkadot/identity'; -import { KeyValueWithButton } from '@webb-tools/webb-ui-components/components/KeyValueWithButton'; -import { ListItem } from '@webb-tools/webb-ui-components/components/ListCard/ListItem'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import { useMemo } from 'react'; -import { twMerge } from 'tailwind-merge'; -import { formatUnits } from 'viem'; -import AvatarWithText from '../../../components/AvatarWithText'; -import ModalContent from '../ModalContent'; -import ModalContentList from '../ModalContentList'; - -type Props = { - delegatorInfo: DelegatorInfo | null; - operatorIdentities?: Record | null; - isOpen: boolean; - onClose: () => void; - onItemSelected: ( - item: DelegatorInfo['delegations'][number] & { - formattedAmount: string; - }, - ) => void; -}; - -const UnstakeModal = ({ - delegatorInfo, - isOpen, - onClose, - onItemSelected, - operatorIdentities, -}: Props) => { - const { assetMap } = useRestakeContext(); - - // Aggregate the delegations based on the operator account id and asset id - const delegations = useMemo(() => { - if (!Array.isArray(delegatorInfo?.delegations)) { - return []; - } - - return delegatorInfo.delegations; - }, [delegatorInfo]); - - return ( - - { - if (!searchText) { - return true; - } - - const asset = assetMap[assetId]; - const assetSymbol = asset?.symbol || 'Unknown'; - const identityName = - operatorIdentities?.[operatorAccountId]?.name || ''; - - return ( - operatorAccountId - .toLowerCase() - .includes(searchText.toLowerCase()) || - assetSymbol.toLowerCase().includes(searchText.toLowerCase()) || - (identityName - ? identityName.toLowerCase().includes(searchText.toLowerCase()) - : false) - ); - }} - renderEmpty={{ - title: 'No Delegation Found', - description: - 'You can try to deposit or delegate an asset to an operator.', - }} - renderItem={(item) => { - const { amountBonded, assetId, operatorAccountId } = item; - const asset = assetMap[assetId]; - - const decimals = asset?.decimals || DEFAULT_DECIMALS; - const assetSymbol = asset?.symbol || 'Unknown'; - - const fmtAmount = formatUnits(amountBonded, decimals); - - return ( - - onItemSelected({ - ...item, - formattedAmount: fmtAmount, - }) - } - > - ' - } - description={ - - } - /> - -
- - {fmtAmount} {assetSymbol} - - - {asset.vaultId && ( - - Vault ID: {asset.vaultId} - - )} -
-
- ); - }} - /> -
- ); -}; - -export default UnstakeModal; diff --git a/apps/tangle-dapp/src/pages/restake/unstake/UnstakeRequestTable.tsx b/apps/tangle-dapp/src/pages/restake/unstake/UnstakeRequestTable.tsx index 99f7bc4ddc..6b62bb63b3 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/UnstakeRequestTable.tsx +++ b/apps/tangle-dapp/src/pages/restake/unstake/UnstakeRequestTable.tsx @@ -26,6 +26,7 @@ import TableCell from '../TableCell'; import { calculateTimeRemaining } from '../utils'; import type { UnstakeRequestTableData } from './types'; import UnstakeRequestTableActions from './UnstakeRequestTableActions'; +import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; const columnsHelper = createColumnHelper(); @@ -130,10 +131,12 @@ const UnstakeRequestTable = ({ [assetMap, currentRound, delegationBondLessDelay, operatorIdentities, unstakeRequests], ); + const rows = useMemo(() => Object.values(dataWithId), [dataWithId]); + const table = useReactTable( useMemo>( () => ({ - data: Object.values(dataWithId), + data: rows, columns, initialState: { pagination: { @@ -151,7 +154,7 @@ const UnstakeRequestTable = ({ getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), }), - [dataWithId], + [rows], ), ); @@ -164,7 +167,12 @@ const UnstakeRequestTable = ({ return ( <> - +
{ +const RestakeUnstakePage = () => { const [isUnstakeRequestTableOpen, setIsUnstakeRequestTableOpen] = useState(false); @@ -71,11 +69,7 @@ const Page = () => { const activeTypedChainId = useActiveTypedChainId(); const { assetMap } = useRestakeContext(); - const { - status: isOperatorModalOpen, - open: openOperatorModal, - close: closeOperatorModal, - } = useModal(); + const [isOperatorModalOpen, setIsOperatorModalOpen] = useState(false); const { status: isChainModalOpen, @@ -235,22 +229,11 @@ const Page = () => { ); return ( -
- +
+
- + {!isUnstakeRequestTableOpen && isMediumScreen && ( { )} - - - ( - - ), - } - : {})} - /> - + + + setIsOperatorModalOpen(true)} + {...(selectedOperatorAccountId + ? { + renderBody: () => ( + + ), + } + : {})} + /> + , + disabled: , + }).current + } + /> + + + , - disabled: , + placeholder: , + isDisabled: true, }).current } /> - - - , - isDisabled: true, - }).current - } - /> + {errors.amount?.message} - +
@@ -346,13 +334,13 @@ const Page = () => { - +
- +
{ className="text-mono-120 dark:text-mono-100" > You will be able to withdraw your tokens after the unstake request - has been processed. To unstake your tokens go to the unstake tab - to schedule a request. + has been processed. )} - { - closeOperatorModal(); + setIsOperatorModalOpen(false); const { formattedAmount, assetId, operatorAccountId } = item; + const commonOpts = { shouldDirty: true, shouldValidate: true, @@ -423,4 +411,4 @@ const Page = () => { ); }; -export default Page; +export default RestakeUnstakePage; diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/TxInfo.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/TxInfo.tsx index ef685fdaa7..b8f5462969 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/TxInfo.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/TxInfo.tsx @@ -4,6 +4,7 @@ import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constant import DetailsContainer from '../../../components/DetailsContainer'; import DetailItem from '../../../components/LiquidStaking/stakeAndUnstake/DetailItem'; import useRestakeConsts from '../../../data/restake/useRestakeConsts'; +import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; const TxInfo = () => { const { leaveDelegatorsDelay } = useRestakeConsts(); @@ -17,7 +18,7 @@ const TxInfo = () => { title="Withdraw delay" value={ isDefined(leaveDelegatorsDelay) - ? `${leaveDelegatorsDelay} sessions` + ? `${leaveDelegatorsDelay} ${pluralize('session', leaveDelegatorsDelay !== 1)}` : leaveDelegatorsDelay } /> diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/WithdrawModal.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/WithdrawModal.tsx index 1ef8d47a4b..c21ef323c9 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/WithdrawModal.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/WithdrawModal.tsx @@ -2,18 +2,17 @@ import { DEFAULT_DECIMALS } from '@webb-tools/dapp-config/constants'; import { TokenIcon } from '@webb-tools/icons/TokenIcon'; import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import { DelegatorInfo } from '@webb-tools/tangle-shared-ui/types/restake'; -import { ListItem } from '@webb-tools/webb-ui-components/components/ListCard/ListItem'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; import { useMemo } from 'react'; -import { twMerge } from 'tailwind-merge'; import { formatUnits } from 'viem'; -import ModalContent from '../ModalContent'; -import ModalContentList from '../ModalContentList'; +import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; +import searchBy from '../../../utils/searchBy'; +import LogoListItem from '../../../components/Lists/LogoListItem'; +import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; type Props = { delegatorInfo: DelegatorInfo | null; isOpen: boolean; - onClose: () => void; + setIsOpen: (isOpen: boolean) => void; onItemSelected: (item: { assetId: string; amount: bigint; @@ -24,7 +23,7 @@ type Props = { const WithdrawModal = ({ delegatorInfo, isOpen, - onClose, + setIsOpen, onItemSelected, }: Props) => { const { assetMap } = useRestakeContext(); @@ -44,99 +43,54 @@ const WithdrawModal = ({ }, [delegatorInfo]); return ( - - { - if (!searchText) { - return true; - } + setIsOpen={setIsOpen} + searchInputId="restake-withdraw-asset-search" + searchPlaceholder="Search assets..." + items={deposits} + titleWhenEmpty="No Assets Found" + descriptionWhenEmpty="This account has no assets to withdraw." + onSelect={(item) => { + const asset = assetMap[item.assetId]; + const decimals = asset?.decimals || DEFAULT_DECIMALS; + const fmtAmount = formatUnits(item.amount, decimals); - const asset = assetMap[assetId]; - const assetSymbol = asset?.symbol || 'Unknown'; + onItemSelected({ + ...item, + formattedAmount: fmtAmount, + }); + }} + filterItem={({ assetId }, query) => { + const asset = assetMap[assetId]; - return ( - assetSymbol.toLowerCase().includes(searchText.toLowerCase()) || - amount.toString().includes(searchText) - ); - }} - renderEmpty={{ - title: 'No Asset Found', - description: - 'You can try to deposit or delegate an asset to an operator.', - }} - renderItem={(item) => { - const { amount, assetId } = item; - const asset = assetMap[assetId]; + return searchBy(query, [asset?.name, asset?.id, asset?.vaultId]); + }} + renderItem={({ amount, assetId }) => { + const asset = assetMap[assetId]; - const decimals = asset?.decimals || DEFAULT_DECIMALS; - const assetSymbol = asset?.symbol || 'Unknown'; + if (asset === undefined) { + return null; + } - const fmtAmount = formatUnits(amount, decimals); + const fmtAmount = addCommasToNumber( + formatUnits(amount, asset.decimals), + ); - return ( - - onItemSelected({ - ...item, - formattedAmount: fmtAmount, - }) - } - > -
- - -
- - {assetSymbol} - - - - Asset ID: {assetId} - -
-
- -
- - {fmtAmount} - - - {asset.vaultId && ( - - Vault ID: {asset.vaultId} - - )} -
-
- ); - }} - /> -
+ return ( + } + leftUpperContent={`${asset.name} (${asset.symbol})`} + leftBottomContent={`Asset ID: ${assetId}`} + rightUpperText={fmtAmount} + rightBottomText={ + asset.vaultId !== null ? `Vault ID: ${asset.vaultId}` : undefined + } + /> + ); + }} + /> ); }; diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx index 9c53ede456..57dcfedb38 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx @@ -19,7 +19,6 @@ import type { TextFieldInputProps } from '@webb-tools/webb-ui-components/compone import { TransactionInputCard } from '@webb-tools/webb-ui-components/components/TransactionInputCard'; import { useModal } from '@webb-tools/webb-ui-components/hooks/useModal'; import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import cx from 'classnames'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { formatUnits, parseUnits } from 'viem'; @@ -42,15 +41,15 @@ import ActionButtonBase from '../ActionButtonBase'; import { AnimatedTable } from '../AnimatedTable'; import AssetPlaceholder from '../AssetPlaceholder'; import { ExpandTableButton } from '../ExpandTableButton'; -import RestakeTabs from '../RestakeTabs'; import StyleContainer from '../StyleContainer'; import SupportedChainModal from '../SupportedChainModal'; import useSwitchChain from '../useSwitchChain'; import TxInfo from './TxInfo'; import WithdrawModal from './WithdrawModal'; import WithdrawRequestTable from './WithdrawRequestTable'; +import RestakeTabs from '../RestakeTabs'; -const Page = () => { +const RestakeWithdrawPage = () => { const { register, setValue: setFormValue, @@ -67,11 +66,7 @@ const Page = () => { const { activeChain } = useWebContext(); const { assetMap } = useRestakeContext(); - const { - status: isWithdrawModalOpen, - open: openWithdrawModal, - close: closeWithdrawModal, - } = useModal(); + const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); const { status: isChainModalOpen, @@ -207,22 +202,11 @@ const Page = () => { ); return ( -
- +
+ - + {!isWithdrawRequestTableOpen && isMediumScreen && ( { )}
- - - ( -
- - - - {activeChain.name} - -
- ), - } - : {})} - /> - + + + ( +
+ + + + {activeChain.name} + +
+ ), + } + : {})} + /> + , + disabled: , + }).current + } + /> +
+ + , - disabled: , + placeholder: , + onClick: () => setIsWithdrawModalOpen(true), }).current } /> -
- - , - onClick: openWithdrawModal, - }).current - } - /> +
{errors.amount?.message} - +
@@ -327,7 +316,7 @@ const Page = () => { isTableOpen={isWithdrawRequestTableOpen} isMediumScreen={isMediumScreen} > - +
{ className="text-mono-120 dark:text-mono-100" > You will be able to withdraw your tokens after the unstake - schedule is completed. To unstake your tokens go to the unstake - tab to schedule request. + schedule is completed. To unstake your tokens, use the unstake + tab. )} @@ -361,11 +350,12 @@ const Page = () => { { - closeWithdrawModal(); + setIsWithdrawModalOpen(false); const { formattedAmount, assetId } = item; + const commonOpts = { shouldDirty: true, shouldValidate: true, @@ -393,4 +383,4 @@ const Page = () => { ); }; -export default Page; +export default RestakeWithdrawPage; diff --git a/apps/tangle-dapp/src/types/index.ts b/apps/tangle-dapp/src/types/index.ts index 364d865739..306b2f68e4 100644 --- a/apps/tangle-dapp/src/types/index.ts +++ b/apps/tangle-dapp/src/types/index.ts @@ -16,10 +16,8 @@ export enum PagePath { BLUEPRINTS_DETAILS = '/blueprints/:id', SERVICES = '/services', RESTAKE = '/restake', - RESTAKE_OVERVIEW = '/restake/overview', - RESTAKE_DEPOSIT = '/restake/deposit', - RESTAKE_STAKE = '/restake/stake', - RESTAKE_UNSTAKE = '/restake/unstake', + RESTAKE_DELEGATE = '/restake/delegate', + RESTAKE_UNDELEGATE = '/restake/undelegate', RESTAKE_WITHDRAW = '/restake/withdraw', RESTAKE_OPERATOR = '/restake/operators', LIQUID_STAKING = '/liquid-staking', diff --git a/apps/tangle-dapp/src/utils/createSearchFilterFn.ts b/apps/tangle-dapp/src/utils/createSearchFilterFn.ts new file mode 100644 index 0000000000..57431db9d7 --- /dev/null +++ b/apps/tangle-dapp/src/utils/createSearchFilterFn.ts @@ -0,0 +1,26 @@ +const createSearchFilterFn = (keys: Array) => { + return (item: T, query: string): boolean => { + const normalizedQuery = query.trim().toLowerCase(); + + if (normalizedQuery === '') { + return true; + } + + // In case that the keys contain duplicates. + const uniqueKeys = new Set(keys); + + return Array.from(uniqueKeys).some((key) => { + const value = item[key]; + + if (!(typeof value === 'string' || typeof value === 'number')) { + return false; + } + + const normalizedValue = value.toString().toLowerCase(); + + return normalizedValue.includes(normalizedValue); + }); + }; +}; + +export default createSearchFilterFn; diff --git a/apps/tangle-dapp/src/utils/isEnumValue.ts b/apps/tangle-dapp/src/utils/isEnumValue.ts new file mode 100644 index 0000000000..e077c300d0 --- /dev/null +++ b/apps/tangle-dapp/src/utils/isEnumValue.ts @@ -0,0 +1,21 @@ +/** + * Check whether a value is a valid enum value. + * + * Note that this won't work for enums with overlapping values + * (e.g. `enum Foo { A = 1, B = 1 }`). + */ +const isEnumValue = (value: unknown, enumObj: T): value is T[keyof T] => { + // Just to be safe. + if ( + value === undefined || + value === null || + typeof enumObj !== 'object' || + enumObj === null + ) { + return false; + } + + return Object.values(enumObj).includes(value); +}; + +export default isEnumValue; diff --git a/apps/tangle-dapp/src/utils/searchBy.ts b/apps/tangle-dapp/src/utils/searchBy.ts new file mode 100644 index 0000000000..49dbf3a927 --- /dev/null +++ b/apps/tangle-dapp/src/utils/searchBy.ts @@ -0,0 +1,22 @@ +const searchBy = ( + query: string, + data: Array, +): boolean => { + const normalizedQuery = query.trim().toLowerCase(); + + if (normalizedQuery === '') { + return true; + } + + return data.some((value) => { + if (value === undefined || value === null) { + return false; + } + + const normalizedValue = value.toString().toLowerCase(); + + return normalizedValue.includes(normalizedQuery); + }); +}; + +export default searchBy; diff --git a/apps/tangle-dapp/tailwind.config.js b/apps/tangle-dapp/tailwind.config.js index 52dc940589..101dc14512 100644 --- a/apps/tangle-dapp/tailwind.config.js +++ b/apps/tangle-dapp/tailwind.config.js @@ -35,7 +35,7 @@ export default { validator_table_dark: 'linear-gradient(180deg, #707AA600 0%, #2B2F4066 40%)', purple_gradient: - 'linear-gradient(79deg, #b6b8dd 8.85%, #d9ddf2 55.91%, #dbbdcd 127.36%), #fff', + 'linear-gradient(79deg, #b6b8dd 8.85%, #d9ddf2 55.91%, #dbbdcd 127.36%)', purple_gradient_dark: 'linear-gradient(79deg, rgba(30, 32, 65, 0.8) 8.85%, rgba(38, 52, 116, 0.8) 55.91%, rgba(113, 61, 89, 0.8) 127.36%)', }, diff --git a/libs/dapp-config/src/chains/evm/customChains/tangleLocalEvm.ts b/libs/dapp-config/src/chains/evm/customChains/tangleLocalEvm.ts index c7e335894f..1719ae8222 100644 --- a/libs/dapp-config/src/chains/evm/customChains/tangleLocalEvm.ts +++ b/libs/dapp-config/src/chains/evm/customChains/tangleLocalEvm.ts @@ -5,7 +5,7 @@ import { TANGLE_LOCAL_HTTP_RPC_ENDPOINT } from '../../../constants'; const tangleLocalEVM = defineChain({ id: EVMChainId.TangleLocalEVM, - name: 'Tangle Local EVM', + name: 'Tangle Local (EVM)', nativeCurrency: { name: 'Local Test Tangle Network Token', symbol: 'tTNT', diff --git a/libs/dapp-config/src/chains/evm/customChains/tangleTestnetEVM.ts b/libs/dapp-config/src/chains/evm/customChains/tangleTestnetEVM.ts index f1d28b55f7..added3b84a 100644 --- a/libs/dapp-config/src/chains/evm/customChains/tangleTestnetEVM.ts +++ b/libs/dapp-config/src/chains/evm/customChains/tangleTestnetEVM.ts @@ -8,7 +8,7 @@ import { const tangleTestnetEVM = defineChain({ id: EVMChainId.TangleTestnetEVM, - name: 'Tangle Testnet EVM', + name: 'Tangle Testnet (EVM)', nativeCurrency: { name: 'Test Tangle Network Token', symbol: 'tTNT', diff --git a/libs/dapp-config/src/chains/evm/index.tsx b/libs/dapp-config/src/chains/evm/index.tsx index d4940b45a2..efafb3000f 100644 --- a/libs/dapp-config/src/chains/evm/index.tsx +++ b/libs/dapp-config/src/chains/evm/index.tsx @@ -206,7 +206,7 @@ export const chainsConfig = { chainType: ChainType.EVM, group: 'tangle', tag: 'test', - displayName: 'Tangle Testnet', + displayName: 'Tangle Testnet (EVM)', } satisfies ChainConfig, [PresetTypedChainId.TangleLocalEVM]: { @@ -214,6 +214,7 @@ export const chainsConfig = { chainType: ChainType.EVM, group: 'tangle', tag: 'dev', + displayName: 'Tangle Local (EVM)', } satisfies ChainConfig, // Localnet diff --git a/libs/dapp-config/src/chains/substrate/index.tsx b/libs/dapp-config/src/chains/substrate/index.tsx index 5c350c5226..9ec85e6325 100644 --- a/libs/dapp-config/src/chains/substrate/index.tsx +++ b/libs/dapp-config/src/chains/substrate/index.tsx @@ -44,7 +44,7 @@ export const chainsConfig = { group: 'tangle', tag: 'test', id: SubstrateChainId.TangleTestnetNative, - name: 'Tangle Testnet Native', + name: 'Tangle Testnet', nativeCurrency: { name: 'Tangle', symbol: TANGLE_TESTNET_NATIVE_TOKEN_SYMBOL, @@ -70,7 +70,7 @@ export const chainsConfig = { group: 'tangle', tag: 'dev', id: SubstrateChainId.TangleLocalNative, - name: 'Tangle Local Native', + name: 'Tangle Local', nativeCurrency: { name: 'Local Tangle Token', symbol: 'tTNT', diff --git a/libs/icons/src/TokenIcon.tsx b/libs/icons/src/TokenIcon.tsx index 39fa0d0b70..f2351e1cfc 100644 --- a/libs/icons/src/TokenIcon.tsx +++ b/libs/icons/src/TokenIcon.tsx @@ -12,7 +12,7 @@ export const TokenIcon: React.FC< TokenIconBase & { isActive?: boolean; customLoadingCmp?: React.ReactNode; - spinnersize?: TokenIconBase['size']; + spinnerSize?: TokenIconBase['size']; } > = (props) => { const { @@ -24,7 +24,7 @@ export const TokenIcon: React.FC< size = 'md', onClick, customLoadingCmp, - spinnersize, + spinnerSize, ...restProps } = props; @@ -56,7 +56,7 @@ export const TokenIcon: React.FC< if (error !== undefined) { return {error.message}; } else if (loading) { - return customLoadingCmp ?? ; + return customLoadingCmp ?? ; } if (svgElement) { diff --git a/libs/icons/src/utils.ts b/libs/icons/src/utils.ts index 1de66b74d1..1d4e64b7b6 100644 --- a/libs/icons/src/utils.ts +++ b/libs/icons/src/utils.ts @@ -32,29 +32,16 @@ export function getFillColor(darkMode?: boolean) { } } -/** - * Get the icon size in pixel based on text - * @param size Represent the icon size in text - * @returns The icon in pixel - */ export function getIconSizeInPixel(size: IconSize) { switch (size) { - case '2xl': { + case '2xl': return '48px' as const; - } - - case 'xl': { + case 'xl': return '40px' as const; - } - - case 'lg': { - return '32px' as const; - } - - case 'md': { + case 'lg': return '24px' as const; - } - + case 'md': + return '16px' as const; default: { throw new Error('Unknown icon size'); } diff --git a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx index b5896688f1..d2711113aa 100644 --- a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx +++ b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx @@ -65,7 +65,6 @@ const WalletDropdown: FC<{ accountName={accountName} wallet={wallet} address={accountAddress} - addressClassname="hidden lg:block" /> diff --git a/libs/tangle-shared-ui/src/components/ListModal.tsx b/libs/tangle-shared-ui/src/components/ListModal.tsx new file mode 100644 index 0000000000..9c68d2fd57 --- /dev/null +++ b/libs/tangle-shared-ui/src/components/ListModal.tsx @@ -0,0 +1,179 @@ +import { Search } from '@webb-tools/icons'; +import { + Input, + ListItem, + ListStatus, + Modal, + ModalContent, + ModalHeader, +} from '@webb-tools/webb-ui-components'; +import { useMemo, useState } from 'react'; +import SkeletonRows from './SkeletonRows'; +import { twMerge } from 'tailwind-merge'; +import { ScrollArea } from '@radix-ui/react-scroll-area'; + +export type ListModalProps = { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + title: string; + showSearch?: boolean; + searchInputId: string; + searchPlaceholder: string; + titleWhenEmpty: string; + descriptionWhenEmpty: string; + renderItem: (item: T) => JSX.Element | null; + getItemKey?: (item: T) => string; + onSelect: (item: T) => void; + sorting?: (a: T, b: T) => number; + + /** + * Provide a function to help determine whether to include an item in the list once a search query is provided. + * + * If not defined, the search input will not be displayed because it cannot be determined how to filter the items. + */ + filterItem?: (item: T, searchQuery: string) => boolean; + + /** + * The items to display in the list. + * + * If `null`, `undefined`, or `false`, the list will display a loading state. + */ + items: T[] | null | undefined | false | Error; +}; + +const ListModal = ({ + title, + isOpen, + setIsOpen, + filterItem, + sorting, + titleWhenEmpty, + descriptionWhenEmpty, + showSearch = true, + searchPlaceholder, + searchInputId, + items, + renderItem, + getItemKey, + onSelect, +}: ListModalProps) => { + const [searchQuery, setSearchQuery] = useState(''); + + const isSearching = searchQuery.trim().length > 0; + + const sortedItems = useMemo(() => { + if (!Array.isArray(items) || sorting === undefined) { + return items; + } + + return items.toSorted(sorting); + }, [items, sorting]); + + const processedItems = useMemo(() => { + if ( + !isSearching || + filterItem === undefined || + !Array.isArray(sortedItems) + ) { + return sortedItems; + } + + return sortedItems.filter((item) => filterItem(item, searchQuery)); + }, [filterItem, isSearching, sortedItems, searchQuery]); + + const isLoading = !Array.isArray(processedItems); + const isEmpty = !isSearching && !isLoading && processedItems.length === 0; + + return ( + + setIsOpen(false)} + size="md" + className={twMerge( + 'max-h-[600px]', + Array.isArray(processedItems) && + processedItems.length > 0 && + 'h-full', + )} + > + setIsOpen(false)}> + {title} + + +
+ {showSearch && filterItem !== undefined && !isEmpty && !isLoading && ( +
+ } + placeholder={searchPlaceholder} + value={searchQuery} + onChange={setSearchQuery} + inputClassName="placeholder:text-mono-80 dark:placeholder:text-mono-120" + /> +
+ )} + +
+ + {items instanceof Error ? ( + + ) : isLoading ? ( +
+ +
+ ) : isEmpty ? ( + + ) : isSearching && processedItems.length === 0 ? ( + + ) : ( + +
    + {processedItems.map((item, index) => { + const key = + getItemKey !== undefined + ? getItemKey(item) + : index.toString(); + + const itemContent = renderItem(item); + + // Ignore the item if the render function returns `null`. + if (itemContent === null) { + return null; + } + + return ( + onSelect(item)} + className="w-full flex items-center gap-4 justify-between max-w-full min-h-[60px] py-3 cursor-pointer" + > + {itemContent} + + ); + })} +
+
+ )} +
+
+
+ ); +}; + +export default ListModal; diff --git a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx index dc93cdb44b..2116e2e325 100644 --- a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx +++ b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx @@ -140,7 +140,7 @@ const TriggerButton: FC = ({ )} > {isLoading ? ( - + ) : ( )} diff --git a/apps/tangle-dapp/src/components/SkeletonRows.tsx b/libs/tangle-shared-ui/src/components/SkeletonRows.tsx similarity index 100% rename from apps/tangle-dapp/src/components/SkeletonRows.tsx rename to libs/tangle-shared-ui/src/components/SkeletonRows.tsx diff --git a/libs/tangle-shared-ui/src/components/blueprints/RestakeBanner.tsx b/libs/tangle-shared-ui/src/components/blueprints/RestakeBanner.tsx new file mode 100644 index 0000000000..cc1f1429e2 --- /dev/null +++ b/libs/tangle-shared-ui/src/components/blueprints/RestakeBanner.tsx @@ -0,0 +1,56 @@ +import { ArrowRight } from '@webb-tools/icons'; +import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; +import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; +import { FC } from 'react'; +import { twMerge } from 'tailwind-merge'; +import './top-banner.css'; + +export type RestakeBannerProps = { + title: string; + description: string; + buttonText: string; + buttonHref: string; +}; + +const RestakeBanner: FC = ({ + title, + description, + buttonText, + buttonHref, +}) => { + return ( +
+
+
+ + {title} + + + + {description} + +
+ + +
+
+ ); +}; + +export default RestakeBanner; diff --git a/libs/tangle-shared-ui/src/components/blueprints/TopBanner.tsx b/libs/tangle-shared-ui/src/components/blueprints/TopBanner.tsx deleted file mode 100644 index 19bf632636..0000000000 --- a/libs/tangle-shared-ui/src/components/blueprints/TopBanner.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ArrowRight } from '@webb-tools/icons'; -import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; -import { BLUEPRINT_DOCS } from '@webb-tools/webb-ui-components/constants/tangleDocs'; -import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; -import { FC } from 'react'; -import { twMerge } from 'tailwind-merge'; -import './top-banner.css'; - -const TopBanner: FC = () => { - return ( -
-
- - Create your first{' '} - Blueprint - - - Set up a minimal Tangle Blueprint in minutes accompanied by a - step-by-step guide. - - -
-
- ); -}; - -export default TopBanner; diff --git a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx index 192619ec38..b9aaec59e7 100644 --- a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx +++ b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx @@ -13,7 +13,6 @@ import RestakeContext from './RestakeContext'; const RestakeContextProvider = (props: PropsWithChildren) => { const { assetMap, assetMap$ } = useRestakeAssetMap(); - const { balances, balances$ } = useRestakeBalances(); const assetWithBalances$ = useMemo( diff --git a/libs/tangle-shared-ui/src/types/restake.ts b/libs/tangle-shared-ui/src/types/restake.ts index b5bcd6c323..d7c7acfc13 100644 --- a/libs/tangle-shared-ui/src/types/restake.ts +++ b/libs/tangle-shared-ui/src/types/restake.ts @@ -82,13 +82,13 @@ export type DelegatorWithdrawRequest = { }; export type DelegatorBondInfo = { - readonly operatorAccountId: string; + readonly operatorAccountId: SubstrateAddress; readonly amountBonded: bigint; readonly assetId: string; }; export type DelegatorUnstakeRequest = { - readonly operatorAccountId: string; + readonly operatorAccountId: SubstrateAddress; readonly assetId: string; readonly amount: bigint; readonly requestedRound: number; diff --git a/libs/webb-ui-components/src/components/Card/Card.tsx b/libs/webb-ui-components/src/components/Card/Card.tsx index 9abb03595f..94a5b92a97 100644 --- a/libs/webb-ui-components/src/components/Card/Card.tsx +++ b/libs/webb-ui-components/src/components/Card/Card.tsx @@ -6,9 +6,10 @@ import CardVariant from './CardVariant'; export type CardProps = WebbComponentBase & { variant?: CardVariant; withShadow?: boolean; + tightPadding?: boolean; }; -const getVariantClass = (variant: CardVariant) => { +const getVariantClass = (variant: CardVariant): string => { switch (variant) { case CardVariant.GLASS: return 'p-6 rounded-2xl border border-mono-0 dark:border-mono-160 bg-glass dark:bg-glass_dark'; @@ -44,6 +45,7 @@ export const Card = forwardRef( children, className, withShadow = false, + tightPadding = false, variant = CardVariant.DEFAULT, ...props }, @@ -53,10 +55,11 @@ export const Card = forwardRef(
( - ( - { - onChange, - onClose, - popularTokens, - selectTokens, - title = 'Select a Token', - type = 'token', - unavailableTokens, - value: selectedAsset, - overrideInputProps, - renderEmpty, - alertTitle, - overrideScrollAreaProps, - ...props - }, - ref, - ) => { - // Search text - const [searchText, setSearchText] = useState(''); - - const isEmpty = useMemo( - () => - !popularTokens.length && - !selectTokens.length && - !unavailableTokens.length, - [popularTokens, selectTokens, unavailableTokens], - ); - - const getFilterList = useCallback( - (list: AssetType[]) => - list.filter( - (r) => - r.name.toLowerCase().includes(searchText.toLowerCase()) || - r.symbol.toString().includes(searchText.toLowerCase()) || - r.assetBalanceProps?.balance - ?.toString() - .includes(searchText.toLowerCase()), - ), - [searchText], - ); - - const { filteredPopular, filteredSelect } = useMemo( - () => ({ - filteredPopular: getFilterList(popularTokens), - filteredSelect: getFilterList(selectTokens), - filteredUnavailable: getFilterList(unavailableTokens), - }), - [getFilterList, popularTokens, selectTokens, unavailableTokens], - ); - - return ( - - {/** The search input */} - {!isEmpty && ( -
- } - placeholder="Search pool or enter token address" - value={searchText} - onChange={(val) => setSearchText(val.toString())} - {...overrideInputProps} - /> -
- )} - - {/** Popular tokens */} - {filteredPopular.length > 0 ? ( -
- - Popular {type} - - -
- {filteredPopular.map((current, idx) => ( - onChange?.(current)} - isDropdown={false} - > - {current.symbol} - - ))} -
-
- ) : null} - - {/** Select tokens */} -
- {filteredSelect.length > 0 && ( -
- - Select {type} - - - {/** Token list */} - -
    - {filteredSelect.map((current, idx) => ( - onChange?.(current)} - /> - ))} -
-
-
- )} - - {unavailableTokens.length ? ( -
- - Unavailable {type} - - - {/** Token list */} - -
    - {unavailableTokens.map((current, idx) => ( - - ))} -
-
-
- ) : null} - - {filteredSelect.length === 0 && ( -
- {typeof renderEmpty === 'function' ? ( - renderEmpty() - ) : ( - - No {type} Found. - - )} -
- )} -
- - {/* Alert Component */} - {alertTitle && } -
- ); - }, -); diff --git a/libs/webb-ui-components/src/components/ListCard/index.ts b/libs/webb-ui-components/src/components/ListCard/index.ts index 9b878f436d..099eb460f7 100644 --- a/libs/webb-ui-components/src/components/ListCard/index.ts +++ b/libs/webb-ui-components/src/components/ListCard/index.ts @@ -3,5 +3,4 @@ export { default as ContractListCard } from './ContractListCard'; export * from './ListCardWrapper'; export * from './ListItem'; export { default as RelayerListCard } from './RelayerListCard'; -export * from './TokenListCard'; export { default as TokenListItem } from './TokenListItem'; diff --git a/libs/webb-ui-components/src/components/ListCard/types.ts b/libs/webb-ui-components/src/components/ListCard/types.ts index c3a671db88..41696bd0ac 100644 --- a/libs/webb-ui-components/src/components/ListCard/types.ts +++ b/libs/webb-ui-components/src/components/ListCard/types.ts @@ -6,15 +6,9 @@ import type { InputProps } from '../Input'; import type { Typography } from '../../typography/Typography'; export type ChainType = { - /** - * The typed chain id - */ typedChainId: number; - - /** - * The chain name - */ name: string; + isDisabled?: boolean; /** * The chain tag (use to categorize the chain) @@ -25,11 +19,6 @@ export type ChainType = { * Whether the current chain needs to switch wallet */ needSwitchWallet?: boolean; - - /** - * Whether the chain is disabled - */ - isDisabled?: boolean; }; export type RelayerType = { @@ -44,26 +33,15 @@ export type RelayerType = { */ name?: string; - /** - * External url - */ - externalUrl: string; - - /** - * Relayer percentage - */ - percentage?: number; - /** * Relayer theme (use for Identicon theme) * @default 'polkadot' */ theme?: AvatarProps['theme']; - /** - * Whether the relayer is disabled - */ + percentage?: number; isDisabled?: boolean; + externalUrl: string; }; export type AssetBalanceType = { @@ -188,14 +166,7 @@ export interface ListCardWrapperProps */ overrideTitleProps?: ComponentProps; - /** - * The callback involke when pressing the close button - */ onClose?: () => void; - - /** - * Hide Close buttonƒ - */ hideCloseButton?: boolean; } @@ -205,9 +176,6 @@ export interface ChainListCardProps extends Omit, 'onChange'> { */ chainType: 'source' | 'dest'; - /** - * The callback involke when pressing the close button - */ onClose?: () => void; /** @@ -273,9 +241,6 @@ export interface RelayerListCardProps */ isDisconnected?: boolean; - /** - * The callback involke when pressing the close button - */ onClose?: () => void; /** @@ -320,21 +285,6 @@ export interface TokenListCardProps */ type?: 'token' | 'pool' | 'asset'; - /** - * The popular token list - */ - popularTokens: AssetType[]; - - /** - * The selected token list - */ - selectTokens: AssetType[]; - - /** - * The unavailable token list - */ - unavailableTokens: AssetType[]; - /** * The current selected token, use to control the component */ @@ -351,14 +301,15 @@ export interface TokenListCardProps alertTitle?: string; /** - * Override the input props + * Function to render body when the list is empty */ - overrideInputProps?: Partial; + renderEmpty?: () => React.ReactNode; /** - * Function to render body when the list is empty + * The description to show when the list is empty. */ - renderEmpty?: () => React.ReactNode; + descriptionWhenEmpty?: string; - overrideScrollAreaProps?: ComponentProps; + selectTokens: AssetType[]; + overrideInputProps?: Partial; } diff --git a/libs/webb-ui-components/src/components/ListStatus.tsx b/libs/webb-ui-components/src/components/ListStatus.tsx new file mode 100644 index 0000000000..18cadb7289 --- /dev/null +++ b/libs/webb-ui-components/src/components/ListStatus.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react'; +import { twMerge } from 'tailwind-merge'; +import { Typography } from '../typography'; + +export type ListStatusProps = { + emoji?: string; + className?: string; + title: string; + description?: string; +}; + +export const ListStatus: FC = ({ + emoji = '🔍', + title, + description, + className, +}) => { + return ( +
+ + {emoji} + + + + {title} + + + {description !== undefined && ( + + {description} + + )} +
+ ); +}; diff --git a/libs/webb-ui-components/src/components/SideBar/SideBar.tsx b/libs/webb-ui-components/src/components/SideBar/SideBar.tsx index 5d35e8ff1a..59ebc1465d 100644 --- a/libs/webb-ui-components/src/components/SideBar/SideBar.tsx +++ b/libs/webb-ui-components/src/components/SideBar/SideBar.tsx @@ -81,8 +81,6 @@ export const SideBar = forwardRef( } }, [isSidebarOpen, onSideBarToggle, setIsSidebarOpen]); - console.log('items', items); - return (
( > {items .filter((item) => !item.isInternal) - .map((item) => ( - + .map((item, index) => ( + ( () => twMerge( cx( - 'group p-2 md:px-4 rounded-lg', - 'flex items-center gap-2 max-w-fit', + 'group px-4 py-2 rounded-full', + 'flex items-center gap-1 max-w-fit', + 'border border-mono-100 dark:border-mono-140', 'bg-mono-40 dark:bg-mono-170', - 'enabled:hover:bg-mono-20 enabled:hover:dark:bg-mono-160', + 'enabled:hover:bg-mono-60 enabled:hover:dark:bg-mono-160', 'disabled:bg-[#E2E5EB]/20 dark:disabled:bg-[#3A3E53]/70', ), className, @@ -56,36 +57,42 @@ const TokenSelector = forwardRef( const disabled = isActive || isDisabled; + const icon = + tokenType === 'shielded' ? ( + + ) : typeof children === 'string' ? ( + + ) : Icon ? ( + Icon + ) : null; + return (
- {!isMobile ? ( - - ) : ( - - )} +
); }, diff --git a/libs/webb-ui-components/src/containers/GovernanceContractDetailCard/types.ts b/libs/webb-ui-components/src/containers/GovernanceContractDetailCard/types.ts index 7067b7a1c4..25fe764ecb 100644 --- a/libs/webb-ui-components/src/containers/GovernanceContractDetailCard/types.ts +++ b/libs/webb-ui-components/src/containers/GovernanceContractDetailCard/types.ts @@ -20,7 +20,7 @@ export interface ContractDetailCardProps { governanceFncNames: string[]; /** - * All the chain options to be be chosen + * All the chain options to be chosen */ typedChainIdSelections: number[]; } diff --git a/libs/webb-ui-components/src/containers/TransferCard/TransferCard.tsx b/libs/webb-ui-components/src/containers/TransferCard/TransferCard.tsx index d26b5906d3..593305e3ce 100644 --- a/libs/webb-ui-components/src/containers/TransferCard/TransferCard.tsx +++ b/libs/webb-ui-components/src/containers/TransferCard/TransferCard.tsx @@ -12,9 +12,7 @@ import { RecipientInput, RelayerInput, TokenInput, - ConnectWalletMobileButton, } from '../../components'; -import { useCheckMobile } from '../../hooks'; import { Typography } from '../../typography'; import { TransferCardProps } from './types'; @@ -40,7 +38,6 @@ export const TransferCard = forwardRef( }, ref, ) => { - const { isMobile } = useCheckMobile(); const bridgeAssetProps = useMemo( () => ({ ...bridgeAssetInputProps, @@ -89,17 +86,13 @@ export const TransferCard = forwardRef(
- {!isMobile ? ( - - ) : ( - - )} + {buttonDesc && ( { - state: WebbUIErrorBoudaryState = { + state: WebbUIErrorBoundaryState = { hasError: false, error: null, errorInfo: null, diff --git a/libs/webb-ui-components/src/containers/WebbUIErrorBoundary/index.ts b/libs/webb-ui-components/src/containers/WebbUIErrorBoundary/index.ts new file mode 100644 index 0000000000..c51dbb9aaa --- /dev/null +++ b/libs/webb-ui-components/src/containers/WebbUIErrorBoundary/index.ts @@ -0,0 +1 @@ +export * from './WebbUIErrorBoundary'; diff --git a/libs/webb-ui-components/src/containers/WebbUIErrorBoudary/types.ts b/libs/webb-ui-components/src/containers/WebbUIErrorBoundary/types.ts similarity index 74% rename from libs/webb-ui-components/src/containers/WebbUIErrorBoudary/types.ts rename to libs/webb-ui-components/src/containers/WebbUIErrorBoundary/types.ts index 9d581dd193..d17159e5c0 100644 --- a/libs/webb-ui-components/src/containers/WebbUIErrorBoudary/types.ts +++ b/libs/webb-ui-components/src/containers/WebbUIErrorBoundary/types.ts @@ -1,13 +1,13 @@ import type LoggerService from '@webb-tools/browser-utils/logger/LoggerService'; import type { ErrorInfo, ReactNode } from 'react'; -export interface WebbUIErrorBoudaryState { +export interface WebbUIErrorBoundaryState { hasError: boolean; error?: Error | null; errorInfo?: ErrorInfo | null; } -export interface WebbUIErrorBoudaryProps { +export interface WebbUIErrorBoundaryProps { children: ReactNode; logger: LoggerService; } diff --git a/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx b/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx index 5f0ee33bce..8ebffeb552 100644 --- a/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx +++ b/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx @@ -15,9 +15,7 @@ import { RelayerInput, Switcher, TokenInput, - ConnectWalletMobileButton, } from '../../components'; -import { useCheckMobile } from '../../hooks'; import { Typography } from '../../typography'; import { WithdrawCardProps } from './types'; @@ -46,13 +44,11 @@ export const WithdrawCard = forwardRef( }, ref, ) => { - const { isMobile } = useCheckMobile(); - // Internal switcher state const [switcherChecked, setSwitcherChecked] = useState( () => unwrapSwitcherProps?.defaultChecked || unwrapSwitcherProps?.checked, ); - // Effect to reset the switcher state when props change + // Reset the switcher state when props change. useEffect(() => { let isSub = true; @@ -148,20 +144,16 @@ export const WithdrawCard = forwardRef(
- {!isMobile ? ( - - ) : ( - - )} + {buttonDesc && ( void; - /** - * The copied text - */ + copiedText: string | undefined; }; /** - * - * @param {number} display The display time to reset time copy state in miliseconds (default 3000) - * @returns An object contains `isCopied`, `copiedText` and `copy` function + * @param display The display time to reset time copy state in milliseconds (default 3000) */ export const useCopyable = (display = 3000): UseCopyableReturnType => { - // Reference to to copied string const ref = useRef(''); - - // Internal state to manage and reset the copy state const [isCopied, setIsCopied] = useState(false); - - // Timeout ref - const _timeoutRef = useRef>(); + const timeoutRef_ = useRef>(); const copy = (value: string) => { if (isCopied) { @@ -48,16 +37,16 @@ export const useCopyable = (display = 3000): UseCopyableReturnType => { const timeoutObj = setTimeout(() => setIsCopied(false), display); - if (_timeoutRef.current) { - clearTimeout(_timeoutRef.current); + if (timeoutRef_.current) { + clearTimeout(timeoutRef_.current); } - _timeoutRef.current = timeoutObj; + timeoutRef_.current = timeoutObj; }; - // Clear the timeout if the component unmount + // Clear the timeout upon unmount. useEffect(() => { - return () => clearTimeout(_timeoutRef.current); + return () => clearTimeout(timeoutRef_.current); }, []); return { diff --git a/libs/webb-ui-components/src/hooks/useDarkMode.ts b/libs/webb-ui-components/src/hooks/useDarkMode.ts index bdd388882e..436229eec9 100644 --- a/libs/webb-ui-components/src/hooks/useDarkMode.ts +++ b/libs/webb-ui-components/src/hooks/useDarkMode.ts @@ -12,9 +12,6 @@ function isBrowser(): boolean { ); } -/** - * Function to toggle or change the theme mode (possible value: `light`, `dark`, `undefined`) - */ export type ToggleThemeModeFunc = ( nextThemeMode?: SupportTheme | undefined, ) => void; diff --git a/libs/webb-ui-components/src/hooks/useCheckMobile.ts b/libs/webb-ui-components/src/hooks/useIsMobile.ts similarity index 68% rename from libs/webb-ui-components/src/hooks/useCheckMobile.ts rename to libs/webb-ui-components/src/hooks/useIsMobile.ts index ce2a605685..976acd011e 100644 --- a/libs/webb-ui-components/src/hooks/useCheckMobile.ts +++ b/libs/webb-ui-components/src/hooks/useIsMobile.ts @@ -2,24 +2,20 @@ import { useState, useEffect } from 'react'; -export type UseCheckMobileReturnType = { - isMobile: boolean; -}; - -export const useCheckMobile = (): UseCheckMobileReturnType => { +export const useIsMobile = (): boolean => { const [isMobile, setIsMobile] = useState(false); useEffect(() => { const checkIsMobile = () => { - const isMobileCheck = + const isMobileUserAgent = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent, ); - setIsMobile(isMobileCheck); + + setIsMobile(isMobileUserAgent); }; checkIsMobile(); - window.addEventListener('resize', checkIsMobile); return () => { @@ -27,5 +23,5 @@ export const useCheckMobile = (): UseCheckMobileReturnType => { }; }, []); - return { isMobile }; + return isMobile; }; diff --git a/libs/webb-ui-components/src/provider/index.tsx b/libs/webb-ui-components/src/provider/index.tsx index c717c47f26..59fc3bef65 100644 --- a/libs/webb-ui-components/src/provider/index.tsx +++ b/libs/webb-ui-components/src/provider/index.tsx @@ -7,7 +7,7 @@ import { notificationApi, } from '../components/Notification'; -import { WebbUIErrorBoudary } from '../containers/WebbUIErrorBoudary'; +import { WebbUIErrorBoundary } from '../containers/WebbUIErrorBoundary'; import { useNextDarkMode, useDarkMode as useNormalDarkMode, @@ -20,7 +20,7 @@ const appLogger = LoggerService.new('Stats App'); export const WebbUIProvider: React.FC = ({ children, defaultThemeMode = 'dark', - hasErrorBoudary, + hasErrorBoundary, notificationOptions, isNextApp = false, }) => { @@ -54,7 +54,7 @@ export const WebbUIProvider: React.FC = ({ ); const WebbUIEErrorBoundaryElement = useMemo(() => { - return createElement(WebbUIErrorBoudary, { + return createElement(WebbUIErrorBoundary, { logger: appLogger, children, }); @@ -71,7 +71,7 @@ export const WebbUIProvider: React.FC = ({ }} > - {hasErrorBoudary ? WebbUIEErrorBoundaryElement : children} + {hasErrorBoundary ? WebbUIEErrorBoundaryElement : children} ); diff --git a/libs/webb-ui-components/src/provider/types.ts b/libs/webb-ui-components/src/provider/types.ts index fa61924f1b..051ae36593 100644 --- a/libs/webb-ui-components/src/provider/types.ts +++ b/libs/webb-ui-components/src/provider/types.ts @@ -21,17 +21,13 @@ export type WebbUIProviderProps = { /** * If `true`, the `WebbUIProvider` will provide a error handler for inside components */ - hasErrorBoudary?: boolean; + hasErrorBoundary?: boolean; /** - * Default theme mode * @default "dark" */ defaultThemeMode?: 'dark' | 'light'; - /** - * Notification options - */ notificationOptions?: Omit< ComponentProps, 'children' @@ -40,7 +36,7 @@ export type WebbUIProviderProps = { children?: React.ReactNode; /** - * Check if the provider is used in a Next.js app or not + * Check if the provider is used in a Next.js app or not. * @default false */ isNextApp?: boolean;