Skip to content

Commit

Permalink
CentrifugeApp: Portfolio transaction table (#1605)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophialittlejohn authored and gpmayorga committed Nov 23, 2023
1 parent 7142734 commit 38dec5a
Show file tree
Hide file tree
Showing 17 changed files with 427 additions and 586 deletions.
2 changes: 1 addition & 1 deletion centrifuge-app/.env-config/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctio
REACT_APP_POOL_CREATION_TYPE=immediate
REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com
REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development
REACT_APP_SUBSCAN_URL=
REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io
REACT_APP_TINLAKE_NETWORK=goerli
REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
REACT_APP_WHITELISTED_ACCOUNTS=
Expand Down
8 changes: 8 additions & 0 deletions centrifuge-app/src/components/Menu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Box,
IconClock,
IconGlobe,
IconInvestments,
IconNft,
Expand Down Expand Up @@ -61,6 +62,13 @@ export function Menu() {
</PageLink>
)}

{showPortfolio && address && (
<PageLink to="/history" stacked={!isLarge}>
<IconClock />
History
</PageLink>
)}

{(pools.length > 0 || config.poolCreationType === 'immediate') && (
<IssuerMenu defaultOpen={isLarge} stacked={!isLarge}>
{isLarge ? (
Expand Down
4 changes: 2 additions & 2 deletions centrifuge-app/src/components/PoolList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ export function PoolList() {
? Array(6)
.fill(true)
.map((_, index) => (
<Box as="li" key={index}>
<Box as="li" key={`pool-list-loading-${index}`}>
<PoolCard isLoading={true} />
</Box>
))
: filteredPools.map((pool) => (
<PoolCardBox as="li" key={pool.poolId} status={pool.status}>
<PoolCardBox as="li" key={`pool-list-${pool.name}-${pool.poolId}`} status={pool.status}>
<PoolCard {...pool} />
</PoolCardBox>
))}
Expand Down
4 changes: 2 additions & 2 deletions centrifuge-app/src/components/Portfolio/AssetAllocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function AssetAllocation({ address }: { address: string }) {
<AssetClassChart data={shares} currency="USD" total={total.toNumber()} />
<Shelf as="ul" alignSelf="stretch" alignItems="stretch" flex={1} gap={6}>
{shares.map((cell, i) => (
<>
<Box key={`asset-allocation-${cell.name}-${i}`}>
{i > 0 && <Box width="1px" backgroundColor="borderSecondary" />}
<LabelValueStack
label={
Expand All @@ -84,7 +84,7 @@ export function AssetAllocation({ address }: { address: string }) {
}
key={i}
/>
</>
</Box>
))}
</Shelf>
</Shelf>
Expand Down
212 changes: 143 additions & 69 deletions centrifuge-app/src/components/Portfolio/InvestedTokens.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,117 @@
import { useAddress, useBalances } from '@centrifuge/centrifuge-react'
import { Box, Grid, Stack, Text } from '@centrifuge/fabric'
import { useMemo, useState } from 'react'
import { Token, TokenBalance } from '@centrifuge/centrifuge-js'
import { formatBalance, useAddress, useBalances, useCentrifuge } from '@centrifuge/centrifuge-react'
import {
AnchorButton,
Box,
Button,
Grid,
IconExternalLink,
IconMinus,
IconPlus,
Shelf,
Stack,
Text,
Thumbnail,
} from '@centrifuge/fabric'
import { useMemo } from 'react'
import { useTheme } from 'styled-components'
import { Dec } from '../../utils/Decimal'
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'
import { usePool, usePoolMetadata, usePools } from '../../utils/usePools'
import { Column, DataTable, SortableTableHeader } from '../DataTable'
import { Eththumbnail } from '../EthThumbnail'

export const COLUMN_GAPS = '200px 140px 140px 140px'

export type SortOptions = {
sortBy: 'position' | 'market-value'
sortDirection: 'asc' | 'desc'
type Row = {
currency: Token['currency']
poolId: string
trancheId: string
marketValue: TokenBalance
position: TokenBalance
tokenPrice: TokenBalance
canInvestRedeem: boolean
}

const columns: Column[] = [
{
align: 'left',
header: 'Token',
cell: (token: Row) => {
return <TokenWithIcon {...token} />
},
width: '2fr',
},
{
header: 'Token price',
cell: ({ tokenPrice }: Row) => {
return (
<Text textOverflow="ellipsis" variant="body3">
{formatBalance(tokenPrice.toDecimal() || 1, 'USDT', 4)}
</Text>
)
},
},
{
header: <SortableTableHeader label="Position" />,
cell: ({ currency, position }: Row) => {
return (
<Text textOverflow="ellipsis" variant="body3">
{formatBalance(position, currency.symbol)}
</Text>
)
},
sortKey: 'position',
},
{
header: <SortableTableHeader label="Market value" />,
cell: ({ marketValue }: Row) => {
return (
<Text textOverflow="ellipsis" variant="body3">
{formatBalance(marketValue, 'USDT', 4)}
</Text>
)
},
sortKey: 'marketValue',
},
{
align: 'left',
header: '', // invest redeem buttons
cell: ({ canInvestRedeem, poolId }: Row) => {
const isTinlakePool = poolId.startsWith('0x')
return (
canInvestRedeem && (
<Shelf gap={1} justifySelf="end">
{isTinlakePool ? (
<AnchorButton
variant="tertiary"
small
icon={IconExternalLink}
href="https://legacy.tinlake.centrifuge.io/portfolio"
target="_blank"
>
View on Tinlake
</AnchorButton>
) : (
<>
<Button small variant="tertiary" icon={IconMinus}>
Redeem
</Button>
<Button small variant="tertiary" icon={IconPlus}>
Invest
</Button>
</>
)}
</Shelf>
)
)
},
},
]

// TODO: change canInvestRedeem to default to true once the drawer is implemented
export const InvestedTokens = ({ canInvestRedeem = false }) => {
const [sortOptions, setSortOptions] = useState<SortOptions>({ sortBy: 'position', sortDirection: 'desc' })

const address = useAddress()
const centBalances = useBalances(address)
const { data: tinlakeBalances } = useTinlakeBalances()

const { data: tinlakePools } = useTinlakePools()
const pools = usePools()

const balances = useMemo(() => {
Expand All @@ -34,62 +121,49 @@ export const InvestedTokens = ({ canInvestRedeem = false }) => {
]
}, [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',
}))
}
const tableData = balances.map((balance) => {
const pool = pools?.find((pool) => pool.id === balance.poolId)
const tranche = pool?.tranches.find((tranche) => tranche.id === balance.trancheId)
return {
currency: balance.currency,
poolId: balance.poolId,
trancheId: balance.trancheId,
position: balance.balance,
tokenPrice: tranche?.tokenPrice || Dec(1),
marketValue: tranche?.tokenPrice ? balance.balance.toDecimal().mul(tranche?.tokenPrice.toDecimal()) : Dec(0),
canInvestRedeem,
}
})

return sortedTokens.length ? (
return tableData.length ? (
<Stack as="article" gap={2}>
<Text as="h2" variant="heading2">
Portfolio
</Text>

<Box overflow="auto">
<Grid gridTemplateColumns={COLUMN_GAPS} gap={3} alignItems="start" px={2}>
<Text as="span" variant="body3">
Token
</Text>

<FilterButton forwardedAs="span" variant="body3" onClick={() => handleSort('position')}>
Position
<SortChevrons
sorting={{ isActive: sortOptions.sortBy === 'position', direction: sortOptions.sortDirection }}
/>
</FilterButton>

<Text as="span" variant="body3">
Token price
</Text>

<FilterButton forwardedAs="span" variant="body3" onClick={() => handleSort('market-value')}>
Market Value
<SortChevrons
sorting={{ isActive: sortOptions.sortBy === 'market-value', direction: sortOptions.sortDirection }}
/>
</FilterButton>
</Grid>

<Stack as="ul" role="list" gap={1} py={1}>
{balances.map((balance, index) => (
<TokenListItem key={index} canInvestRedeem={canInvestRedeem} {...balance} />
))}
</Stack>
</Box>
<DataTable columns={columns} data={tableData} />
</Stack>
) : null
}

const TokenWithIcon = ({ poolId, currency }: Row) => {
const pool = usePool(poolId, false)
const { data: metadata } = usePoolMetadata(pool)
const cent = useCentrifuge()
const { sizes } = useTheme()
const icon = metadata?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metadata.pool.icon.uri) : null
return (
<Grid as="header" gridTemplateColumns={`${sizes.iconMedium}px 1fr`} alignItems="center" gap={2}>
<Eththumbnail show={!!poolId.startsWith('0x')}>
{icon ? (
<Box as="img" src={icon} alt="" height="iconMedium" width="iconMedium" />
) : (
<Thumbnail type="pool" label="LP" size="small" />
)}
</Eththumbnail>

<Text textOverflow="ellipsis" variant="body3">
{currency.name}
</Text>
</Grid>
)
}
Loading

0 comments on commit 38dec5a

Please sign in to comment.