Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: composition table #1623

Merged
merged 12 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions centrifuge-app/src/components/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Text } from '@centrifuge/fabric'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broke out the code from centrifuge-app/src/components/PoolFilter/styles.ts to different components so that we can use it outside of the context of PoolFilter.

import styled from 'styled-components'
import { buttonActionStyles } from './styles'

export const FilterButton = styled(Text)`
display: flex;
align-items: center;
gap: 0.3em;

${buttonActionStyles}
`
2 changes: 1 addition & 1 deletion centrifuge-app/src/components/PoolCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion centrifuge-app/src/components/PoolFilter/FilterMenu.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -22,12 +23,12 @@
const searchParams = new URLSearchParams(search)
searchParams.delete(searchKey)
return searchParams
}, [search])

Check warning on line 26 in centrifuge-app/src/components/PoolFilter/FilterMenu.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has a missing dependency: 'searchKey'. Either include it or remove the dependency array

const selectedOptions = React.useMemo(() => {
const searchParams = new URLSearchParams(search)
return searchParams.getAll(searchKey)
}, [search])

Check warning on line 31 in centrifuge-app/src/components/PoolFilter/FilterMenu.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has a missing dependency: 'searchKey'. Either include it or remove the dependency array

function handleChange() {
const formData = new FormData(form.current ?? undefined)
Expand Down
25 changes: 5 additions & 20 deletions centrifuge-app/src/components/PoolFilter/SortButton.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -28,7 +29,7 @@
isActive: searchParams.get(SEARCH_KEYS.SORT_BY) === searchKey,
direction: searchParams.get(SEARCH_KEYS.SORT),
}
}, [search])

Check warning on line 32 in centrifuge-app/src/components/PoolFilter/SortButton.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has a missing dependency: 'searchKey'. Either include it or remove the dependency array

function handleClick() {
const restSearchParams = new URLSearchParams(search)
Expand Down Expand Up @@ -58,13 +59,13 @@
? `Sort ${label} descending`
: `Sort ${label} ascending`
}
aria-live

Check warning on line 62 in centrifuge-app/src/components/PoolFilter/SortButton.tsx

View workflow job for this annotation

GitHub Actions / build-app

The value for aria-live must be a single token from the following: assertive,off,polite
style={{ justifySelf }}
>
<FilterButton forwardedAs="span" variant="body3">
{label}

<Inner sorting={sorting} />
<SortChevrons sorting={sorting} />
</FilterButton>
</Tooltip>
)
Expand All @@ -82,28 +83,12 @@
? `Sort ${label} descending`
: `Sort ${label} ascending`
}
aria-live

Check warning on line 86 in centrifuge-app/src/components/PoolFilter/SortButton.tsx

View workflow job for this annotation

GitHub Actions / build-app

The value for aria-live must be a single token from the following: assertive,off,polite
style={{ justifySelf }}
>
{label}

<Inner sorting={sorting} />
<SortChevrons sorting={sorting} />
</FilterButton>
)
}

function Inner({ sorting }: { sorting: Sorting }) {
return (
<Stack as="span" width="1em">
<IconChevronUp
size="1em"
color={sorting.isActive && sorting.direction === 'asc' ? 'textSelected' : 'textSecondary'}
/>
<IconChevronDown
size="1em"
color={sorting.isActive && sorting.direction === 'desc' ? 'textSelected' : 'textSecondary'}
style={{ marginTop: '-.4em' }}
/>
</Stack>
)
}
26 changes: 0 additions & 26 deletions centrifuge-app/src/components/PoolFilter/styles.ts

This file was deleted.

148 changes: 77 additions & 71 deletions centrifuge-app/src/components/Portfolio/InvestedTokens.tsx
Original file line number Diff line number Diff line change
@@ -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<SortOptions>({ 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 ? (
<>
<Box as="article">
<Text as="h2" variant="heading2">
Portfolio Composition
</Text>
</Box>
<Stack gap={1}>
<Grid gridTemplateColumns={TOKEN_ITEM_COLUMNS} gap={TOKEN_ITEM_GAP} px={2}>
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 ? (
<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>
<Text as="button" variant="body3">

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

<Text as="span" variant="body3">
Token price
</Text>
<Text as="button" variant="body3">
Market value
</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}>
{balances.tranches.map((tranche, index) => (
<Box key={`${tranche.trancheId}${index}`} as="li">
<TokenListItem {...tranche} />
</Box>
<Stack as="ul" role="list" gap={1} py={1}>
{balances.map((balance, index) => (
<TokenListItem key={index} canInvestRedeem={canInvestRedeem} {...balance} />
))}
</Stack>
</Stack>
</>
</Box>
</Stack>
) : 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 (
<Grid
gridTemplateColumns={TOKEN_ITEM_COLUMNS}
gap={TOKEN_ITEM_GAP}
padding={2}
borderStyle="solid"
borderWidth={1}
borderColor="borderSecondary"
>
<Text as="span" variant="body2">
{currency.name}
</Text>

<Text as="span" variant="body2">
{formatBalance(balance, tranche?.currency.symbol)}
</Text>

<Text as="span" variant="body2">
{tranche?.tokenPrice ? formatBalance(tranche.tokenPrice.toDecimal(), tranche.currency.symbol, 4) : '-'}
</Text>

<Text as="span" variant="body2">
{tranche?.tokenPrice
? formatBalance(balance.toDecimal().mul(tranche.tokenPrice.toDecimal()), tranche.currency.symbol, 4)
: '-'}
</Text>
</Grid>
)
}
100 changes: 100 additions & 0 deletions centrifuge-app/src/components/Portfolio/TokenListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Root as="article" minWidth={canInvestRedeem ? '960px' : '750px'}>
<Grid gridTemplateColumns={`${COLUMN_GAPS} 1fr`} gap={3} p={2} alignItems="center">
<Grid as="header" gridTemplateColumns={`${sizes.iconMedium}px 1fr`} alignItems="center" gap={2}>
<Eththumbnail show={isTinlakePool}>
{icon ? (
<Box as="img" src={icon} alt="" height="iconMedium" width="iconMedium" />
) : (
<Thumbnail type="pool" label="LP" size="small" />
)}
</Eththumbnail>

<TokenName textOverflow="ellipsis" variant="body2">
{currency.name}
</TokenName>
</Grid>

<Text textOverflow="ellipsis" variant="body2">
{formatBalance(balance, currency.symbol)}
</Text>

<Text textOverflow="ellipsis" variant="body2">
{trancheInfo?.tokenPrice
? formatBalance(trancheInfo.tokenPrice.toDecimal(), trancheInfo.currency.symbol, 4)
: '-'}
</Text>

<Text textOverflow="ellipsis" variant="body2">
{trancheInfo?.tokenPrice
? formatBalance(balance.toDecimal().mul(trancheInfo.tokenPrice.toDecimal()), trancheInfo.currency.symbol, 4)
: '-'}
</Text>

{canInvestRedeem && (
<Shelf gap={2} justifySelf="end">
{isTinlakePool ? (
<AnchorButton
variant="tertiary"
icon={IconExternalLink}
href="https://legacy.tinlake.centrifuge.io/portfolio"
target="_blank"
>
View on Tinlake
</AnchorButton>
) : (
<>
<Button variant="tertiary" icon={IconMinus}>
Redeem
</Button>
<Button variant="tertiary" icon={IconPlus}>
Invest
</Button>
</>
)}
</Shelf>
)}
</Grid>
</Root>
)
}
Loading
Loading