diff --git a/centrifuge-app/src/components/FilterButton.tsx b/centrifuge-app/src/components/FilterButton.tsx new file mode 100644 index 0000000000..adbc29baf9 --- /dev/null +++ b/centrifuge-app/src/components/FilterButton.tsx @@ -0,0 +1,11 @@ +import { Text } from '@centrifuge/fabric' +import styled from 'styled-components' +import { buttonActionStyles } from './styles' + +export const FilterButton = styled(Text)` + display: flex; + align-items: center; + gap: 0.3em; + + ${buttonActionStyles} +` diff --git a/centrifuge-app/src/components/PoolCard/styles.tsx b/centrifuge-app/src/components/ListItemCardStyles.tsx similarity index 100% rename from centrifuge-app/src/components/PoolCard/styles.tsx rename to centrifuge-app/src/components/ListItemCardStyles.tsx diff --git a/centrifuge-app/src/components/PoolCard/index.tsx b/centrifuge-app/src/components/PoolCard/index.tsx index 6da4194d7d..0663d99afc 100644 --- a/centrifuge-app/src/components/PoolCard/index.tsx +++ b/centrifuge-app/src/components/PoolCard/index.tsx @@ -6,8 +6,8 @@ import { useRouteMatch } from 'react-router' import { useTheme } from 'styled-components' import { formatBalance, formatPercentage } from '../../utils/formatting' import { Eththumbnail } from '../EthThumbnail' +import { Anchor, Ellipsis, Root } from '../ListItemCardStyles' import { PoolStatus, PoolStatusKey } from './PoolStatus' -import { Anchor, Ellipsis, Root } from './styles' const columns_base = 'minmax(150px, 2fr) minmax(100px, 1fr) 140px 70px 150px' const columns_extended = 'minmax(200px, 2fr) minmax(100px, 1fr) 140px 100px 150px' diff --git a/centrifuge-app/src/components/PoolFilter/FilterMenu.tsx b/centrifuge-app/src/components/PoolFilter/FilterMenu.tsx index feadc2fc9a..c725f01af1 100644 --- a/centrifuge-app/src/components/PoolFilter/FilterMenu.tsx +++ b/centrifuge-app/src/components/PoolFilter/FilterMenu.tsx @@ -1,7 +1,8 @@ import { Box, Checkbox, Divider, IconFilter, Menu, Popover, Stack, Tooltip } from '@centrifuge/fabric' import * as React from 'react' import { useHistory, useLocation } from 'react-router-dom' -import { FilterButton, QuickAction } from './styles' +import { FilterButton } from '../FilterButton' +import { QuickAction } from '../QuickAction' import { SearchKeys } from './types' import { toKebabCase } from './utils' diff --git a/centrifuge-app/src/components/PoolFilter/SortButton.tsx b/centrifuge-app/src/components/PoolFilter/SortButton.tsx index ba31ffe790..44af2f4c7c 100644 --- a/centrifuge-app/src/components/PoolFilter/SortButton.tsx +++ b/centrifuge-app/src/components/PoolFilter/SortButton.tsx @@ -1,8 +1,9 @@ -import { IconChevronDown, IconChevronUp, Stack, Tooltip } from '@centrifuge/fabric' +import { Tooltip } from '@centrifuge/fabric' import * as React from 'react' import { useHistory, useLocation } from 'react-router-dom' +import { FilterButton } from '../FilterButton' +import { SortChevrons } from '../SortChevrons' import { SEARCH_KEYS } from './config' -import { FilterButton } from './styles' import { SortBy } from './types' export type SortButtonProps = { @@ -64,7 +65,7 @@ export function SortButton({ label, searchKey, tooltip, justifySelf = 'end' }: S {label} - + ) @@ -87,23 +88,7 @@ export function SortButton({ label, searchKey, tooltip, justifySelf = 'end' }: S > {label} - + ) } - -function Inner({ sorting }: { sorting: Sorting }) { - return ( - - - - - ) -} diff --git a/centrifuge-app/src/components/PoolFilter/styles.ts b/centrifuge-app/src/components/PoolFilter/styles.ts deleted file mode 100644 index f94d6c7d85..0000000000 --- a/centrifuge-app/src/components/PoolFilter/styles.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Text } from '@centrifuge/fabric' -import styled, { css } from 'styled-components' - -const sharedStyles = css` - appearance: none; - border: none; - cursor: pointer; - background-color: transparent; - border-radius: ${({ theme }) => theme.radii.tooltip}px; - - &:focus-visible { - outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`}; - outline-offset: 4px; - } -` - -export const FilterButton = styled(Text)` - display: flex; - align-items: center; - gap: 0.3em; - - ${sharedStyles} -` -export const QuickAction = styled(Text)` - ${sharedStyles} -` diff --git a/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx b/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx index e953a29ce3..3dc3f146a0 100644 --- a/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx +++ b/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx @@ -1,89 +1,95 @@ -import { AccountTokenBalance, Pool } from '@centrifuge/centrifuge-js' -import { formatBalance, useBalances } from '@centrifuge/centrifuge-react' +import { useAddress, useBalances } from '@centrifuge/centrifuge-react' import { Box, Grid, Stack, Text } from '@centrifuge/fabric' -import * as React from 'react' -import { useAddress } from '../../utils/useAddress' -import { usePool } from '../../utils/usePools' +import { useMemo, useState } from 'react' +import { useTinlakeBalances } from '../../utils/tinlake/useTinlakeBalances' +import { useTinlakePools } from '../../utils/tinlake/useTinlakePools' +import { usePools } from '../../utils/usePools' +import { FilterButton } from '../FilterButton' +import { SortChevrons } from '../SortChevrons' +import { sortTokens } from './sortTokens' +import { TokenListItem } from './TokenListItem' -const TOKEN_ITEM_COLUMNS = `250px 200px 100px 150px 1FR` -const TOKEN_ITEM_GAP = 4 +export const COLUMN_GAPS = '200px 140px 140px 140px' + +export type SortOptions = { + sortBy: 'position' | 'market-value' + sortDirection: 'asc' | 'desc' +} + +// TODO: change canInvestRedeem to default to true once the drawer is implemented +export const InvestedTokens = ({ canInvestRedeem = false }) => { + const [sortOptions, setSortOptions] = useState({ sortBy: 'position', sortDirection: 'desc' }) -export function InvestedTokens() { const address = useAddress() - const balances = useBalances(address) + const centBalances = useBalances(address) + const { data: tinlakeBalances } = useTinlakeBalances() - return !!balances?.tranches && !!balances?.tranches.length ? ( - <> - - - Portfolio Composition - - - - + const { data: tinlakePools } = useTinlakePools() + const pools = usePools() + + const balances = useMemo(() => { + return [ + ...(centBalances?.tranches || []), + ...(tinlakeBalances?.tranches.filter((tranche) => !tranche.balance.isZero) || []), + ] + }, [centBalances, tinlakeBalances]) + + const sortedTokens = + balances.length && pools && tinlakePools + ? sortTokens( + balances, + { + centPools: pools, + tinlakePools: tinlakePools.pools, + }, + sortOptions + ) + : [] + + const handleSort = (sortOption: SortOptions['sortBy']) => { + setSortOptions((prev) => ({ + sortBy: sortOption, + sortDirection: prev.sortBy !== sortOption ? 'desc' : prev.sortDirection === 'asc' ? 'desc' : 'asc', + })) + } + + return sortedTokens.length ? ( + + + Portfolio + + + + Token - + + handleSort('position')}> Position - + + + Token price - - Market value - + + handleSort('market-value')}> + Market Value + + - - {balances.tranches.map((tranche, index) => ( - - - + + {balances.map((balance, index) => ( + ))} - - + + ) : null } - -type TokenCardProps = AccountTokenBalance -export function TokenListItem({ balance, currency, poolId, trancheId }: TokenCardProps) { - const pool = usePool(poolId) as Pool - const isTinlakePool = poolId?.startsWith('0x') - - if (isTinlakePool) { - return null - } - - const tranche = pool.tranches.find(({ id }) => id === trancheId) - - return ( - - - {currency.name} - - - - {formatBalance(balance, tranche?.currency.symbol)} - - - - {tranche?.tokenPrice ? formatBalance(tranche.tokenPrice.toDecimal(), tranche.currency.symbol, 4) : '-'} - - - - {tranche?.tokenPrice - ? formatBalance(balance.toDecimal().mul(tranche.tokenPrice.toDecimal()), tranche.currency.symbol, 4) - : '-'} - - - ) -} diff --git a/centrifuge-app/src/components/Portfolio/TokenListItem.tsx b/centrifuge-app/src/components/Portfolio/TokenListItem.tsx new file mode 100644 index 0000000000..2257e7beff --- /dev/null +++ b/centrifuge-app/src/components/Portfolio/TokenListItem.tsx @@ -0,0 +1,100 @@ +import { AccountTokenBalance } from '@centrifuge/centrifuge-js' +import { formatBalance, useCentrifuge } from '@centrifuge/centrifuge-react' +import { + AnchorButton, + Box, + Button, + Grid, + IconExternalLink, + IconMinus, + IconPlus, + Shelf, + Text, + Thumbnail, +} from '@centrifuge/fabric' +import styled, { useTheme } from 'styled-components' +import { usePool, usePoolMetadata } from '../../utils/usePools' +import { Eththumbnail } from '../EthThumbnail' +import { Root } from '../ListItemCardStyles' +import { COLUMN_GAPS } from './InvestedTokens' + +export type TokenCardProps = AccountTokenBalance & { + canInvestRedeem?: boolean +} + +const TokenName = styled(Text)` + text-wrap: nowrap; +` + +export function TokenListItem({ balance, currency, poolId, trancheId, canInvestRedeem }: TokenCardProps) { + const { sizes } = useTheme() + const pool = usePool(poolId, false) + const { data: metadata } = usePoolMetadata(pool) + const cent = useCentrifuge() + + const isTinlakePool = poolId.startsWith('0x') + + // @ts-expect-error known typescript issue: https://github.com/microsoft/TypeScript/issues/44373 + const trancheInfo = pool?.tranches.find(({ id }) => id === trancheId) + const icon = metadata?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metadata.pool.icon.uri) : null + + return ( + + + + + {icon ? ( + + ) : ( + + )} + + + + {currency.name} + + + + + {formatBalance(balance, currency.symbol)} + + + + {trancheInfo?.tokenPrice + ? formatBalance(trancheInfo.tokenPrice.toDecimal(), trancheInfo.currency.symbol, 4) + : '-'} + + + + {trancheInfo?.tokenPrice + ? formatBalance(balance.toDecimal().mul(trancheInfo.tokenPrice.toDecimal()), trancheInfo.currency.symbol, 4) + : '-'} + + + {canInvestRedeem && ( + + {isTinlakePool ? ( + + View on Tinlake + + ) : ( + <> + + + + )} + + )} + + + ) +} diff --git a/centrifuge-app/src/components/Portfolio/sortTokens.ts b/centrifuge-app/src/components/Portfolio/sortTokens.ts new file mode 100644 index 0000000000..ed12057e09 --- /dev/null +++ b/centrifuge-app/src/components/Portfolio/sortTokens.ts @@ -0,0 +1,50 @@ +import { Pool } from '@centrifuge/centrifuge-js' +import { TinlakePool } from '../../utils/tinlake/useTinlakePools' +import { SortOptions } from './InvestedTokens' +import { TokenCardProps } from './TokenListItem' + +export const sortTokens = ( + tokens: TokenCardProps[], + pools: { + centPools: Pool[] + tinlakePools: TinlakePool[] + }, + sortOptions: SortOptions +) => { + const { sortBy, sortDirection } = sortOptions + if (sortBy === 'market-value') { + tokens.sort((trancheA, trancheB) => { + const valueA = sortMarketValue(trancheA, pools) + const valueB = sortMarketValue(trancheB, pools) + + return sortDirection === 'asc' ? valueA - valueB : valueB - valueA + }) + } + + if (sortBy === 'position' || (!sortDirection && !sortBy)) { + tokens.sort(({ balance: balanceA }, { balance: balanceB }) => + sortDirection === 'asc' + ? balanceA.toDecimal().toNumber() - balanceB.toDecimal().toNumber() + : balanceB.toDecimal().toNumber() - balanceA.toDecimal().toNumber() + ) + } + + return tokens +} + +const sortMarketValue = ( + token: TokenCardProps, + pools: { + centPools: Pool[] + tinlakePools: TinlakePool[] + } +) => { + const pool = token.poolId.startsWith('0x') + ? pools.tinlakePools?.find((p) => p.id.toLowerCase() === token.poolId.toLowerCase()) + : pools.centPools?.find((p) => p.id === token.poolId) + + // @ts-expect-error known typescript issue: https://github.com/microsoft/TypeScript/issues/44373 + const poolTranche = pool?.tranches.find(({ id }) => id === token.trancheId) + + return poolTranche?.tokenPrice ? token.balance.toDecimal().mul(poolTranche.tokenPrice.toDecimal()).toNumber() : 0 +} diff --git a/centrifuge-app/src/components/QuickAction.tsx b/centrifuge-app/src/components/QuickAction.tsx new file mode 100644 index 0000000000..eb196114c9 --- /dev/null +++ b/centrifuge-app/src/components/QuickAction.tsx @@ -0,0 +1,7 @@ +import { Text } from '@centrifuge/fabric' +import styled from 'styled-components' +import { buttonActionStyles } from './styles' + +export const QuickAction = styled(Text)` + ${buttonActionStyles} +` diff --git a/centrifuge-app/src/components/SortChevrons.tsx b/centrifuge-app/src/components/SortChevrons.tsx new file mode 100644 index 0000000000..fa9674b729 --- /dev/null +++ b/centrifuge-app/src/components/SortChevrons.tsx @@ -0,0 +1,22 @@ +import { IconChevronDown, IconChevronUp, Stack } from '@centrifuge/fabric' + +type Sorting = { + isActive: boolean + direction: string | null +} + +export function SortChevrons({ sorting }: { sorting: Sorting }) { + return ( + + + + + ) +} diff --git a/centrifuge-app/src/components/styles.ts b/centrifuge-app/src/components/styles.ts new file mode 100644 index 0000000000..2203668ef8 --- /dev/null +++ b/centrifuge-app/src/components/styles.ts @@ -0,0 +1,14 @@ +import { css } from 'styled-components' + +export const buttonActionStyles = css` + appearance: none; + border: none; + cursor: pointer; + background-color: transparent; + border-radius: ${({ theme }) => theme.radii.tooltip}px; + + &:focus-visible { + outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`}; + outline-offset: 4px; + } +`