From 50fd39e926a5ad83f7b1d2582f7c589f2460a7c0 Mon Sep 17 00:00:00 2001 From: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:39:14 +0200 Subject: [PATCH] Mp 3412 (#487) * first iteration * finish implementation * finish * fix pr comments * fix: added Card Title to Overview --------- Co-authored-by: Linkie Link --- src/api/wallets/getAccountIds.ts | 3 +- src/api/wallets/getAccounts.ts | 5 +- src/components/Account/AccountComposition.tsx | 19 +-- .../AccountDetails/AccountDetailsLeverage.tsx | 14 +- .../Account/AccountDetails/index.tsx | 15 +- .../AccountFund/AccountFundFullPage.tsx | 4 +- src/components/Account/AccountMenuContent.tsx | 11 +- src/components/Account/AccountOverview.tsx | 148 ------------------ src/components/Account/HealthBar.tsx | 2 +- src/components/Button/ActionButton.tsx | 14 +- src/components/Earn/Farm/VaultExpanded.tsx | 4 +- .../Earn/Farm/VaultUnlockBanner.tsx | 4 +- src/components/Earn/Farm/Vaults.tsx | 4 +- .../Earn/Lend/LendingActionButtons.tsx | 4 +- .../Modals/Unlock/UnlockModalContent.tsx | 5 +- .../Modals/WithdrawFromVaultsModal.tsx | 5 +- .../Navigation/DesktopNavigation.tsx | 5 +- src/components/Portfolio/Account/Balances.tsx | 63 ++++++++ .../Portfolio/Account/BreadCrumbs.tsx | 26 +++ src/components/Portfolio/Account/Summary.tsx | 137 ++++++++++++++++ src/components/Portfolio/Card/Skeleton.tsx | 37 +++++ src/components/Portfolio/Card/index.tsx | 105 +++++++++++++ .../Portfolio/Overview/ConnectInfo.tsx | 18 +++ src/components/Portfolio/Overview/index.tsx | 78 +++++++++ src/components/Routes.tsx | 17 +- src/components/TableSkeleton.tsx | 77 +++++++++ src/components/TitleAndSubCell.tsx | 2 +- .../Wallet/WalletFetchBalancesAndAccounts.tsx | 14 +- src/hooks/useAccount.tsx | 4 +- src/hooks/useAccountId.tsx | 6 + src/hooks/useAccountIds.tsx | 11 ++ src/hooks/useAccounts.tsx | 6 +- src/hooks/useBorrowMarketAssetsTableData.ts | 9 +- src/hooks/useCurrentAccount.tsx | 6 +- src/hooks/useLendingMarketAssetsTableData.ts | 7 +- src/pages/PortfolioAccountPage.tsx | 27 ++++ src/pages/PortfolioPage.tsx | 2 +- src/types/interfaces/route.d.ts | 2 +- src/utils/route.ts | 42 +++-- 39 files changed, 712 insertions(+), 250 deletions(-) delete mode 100644 src/components/Account/AccountOverview.tsx create mode 100644 src/components/Portfolio/Account/Balances.tsx create mode 100644 src/components/Portfolio/Account/BreadCrumbs.tsx create mode 100644 src/components/Portfolio/Account/Summary.tsx create mode 100644 src/components/Portfolio/Card/Skeleton.tsx create mode 100644 src/components/Portfolio/Card/index.tsx create mode 100644 src/components/Portfolio/Overview/ConnectInfo.tsx create mode 100644 src/components/Portfolio/Overview/index.tsx create mode 100644 src/components/TableSkeleton.tsx create mode 100644 src/hooks/useAccountId.tsx create mode 100644 src/hooks/useAccountIds.tsx create mode 100644 src/pages/PortfolioAccountPage.tsx diff --git a/src/api/wallets/getAccountIds.ts b/src/api/wallets/getAccountIds.ts index d29c6bce..df540f18 100644 --- a/src/api/wallets/getAccountIds.ts +++ b/src/api/wallets/getAccountIds.ts @@ -1,6 +1,7 @@ import { getAccountNftQueryClient } from 'api/cosmwasm-client' -export default async function getAccountIds(address: string): Promise { +export default async function getAccountIds(address?: string): Promise { + if (!address) return [] const accountNftQueryClient = await getAccountNftQueryClient() const data = await accountNftQueryClient.tokens({ owner: address }) diff --git a/src/api/wallets/getAccounts.ts b/src/api/wallets/getAccounts.ts index ab20f299..3e5e89f4 100644 --- a/src/api/wallets/getAccounts.ts +++ b/src/api/wallets/getAccounts.ts @@ -1,7 +1,8 @@ -import getWalletAccountIds from 'api/wallets/getAccountIds' import getAccount from 'api/accounts/getAccount' +import getWalletAccountIds from 'api/wallets/getAccountIds' -export default async function getAccounts(address: string): Promise { +export default async function getAccounts(address?: string): Promise { + if (!address) return [] const accountIds: string[] = await getWalletAccountIds(address) const $accounts = accountIds.map((accountId) => getAccount(accountId)) diff --git a/src/components/Account/AccountComposition.tsx b/src/components/Account/AccountComposition.tsx index 8d4ba05d..3c4b3c61 100644 --- a/src/components/Account/AccountComposition.tsx +++ b/src/components/Account/AccountComposition.tsx @@ -54,7 +54,7 @@ export default function AccountComposition(props: Props) { () => getAccountPositionValues(account, prices), [account, prices], ) - const positionValue = depositsBalance.plus(lendsBalance).plus(vaultsBalance) + const totalBalance = depositsBalance.plus(lendsBalance).plus(vaultsBalance) const [updatedPositionValue, updatedDebtsBalance] = useMemo(() => { const [updatedDepositsBalance, updatedLendsBalance, updatedDebtsBalance, updatedVaultsBalance] = @@ -69,10 +69,7 @@ export default function AccountComposition(props: Props) { return [updatedPositionValue, updatedDebtsBalance] }, [updatedAccount, prices]) - const totalBalance = useMemo( - () => calculateAccountBalanceValue(account, prices), - [account, prices], - ) + const netWorth = useMemo(() => calculateAccountBalanceValue(account, prices), [account, prices]) const updatedTotalBalance = useMemo( () => (updatedAccount ? calculateAccountBalanceValue(updatedAccount, prices) : BN_ZERO), [updatedAccount, prices], @@ -93,9 +90,9 @@ export default function AccountComposition(props: Props) { return (
leverage && 'text-loss', + updatedLeverage < leverage && 'text-profit', )} - amount={isNaN(updatedLeverage.toNumber()) ? 0 : updatedLeverage.toNumber()} + amount={isNaN(updatedLeverage) ? 0 : updatedLeverage} options={{ maxDecimals: 1, minDecimals: 1, rounded: true }} animate /> diff --git a/src/components/Account/AccountDetails/index.tsx b/src/components/Account/AccountDetails/index.tsx index 84b640ff..68a88c7a 100644 --- a/src/components/Account/AccountDetails/index.tsx +++ b/src/components/Account/AccountDetails/index.tsx @@ -106,18 +106,21 @@ function AccountDetails(props: Props) { Account Health
-
- - Leverage - - -
Net worth
+
+ + Leverage + + +
APR diff --git a/src/components/Account/AccountFund/AccountFundFullPage.tsx b/src/components/Account/AccountFund/AccountFundFullPage.tsx index 312e56d8..175f2552 100644 --- a/src/components/Account/AccountFund/AccountFundFullPage.tsx +++ b/src/components/Account/AccountFund/AccountFundFullPage.tsx @@ -1,17 +1,17 @@ import { useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' import AccountFundContent from 'components/Account/AccountFund/AccountFundContent' import Card from 'components/Card' import { CircularProgress } from 'components/CircularProgress' import FullOverlayContent from 'components/FullOverlayContent' +import useAccountId from 'hooks/useAccountId' import useAccounts from 'hooks/useAccounts' import useCurrentAccount from 'hooks/useCurrentAccount' import useStore from 'store' export default function AccountFundFullPage() { const address = useStore((s) => s.address) - const { accountId } = useParams() + const accountId = useAccountId() const { data: accounts, isLoading } = useAccounts(address) const currentAccount = useCurrentAccount() diff --git a/src/components/Account/AccountMenuContent.tsx b/src/components/Account/AccountMenuContent.tsx index 1c2659ce..56a8454b 100644 --- a/src/components/Account/AccountMenuContent.tsx +++ b/src/components/Account/AccountMenuContent.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { useCallback, useEffect } from 'react' +import { useCallback } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' import AccountCreateFirst from 'components/Account/AccountCreateFirst' @@ -13,6 +13,7 @@ import Text from 'components/Text' import WalletBridges from 'components/Wallet/WalletBridges' import { DEFAULT_SETTINGS } from 'constants/defaultSettings' import { LEND_ASSETS_KEY } from 'constants/localStore' +import useAccountId from 'hooks/useAccountId' import useAutoLend from 'hooks/useAutoLend' import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance' import useLocalStorage from 'hooks/useLocalStorage' @@ -33,7 +34,9 @@ interface Props { export default function AccountMenuContent(props: Props) { const navigate = useNavigate() const { pathname } = useLocation() - const { accountId, address } = useParams() + const { address } = useParams() + const accountId = useAccountId() + const createAccount = useStore((s) => s.createAccount) const baseCurrency = useStore((s) => s.baseCurrency) const [showMenu, setShowMenu] = useToggle() @@ -97,10 +100,6 @@ export default function AccountMenuContent(props: Props) { } }, [checkHasFunds, hasCreditAccounts, setShowMenu, showMenu]) - useEffect(() => { - useStore.setState({ accounts: props.accounts }) - }, [props.accounts]) - if (!address) return null return ( diff --git a/src/components/Account/AccountOverview.tsx b/src/components/Account/AccountOverview.tsx deleted file mode 100644 index c5a20eb6..00000000 --- a/src/components/Account/AccountOverview.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import classNames from 'classnames' -import { Suspense, useCallback, useMemo } from 'react' -import { useParams } from 'react-router-dom' - -import AccountBalancesTable from 'components/Account/AccountBalancesTable' -import AccountComposition from 'components/Account/AccountComposition' -import AccountCreateFirst from 'components/Account/AccountCreateFirst' -import Button from 'components/Button' -import Card from 'components/Card' -import { PlusCircled } from 'components/Icons' -import Loading from 'components/Loading' -import Text from 'components/Text' -import WalletBridges from 'components/Wallet/WalletBridges' -import WalletConnectButton from 'components/Wallet/WalletConnectButton' -import useAccounts from 'hooks/useAccounts' -import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData' -import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance' -import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData' -import useStore from 'store' -import { defaultFee } from 'utils/constants' -import { BN } from 'utils/helpers' - -function ConnectInfo() { - return ( - - - You need to be connected to view the porfolio page. - - - - ) -} - -function Content() { - const { address: urlAddress } = useParams() - const { data: accounts, isLoading } = useAccounts(urlAddress ?? '') - const walletAddress = useStore((s) => s.address) - const baseCurrency = useStore((s) => s.baseCurrency) - const { availableAssets: borrowAvailableAssets, accountBorrowedAssets } = - useBorrowMarketAssetsTableData() - const { availableAssets: lendingAvailableAssets, accountLentAssets } = - useLendingMarketAssetsTableData() - const borrowAssetsData = useMemo( - () => [...borrowAvailableAssets, ...accountBorrowedAssets], - [borrowAvailableAssets, accountBorrowedAssets], - ) - const lendingAssetsData = useMemo( - () => [...lendingAvailableAssets, ...accountLentAssets], - [lendingAvailableAssets, accountLentAssets], - ) - const transactionFeeCoinBalance = useCurrentWalletBalance(baseCurrency.denom) - - const checkHasFunds = useCallback(() => { - return ( - transactionFeeCoinBalance && - BN(transactionFeeCoinBalance.amount).isGreaterThan(defaultFee.amount[0].amount) - ) - }, [transactionFeeCoinBalance]) - - const handleCreateAccountClick = useCallback(() => { - if (!checkHasFunds()) { - useStore.setState({ focusComponent: { component: } }) - return - } - - useStore.setState({ focusComponent: { component: } }) - }, [checkHasFunds]) - - if (isLoading) return - - if (!walletAddress && !urlAddress) return - - if (!accounts || accounts.length === 0) - return ( - - - You need to create an Account first. - - - - ) - - return ( -
- {accounts.map((account: Account, index: number) => ( - - - Balances - - - ))} -
- ) -} - -function Fallback() { - const { address } = useParams() - const cardCount = 3 - if (!address) return - return ( -
- {Array.from({ length: cardCount }, (_, i) => ( - -
- -
- Balances - -
- ))} -
- ) -} - -export default function AccountOverview() { - return ( - }> - - - ) -} diff --git a/src/components/Account/HealthBar.tsx b/src/components/Account/HealthBar.tsx index 796f304f..b764f54a 100644 --- a/src/components/Account/HealthBar.tsx +++ b/src/components/Account/HealthBar.tsx @@ -48,7 +48,7 @@ export default function HealthBar(props: Props) { type='info' className='flex items-center w-full' > -
+
diff --git a/src/components/Button/ActionButton.tsx b/src/components/Button/ActionButton.tsx index 34a63e3d..d1ce6e2a 100644 --- a/src/components/Button/ActionButton.tsx +++ b/src/components/Button/ActionButton.tsx @@ -1,19 +1,21 @@ import { useCallback } from 'react' -import { useParams } from 'react-router-dom' import AccountCreateFirst from 'components/Account/AccountCreateFirst' import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent' import Button from 'components/Button' import { Account, PlusCircled } from 'components/Icons' import WalletConnectButton from 'components/Wallet/WalletConnectButton' +import useAccountId from 'hooks/useAccountId' +import useAccountIds from 'hooks/useAccountIds' import useStore from 'store' export default function ActionButton(props: ButtonProps) { const { className, color, variant, size } = props const defaultProps = { className, color, variant, size } const address = useStore((s) => s.address) - const accounts = useStore((s) => s.accounts) - const { accountId } = useParams() + + const { data: accountIds } = useAccountIds(address || '') + const selectedAccountId = useAccountId() const handleCreateAccountClick = useCallback(() => { useStore.setState({ focusComponent: { component: } }) @@ -21,7 +23,7 @@ export default function ActionButton(props: ButtonProps) { if (!address) return - if (accounts && accounts.length === 0) + if (accountIds.length === 0) { return ( ) + } - if (!accountId) + if (!selectedAccountId) { return ( ) + } return } diff --git a/src/components/Earn/Farm/VaultExpanded.tsx b/src/components/Earn/Farm/VaultExpanded.tsx index 1c84424c..5c4d7532 100644 --- a/src/components/Earn/Farm/VaultExpanded.tsx +++ b/src/components/Earn/Farm/VaultExpanded.tsx @@ -1,13 +1,13 @@ import { Row } from '@tanstack/react-table' import moment from 'moment' import { useState } from 'react' -import { useParams } from 'react-router-dom' import Button from 'components/Button' import { AccountArrowDown, LockLocked, LockUnlocked, Plus } from 'components/Icons' import { Tooltip } from 'components/Tooltip' import { DEFAULT_SETTINGS } from 'constants/defaultSettings' import { SLIPPAGE_KEY } from 'constants/localStore' +import useAccountId from 'hooks/useAccountId' import useLocalStorage from 'hooks/useLocalStorage' import useStore from 'store' import { VaultStatus } from 'types/enums/vault' @@ -19,7 +19,7 @@ interface Props { export default function VaultExpanded(props: Props) { const vault = props.row.original as DepositedVault - const { accountId } = useParams() + const accountId = useAccountId() const [isConfirming, setIsConfirming] = useState(false) const withdrawFromVaults = useStore((s) => s.withdrawFromVaults) const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage) diff --git a/src/components/Earn/Farm/VaultUnlockBanner.tsx b/src/components/Earn/Farm/VaultUnlockBanner.tsx index a183da98..84fbcaea 100644 --- a/src/components/Earn/Farm/VaultUnlockBanner.tsx +++ b/src/components/Earn/Farm/VaultUnlockBanner.tsx @@ -1,11 +1,11 @@ import { useState } from 'react' -import { useParams } from 'react-router-dom' import Button from 'components/Button' import { ChevronRight } from 'components/Icons' import NotificationBanner from 'components/NotificationBanner' import { DEFAULT_SETTINGS } from 'constants/defaultSettings' import { SLIPPAGE_KEY } from 'constants/localStore' +import useAccountId from 'hooks/useAccountId' import useLocalStorage from 'hooks/useLocalStorage' import useStore from 'store' @@ -14,7 +14,7 @@ interface Props { } export default function VaultUnlockBanner(props: Props) { - const { accountId } = useParams() + const accountId = useAccountId() const [isConfirming, setIsConfirming] = useState(false) const withdrawFromVaults = useStore((s) => s.withdrawFromVaults) const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage) diff --git a/src/components/Earn/Farm/Vaults.tsx b/src/components/Earn/Farm/Vaults.tsx index 3772572b..7725dae2 100644 --- a/src/components/Earn/Farm/Vaults.tsx +++ b/src/components/Earn/Farm/Vaults.tsx @@ -1,5 +1,4 @@ import { Suspense, useMemo } from 'react' -import { useParams } from 'react-router-dom' import Card from 'components/Card' import { VaultTable } from 'components/Earn/Farm/VaultTable' @@ -7,6 +6,7 @@ import VaultUnlockBanner from 'components/Earn/Farm/VaultUnlockBanner' import { IS_TESTNET } from 'constants/env' import { BN_ZERO } from 'constants/math' import { TESTNET_VAULTS_META_DATA, VAULTS_META_DATA } from 'constants/vaults' +import useAccountId from 'hooks/useAccountId' import useDepositedVaults from 'hooks/useDepositedVaults' import useVaults from 'hooks/useVaults' import { VaultStatus } from 'types/enums/vault' @@ -16,7 +16,7 @@ interface Props { } function Content(props: Props) { - const { accountId } = useParams() + const accountId = useAccountId() const { data: vaults } = useVaults() const { data: depositedVaults } = useDepositedVaults(accountId || '') const isAvailable = props.type === 'available' diff --git a/src/components/Earn/Lend/LendingActionButtons.tsx b/src/components/Earn/Lend/LendingActionButtons.tsx index 46d32e29..56988b0e 100644 --- a/src/components/Earn/Lend/LendingActionButtons.tsx +++ b/src/components/Earn/Lend/LendingActionButtons.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react' -import { useParams } from 'react-router-dom' import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent' import Button from 'components/Button' @@ -8,6 +7,7 @@ import { ArrowDownLine, ArrowUpLine, Enter } from 'components/Icons' import Text from 'components/Text' import { Tooltip } from 'components/Tooltip' import ConditionalWrapper from 'hocs/ConditionalWrapper' +import useAccountId from 'hooks/useAccountId' import useAlertDialog from 'hooks/useAlertDialog' import useAutoLend from 'hooks/useAutoLend' import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits' @@ -30,7 +30,7 @@ export default function LendingActionButtons(props: Props) { const { isAutoLendEnabledForCurrentAccount } = useAutoLend() const assetDepositAmount = accountDeposits.find(byDenom(asset.denom))?.amount const address = useStore((s) => s.address) - const { accountId } = useParams() + const accountId = useAccountId() const hasNoDeposit = !!(!assetDepositAmount && address && accountId) const handleWithdraw = useCallback(() => { diff --git a/src/components/Modals/Unlock/UnlockModalContent.tsx b/src/components/Modals/Unlock/UnlockModalContent.tsx index 0f89ba44..a42205b4 100644 --- a/src/components/Modals/Unlock/UnlockModalContent.tsx +++ b/src/components/Modals/Unlock/UnlockModalContent.tsx @@ -1,8 +1,7 @@ -import { useParams } from 'react-router-dom' - import Button from 'components/Button' import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons' import Text from 'components/Text' +import useAccountId from 'hooks/useAccountId' import useStore from 'store' interface Props { @@ -12,7 +11,7 @@ interface Props { export default function UnlockModalContent(props: Props) { const unlock = useStore((s) => s.unlock) - const { accountId } = useParams() + const accountId = useAccountId() function onConfirm() { if (!accountId) return diff --git a/src/components/Modals/WithdrawFromVaultsModal.tsx b/src/components/Modals/WithdrawFromVaultsModal.tsx index d81b69bd..a9eb4436 100644 --- a/src/components/Modals/WithdrawFromVaultsModal.tsx +++ b/src/components/Modals/WithdrawFromVaultsModal.tsx @@ -1,5 +1,3 @@ -import { useParams } from 'react-router-dom' - import Button from 'components/Button' import { CircularProgress } from 'components/CircularProgress' import DisplayCurrency from 'components/DisplayCurrency' @@ -9,6 +7,7 @@ import Modal from 'components/Modal' import Text from 'components/Text' import { DEFAULT_SETTINGS } from 'constants/defaultSettings' import { SLIPPAGE_KEY } from 'constants/localStore' +import useAccountId from 'hooks/useAccountId' import useLocalStorage from 'hooks/useLocalStorage' import useStore from 'store' import { BNCoin } from 'types/classes/BNCoin' @@ -17,7 +16,7 @@ import { demagnify } from 'utils/formatters' export default function WithdrawFromVaultsModal() { const modal = useStore((s) => s.withdrawFromVaultsModal) - const { accountId } = useParams() + const accountId = useAccountId() const withdrawFromVaults = useStore((s) => s.withdrawFromVaults) const baseCurrency = useStore((s) => s.baseCurrency) const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage) diff --git a/src/components/Navigation/DesktopNavigation.tsx b/src/components/Navigation/DesktopNavigation.tsx index 18bb58f1..596164f9 100644 --- a/src/components/Navigation/DesktopNavigation.tsx +++ b/src/components/Navigation/DesktopNavigation.tsx @@ -4,11 +4,14 @@ import { useParams } from 'react-router-dom' import { menuTree } from 'components/Header/DesktopHeader' import { Logo } from 'components/Icons' import { NavLink } from 'components/Navigation/NavLink' +import useAccountId from 'hooks/useAccountId' import useStore from 'store' import { getRoute } from 'utils/route' export default function DesktopNavigation() { - const { address, accountId } = useParams() + const { address } = useParams() + const accountId = useAccountId() + const focusComponent = useStore((s) => s.focusComponent) function getIsActive(pages: string[]) { diff --git a/src/components/Portfolio/Account/Balances.tsx b/src/components/Portfolio/Account/Balances.tsx new file mode 100644 index 00000000..8bf7a990 --- /dev/null +++ b/src/components/Portfolio/Account/Balances.tsx @@ -0,0 +1,63 @@ +import React, { Suspense } from 'react' + +import AccountBalancesTable from 'components/Account/AccountBalancesTable' +import Card from 'components/Card' +import TableSkeleton from 'components/TableSkeleton' +import Text from 'components/Text' +import useAccount from 'hooks/useAccount' +import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData' +import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData' + +interface Props { + accountId: string +} + +function Content(props: Props) { + const { data: account } = useAccount(props.accountId, true) + + const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData() + const { allAssets: lendingAssets } = useLendingMarketAssetsTableData() + + if (!account || !borrowAssets.length || !lendingAssets.length) { + return + } + + return ( + + + + ) +} + +export default function Balances(props: Props) { + return ( + }> + + + ) +} + +interface SkeletonProps { + children?: React.ReactNode +} + +function Skeleton(props: SkeletonProps) { + return ( +
+ + Balances + + + {props.children ? ( + props.children + ) : ( + + )} + +
+ ) +} diff --git a/src/components/Portfolio/Account/BreadCrumbs.tsx b/src/components/Portfolio/Account/BreadCrumbs.tsx new file mode 100644 index 00000000..5f49f56c --- /dev/null +++ b/src/components/Portfolio/Account/BreadCrumbs.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { NavLink, useParams } from 'react-router-dom' + +import { ArrowRight } from 'components/Icons' +import Text from 'components/Text' +import useAccountId from 'hooks/useAccountId' +import { getRoute } from 'utils/route' + +interface Props { + accountId: string +} + +export default function PortfolioAccountPageHeader(props: Props) { + const { address } = useParams() + const selectedAccountId = useAccountId() + + return ( +
+ + Portfolio + + + Credit Account {props.accountId} +
+ ) +} diff --git a/src/components/Portfolio/Account/Summary.tsx b/src/components/Portfolio/Account/Summary.tsx new file mode 100644 index 00000000..bed2f243 --- /dev/null +++ b/src/components/Portfolio/Account/Summary.tsx @@ -0,0 +1,137 @@ +import React, { Suspense, useMemo } from 'react' + +import HealthBar from 'components/Account/HealthBar' +import Card from 'components/Card' +import DisplayCurrency from 'components/DisplayCurrency' +import { FormattedNumber } from 'components/FormattedNumber' +import { Heart } from 'components/Icons' +import Loading from 'components/Loading' +import Text from 'components/Text' +import TitleAndSubCell from 'components/TitleAndSubCell' +import { ORACLE_DENOM } from 'constants/oracle' +import useAccount from 'hooks/useAccount' +import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData' +import useHealthComputer from 'hooks/useHealthComputer' +import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData' +import usePrices from 'hooks/usePrices' +import { BNCoin } from 'types/classes/BNCoin' +import { + calculateAccountApr, + calculateAccountLeverage, + getAccountPositionValues, +} from 'utils/accounts' + +interface Props { + accountId: string +} + +function Content(props: Props) { + const { data: account } = useAccount(props.accountId, true) + const { data: prices } = usePrices() + const { health } = useHealthComputer(account) + const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData() + const { allAssets: lendingAssets } = useLendingMarketAssetsTableData() + + const stats = useMemo(() => { + if (!account || !borrowAssets.length || !lendingAssets.length) return STATS + + const [deposits, lends, debts, vaults] = getAccountPositionValues(account, prices) + const positionValue = deposits.plus(lends).plus(vaults) + const apr = calculateAccountApr(account, borrowAssets, lendingAssets, prices) + const leverage = calculateAccountLeverage(account, prices) + + return [ + { + title: ( + + ), + sub: STATS[0].sub, + }, + { + title: ( + + ), + sub: STATS[1].sub, + }, + { + title: ( + + ), + sub: STATS[2].sub, + }, + { + title: ( + + ), + sub: STATS[3].sub, + }, + { + title: ( + + ), + sub: STATS[4].sub, + }, + ] + }, [account, borrowAssets, lendingAssets, prices]) + + return +} + +export default function Summary(props: Props) { + return ( + }> + + + ) +} + +interface SkeletonProps extends Props { + stats: { title: React.ReactNode | null; sub: string }[] + health: number +} + +function Skeleton(props: SkeletonProps) { + return ( +
+
+ Credit Account {props.accountId} +
+ + +
+
+
+ {props.stats.map((stat) => ( + + } + sub={stat.sub} + className='mb-1' + /> + + ))} +
+
+ ) +} + +const STATS = [ + { title: null, sub: 'Total Balance' }, + { title: null, sub: 'Total Debt' }, + { title: null, sub: 'Net Worth' }, + { title: null, sub: 'APR' }, + { title: null, sub: 'Account Leverage' }, +] diff --git a/src/components/Portfolio/Card/Skeleton.tsx b/src/components/Portfolio/Card/Skeleton.tsx new file mode 100644 index 00000000..8e7ec35e --- /dev/null +++ b/src/components/Portfolio/Card/Skeleton.tsx @@ -0,0 +1,37 @@ +import React from 'react' + +import HealthBar from 'components/Account/HealthBar' +import Card from 'components/Card' +import { Heart } from 'components/Icons' +import Loading from 'components/Loading' +import Text from 'components/Text' +import TitleAndSubCell from 'components/TitleAndSubCell' + +interface Props { + stats: { title: React.ReactNode; sub: string }[] + health: number + isCurrent?: boolean + accountId?: string +} + +export default function Skeleton(props: Props) { + return ( + +
+ Credit account {props.accountId || } + + {props.isCurrent && '(current)'} + +
+
+ {props.stats.map(({ title, sub }) => ( + + ))} +
+
+ + +
+
+ ) +} diff --git a/src/components/Portfolio/Card/index.tsx b/src/components/Portfolio/Card/index.tsx new file mode 100644 index 00000000..54defd5e --- /dev/null +++ b/src/components/Portfolio/Card/index.tsx @@ -0,0 +1,105 @@ +import classNames from 'classnames' +import React, { ReactNode, useMemo } from 'react' +import { NavLink } from 'react-router-dom' + +import { FormattedNumber } from 'components/FormattedNumber' +import Loading from 'components/Loading' +import Skeleton from 'components/Portfolio/Card/Skeleton' +import { DEFAULT_SETTINGS } from 'constants/defaultSettings' +import { REDUCE_MOTION_KEY } from 'constants/localStore' +import { BN_ZERO } from 'constants/math' +import useAccount from 'hooks/useAccount' +import useAccountId from 'hooks/useAccountId' +import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData' +import useHealthComputer from 'hooks/useHealthComputer' +import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData' +import useLocalStorage from 'hooks/useLocalStorage' +import usePrices from 'hooks/usePrices' +import useStore from 'store' +import { + calculateAccountApr, + calculateAccountLeverage, + getAccountPositionValues, +} from 'utils/accounts' +import { getRoute } from 'utils/route' + +interface Props { + accountId: string +} + +export default function PortfolioCard(props: Props) { + const { data: account } = useAccount(props.accountId) + const { health } = useHealthComputer(account) + const { data: prices } = usePrices() + const currentAccountId = useAccountId() + const { allAssets: lendingAssets } = useLendingMarketAssetsTableData() + const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData() + const [reduceMotion] = useLocalStorage(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion) + const address = useStore((s) => s.address) + + const [deposits, lends, debts, vaults] = useMemo(() => { + if (!prices.length || !account) return Array(4).fill(BN_ZERO) + return getAccountPositionValues(account, prices) + }, [prices, account]) + + const leverage = useMemo(() => { + if (!prices.length || !account) return BN_ZERO + return calculateAccountLeverage(account, prices) + }, [account, prices]) + + const apr = useMemo(() => { + if (!lendingAssets.length || !borrowAssets.length || !prices.length || !account) return null + return calculateAccountApr(account, borrowAssets, lendingAssets, prices) + }, [lendingAssets, borrowAssets, prices, account]) + + const stats: { title: ReactNode; sub: string }[] = useMemo(() => { + const isLoaded = account && prices.length && apr !== null + return [ + { + title: isLoaded ? ( + + ) : ( + + ), + sub: 'Net worth', + }, + { + title: isLoaded ? ( + + ) : ( + + ), + sub: 'Leverage', + }, + { + title: isLoaded ? ( + + ) : ( + + ), + sub: 'APR', + }, + ] + }, [account, prices.length, deposits, lends, vaults, debts, leverage, apr]) + + if (!account) { + return + } + + return ( + + + + ) +} diff --git a/src/components/Portfolio/Overview/ConnectInfo.tsx b/src/components/Portfolio/Overview/ConnectInfo.tsx new file mode 100644 index 00000000..c555dce3 --- /dev/null +++ b/src/components/Portfolio/Overview/ConnectInfo.tsx @@ -0,0 +1,18 @@ +import Card from 'components/Card' +import Text from 'components/Text' +import WalletConnectButton from 'components/Wallet/WalletConnectButton' + +export default function ConnectInfo() { + return ( + + + You need to be connected to view the portfolio page. + + + + ) +} diff --git a/src/components/Portfolio/Overview/index.tsx b/src/components/Portfolio/Overview/index.tsx new file mode 100644 index 00000000..5256bed1 --- /dev/null +++ b/src/components/Portfolio/Overview/index.tsx @@ -0,0 +1,78 @@ +import classNames from 'classnames' +import { useCallback } from 'react' +import { useParams } from 'react-router-dom' + +import AccountCreateFirst from 'components/Account/AccountCreateFirst' +import Button from 'components/Button' +import Card from 'components/Card' +import { PlusCircled } from 'components/Icons' +import PortfolioCard from 'components/Portfolio/Card' +import ConnectInfo from 'components/Portfolio/Overview/ConnectInfo' +import Text from 'components/Text' +import WalletBridges from 'components/Wallet/WalletBridges' +import useAccountIds from 'hooks/useAccountIds' +import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance' +import useStore from 'store' +import { defaultFee } from 'utils/constants' +import { BN } from 'utils/helpers' + +export default function Content() { + const { address: urlAddress } = useParams() + const walletAddress = useStore((s) => s.address) + const { data: accountIds, isLoading } = useAccountIds(urlAddress) + + const baseCurrency = useStore((s) => s.baseCurrency) + const transactionFeeCoinBalance = useCurrentWalletBalance(baseCurrency.denom) + + const checkHasFunds = useCallback(() => { + return ( + transactionFeeCoinBalance && + BN(transactionFeeCoinBalance.amount).isGreaterThan(defaultFee.amount[0].amount) + ) + }, [transactionFeeCoinBalance]) + + const handleCreateAccountClick = useCallback(() => { + if (!checkHasFunds()) { + useStore.setState({ focusComponent: { component: } }) + return + } + + useStore.setState({ focusComponent: { component: } }) + }, [checkHasFunds]) + + if (!walletAddress && !urlAddress) return + + if (!isLoading && accountIds?.length === 0) { + return ( + + + You need to create an Account first. + + + + ) + } + + return ( + +
+ {accountIds.map((accountId: string, index: number) => { + return + })} +
+
+ ) +} diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx index 546e5138..e4e0b096 100644 --- a/src/components/Routes.tsx +++ b/src/components/Routes.tsx @@ -1,12 +1,13 @@ -import { Outlet, Route, Routes as RoutesWrapper } from 'react-router-dom' +import { Navigate, Outlet, Route, Routes as RoutesWrapper } from 'react-router-dom' +import Layout from 'pages/_layout' import BorrowPage from 'pages/BorrowPage' import FarmPage from 'pages/FarmPage' import LendPage from 'pages/LendPage' import MobilePage from 'pages/MobilePage' +import PortfolioAccountPage from 'pages/PortfolioAccountPage' import PortfolioPage from 'pages/PortfolioPage' import TradePage from 'pages/TradePage' -import Layout from 'pages/_layout' export default function Routes() { return ( @@ -26,21 +27,17 @@ export default function Routes() { } /> } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> } /> } /> } /> } /> + + } /> + } /> + } /> ) diff --git a/src/components/TableSkeleton.tsx b/src/components/TableSkeleton.tsx new file mode 100644 index 00000000..55a83b76 --- /dev/null +++ b/src/components/TableSkeleton.tsx @@ -0,0 +1,77 @@ +import classNames from 'classnames' +import React from 'react' + +import { SortNone } from 'components/Icons' +import Loading from 'components/Loading' +import Text from 'components/Text' + +interface Props { + labels: string[] + rowCount: number +} + +export default function TableSkeleton(props: Props) { + return ( + + + + {props.labels.map((label, index) => { + return ( + + ) + })} + + + + {Array(props.rowCount) + .fill(null) + .map((_, index) => { + return ( + + {props.labels.map((_, index2) => { + return ( + + ) + })} + + ) + })} + +
+
+ + + + + {label} + +
+
+ +
+ ) +} diff --git a/src/components/TitleAndSubCell.tsx b/src/components/TitleAndSubCell.tsx index 49cfc0b3..638468aa 100644 --- a/src/components/TitleAndSubCell.tsx +++ b/src/components/TitleAndSubCell.tsx @@ -15,7 +15,7 @@ export default function TitleAndSubCell(props: Props) { {props.title} - + {props.sub}
diff --git a/src/components/Wallet/WalletFetchBalancesAndAccounts.tsx b/src/components/Wallet/WalletFetchBalancesAndAccounts.tsx index 4bf51956..034e5323 100644 --- a/src/components/Wallet/WalletFetchBalancesAndAccounts.tsx +++ b/src/components/Wallet/WalletFetchBalancesAndAccounts.tsx @@ -5,7 +5,7 @@ import AccountCreateFirst from 'components/Account/AccountCreateFirst' import { CircularProgress } from 'components/CircularProgress' import FullOverlayContent from 'components/FullOverlayContent' import WalletBridges from 'components/Wallet/WalletBridges' -import useAccounts from 'hooks/useAccounts' +import useAccountIds from 'hooks/useAccountIds' import useWalletBalances from 'hooks/useWalletBalances' import useStore from 'store' import { byDenom } from 'utils/array' @@ -29,7 +29,7 @@ function Content() { const address = useStore((s) => s.address) const navigate = useNavigate() const { pathname } = useLocation() - const { data: accounts, isLoading: isLoadingAccounts } = useAccounts(address) + const { data: accountIds, isLoading: isLoadingAccounts } = useAccountIds(address || '') const { data: walletBalances, isLoading: isLoadingBalances } = useWalletBalances(address) const baseAsset = getBaseAsset() @@ -40,17 +40,17 @@ function Content() { useEffect(() => { if ( - accounts.length !== 0 && + accountIds.length !== 0 && BN(baseBalance).isGreaterThanOrEqualTo(defaultFee.amount[0].amount) ) { - navigate(getRoute(getPage(pathname), address, accounts[0].id)) - useStore.setState({ accounts: accounts, balances: walletBalances, focusComponent: null }) + navigate(getRoute(getPage(pathname), address, accountIds[0])) + useStore.setState({ balances: walletBalances, focusComponent: null }) } - }, [accounts, baseBalance, navigate, pathname, address, walletBalances]) + }, [accountIds, baseBalance, navigate, pathname, address, walletBalances]) if (isLoadingAccounts || isLoadingBalances) return if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) return - if (accounts.length === 0) return + if (accountIds.length === 0) return return null } diff --git a/src/hooks/useAccount.tsx b/src/hooks/useAccount.tsx index d0570a0d..6749fe4a 100644 --- a/src/hooks/useAccount.tsx +++ b/src/hooks/useAccount.tsx @@ -2,9 +2,9 @@ import useSWR from 'swr' import getAccount from 'api/accounts/getAccount' -export default function useAccounts(accountId?: string) { +export default function useAccounts(accountId?: string, suspense?: boolean) { return useSWR(`account${accountId}`, () => getAccount(accountId || ''), { - suspense: true, + suspense: suspense, revalidateOnFocus: false, }) } diff --git a/src/hooks/useAccountId.tsx b/src/hooks/useAccountId.tsx new file mode 100644 index 00000000..7f8c231d --- /dev/null +++ b/src/hooks/useAccountId.tsx @@ -0,0 +1,6 @@ +import { useSearchParams } from 'react-router-dom' + +export default function useAccountId() { + const [searchParams] = useSearchParams() + return searchParams.get('accountId') +} diff --git a/src/hooks/useAccountIds.tsx b/src/hooks/useAccountIds.tsx new file mode 100644 index 00000000..41f804e2 --- /dev/null +++ b/src/hooks/useAccountIds.tsx @@ -0,0 +1,11 @@ +import useSWR from 'swr' + +import getAccountIds from 'api/wallets/getAccountIds' + +export default function useAccountIds(address?: string) { + return useSWR(`wallets/${address}/account-ids`, () => getAccountIds(address), { + suspense: true, + fallback: [], + revalidateOnFocus: false, + }) +} diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx index 23fd8cb3..d38eefe3 100644 --- a/src/hooks/useAccounts.tsx +++ b/src/hooks/useAccounts.tsx @@ -1,11 +1,15 @@ import useSWR from 'swr' import getAccounts from 'api/wallets/getAccounts' +import useStore from 'store' export default function useAccounts(address?: string) { - return useSWR(`accounts${address}`, () => getAccounts(address || ''), { + return useSWR(`accounts${address}`, () => getAccounts(address), { suspense: true, fallbackData: [], revalidateOnFocus: false, + onSuccess: (accounts) => { + useStore.setState({ accounts: accounts }) + }, }) } diff --git a/src/hooks/useBorrowMarketAssetsTableData.ts b/src/hooks/useBorrowMarketAssetsTableData.ts index adb35f7e..e3019734 100644 --- a/src/hooks/useBorrowMarketAssetsTableData.ts +++ b/src/hooks/useBorrowMarketAssetsTableData.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' +import useCurrentAccountDebts from 'hooks/useCurrentAccountDebts' import useDepositEnabledMarkets from 'hooks/useDepositEnabledMarkets' import useMarketBorrowings from 'hooks/useMarketBorrowings' import useMarketDeposits from 'hooks/useMarketDeposits' @@ -7,11 +8,11 @@ import useMarketLiquidities from 'hooks/useMarketLiquidities' import { byDenom } from 'utils/array' import { getAssetByDenom } from 'utils/assets' import { BN } from 'utils/helpers' -import useCurrentAccountDebts from 'hooks/useCurrentAccountDebts' export default function useBorrowMarketAssetsTableData(): { accountBorrowedAssets: BorrowMarketTableData[] availableAssets: BorrowMarketTableData[] + allAssets: BorrowMarketTableData[] } { const markets = useDepositEnabledMarkets() const accountDebts = useCurrentAccountDebts() @@ -45,6 +46,10 @@ export default function useBorrowMarketAssetsTableData(): { ;(borrowMarketAsset.debt ? accountBorrowedAssets : availableAssets).push(borrowMarketAsset) }) - return { accountBorrowedAssets, availableAssets } + return { + accountBorrowedAssets, + availableAssets, + allAssets: [...accountBorrowedAssets, ...availableAssets], + } }, [accountDebts, borrowData, markets, marketDeposits, marketLiquidities]) } diff --git a/src/hooks/useCurrentAccount.tsx b/src/hooks/useCurrentAccount.tsx index 72e4bd99..0d747ec8 100644 --- a/src/hooks/useCurrentAccount.tsx +++ b/src/hooks/useCurrentAccount.tsx @@ -1,9 +1,9 @@ -import { useParams } from 'react-router-dom' - +import useAccountId from 'hooks/useAccountId' import useStore from 'store' export default function useCurrentAccount(): Account | undefined { - const { accountId } = useParams() + const accountId = useAccountId() + const accounts = useStore((s) => s.accounts) return accounts?.find((account) => account.id === accountId) } diff --git a/src/hooks/useLendingMarketAssetsTableData.ts b/src/hooks/useLendingMarketAssetsTableData.ts index 0a8a8ae1..189eafb4 100644 --- a/src/hooks/useLendingMarketAssetsTableData.ts +++ b/src/hooks/useLendingMarketAssetsTableData.ts @@ -12,6 +12,7 @@ import { BN } from 'utils/helpers' function useLendingMarketAssetsTableData(): { accountLentAssets: LendingMarketTableData[] availableAssets: LendingMarketTableData[] + allAssets: LendingMarketTableData[] } { const markets = useDepositEnabledMarkets() const accountLentAmounts = useCurrentAccountLends() @@ -52,7 +53,11 @@ function useLendingMarketAssetsTableData(): { }, ) - return { accountLentAssets, availableAssets } + return { + accountLentAssets, + availableAssets, + allAssets: [...accountLentAssets, ...availableAssets], + } }, [markets, marketDeposits, marketLiquidities, accountLentAmounts, convertAmount]) } diff --git a/src/pages/PortfolioAccountPage.tsx b/src/pages/PortfolioAccountPage.tsx new file mode 100644 index 00000000..93081176 --- /dev/null +++ b/src/pages/PortfolioAccountPage.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { useNavigate, useParams } from 'react-router-dom' + +import Balances from 'components/Portfolio/Account/Balances' +import BreadCrumbs from 'components/Portfolio/Account/BreadCrumbs' +import Summary from 'components/Portfolio/Account/Summary' +import useAccountId from 'hooks/useAccountId' +import { getRoute } from 'utils/route' + +export default function PortfolioAccountPage() { + const selectedAccountId = useAccountId() + const { address, accountId } = useParams() + const navigate = useNavigate() + + if (!accountId) { + navigate(getRoute('portfolio', address, selectedAccountId)) + return null + } + + return ( +
+ + + +
+ ) +} diff --git a/src/pages/PortfolioPage.tsx b/src/pages/PortfolioPage.tsx index b3cfa28d..c3ac5800 100644 --- a/src/pages/PortfolioPage.tsx +++ b/src/pages/PortfolioPage.tsx @@ -1,4 +1,4 @@ -import AccountOverview from 'components/Account/AccountOverview' +import AccountOverview from 'components/Portfolio/Overview' import PortfolioIntro from 'components/Portfolio/PortfolioIntro' export default function PortfolioPage() { diff --git a/src/types/interfaces/route.d.ts b/src/types/interfaces/route.d.ts index 3769e2f6..48101e31 100644 --- a/src/types/interfaces/route.d.ts +++ b/src/types/interfaces/route.d.ts @@ -1 +1 @@ -type Page = 'trade' | 'borrow' | 'farm' | 'lend' | 'portfolio' | 'council' +type Page = 'trade' | 'borrow' | 'farm' | 'lend' | 'portfolio' | 'portfolio/{accountId}' diff --git a/src/utils/route.ts b/src/utils/route.ts index 58b36929..7aa795d5 100644 --- a/src/utils/route.ts +++ b/src/utils/route.ts @@ -1,26 +1,34 @@ -export function getRoute(page: Page, address?: string, accountId?: string) { +export function getRoute(page: Page, address?: string, accountId?: string | null) { let nextUrl = '' if (address) { nextUrl += `/wallets/${address}` + } - if (accountId) { - nextUrl += `/accounts/${accountId}` + nextUrl += `/${page}` + + let url = new URL(nextUrl, 'https://app.marsprotocol.io') + + if (accountId) { + url.searchParams.append('accountId', accountId) + } + + return url.pathname + url.search +} + +export function getPage(pathname: string): Page { + const pages: Page[] = ['trade', 'borrow', 'farm', 'lend', 'portfolio'] + const segments = pathname.split('/') + + const page = segments.find((segment) => pages.includes(segment as Page)) + + if (page) { + if (page === 'portfolio') { + const path = pathname.split('portfolio')[1] + return (page + path) as Page } + return page as Page } - return (nextUrl += `/${page}`) -} - -export function getPage(pathname: string) { - const pages: Page[] = ['trade', 'borrow', 'farm', 'lend', 'portfolio', 'council'] - const lastSegment = pathname.split('/').pop() as Page - - if (!lastSegment) return 'trade' - - if (pages.includes(lastSegment)) { - return lastSegment - } - - return 'trade' + return 'trade' as Page }