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;
+ }
+`