diff --git a/centrifuge-app/src/components/ListItemCardStyles.tsx b/centrifuge-app/src/components/ListItemCardStyles.tsx index e06ab21f8..0b1a290d1 100644 --- a/centrifuge-app/src/components/ListItemCardStyles.tsx +++ b/centrifuge-app/src/components/ListItemCardStyles.tsx @@ -34,3 +34,7 @@ export const Ellipsis = styled.span` overflow: hidden; text-overflow: ellipsis; ` +export const CardHeader = styled(Box)` + display: flex; + justify-content: space-between; +` diff --git a/centrifuge-app/src/components/PoolCard/PoolStatus.tsx b/centrifuge-app/src/components/PoolCard/PoolStatus.tsx index ad254c685..ef76f5071 100644 --- a/centrifuge-app/src/components/PoolCard/PoolStatus.tsx +++ b/centrifuge-app/src/components/PoolCard/PoolStatus.tsx @@ -3,7 +3,7 @@ import { StatusChip, StatusChipProps } from '@centrifuge/fabric' export type PoolStatusKey = 'Open for investments' | 'Closed' | 'Upcoming' | 'Archived' const statusColor: { [key in PoolStatusKey]: StatusChipProps['status'] } = { - 'Open for investments': 'ok', + 'Open for investments': 'warning', Closed: 'default', Upcoming: 'default', Archived: 'default', diff --git a/centrifuge-app/src/components/PoolCard/index.tsx b/centrifuge-app/src/components/PoolCard/index.tsx index 1f3e9495f..8befebd01 100644 --- a/centrifuge-app/src/components/PoolCard/index.tsx +++ b/centrifuge-app/src/components/PoolCard/index.tsx @@ -1,18 +1,48 @@ -import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath' -import { Rate } from '@centrifuge/centrifuge-js' -import { Box, Grid, Text, TextWithPlaceholder, Thumbnail } from '@centrifuge/fabric' +import { CurrencyBalance, Rate } from '@centrifuge/centrifuge-js' +import { formatBalance } from '@centrifuge/centrifuge-react' +import { Box, Card, Divider, Stack, Text, Thumbnail } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' -import { useTheme } from 'styled-components' -import { formatBalance, formatPercentage } from '../../utils/formatting' -import { useIsAboveBreakpoint } from '../../utils/useIsAboveBreakpoint' +import { useNavigate } from 'react-router' +import styled from 'styled-components' +import { formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting' import { Eththumbnail } from '../EthThumbnail' -import { Anchor, Ellipsis, Root } from '../ListItemCardStyles' -import { Tooltips } from '../Tooltips' +import { CardHeader } from '../ListItemCardStyles' +import { RouterTextLink } from '../TextLink' import { PoolStatus, PoolStatusKey } from './PoolStatus' -const columns_base = 'minmax(150px, 2fr) minmax(100px, 1fr) 140px 70px 150px' -const columns_extended = 'minmax(200px, 2fr) minmax(100px, 1fr) 140px 100px 150px' -export const COLUMNS = ['minmax(100px, 1fr) 1fr', 'minmax(100px, 1fr) 1fr', columns_base, columns_extended] -export const COLUMN_GAPS = [3, 3, 6, 8] + +type TrancheData = { + name: string + apr: string + minInvestment: string +} + +export type InnerMetadata = { + minInitialInvestment?: CurrencyBalance +} + +export type MetaData = { + tranches: { + [key: string]: InnerMetadata + } +} +export type Tranche = { + id: string + currency: { + name: string + decimals: CurrencyBalance | number + } + interestRatePerSec: { + toAprPercent: () => Decimal + } | null + capacity?: CurrencyBalance | number + metadata?: MetaData +} + +const StyledRouterTextLink = styled(RouterTextLink)` + font-size: 12px; + margin-top: 8px; + text-decoration: none; +` export type PoolCardProps = { poolId?: string @@ -23,7 +53,8 @@ export type PoolCardProps = { apr?: Rate | null | undefined status?: PoolStatusKey iconUri?: string - isLoading?: boolean + tranches?: Tranche[] + metaData?: MetaData } export function PoolCard({ @@ -32,100 +63,116 @@ export function PoolCard({ assetClass, valueLocked, currencySymbol, - apr, status, iconUri, - isLoading, + tranches, + metaData, }: PoolCardProps) { - const isMedium = useIsAboveBreakpoint('M') - const basePath = useBasePath('/pools') - const { sizes, zIndices } = useTheme() - - return ( - - - - - {iconUri ? ( - - ) : ( - - )} - - - - {name} - - - - {isMedium && ( - - {assetClass} - - )} + const navigate = useNavigate() + const isOneTranche = tranches && tranches?.length === 1 + const renderText = (text: string) => ( + + {text} + + ) - - {valueLocked ? formatBalance(valueLocked, currencySymbol) : '-'} - + const tranchesData: TrancheData[] = tranches?.map((tranche: Tranche) => { + const words = tranche.currency.name.trim().split(' ') + const metadata = metaData?.tranches[tranche.id] ?? null + const trancheName = words[words.length - 1] - {isMedium && ( - - - {apr ? ( - formatPercentage(apr.toAprPercent(), true, { - minimumFractionDigits: 1, - maximumFractionDigits: 1, - }) - ) : poolId === '4139607887' ? ( - - - 5.0% - - target{' '} - - } - /> - ) : poolId === '1655476167' ? ( - - - 15.0% - - target{' '} - - } - /> - ) : ( - '—' - )} - - {status === 'Upcoming' && apr ? target : ''} - - )} + return { + name: trancheName, + apr: tranche.interestRatePerSec + ? formatPercentage(tranche.interestRatePerSec.toAprPercent(), true, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }) + : '-', + minInvestment: + metadata && metadata.minInitialInvestment + ? formatBalanceAbbreviated(metadata.minInitialInvestment, '', 0) + : '-', + } + }) as TrancheData[] - {isMedium && ( - - - + return ( + navigate(`/pools/${poolId}`)} + padding={18} + style={{ cursor: 'pointer' }} + height={320} + > + + + + + {name} + + + + {iconUri ? ( + + ) : ( + + )} + + + + + + TVL ({currencySymbol}) + + {valueLocked ? formatBalance(valueLocked, '') : '-'} + + + {!isOneTranche && ( + + + Tranches + + {tranchesData?.map((tranche) => renderText(tranche.name))} + {tranches && tranches.length > 2 ? ( + View all + ) : null} + )} - - - {status === 'Upcoming' ? null : } - + + + APY + + {tranchesData?.map((tranche) => renderText(`${tranche.apr}`))} + + + + Min Investment + + {tranchesData?.map((tranche) => renderText(`${tranche.minInvestment}`))} + + + + + Fully onchain, BVI-licensed fund holding T-Bills with a maximum maturity of 6 months. + + + + Asset Type + {assetClass ?? '-'} + + + Investor Type + Non-US professionals + + ) } diff --git a/centrifuge-app/src/components/PoolList.tsx b/centrifuge-app/src/components/PoolList.tsx index f0881b5c4..26680ead8 100644 --- a/centrifuge-app/src/components/PoolList.tsx +++ b/centrifuge-app/src/components/PoolList.tsx @@ -1,6 +1,6 @@ import Centrifuge, { Pool, PoolMetadata } from '@centrifuge/centrifuge-js' import { useCentrifuge } from '@centrifuge/centrifuge-react' -import { Box, Grid, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { useLocation } from 'react-router' import styled from 'styled-components' @@ -9,11 +9,9 @@ import { TinlakePool } from '../utils/tinlake/useTinlakePools' import { useIsAboveBreakpoint } from '../utils/useIsAboveBreakpoint' import { useListedPools } from '../utils/useListedPools' import { useMetadataMulti } from '../utils/useMetadata' -import { COLUMNS, COLUMN_GAPS, PoolCard, PoolCardProps } from './PoolCard' +import { MetaData, PoolCard, PoolCardProps, Tranche } from './PoolCard' import { PoolStatusKey } from './PoolCard/PoolStatus' -import { PoolFilter } from './PoolFilter' import { filterPools } from './PoolFilter/utils' -import { ButtonTextLink } from './TextLink' export type MetaDataById = Record export type PoolMetaDataPartial = Partial | undefined @@ -31,6 +29,7 @@ export function PoolList() { const { search } = useLocation() const [showArchived, setShowArchived] = React.useState(false) const [listedPools, , metadataIsLoading] = useListedPools() + const isLarge = useIsAboveBreakpoint('L') const isMedium = useIsAboveBreakpoint('M') const centPools = listedPools.filter(({ id }) => !id.startsWith('0x')) as Pool[] @@ -67,44 +66,40 @@ export function PoolList() { } return ( - - + + - - - {!filteredPools.length ? ( - - - - No results found with these filters. Try different filters. - - - - ) : ( - - {metadataIsLoading - ? Array(6) - .fill(true) - .map((_, index) => ( - - - - )) - : filteredPools.map((pool) => ( - - - - ))} - - )} + + {metadataIsLoading + ? Array(6) + .fill(true) + .map((_, index) => ( + + + + )) + : filteredPools.map((pool) => ( + + + + ))} + {!metadataIsLoading && archivedPools.length > 0 && ( <> - - setShowArchived((show) => !show)}> - {showArchived ? 'Hide archived pools' : 'View archived pools'} - + setShowArchived((show) => !show)} + variant="body2" + > + {showArchived ? 'Hide archived pools' : 'View archived pools >'} {showArchived && } @@ -115,39 +110,21 @@ export function PoolList() { function ArchivedPools({ pools }: { pools: PoolCardProps[] }) { const isMedium = useIsAboveBreakpoint('M') - + const isLarge = useIsAboveBreakpoint('L') return ( - - - Pool name - - {isMedium && ( - - Asset class - - )} - - Value locked - - {isMedium && ( - - APR - - )} - {isMedium && ( - - Pool status - - )} - - + {pools.map((pool) => ( ))} - + ) } @@ -159,7 +136,6 @@ export function poolsToPoolCardProps( ): PoolCardProps[] { return pools.map((pool) => { const tinlakePool = pool.id?.startsWith('0x') && (pool as TinlakePool) - const mostSeniorTranche = pool?.tranches?.slice(1).at(-1) const metaData = typeof pool.metadata === 'string' ? metaDataById[pool.id] : pool.metadata return { @@ -168,16 +144,17 @@ export function poolsToPoolCardProps( assetClass: metaData?.pool?.asset.subClass, valueLocked: getPoolValueLocked(pool), currencySymbol: pool.currency.symbol, - apr: mostSeniorTranche?.interestRatePerSec, status: tinlakePool && tinlakePool.tinlakeMetadata.isArchived ? 'Archived' : tinlakePool && tinlakePool.addresses.CLERK !== undefined && tinlakePool.tinlakeMetadata.maker?.ilk ? 'Closed' - : pool.tranches.at(0)?.capacity.toFloat() // pool is displayed as "open for investments" if the most junior tranche has a capacity + : pool.tranches.at(0)?.capacity?.toFloat() // pool is displayed as "open for investments" if the most junior tranche has a capacity ? 'Open for investments' : ('Closed' as PoolStatusKey), iconUri: metaData?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metaData?.pool?.icon?.uri) : undefined, + tranches: pool.tranches as Tranche[], + metaData: metaData as MetaData, } }) } diff --git a/centrifuge-app/src/pages/Pools.tsx b/centrifuge-app/src/pages/Pools.tsx index 5bb1574a8..cd7f41d52 100644 --- a/centrifuge-app/src/pages/Pools.tsx +++ b/centrifuge-app/src/pages/Pools.tsx @@ -33,8 +33,8 @@ export default function PoolsPage() { }, []) return ( - - + + Pools of real-world assets diff --git a/fabric/src/theme/tokens/colors.ts b/fabric/src/theme/tokens/colors.ts index 5cfa4064e..4db028f5a 100644 --- a/fabric/src/theme/tokens/colors.ts +++ b/fabric/src/theme/tokens/colors.ts @@ -14,7 +14,7 @@ export const yellowScale = { 50: '#FFF4D9', 100: '#FFE299', 500: '#FFC012', - 800: '#CC9700', + 800: '#6B4F00', } export const blackScale = { diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts index c9d0ed054..c9b9bd342 100644 --- a/fabric/src/theme/tokens/theme.ts +++ b/fabric/src/theme/tokens/theme.ts @@ -3,7 +3,7 @@ import { black, blackScale, centrifugeBlue, gold, grayScale, yellowScale } from const statusDefault = grayScale[800] const statusInfo = '#1253ff' const statusOk = '#519b10' -const statusWarning = yellowScale[500] +const statusWarning = yellowScale[800] const statusCritical = '#d43f2b' const statusPromote = '#f81071' @@ -31,7 +31,7 @@ const colors = { backgroundThumbnail: grayScale[500], backgroundInverted: grayScale[900], - borderPrimary: grayScale[100], + borderPrimary: grayScale[300], borderSecondary: grayScale[300], statusDefault,