Share accounts (#539)
* feat: do not redirect to wallet on portfolio page * fix: use connected wallet for AccountMenu * fix: fixed ghost AccountDetails * feat: created ShareBar and share functionality * fix: don’t show shareBar if no address is present * fix: stupid 'next/navigation' * tidy: format * fix: fixed tests * ✨ routing and pages for HLS (#538) * 🐛 use useAccountIds * fix: fixed the tests * fix: accountIds is now a suspense --------- Co-authored-by: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com>
This commit is contained in:
parent
4915729ed5
commit
ea614997a7
@ -14,15 +14,28 @@ jest.mock('hooks/useHealthComputer', () =>
|
||||
jest.mock('components/Account/AccountBalancesTable', () => jest.fn(() => null))
|
||||
|
||||
const mockedUseCurrentAccount = useCurrentAccount as jest.Mock
|
||||
const mockedAccount = { id: '1', deposits: [], lends: [], debts: [], vaults: [] }
|
||||
const mockedAccounts = [
|
||||
{ id: '1', deposits: [], lends: [], debts: [], vaults: [] },
|
||||
{ id: '2', deposits: [], lends: [], debts: [], vaults: [] },
|
||||
]
|
||||
jest.mock('hooks/useAccountId', () => jest.fn(() => '1'))
|
||||
jest.mock('hooks/useAccounts', () => jest.fn(() => [mockedAccount]))
|
||||
jest.mock('hooks/useAccounts', () =>
|
||||
jest.fn(() => ({
|
||||
data: mockedAccounts,
|
||||
})),
|
||||
)
|
||||
jest.mock('hooks/useAccountIds', () =>
|
||||
jest.fn(() => ({
|
||||
data: ['1', '2'],
|
||||
})),
|
||||
)
|
||||
jest.mock('hooks/useCurrentAccount', () => jest.fn(() => mockedAccounts[0]))
|
||||
|
||||
describe('<AccountDetails />', () => {
|
||||
beforeAll(() => {
|
||||
useStore.setState({
|
||||
address: 'walletAddress',
|
||||
accounts: [mockedAccount],
|
||||
accounts: mockedAccounts,
|
||||
})
|
||||
})
|
||||
|
||||
@ -31,7 +44,7 @@ describe('<AccountDetails />', () => {
|
||||
})
|
||||
|
||||
it('renders account details WHEN account is selected', () => {
|
||||
mockedUseCurrentAccount.mockReturnValue(mockedAccount)
|
||||
mockedUseCurrentAccount.mockReturnValue(mockedAccounts)
|
||||
render(<AccountDetails />)
|
||||
|
||||
const container = screen.queryByTestId('account-details')
|
||||
|
@ -5,8 +5,8 @@ export default async function getAccountIds(
|
||||
address?: string,
|
||||
previousResults?: string[],
|
||||
): Promise<string[]> {
|
||||
if (!address) return []
|
||||
try {
|
||||
if (!address) return []
|
||||
const accountNftQueryClient = await getAccountNftQueryClient()
|
||||
|
||||
const lastItem = previousResults && previousResults.at(-1)
|
||||
|
@ -17,6 +17,7 @@ import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
import { REDUCE_MOTION_KEY } from 'constants/localStore'
|
||||
import { ORACLE_DENOM } from 'constants/oracle'
|
||||
import useAccountId from 'hooks/useAccountId'
|
||||
import useAccountIds from 'hooks/useAccountIds'
|
||||
import useAccounts from 'hooks/useAccounts'
|
||||
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
@ -34,12 +35,15 @@ import {
|
||||
|
||||
export default function AccountDetailsController() {
|
||||
const address = useStore((s) => s.address)
|
||||
const { isLoading } = useAccounts(address)
|
||||
const { data: accounts, isLoading } = useAccounts(address)
|
||||
const { data: accountIds } = useAccountIds(address, false)
|
||||
const accountId = useAccountId()
|
||||
const account = useCurrentAccount()
|
||||
const focusComponent = useStore((s) => s.focusComponent)
|
||||
|
||||
if (!address || focusComponent) return null
|
||||
const focusComponent = useStore((s) => s.focusComponent)
|
||||
const isOwnAccount = accountId && accountIds?.includes(accountId)
|
||||
|
||||
if (!address || focusComponent || !isOwnAccount) return null
|
||||
|
||||
if ((isLoading && accountId && !focusComponent) || !account) return <Skeleton />
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import AccountStats from 'components/Account/AccountList/AccountStats'
|
||||
import Card from 'components/Card'
|
||||
@ -26,7 +26,7 @@ export default function AccountList(props: Props) {
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const currentAccountId = useAccountId()
|
||||
const { address } = useParams()
|
||||
const address = useStore((s) => s.address)
|
||||
const { data: accountIds } = useAccountIds(address)
|
||||
|
||||
useEffect(() => {
|
||||
@ -37,7 +37,7 @@ export default function AccountList(props: Props) {
|
||||
}
|
||||
}, [currentAccountId])
|
||||
|
||||
if (!accountIds?.length) return null
|
||||
if (!accountIds || !accountIds.length) return null
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap w-full p-4'>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import { useCallback } from 'react'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
|
||||
import AccountFund from 'components/Account/AccountFund/AccountFundFullPage'
|
||||
@ -30,7 +30,7 @@ const ACCOUNT_MENU_BUTTON_ID = 'account-menu-button'
|
||||
export default function AccountMenuContent() {
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const { address } = useParams()
|
||||
const address = useStore((s) => s.address)
|
||||
const { data: accountIds } = useAccountIds(address)
|
||||
const accountId = useAccountId()
|
||||
|
||||
@ -42,8 +42,9 @@ export default function AccountMenuContent() {
|
||||
const [lendAssets] = useLocalStorage<boolean>(LEND_ASSETS_KEY, DEFAULT_SETTINGS.lendAssets)
|
||||
const { enableAutoLendAccountId } = useAutoLend()
|
||||
|
||||
const hasCreditAccounts = !!accountIds.length
|
||||
const isAccountSelected = isNumber(accountId)
|
||||
const hasCreditAccounts = !!accountIds?.length
|
||||
const isAccountSelected =
|
||||
hasCreditAccounts && accountId && isNumber(accountId) && accountIds.includes(accountId)
|
||||
|
||||
const checkHasFunds = useCallback(() => {
|
||||
return (
|
||||
|
@ -23,7 +23,7 @@ export default function ActionButton(props: ButtonProps) {
|
||||
|
||||
if (!address) return <WalletConnectButton {...defaultProps} />
|
||||
|
||||
if (accountIds.length === 0) {
|
||||
if (accountIds && accountIds.length === 0) {
|
||||
return (
|
||||
<Button
|
||||
onClick={handleCreateAccountClick}
|
||||
|
6
src/components/Icons/Chain.svg
Normal file
6
src/components/Icons/Chain.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 512 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M459.654,233.373l-90.531,90.5c-49.969,50-131.031,50-181,0c-7.875-7.844-14.031-16.688-19.438-25.813 l42.063-42.063c2-2.016,4.469-3.172,6.828-4.531c2.906,9.938,7.984,19.344,15.797,27.156c24.953,24.969,65.563,24.938,90.5,0 l90.5-90.5c24.969-24.969,24.969-65.563,0-90.516c-24.938-24.953-65.531-24.953-90.5,0l-32.188,32.219 c-26.109-10.172-54.25-12.906-81.641-8.891l68.578-68.578c50-49.984,131.031-49.984,181.031,0 C509.623,102.342,509.623,183.389,459.654,233.373z M220.326,382.186l-32.203,32.219c-24.953,24.938-65.563,24.938-90.516,0 c-24.953-24.969-24.953-65.563,0-90.531l90.516-90.5c24.969-24.969,65.547-24.969,90.5,0c7.797,7.797,12.875,17.203,15.813,27.125 c2.375-1.375,4.813-2.5,6.813-4.5l42.063-42.047c-5.375-9.156-11.563-17.969-19.438-25.828c-49.969-49.984-131.031-49.984-181.016,0 l-90.5,90.5c-49.984,50-49.984,131.031,0,181.031c49.984,49.969,131.031,49.969,181.016,0l68.594-68.594 C274.561,395.092,246.42,392.342,220.326,382.186z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
7
src/components/Icons/Twitter.svg
Normal file
7
src/components/Icons/Twitter.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 120 120">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0.3,3.8l46.3,61.9L0,116.2h10.5l40.8-44.1l33,44.1H120L71.1,50.7l43.4-46.9H104L66.4,44.5L36,3.8
|
||||
H0.3z M15.7,11.6h16.4l72.4,96.9H88.2L15.7,11.6z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 289 B |
@ -7,6 +7,7 @@ export { default as ArrowCircledTopRight } from 'components/Icons/ArrowCircledTo
|
||||
export { default as ArrowDownLine } from 'components/Icons/ArrowDownLine.svg'
|
||||
export { default as ArrowRight } from 'components/Icons/ArrowRight.svg'
|
||||
export { default as ArrowUpLine } from 'components/Icons/ArrowUpLine.svg'
|
||||
export { default as Chain } from 'components/Icons/Chain.svg'
|
||||
export { default as Check } from 'components/Icons/Check.svg'
|
||||
export { default as CheckCircled } from 'components/Icons/CheckCircled.svg'
|
||||
export { default as ChevronDown } from 'components/Icons/ChevronDown.svg'
|
||||
@ -54,6 +55,7 @@ export { default as SwapIcon } from 'components/Icons/SwapIcon.svg'
|
||||
export { default as ThreeDots } from 'components/Icons/ThreeDots.svg'
|
||||
export { default as TooltipArrow } from 'components/Icons/TooltipArrow.svg'
|
||||
export { default as TrashBin } from 'components/Icons/TrashBin.svg'
|
||||
export { default as Twitter } from 'components/Icons/Twitter.svg'
|
||||
export { default as VerticalThreeLine } from 'components/Icons/VerticalThreeLine.svg'
|
||||
export { default as Wallet } from 'components/Icons/Wallet.svg'
|
||||
// @endindex
|
||||
|
@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { menuTree } from 'components/Header/DesktopHeader'
|
||||
import { Logo } from 'components/Icons'
|
||||
@ -9,7 +8,7 @@ import useStore from 'store'
|
||||
import { getRoute } from 'utils/route'
|
||||
|
||||
export default function DesktopNavigation() {
|
||||
const { address } = useParams()
|
||||
const address = useStore((s) => s.address)
|
||||
const accountId = useAccountId()
|
||||
|
||||
const focusComponent = useStore((s) => s.focusComponent)
|
||||
@ -26,7 +25,7 @@ export default function DesktopNavigation() {
|
||||
)}
|
||||
>
|
||||
<NavLink href={getRoute('trade', address, accountId)}>
|
||||
<span className='block h-10 w-10'>
|
||||
<span className='block w-10 h-10'>
|
||||
<Logo className='text-white' />
|
||||
</span>
|
||||
</NavLink>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactNode, useMemo } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { NavLink, useParams } from 'react-router-dom'
|
||||
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import Loading from 'components/Loading'
|
||||
@ -30,6 +30,7 @@ interface Props {
|
||||
export default function PortfolioCard(props: Props) {
|
||||
const { data: account } = useAccount(props.accountId)
|
||||
const { health } = useHealthComputer(account)
|
||||
const { address: urlAddress } = useParams()
|
||||
const { data: prices } = usePrices()
|
||||
const currentAccountId = useAccountId()
|
||||
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
|
||||
@ -91,7 +92,7 @@ export default function PortfolioCard(props: Props) {
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={getRoute(`portfolio/${props.accountId}` as Page, address, currentAccountId)}
|
||||
to={getRoute(`portfolio/${props.accountId}` as Page, urlAddress, currentAccountId)}
|
||||
className={classNames('w-full hover:bg-white/5', !reduceMotion && 'transition-all')}
|
||||
>
|
||||
<Skeleton
|
||||
|
@ -42,7 +42,7 @@ export default function AccountSummary() {
|
||||
|
||||
if (!walletAddress && !urlAddress) return <ConnectInfo />
|
||||
|
||||
if (!isLoading && accountIds?.length === 0) {
|
||||
if (!isLoading && accountIds && accountIds.length === 0) {
|
||||
return (
|
||||
<Card
|
||||
className='w-full h-fit bg-white/5'
|
||||
@ -72,7 +72,7 @@ export default function AccountSummary() {
|
||||
<div
|
||||
className={classNames('grid w-full grid-cols-1 gap-6', 'md:grid-cols-2', 'lg:grid-cols-3')}
|
||||
>
|
||||
{accountIds.map((accountId: string, index: number) => {
|
||||
{accountIds?.map((accountId: string, index: number) => {
|
||||
return <PortfolioCard key={accountId} accountId={accountId} />
|
||||
})}
|
||||
</div>
|
||||
|
@ -4,17 +4,18 @@ import Intro from 'components/Intro'
|
||||
import useStore from 'store'
|
||||
|
||||
export default function PortfolioIntro() {
|
||||
const { address } = useParams()
|
||||
const walletAddress = useStore((s) => s.address)
|
||||
const { address: urlAddress } = useParams()
|
||||
const address = useStore((s) => s.address)
|
||||
const isCurrentWalllet = !urlAddress || urlAddress === address
|
||||
|
||||
return (
|
||||
<Intro
|
||||
text={
|
||||
address && !walletAddress ? (
|
||||
!isCurrentWalllet ? (
|
||||
<>
|
||||
This is the <span className='text-white'>Portfolio</span> of the address{' '}
|
||||
<span className='text-white'>{address}</span>. You can see all Credit Accounts of this
|
||||
address, but you can't interact with them.
|
||||
<span className='text-white'>{urlAddress}</span>. You can see all Credit Accounts of
|
||||
this address, but you can't interact with them.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
55
src/components/ShareBar.tsx
Normal file
55
src/components/ShareBar.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import classNames from 'classnames'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
import useClipboard from 'react-use-clipboard'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import { Chain, Check, Twitter } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import ConditionalWrapper from 'hocs/ConditionalWrapper'
|
||||
import { DocURL } from 'types/enums/docURL'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
}
|
||||
|
||||
export default function ShareBar(props: Props) {
|
||||
const { address } = useParams()
|
||||
const { pathname } = useLocation()
|
||||
const currentUrl = `https://${location.host}${pathname}`
|
||||
const [isCopied, setCopied] = useClipboard(currentUrl, {
|
||||
successDuration: 1000 * 5,
|
||||
})
|
||||
|
||||
if (!window || !address) return null
|
||||
return (
|
||||
<div className='flex justify-end w-full gap-4'>
|
||||
<ConditionalWrapper
|
||||
condition={isCopied}
|
||||
wrapper={(children) => (
|
||||
<Tooltip type='info' content={<Text size='2xs'>Link copied!</Text>}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
color='secondary'
|
||||
iconClassName='w-4 h-4'
|
||||
className={classNames('!p-2', isCopied && '!bg-transparent')}
|
||||
leftIcon={isCopied ? <Check /> : <Chain />}
|
||||
onClick={setCopied}
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
<Button
|
||||
color='secondary'
|
||||
iconClassName='w-4 h-4'
|
||||
className='!p-2'
|
||||
leftIcon={<Twitter />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.open(`${DocURL.X_SHARE_URL}?text=${props.text} ${currentUrl}`, '_blank')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -224,7 +224,8 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
|
||||
}
|
||||
|
||||
const filler = Array.from({ length: this.batchSize - bars.length }).map((_, index) => ({
|
||||
time: (bars[0]?.time || new Date().getTime()) - index * this.minutesPerInterval[resolution],
|
||||
time:
|
||||
(bars[0]?.time || new Date().getTime()) - index * this.minutesPerInterval[resolution],
|
||||
close: 0,
|
||||
open: 0,
|
||||
high: 0,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Suspense, useEffect, useMemo } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
@ -27,6 +27,7 @@ function FetchLoading() {
|
||||
|
||||
function Content() {
|
||||
const address = useStore((s) => s.address)
|
||||
const { address: urlAddress } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const { data: accountIds, isLoading: isLoadingAccounts } = useAccountIds(address || '')
|
||||
@ -39,18 +40,26 @@ function Content() {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const page = getPage(pathname)
|
||||
if (page === 'portfolio' && urlAddress && urlAddress !== address) {
|
||||
navigate(getRoute(page, urlAddress as string))
|
||||
useStore.setState({ balances: walletBalances, focusComponent: null })
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
accountIds &&
|
||||
accountIds.length !== 0 &&
|
||||
BN(baseBalance).isGreaterThanOrEqualTo(defaultFee.amount[0].amount)
|
||||
) {
|
||||
navigate(getRoute(getPage(pathname), address, accountIds[0]))
|
||||
navigate(getRoute(page, address, accountIds[0]))
|
||||
useStore.setState({ balances: walletBalances, focusComponent: null })
|
||||
}
|
||||
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances])
|
||||
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances, urlAddress])
|
||||
|
||||
if (isLoadingAccounts || isLoadingBalances) return <FetchLoading />
|
||||
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) return <WalletBridges />
|
||||
if (accountIds.length === 0) return <AccountCreateFirst />
|
||||
if (accountIds && accountIds.length === 0) return <AccountCreateFirst />
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,9 @@ import useSWR from 'swr'
|
||||
|
||||
import getAccountIds from 'api/wallets/getAccountIds'
|
||||
|
||||
export default function useAccountIds(address?: string) {
|
||||
export default function useAccountIds(address?: string, suspense = true) {
|
||||
return useSWR(`wallets/${address}/account-ids`, () => getAccountIds(address), {
|
||||
suspense: true,
|
||||
suspense: suspense,
|
||||
fallback: [] as string[],
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
|
@ -4,6 +4,7 @@ import MigrationBanner from 'components/MigrationBanner'
|
||||
import Balances from 'components/Portfolio/Account/Balances'
|
||||
import BreadCrumbs from 'components/Portfolio/Account/BreadCrumbs'
|
||||
import Summary from 'components/Portfolio/Account/Summary'
|
||||
import ShareBar from 'components/ShareBar'
|
||||
import useAccountId from 'hooks/useAccountId'
|
||||
import { getRoute } from 'utils/route'
|
||||
|
||||
@ -23,6 +24,7 @@ export default function PortfolioAccountPage() {
|
||||
<BreadCrumbs accountId={accountId} />
|
||||
<Summary accountId={accountId} />
|
||||
<Balances accountId={accountId} />
|
||||
<ShareBar text={`Have a look at Credit Account ${accountId} on @mars_protocol!`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import MigrationBanner from 'components/MigrationBanner'
|
||||
import AccountOverview from 'components/Portfolio/Overview'
|
||||
import PortfolioSummary from 'components/Portfolio/Overview/Summary'
|
||||
import PortfolioIntro from 'components/Portfolio/PortfolioIntro'
|
||||
import ShareBar from 'components/ShareBar'
|
||||
|
||||
export default function PortfolioPage() {
|
||||
return (
|
||||
@ -10,6 +11,7 @@ export default function PortfolioPage() {
|
||||
<PortfolioIntro />
|
||||
<PortfolioSummary />
|
||||
<AccountOverview />
|
||||
<ShareBar text='Have a look at this @mars_protocol portfolio!' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -10,4 +10,5 @@ export enum DocURL {
|
||||
TRADING_INTRO_URL = 'https://docs.marsprotocol.io/docs/learn/tutorials/trading/trading-intro',
|
||||
V1_URL = 'https://v1.marsprotocol.io',
|
||||
WALLET_INTRO_URL = 'https://docs.marsprotocol.io/docs/learn/tutorials/basics/connecting-your-wallet',
|
||||
X_SHARE_URL = 'https://x.com/intent/tweet',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user