Skip to content

Commit

Permalink
Assets page redesign (#2513)
Browse files Browse the repository at this point in the history
* Add updates to asset loan list

* Add updates to asset detail page

* Fabric and types updates

* Redesign side drawer variations

* Update toast

* Fix TS warnings

* Add feedback review

* Adjust asset performance chart & make table responsive

* Fix total assets

* Add feedback review

* Add feedback qa

* Fix ts warnings

* Add tooltip to total nav

* Fix bug on path to view all transactions

* Add chart to onchain reserve

* Add loading buttons to side drawer

* Add feedback

* Add ongoing assets

* Add underline

* Fix table sorting & download icon
  • Loading branch information
kattylucy authored Nov 6, 2024
1 parent c043950 commit bf5f6a9
Show file tree
Hide file tree
Showing 46 changed files with 1,688 additions and 1,197 deletions.
38 changes: 15 additions & 23 deletions centrifuge-app/src/components/AssetSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { Loan, TinlakeLoan } from '@centrifuge/centrifuge-js'
import { Box, Shelf, Stack, Text } from '@centrifuge/fabric'
import { Shelf, Stack, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useTheme } from 'styled-components'
import { LoanLabel } from './LoanLabel'

type Props = {
data?: {
label: React.ReactNode
value: React.ReactNode
heading: boolean
}[]
children?: React.ReactNode
loan?: Loan | TinlakeLoan
}

export function AssetSummary({ data, children, loan }: Props) {
export function AssetSummary({ data, children }: Props) {
const theme = useTheme()
return (
<Stack bg={theme.colors.backgroundSecondary} pl={3}>
<Box paddingTop={3}>
<Shelf gap="2">
<Text variant="heading2">Details</Text>
{loan && <LoanLabel loan={loan} />}
</Shelf>
</Box>
<Shelf
gap="6"
py="3"
style={{
boxShadow: `0 1px 0 ${theme.colors.borderPrimary}`,
}}
>
{data?.map(({ label, value }, index) => (
<Stack gap="4px" key={`${value}-${label}-${index}`}>
<Text variant="body3" style={{ fontWeight: 500 }}>
<Stack
bg={theme.colors.backgroundSecondary}
border={`1px solid ${theme.colors.borderSecondary}`}
borderRadius={10}
padding={2}
mx={[2, 2, 2, 2, 5]}
>
<Shelf gap={2}>
{data?.map(({ label, value, heading }, index) => (
<Stack key={`${value}-${label}-${index}`}>
<Text variant={heading ? 'body2' : 'body3'} color="textSecondary" style={{ margin: 0, padding: 0 }}>
{label}
</Text>
<Text variant="body2">{value}</Text>
<Text variant={heading ? 'heading' : 'heading2'}>{value}</Text>
</Stack>
))}
{children}
Expand Down
197 changes: 95 additions & 102 deletions centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { CurrencyBalance, Pool } from '@centrifuge/centrifuge-js'
import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Text } from '@centrifuge/fabric'
import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import styled, { useTheme } from 'styled-components'
import { useTheme } from 'styled-components'
import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl'
import { formatDate } from '../../utils/date'
import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting'
import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
import { TinlakePool } from '../../utils/tinlake/useTinlakePools'
import { useLoan } from '../../utils/useLoans'
import { useAssetSnapshots } from '../../utils/usePools'
Expand All @@ -25,50 +25,13 @@ interface Props {
loanId: string
}

const FilterButton = styled(Stack)`
&:hover {
cursor: pointer;
}
`

const filterOptions = [
{ value: 'price', label: 'Price' },
{ value: 'value', label: 'Asset value' },
] as const

function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
const theme = useTheme()
const chartColor = theme.colors.accentPrimary
const asset = useLoan(poolId, loanId)
const assetSnapshots = useAssetSnapshots(poolId, loanId)

const [activeFilter, setActiveFilter] = React.useState<(typeof filterOptions)[number]>(filterOptions[0])

React.useEffect(() => {
if (assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') {
setActiveFilter(filterOptions[1])
}
}, [assetSnapshots])

const dataUrl: any = React.useMemo(() => {
if (!assetSnapshots || !assetSnapshots?.length) {
return undefined
}

const formatted = assetSnapshots.map((assetObject: Record<string, any>) => {
const keys = Object.keys(assetObject)
const newObj: Record<string, any> = {}

keys.forEach((assetKey) => {
newObj[assetKey] =
assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey]
})

return newObj
})

return getCSVDownloadUrl(formatted as any)
}, [assetSnapshots])
const isNonCash = asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash'
const [selectedTabIndex, setSelectedTabIndex] = React.useState(isNonCash ? 0 : 1)

const data: ChartData[] = React.useMemo(() => {
if (!asset || !assetSnapshots) return []
Expand Down Expand Up @@ -157,58 +120,72 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
[data, assetSnapshots]
)

const dataUrl: any = React.useMemo(() => {
if (!assetSnapshots || !assetSnapshots?.length) {
return undefined
}

const formatted = assetSnapshots.map((assetObject: Record<string, any>) => {
const keys = Object.keys(assetObject)
const newObj: Record<string, any> = {}

keys.forEach((assetKey) => {
newObj[assetKey] =
assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey]
})

return newObj
})

return getCSVDownloadUrl(formatted as any)
}, [assetSnapshots])

if (!assetSnapshots) return <Spinner style={{ margin: 'auto', height: 350 }} />

return (
<Card p={3} height={350}>
<Card p={3} height={320} variant="secondary">
<Stack gap={2}>
<Shelf justifyContent="space-between">
<Text fontSize="18px" fontWeight="500">
{asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash'
? 'Asset performance'
: 'Cash balance'}
</Text>
{!isChartEmpty && (
<AnchorButton
href={dataUrl}
download={`asset-${loanId}-timeseries.csv`}
variant="inverted"
icon={IconDownload}
small
>
Download
</AnchorButton>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Box display="flex">
<Text variant="heading4">{isNonCash ? 'Asset performance' : 'Cash balance'}</Text>
<Text variant="body2" style={{ marginLeft: 4 }}>
({isNonCash ? pool.currency.symbol ?? 'USD' : 'USD'})
</Text>
</Box>
{!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && (
<Stack>
<Shelf justifyContent="flex-end">
{data.length > 0 && (
<Tabs selectedIndex={selectedTabIndex} onChange={(index) => setSelectedTabIndex(index)}>
<TabsItem styleOverrides={{ padding: '8px' }} showBorder variant="secondary">
Price
</TabsItem>
<TabsItem styleOverrides={{ padding: '8px' }} showBorder variant="secondary">
Asset value
</TabsItem>
</Tabs>
)}
</Shelf>
</Stack>
)}
</Shelf>

{isChartEmpty && <Text variant="label1">No data yet</Text>}

{!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && (
<Stack>
<Shelf justifyContent="flex-end">
{data.length > 0 &&
filterOptions.map((filter, index) => (
<React.Fragment key={filter.label}>
<FilterButton gap={1} onClick={() => setActiveFilter(filter)}>
<Text variant="body3" whiteSpace="nowrap">
<Text variant={filter.value === activeFilter.value && 'emphasized'}>{filter.label}</Text>
</Text>
<Box
width="100%"
backgroundColor={filter.value === activeFilter.value ? '#000000' : '#E0E0E0'}
height="2px"
/>
</FilterButton>
{index !== filterOptions.length - 1 && (
<Box width="24px" backgroundColor="#E0E0E0" height="2px" alignSelf="flex-end" />
)}
</React.Fragment>
))}
</Shelf>
</Stack>
<AnchorButton
download={`pool-${poolId}-timeseries.csv`}
href={dataUrl}
variant="inverted"
icon={IconDownload}
small
>
Download
</AnchorButton>
</Box>

{isChartEmpty && (
<Text variant="body3" style={{ margin: '80px auto 0px' }}>
No data available
</Text>
)}

<Shelf gap={4} width="100%" color="textSecondary">
<Shelf gap={4} width="100%">
{data?.length ? (
<ResponsiveContainer width="100%" height={200} minHeight={200} maxHeight={200}>
<LineChart data={data} margin={{ left: -36 }}>
Expand All @@ -225,7 +202,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
tickFormatter={(tick: number) => {
return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' })
}}
style={{ fontSize: 8, fill: theme.colors.textSecondary, letterSpacing: '-0.7px' }}
style={{ fontSize: 8, fill: theme.colors.textPrimary, letterSpacing: '-0.7px' }}
dy={4}
interval={10}
angle={-40}
Expand All @@ -234,9 +211,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
<YAxis
stroke="none"
tickLine={false}
style={{ fontSize: '10px', fill: theme.colors.textSecondary }}
style={{ fontSize: '10px', fill: theme.colors.textPrimary }}
tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 2)}
domain={activeFilter.value === 'price' ? priceRange : [0, 'auto']}
domain={selectedTabIndex === 0 ? priceRange : ['auto', 'auto']}
width={90}
/>
<CartesianGrid stroke={theme.colors.borderPrimary} vertical={false} />
Expand All @@ -249,22 +226,38 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
{payload.map(({ value }, index) => (
<>
<Shelf justifyContent="space-between" pl="4px" key={index}>
<Text variant="label2">{'Value'}</Text>
<Text variant="label2">
<Text variant="body3">Value</Text>
<Text variant="body3">
{payload[0].payload.historicPV
? formatBalance(payload[0].payload.historicPV, 'USD', 2)
? formatBalance(
payload[0].payload.historicPV,
isNonCash ? pool.currency.symbol : 'USD',
2
)
: payload[0].payload.futurePV
? `~${formatBalance(payload[0].payload.futurePV, 'USD', 2)}`
? `~${formatBalance(
payload[0].payload.futurePV,
isNonCash ? pool.currency.symbol : 'USD',
2
)}`
: '-'}
</Text>
</Shelf>
<Shelf justifyContent="space-between" pl="4px" key={index}>
<Text variant="label2">{'Price'}</Text>
<Text variant="label2">
<Text variant="body3">Price</Text>
<Text variant="body3">
{payload[0].payload.historicPrice
? formatBalance(payload[0].payload.historicPrice, 'USD', 6)
? formatBalance(
payload[0].payload.historicPrice,
isNonCash ? pool.currency.symbol : 'USD',
6
)
: payload[0].payload.futurePrice
? `~${formatBalance(payload[0].payload.futurePrice, 'USD', 6)}`
? `~${formatBalance(
payload[0].payload.futurePrice,
isNonCash ? pool.currency.symbol : 'USD',
6
)}`
: '-'}
</Text>
</Shelf>
Expand All @@ -277,7 +270,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
}}
/>

{activeFilter.value === 'price' && (
{selectedTabIndex === 0 && (
<Line
type="monotone"
dataKey="historicPrice"
Expand All @@ -286,7 +279,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
dot={false}
/>
)}
{activeFilter.value === 'price' && (
{selectedTabIndex === 0 && (
<Line
type="monotone"
dataKey="futurePrice"
Expand All @@ -297,7 +290,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
/>
)}

{activeFilter.value === 'value' && (
{selectedTabIndex === 1 && (
<Line
type="monotone"
dataKey="historicPV"
Expand All @@ -306,7 +299,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
dot={false}
/>
)}
{activeFilter.value === 'value' && (
{selectedTabIndex === 1 && (
<Line
type="monotone"
dataKey="futurePV"
Expand Down
Loading

0 comments on commit bf5f6a9

Please sign in to comment.