From bf5f6a9a87409e291a7b8c72b8cababaccbe1d61 Mon Sep 17 00:00:00 2001 From: Katty Barroso <51223655+kattylucy@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:19:33 +0100 Subject: [PATCH] Assets page redesign (#2513) * 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 --- .../src/components/AssetSummary.tsx | 38 +- .../Charts/AssetPerformanceChart.tsx | 197 ++++---- .../src/components/Charts/SimpleLineChart.tsx | 102 ++++ centrifuge-app/src/components/DataTable.tsx | 1 - .../InvestRedeemCentrifugeProvider.tsx | 2 +- .../InvestRedeem/InvestRedeemDrawer.tsx | 2 +- .../src/components/LayoutBase/styles.tsx | 2 +- centrifuge-app/src/components/LoanList.tsx | 453 ++++++++++-------- centrifuge-app/src/components/PageSection.tsx | 12 +- centrifuge-app/src/components/PageSummary.tsx | 26 +- .../src/components/PoolCard/index.tsx | 28 +- .../src/components/PoolFees/index.tsx | 2 +- centrifuge-app/src/components/PoolList.tsx | 3 - .../PoolOverview/TrancheTokenCards.tsx | 29 +- .../PoolOverview/TransactionHistory.tsx | 21 +- .../src/components/Portfolio/Transactions.tsx | 2 - .../src/components/Report/BalanceSheet.tsx | 2 +- centrifuge-app/src/components/Tooltips.tsx | 4 + .../src/pages/Loan/ChargeFeesFields.tsx | 14 +- .../src/pages/Loan/CorrectionForm.tsx | 223 +++++---- .../src/pages/Loan/ExternalFinanceForm.tsx | 258 ++++++---- .../src/pages/Loan/ExternalRepayForm.tsx | 281 ++++++----- centrifuge-app/src/pages/Loan/FinanceForm.tsx | 257 +++++----- .../src/pages/Loan/HoldingsValues.tsx | 6 +- centrifuge-app/src/pages/Loan/KeyMetrics.tsx | 14 +- .../src/pages/Loan/MetricsTable.tsx | 12 +- .../src/pages/Loan/PricingValues.tsx | 12 +- centrifuge-app/src/pages/Loan/RepayForm.tsx | 354 ++++++++------ .../src/pages/Loan/TransactionTable.tsx | 28 +- centrifuge-app/src/pages/Loan/index.tsx | 249 ++++++---- .../src/pages/Pool/Assets/OffchainMenu.tsx | 87 ++++ .../src/pages/Pool/Assets/index.tsx | 103 ++-- centrifuge-js/src/modules/pools.ts | 1 + centrifuge-js/src/types/subquery.ts | 1 + .../components/WalletMenu/ConnectButton.tsx | 2 +- fabric/src/components/Button/WalletButton.tsx | 1 - fabric/src/components/Card/index.ts | 6 +- .../src/components/InlineFeedback/index.tsx | 2 +- fabric/src/components/InputUnit/index.tsx | 4 +- fabric/src/components/Toast/index.tsx | 10 +- fabric/src/components/Tooltip/index.tsx | 15 +- fabric/src/icon-svg/icon-chevron-down.svg | 4 +- fabric/src/icon-svg/icon-download.svg | 4 +- fabric/src/theme/tokens/theme.ts | 4 +- fabric/src/theme/tokens/typography.ts | 6 + fabric/src/theme/types.ts | 1 + 46 files changed, 1688 insertions(+), 1197 deletions(-) create mode 100644 centrifuge-app/src/components/Charts/SimpleLineChart.tsx create mode 100644 centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx diff --git a/centrifuge-app/src/components/AssetSummary.tsx b/centrifuge-app/src/components/AssetSummary.tsx index 3b078fd42f..a3cb456af4 100644 --- a/centrifuge-app/src/components/AssetSummary.tsx +++ b/centrifuge-app/src/components/AssetSummary.tsx @@ -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 ( - - - - Details - {loan && } - - - - {data?.map(({ label, value }, index) => ( - - + + + {data?.map(({ label, value, heading }, index) => ( + + {label} - {value} + {value} ))} {children} diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 85cf10f542..19e0a70428 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -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' @@ -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) => { - const keys = Object.keys(assetObject) - const newObj: Record = {} - - 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 [] @@ -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) => { + const keys = Object.keys(assetObject) + const newObj: Record = {} + + 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 return ( - + - - - {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' - ? 'Asset performance' - : 'Cash balance'} - - {!isChartEmpty && ( - - Download - + + + {isNonCash ? 'Asset performance' : 'Cash balance'} + + ({isNonCash ? pool.currency.symbol ?? 'USD' : 'USD'}) + + + {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( + + + {data.length > 0 && ( + setSelectedTabIndex(index)}> + + Price + + + Asset value + + + )} + + )} - - - {isChartEmpty && No data yet} - - {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( - - - {data.length > 0 && - filterOptions.map((filter, index) => ( - - setActiveFilter(filter)}> - - {filter.label} - - - - {index !== filterOptions.length - 1 && ( - - )} - - ))} - - + + Download + + + + {isChartEmpty && ( + + No data available + )} - + {data?.length ? ( @@ -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} @@ -234,9 +211,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { formatBalanceAbbreviated(tick, '', 2)} - domain={activeFilter.value === 'price' ? priceRange : [0, 'auto']} + domain={selectedTabIndex === 0 ? priceRange : ['auto', 'auto']} width={90} /> @@ -249,22 +226,38 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {payload.map(({ value }, index) => ( <> - {'Value'} - + Value + {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 + )}` : '-'} - {'Price'} - + Price + {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 + )}` : '-'} @@ -277,7 +270,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { }} /> - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( { + const theme = useTheme() + const chartColor = theme.colors.accentPrimary + + return ( + + + {!data.length && ( + + No data available + + )} + + + {data?.length ? ( + + + + + + + + + { + return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) + }} + style={{ fontSize: 8, fill: theme.colors.textPrimary, letterSpacing: '-0.7px' }} + dy={4} + interval={10} + angle={-40} + textAnchor="end" + /> + { + const balance = new CurrencyBalance(tick, currency?.decimals || 0) + return formatBalanceAbbreviated(balance, '', 0) + }} + width={90} + /> + + { + if (payload && payload?.length > 0) { + return ( + + {formatDate(payload[0].payload.name)} + {payload.map(({ value }, index) => { + return ( + + Value + + {formatBalance( + new CurrencyBalance(value?.toString() ?? 0, currency?.decimals || 0), + 'USD', + 2, + 2 + )} + + + ) + })} + + ) + } + return null + }} + /> + + + + + ) : null} + + + + ) +} diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index 37c927dbe0..4a49bd0efe 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -486,6 +486,5 @@ const StyledHeader = styled(Text)` &:hover, &:focus-visible { cursor: pointer; - color: ${({ theme }) => theme.colors.textInteractiveHover}; } ` diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx index f4228c8f34..237ffeb66c 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx @@ -97,7 +97,7 @@ export function InvestRedeemCentrifugeProvider({ poolId, trancheId, children }: }, 300) return () => clearTimeout(timer) - }, [isDataLoading]) + }, [isDataLoading, connectedType]) const state: InvestRedeemState = { poolId, diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx index 2e94a167f0..545298dc85 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx @@ -74,7 +74,7 @@ export function InvestRedeemDrawer({ ) return { sumRealizedProfitFifoByPeriod, sumUnrealizedProfitAtMarketPrice } - }, [dailyPoolStates]) + }, [dailyPoolStates, pool.currency.decimals]) return ( diff --git a/centrifuge-app/src/components/LayoutBase/styles.tsx b/centrifuge-app/src/components/LayoutBase/styles.tsx index 9faef617e6..e51f59ff7a 100644 --- a/centrifuge-app/src/components/LayoutBase/styles.tsx +++ b/centrifuge-app/src/components/LayoutBase/styles.tsx @@ -142,7 +142,7 @@ export const WalletInner = styled(Stack)` height: 80px; justify-content: center; pointer-events: auto; - width: 250px; + width: 200px; @media (min-width: ${({ theme }) => theme.breakpoints[BREAK_POINT_COLUMNS]}) { justify-content: flex-end; diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 9b5876f27f..18be9bec1a 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -1,8 +1,10 @@ import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath' -import { CurrencyBalance, Loan, Rate, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { CurrencyBalance, Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js' import { + AnchorButton, Box, - IconChevronRight, + Button, + IconDownload, Pagination, PaginationContainer, Shelf, @@ -13,105 +15,124 @@ import { } from '@centrifuge/fabric' import get from 'lodash/get' import * as React from 'react' -import { useParams } from 'react-router' -import currencyDollar from '../assets/images/currency-dollar.svg' -import daiLogo from '../assets/images/dai-logo.svg' -import usdcLogo from '../assets/images/usdc-logo.svg' -import { formatNftAttribute } from '../pages/Loan/utils' +import { useNavigate, useParams } from 'react-router' +import { TinlakePool } from 'src/utils/tinlake/useTinlakePools' +import { formatNftAttribute } from '../../src/pages/Loan/utils' +import { LoanTemplate, LoanTemplateAttribute } from '../../src/types' +import { getCSVDownloadUrl } from '../../src/utils/getCSVDownloadUrl' import { nftMetadataSchema } from '../schemas' -import { LoanTemplate, LoanTemplateAttribute } from '../types' import { formatDate } from '../utils/date' -import { formatBalance } from '../utils/formatting' +import { formatBalance, formatPercentage } from '../utils/formatting' import { useFilters } from '../utils/useFilters' import { useMetadata } from '../utils/useMetadata' import { useCentNFT } from '../utils/useNFTs' -import { usePool, usePoolMetadata } from '../utils/usePools' -import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from './DataTable' +import { useAllPoolAssetSnapshots, usePool, usePoolMetadata } from '../utils/usePools' +import { Column, DataTable, SortableTableHeader } from './DataTable' import { LoadBoundary } from './LoadBoundary' -import { LoanLabel, getLoanLabelStatus } from './LoanLabel' import { prefetchRoute } from './Root' -import { Tooltips } from './Tooltips' type Row = (Loan | TinlakeLoan) & { idSortKey: number originationDateSortKey: string status: 'Created' | 'Active' | 'Closed' | '' maturityDate: string | null + marketPrice: CurrencyBalance + marketValue: CurrencyBalance + unrealizedPL: CurrencyBalance + realizedPL: CurrencyBalance + portfolioPercentage: string } type Props = { loans: Loan[] | TinlakeLoan[] } -const getLoanStatus = (loan: Loan | TinlakeLoan) => { - const [labelType, label] = getLoanLabelStatus(loan) - - if (label.includes('Due')) { - return labelType === 'critical' ? 'Overdue' : 'Ongoing' - } - - return label -} - export function LoanList({ loans }: Props) { const { pid: poolId } = useParams<{ pid: string }>() if (!poolId) throw new Error('Pool not found') + const navigate = useNavigate() const pool = usePool(poolId) const isTinlakePool = poolId?.startsWith('0x') const basePath = useBasePath() - + const snapshots = useAllPoolAssetSnapshots(pool.id, new Date().toString()) + const loansData = isTinlakePool + ? loans + : (loans ?? []).filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash') const { data: poolMetadata } = usePoolMetadata(pool) const templateIds = poolMetadata?.loanTemplates?.map((s) => s.id) ?? [] const templateId = templateIds.at(-1) const { data: templateMetadata } = useMetadata(templateId) - const loansWithLabelStatus = React.useMemo(() => { - return loans - .filter((loan) => isTinlakePool || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash')) - .map((loan) => ({ - ...loan, - labelStatus: getLoanStatus(loan), - })) - .sort((a, b) => { - const aId = get(a, 'id') as string - const bId = get(b, 'id') as string - - return aId.localeCompare(bId) - }) - }, [isTinlakePool, loans]) - const filters = useFilters({ - data: loansWithLabelStatus, - }) - - React.useEffect(() => { - prefetchRoute('/pools/1/assets/1') - }, []) const additionalColumns: Column[] = - templateMetadata?.keyAttributes?.map((key) => { + templateMetadata?.keyAttributes?.map((key, index) => { const attr = templateMetadata.attributes![key] return { align: 'left', - header: attr.label, + header: , cell: (l: Row) => , + sortKey: attr.label.toLowerCase(), } }) || [] - const rows: Row[] = filters.data.map((loan) => ({ - nftIdSortKey: loan.asset.nftId, - idSortKey: parseInt(loan.id, 10), - outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), - originationDateSortKey: - loan.status === 'Active' && - loan?.originationDate && - 'interestRate' in loan.pricing && - !loan?.pricing.interestRate?.isZero() && - !loan?.totalBorrowed?.isZero() - ? loan.originationDate - : '', - maturityDate: loan.pricing.maturityDate, - ...loan, - })) + const snapshotsValues = + snapshots?.reduce((acc: { [key: string]: any }, snapshot) => { + const id = snapshot.assetId.split('-')[1] + acc[id] = { + marketPrice: snapshot.currentPrice, + marketValue: snapshot.presentValue, + unrealizedPL: snapshot.unrealizedProfitAtMarketPrice, + realizedPL: snapshot.sumRealizedProfitFifo, + } + return acc + }, {}) ?? {} + + const totalMarketValue = Object.values(snapshotsValues).reduce((sum, snapshot) => { + return sum + (snapshot.marketValue?.toDecimal().toNumber() ?? 0) + }, 0) + + const loansWithLabelStatus = React.useMemo(() => { + return loansData.sort((a, b) => { + const aId = get(a, 'id') as string + const bId = get(b, 'id') as string + + return aId.localeCompare(bId) + }) + }, [loansData]) + + const filters = useFilters({ + data: loansWithLabelStatus as Loan[], + }) + + React.useEffect(() => { + prefetchRoute('/pools/1/assets/1') + }, []) + + const rows: Row[] = filters.data.map((loan) => { + const snapshot = snapshotsValues?.[loan.id] + const marketValue = snapshot?.marketValue?.toDecimal().toNumber() ?? 0 + + const portfolioPercentage = + loan.status === 'Closed' || totalMarketValue === 0 ? 0 : (marketValue / totalMarketValue) * 100 + + return { + ...snapshot, + nftIdSortKey: loan.asset.nftId, + idSortKey: parseInt(loan.id, 10), + outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), + originationDateSortKey: + loan.status === 'Active' && + loan?.originationDate && + 'interestRate' in loan.pricing && + !loan?.pricing.interestRate?.isZero() && + !loan?.totalBorrowed?.isZero() + ? loan.originationDate + : '', + maturityDate: loan.pricing.maturityDate, + portfolioPercentage, + ...loan, + } + }) const hasMaturityDate = rows.some((loan) => loan.maturityDate) @@ -120,11 +141,10 @@ export function LoanList({ loans }: Props) { align: 'left', header: , cell: (l: Row) => , - sortKey: 'idSortKey', - width: 'minmax(300px, 1fr)', + sortKey: 'id', }, ...(additionalColumns?.length - ? additionalColumns + ? additionalColumns.filter((attr) => attr.sortKey !== 'term') : [ { align: 'left', @@ -137,7 +157,7 @@ export function LoanList({ loans }: Props) { ? formatDate(l.originationDate) : '-' }, - sortKey: 'originationDateSortKey', + sortKey: 'originationDate', }, ]), ...(hasMaturityDate @@ -157,98 +177,141 @@ export function LoanList({ loans }: Props) { }, ] : []), - { - align: 'right', - header: , - cell: (l: Row) => , - sortKey: 'outstandingDebtSortKey', - }, - { - align: 'left', - header: ( - labelStatus))]} - /> - ), - cell: (l: Row) => , - width: '100px', - }, - { - header: '', - cell: (l: Row) => (l.status ? : ''), - width: '52px', - }, + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => , + sortKey: 'outstandingDebt', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.marketPrice ?? 0, pool.currency, 2, 0), + sortKey: 'marketPrice', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.marketValue ?? 0, pool.currency, 2, 0), + sortKey: 'marketValue', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.unrealizedPL ?? 0, pool.currency, 2, 0), + sortKey: 'unrealizedPL', + width: '140px', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.realizedPL ?? 0, pool.currency, 2, 0), + sortKey: 'realizedPL', + width: '140px', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatPercentage(l.portfolioPercentage ?? 0, true, undefined, 1), + sortKey: 'portfolioPercentage', + }, + ]), ].filter(Boolean) as Column[] - const pinnedData: Row[] = [ - { - id: '0', - // @ts-expect-error - status: '', - poolId: pool.id, - pricing: { - valuationMethod: 'discountedCashFlow', - maxBorrowAmount: 'upToTotalBorrowed', - value: CurrencyBalance.fromFloat(0, 18), - maturityDate: '', - maturityExtensionDays: 0, - advanceRate: Rate.fromFloat(0), - interestRate: Rate.fromFloat(0), - }, - asset: { collectionId: '', nftId: '' }, - totalBorrowed: CurrencyBalance.fromFloat(0, 18), - totalRepaid: CurrencyBalance.fromFloat(0, 18), - outstandingDebt: CurrencyBalance.fromFloat(0, 18), - }, - ...loans - .filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash') - .map((loan) => { - return { - nftIdSortKey: loan.asset.nftId, - idSortKey: parseInt(loan.id, 10), - outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), - originationDateSortKey: - loan.status === 'Active' && - loan?.originationDate && - 'interestRate' in loan.pricing && - !loan?.pricing.interestRate?.isZero() && - !loan?.totalBorrowed?.isZero() - ? loan.originationDate - : '', - ...loan, - maturityDate: loan.pricing.maturityDate, - } - }), - ] - const pagination = usePagination({ data: rows, pageSize: 20 }) + const csvData = React.useMemo(() => { + if (!rows.length) return undefined + + return rows.map((loan) => { + const quantity = getAmount(loan, pool) + + return { + 'Asset ID': loan.id, + 'Maturity Date': loan.maturityDate ? loan.maturityDate : '-', + Quantity: `${quantity ?? '-'}`, + 'Market Price': loan.marketPrice ? loan.marketPrice : '-', + 'Market Value': loan.marketValue ? loan.marketValue : '-', + 'Unrealized P&L': loan.unrealizedPL ? loan.unrealizedPL : '-', + 'Realized P&L': loan.realizedPL ? loan.realizedPL : '-', + 'Portfolio %': loan.portfolioPercentage ? loan.portfolioPercentage : '-', + } + }) + }, [rows, pool]) + + const csvUrl = React.useMemo(() => csvData && getCSVDownloadUrl(csvData as any), [csvData]) + return ( - - - - - `${basePath}/${poolId}/assets/${row.id}`} - pageSize={20} - page={pagination.page} - pinnedData={pinnedData} - defaultSortKey="maturityDate" - /> - - - {pagination.pageCount > 1 && ( - - - - )} - - + <> + + {filters.data.map((loan) => loan.status === 'Active').length} ongoing assets + + + + Download + + + + + + + + `${basePath}/${poolId}/assets/${row.id}`} + pageSize={20} + page={pagination.page} + defaultSortKey="maturityDate" + /> + + + {pagination.pageCount > 1 && ( + + + + )} + + + ) } @@ -275,23 +338,7 @@ export function AssetName({ loan }: { loan: Pick - - - - - Onchain reserve} /> - - - ) - } + if (loan.id === '0') return if (isTinlakePool) { return ( @@ -299,7 +346,7 @@ export function AssetName({ loan }: { loan: Pick {loan.asset.nftId.length >= 9 @@ -310,30 +357,12 @@ export function AssetName({ loan }: { loan: Pick - - - - - {metadata?.name}} /> - - - ) - } - return ( {metadata?.name} @@ -341,34 +370,32 @@ export function AssetName({ loan }: { loan: Pick ) } +export function getAmount(l: Row, pool: Pool | TinlakePool, format?: boolean) { + switch (l.status) { + case 'Closed': + return format ? formatBalance(l.totalRepaid) : l.totalRepaid -function Amount({ loan }: { loan: Row }) { - const pool = usePool(loan.poolId) - - function getAmount(l: Row) { - switch (l.status) { - case 'Closed': - return formatBalance(l.totalRepaid, pool?.currency.symbol) - - case 'Active': - if ('presentValue' in l) { - return formatBalance(l.presentValue, pool?.currency.symbol) - } + case 'Active': + if ('presentValue' in l) { + return format ? formatBalance(l.presentValue) : l.presentValue + } - if (l.outstandingDebt.isZero()) { - return formatBalance(l.totalRepaid, pool?.currency.symbol) - } + if (l.outstandingDebt.isZero()) { + return format ? formatBalance(l.totalRepaid) : l.totalRepaid + } - return formatBalance(l.outstandingDebt, pool?.currency.symbol) + return format ? formatBalance(l.outstandingDebt) : l.outstandingDebt - // @ts-expect-error - case '': - return formatBalance(pool.reserve.total, pool?.currency.symbol) + // @ts-expect-error + case '': + return format ? formatBalance(pool.reserve.total) : pool.reserve.total - default: - return `0 ${pool?.currency.symbol}` - } + default: + return `0` } +} - return {getAmount(loan)} +function Amount({ loan }: { loan: Row }) { + const pool = usePool(loan.poolId) + return {getAmount(loan, pool, true)} } diff --git a/centrifuge-app/src/components/PageSection.tsx b/centrifuge-app/src/components/PageSection.tsx index 8681196346..0b0082a8b4 100644 --- a/centrifuge-app/src/components/PageSection.tsx +++ b/centrifuge-app/src/components/PageSection.tsx @@ -43,17 +43,7 @@ export function PageSection({ }: Props) { const [open, setOpen] = React.useState(defaultOpen) return ( - + {(title || titleAddition) && ( diff --git a/centrifuge-app/src/components/PageSummary.tsx b/centrifuge-app/src/components/PageSummary.tsx index 17d1dac6b4..edf769f8f7 100644 --- a/centrifuge-app/src/components/PageSummary.tsx +++ b/centrifuge-app/src/components/PageSummary.tsx @@ -1,4 +1,4 @@ -import { Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { useTheme } from 'styled-components' @@ -6,6 +6,7 @@ type Props = { data?: { label: React.ReactNode value: React.ReactNode + heading?: boolean }[] children?: React.ReactNode } @@ -14,21 +15,24 @@ export function PageSummary({ data, children }: Props) { const theme = useTheme() return ( - {data?.map(({ label, value }, index) => ( + {data?.map(({ label, value, heading }, index) => ( {label} - {value} + + {value} + ))} - {children} + {children} ) } diff --git a/centrifuge-app/src/components/PoolCard/index.tsx b/centrifuge-app/src/components/PoolCard/index.tsx index 7959a12476..82b4109a3b 100644 --- a/centrifuge-app/src/components/PoolCard/index.tsx +++ b/centrifuge-app/src/components/PoolCard/index.tsx @@ -161,19 +161,6 @@ export function PoolCard({ ) } - const calculateApy = (tranche: TrancheWithCurrency) => { - const daysSinceCreation = createdAt ? daysBetween(createdAt, new Date()) : 0 - if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[DYF_POOL_ID][0] - if (poolId === NS3_POOL_ID && tranche.seniority === 0) return centrifugeTargetAPYs[NS3_POOL_ID][0] - if (poolId === NS3_POOL_ID && tranche.seniority === 1) return centrifugeTargetAPYs[NS3_POOL_ID][1] - if (daysSinceCreation > 30 && tranche.yield30DaysAnnualized) - return formatPercentage(tranche.yield30DaysAnnualized, true, {}, 1) - if (tranche.interestRatePerSec) { - return formatPercentage(tranche.interestRatePerSec.toAprPercent(), true, {}, 1) - } - return '-' - } - const tranchesData = useMemo(() => { return tranches ?.map((tranche: TrancheWithCurrency) => { @@ -185,6 +172,19 @@ export function PoolCard({ tranche.currency.decimals ).toDecimal() + const calculateApy = (tranche: TrancheWithCurrency) => { + const daysSinceCreation = createdAt ? daysBetween(createdAt, new Date()) : 0 + if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[DYF_POOL_ID][0] + if (poolId === NS3_POOL_ID && tranche.seniority === 0) return centrifugeTargetAPYs[NS3_POOL_ID][0] + if (poolId === NS3_POOL_ID && tranche.seniority === 1) return centrifugeTargetAPYs[NS3_POOL_ID][1] + if (daysSinceCreation > 30 && tranche.yield30DaysAnnualized) + return formatPercentage(tranche.yield30DaysAnnualized, true, {}, 1) + if (tranche.interestRatePerSec) { + return formatPercentage(tranche.interestRatePerSec.toAprPercent(), true, {}, 1) + } + return '-' + } + return { seniority: tranche.seniority, name: trancheName, @@ -199,7 +199,7 @@ export function PoolCard({ } }) .reverse() - }, [calculateApy, isTinlakePool, metaData?.tranches, tinlakeKey, tranches]) + }, [isTinlakePool, metaData?.tranches, tinlakeKey, tranches]) return ( diff --git a/centrifuge-app/src/components/PoolFees/index.tsx b/centrifuge-app/src/components/PoolFees/index.tsx index 6325955e5d..a7014fb055 100644 --- a/centrifuge-app/src/components/PoolFees/index.tsx +++ b/centrifuge-app/src/components/PoolFees/index.tsx @@ -290,7 +290,7 @@ export function PoolFees() { Find a full overview of all pending and executed fee transactions. - View all transactions + View all transactions !id.startsWith('0x')) as Pool[] const centPoolsMetaData: PoolMetaDataPartial[] = useMetadataMulti( diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index a0cc7aeef1..be41ed4ab6 100644 --- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -42,20 +42,6 @@ export const TrancheTokenCards = ({ return 'mezzanine' } - const calculateApy = (trancheToken: Token) => { - if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy) - if (isTinlakePool && trancheToken.seniority === 0) return '15%' - if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] - if (poolId === NS3_POOL_ID && trancheToken.seniority === 0) - return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] - if (poolId === NS3_POOL_ID && trancheToken.seniority === 1) - return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][1] - if (daysSinceCreation < 30) return 'N/A' - return trancheToken.yield30DaysAnnualized - ? formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized)) - : '-' - } - const getTarget = (tranche: Token) => (isTinlakePool && tranche.seniority === 0) || poolId === DYF_POOL_ID || poolId === NS3_POOL_ID @@ -132,10 +118,23 @@ export const TrancheTokenCards = ({ }, }, ] - }, [pool.tranches, metadata, poolId]) + }, [pool.tranches, metadata, poolId, pool?.currency.symbol]) const dataTable = useMemo(() => { return trancheTokens.map((tranche) => { + const calculateApy = (trancheToken: Token) => { + if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy) + if (isTinlakePool && trancheToken.seniority === 0) return '15%' + if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] + if (poolId === NS3_POOL_ID && trancheToken.seniority === 0) + return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] + if (poolId === NS3_POOL_ID && trancheToken.seniority === 1) + return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][1] + if (daysSinceCreation < 30) return 'N/A' + return trancheToken.yield30DaysAnnualized + ? formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized)) + : '-' + } return { tokenName: tranche.name, apy: calculateApy(tranche), diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 38718364ef..779cb85df0 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,5 +1,5 @@ import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' -import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' +import { AnchorButton, Box, IconDownload, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' import BN from 'bn.js' import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' @@ -225,6 +225,7 @@ export const TransactionHistoryTable = ({ ), sortKey: 'transactionDate', + width: '200px', }, { align: 'left', @@ -249,19 +250,21 @@ export const TransactionHistoryTable = ({ ) }, sortKey: 'transaction', + width: '50%', }, { - align: 'right', - header: , + align: 'left', + header: , cell: ({ amount, netFlow }: Row) => ( {amount ? `${activeAssetId && netFlow === 'negative' ? '-' : ''}${formatBalance(amount, 'USD', 2, 2)}` : ''} ), - sortKey: 'amount', + sortKey: 'quantity', + width: '250px', }, { - align: 'right', + align: 'center', header: 'View transaction', cell: ({ hash }: Row) => { return ( @@ -282,9 +285,7 @@ export const TransactionHistoryTable = ({ return ( - - Transaction history - + Transaction history {transactions?.length! > 8 && preview && ( @@ -306,7 +307,9 @@ export const TransactionHistoryTable = ({ )} - + + + ) } diff --git a/centrifuge-app/src/components/Portfolio/Transactions.tsx b/centrifuge-app/src/components/Portfolio/Transactions.tsx index d6189bc428..4887b7d431 100644 --- a/centrifuge-app/src/components/Portfolio/Transactions.tsx +++ b/centrifuge-app/src/components/Portfolio/Transactions.tsx @@ -13,7 +13,6 @@ import { usePagination, } from '@centrifuge/fabric' import * as React from 'react' -import { useTheme } from 'styled-components' import { TransactionTypeChip } from '../../components/Portfolio/TransactionTypeChip' import { formatDate } from '../../utils/date' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' @@ -44,7 +43,6 @@ type Row = { export function Transactions({ onlyMostRecent, narrow, txTypes, address, trancheId }: TransactionsProps) { const explorer = useGetExplorerUrl() - const theme = useTheme() const columns = [ { align: 'left', diff --git a/centrifuge-app/src/components/Report/BalanceSheet.tsx b/centrifuge-app/src/components/Report/BalanceSheet.tsx index 7f1169276f..ff93e44356 100644 --- a/centrifuge-app/src/components/Report/BalanceSheet.tsx +++ b/centrifuge-app/src/components/Report/BalanceSheet.tsx @@ -76,7 +76,7 @@ export function BalanceSheet({ pool }: { pool: Pool }) { ] .concat( poolStates.map((state, index) => ({ - align: 'right', + align: 'left', timestamp: state.timestamp, header: new Date(state.timestamp).toLocaleDateString('en-US', { day: 'numeric', diff --git a/centrifuge-app/src/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx index 94790e92a8..67c170adbe 100644 --- a/centrifuge-app/src/components/Tooltips.tsx +++ b/centrifuge-app/src/components/Tooltips.tsx @@ -342,6 +342,10 @@ export const tooltipText = { label: 'Expense ratio', body: 'The operating expenses of the fund as a percentage of the total NAV', }, + totalNavMinusFees: { + label: 'Total NAV', + body: 'Total nav minus accrued fees', + }, } export type TooltipsProps = { diff --git a/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx b/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx index 2781b6733b..60cfc6c9ac 100644 --- a/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx +++ b/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx @@ -6,7 +6,7 @@ import { useCentrifugeApi, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Box, CurrencyInput, IconMinusCircle, IconPlusCircle, Select, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, CurrencyInput, IconPlus, IconX, Select, Shelf, Stack, Text } from '@centrifuge/fabric' import { Field, FieldArray, FieldProps, useFormikContext } from 'formik' import React from 'react' import { combineLatest, map, of } from 'rxjs' @@ -101,11 +101,11 @@ export const ChargeFeesFields = ({ background="none" border="none" as="button" - mt={4} + mt="34px" style={{ cursor: 'pointer' }} onClick={() => remove(index)} > - + ) @@ -125,7 +125,7 @@ export const ChargeFeesFields = ({ return push({ id: '', amount: '' }) }} > - + Add fee @@ -148,8 +148,10 @@ function ChargePoolFeeSummary({ poolId }: { poolId: string }) { return form.values.fees.length > 0 ? ( - Fees - {formatBalance(Dec(totalFees), pool.currency.symbol, 2)} + + Fees + + {formatBalance(Dec(totalFees), pool.currency.symbol, 2)} ) : null diff --git a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx index 659e191734..24b838db41 100644 --- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx +++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx @@ -1,10 +1,21 @@ import { ActiveLoan, CurrencyBalance, Pool, Price } from '@centrifuge/centrifuge-js' import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + Shelf, + Stack, + Text, + TextInput, +} from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' @@ -14,6 +25,7 @@ import { useBorrower } from '../../utils/usePermissions' import { usePool } from '../../utils/usePools' import { combine, max, maxPriceVariance, positiveNumber, required } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' +import { StyledSuccessButton } from './ExternalFinanceForm' import { isCashLoan, isExternalLoan, isInternalLoan } from './utils' export type CorrectionValues = { @@ -25,9 +37,11 @@ export type CorrectionValues = { } export function CorrectionForm({ loan }: { loan: ActiveLoan }) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const poolFees = useChargePoolFees(loan.poolId, loan.id) + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { initial: availableFinancing } = useAvailableFinancing(loan.poolId, loan.id) const api = useCentrifugeApi() const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction( @@ -80,8 +94,7 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { }, { onSuccess: () => { - correctionForm.setFieldValue('fees', [], false) - correctionForm.setFieldValue('reason', '', false) + setTransactionSuccess(true) }, } ) @@ -116,105 +129,112 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { Correction - - {isExternalLoan(loan) ? ( - <> - - - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('quantity', value)} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - - - - ={' '} - {formatBalance( - Dec(correctionForm.values.price || 0).mul(correctionForm.values.quantity || 0), - pool.currency.symbol, - 2 - )}{' '} - principal - - - - ) : isInternalLoan(loan) ? ( + + + {isExternalLoan(loan) ? ( + <> + + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + + + + ={' '} + {formatBalance( + Dec(correctionForm.values.price || 0).mul(correctionForm.values.quantity || 0), + pool.currency.symbol, + 2 + )}{' '} + principal + + + + ) : isInternalLoan(loan) ? ( + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('principal', value)} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + ) : null} - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - ) : null} - - - - {poolFees.render()} + validate={required()} + name="reason" + as={TextInput} + label="Reason" + placeholder="" + maxLength={40} + /> + {poolFees.render()} + + - + Summary - + - + Old holdings - {formatBalance(oldPrincipal, pool.currency.symbol, 2)} + {formatBalance(oldPrincipal, pool.currency.symbol, 2)} - + New holdings - + {formatBalance(newPrincipal, pool.currency.symbol, 2)} ( {isIncrease ? '+' : ''} @@ -226,15 +246,18 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { {poolFees.renderSummary()} - - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index 041c7faeb6..da9a58140f 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -8,12 +8,24 @@ import { WithdrawAddress, } from '@centrifuge/centrifuge-js' import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + InlineFeedback, + Shelf, + Stack, + Text, + Tooltip, +} from '@centrifuge/fabric' import { BN } from 'bn.js' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import styled, { useTheme } from 'styled-components' import { AnchorTextLink } from '../../components/TextLink' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' @@ -25,6 +37,7 @@ import { combine, maxPriceVariance, positiveNumber, required } from '../../utils import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' import { useWithdraw } from './FinanceForm' +import { SourceSelect } from './SourceSelect' export type FinanceValues = { price: number | '' | Decimal @@ -33,10 +46,34 @@ export type FinanceValues = { fees: { id: string; amount: '' | number | Decimal }[] } +export const StyledSuccessButton = styled(Button)` + span { + color: ${({ theme }) => theme.colors.textPrimary}; + background-color: ${({ theme }) => theme.colors.statusOkBg}; + border-color: ${({ theme }) => theme.colors.statusOk}; + border-width: 1px; + &:hover { + background-color: ${({ theme }) => theme.colors.statusOkBg}; + border-color: ${({ theme }) => theme.colors.statusOk}; + border-width: 1px; + box-shadow: none; + } + } +` + /** * Finance form for loans with `valuationMethod === oracle` */ -export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; source: string }) { +export function ExternalFinanceForm({ + loan, + source, + setSource, +}: { + loan: ExternalLoan + source: string + setSource: (source: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const poolFees = useChargePoolFees(loan.poolId, loan.id) @@ -44,6 +81,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour const loans = useLoans(loan.poolId) const sourceLoan = loans?.find((l) => l.id === source) as CreatedLoan | ActiveLoan const displayCurrency = source === 'reserve' ? pool.currency.symbol : 'USD' + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction( 'Purchase asset', (cent) => (args: [poolId: string, loanId: string, quantity: Price, price: CurrencyBalance], options) => { @@ -79,7 +117,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour }, { onSuccess: () => { - financeForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -125,62 +163,70 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour { - - - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('quantity', value)} - /> - ) - }} - - { - const financeAmount = Dec(val).mul(financeForm.values.quantity || 1) - return financeAmount.gt(maxAvailable) - ? `Amount exceeds available (${formatBalance(maxAvailable, displayCurrency, 2)})` - : '' - }, - maxPriceVariance(loan.pricing) - )} - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} - /> - ) - }} - - - - - ={' '} - {formatBalance( - Dec(financeForm.values.price || 0).mul(financeForm.values.quantity || 0), - displayCurrency, - 2 - )}{' '} - principal - - - - {source === 'reserve' && withdraw.render()} - - {poolFees.render()} + + + + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + /> + ) + }} + + { + const financeAmount = Dec(val).mul(financeForm.values.quantity || 1) + return financeAmount.gt(maxAvailable) + ? `Amount exceeds available (${formatBalance(maxAvailable, displayCurrency, 2)})` + : '' + }, + maxPriceVariance(loan.pricing) + )} + > + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + /> + ) + }} + + + + + ={' '} + {formatBalance( + Dec(financeForm.values.price || 0).mul(financeForm.values.quantity || 0), + displayCurrency, + 2 + )}{' '} + principal + + + {source === 'reserve' && withdraw.render()} + {poolFees.render()} + + Principal amount ({formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance @@ -202,61 +248,65 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour Liquidity tab. - + Transaction summary - - - - Available balance - - + + + - {formatBalance(maxAvailable, displayCurrency, 2)} + + Available balance + - - - - - - Principal amount - - {formatBalance(totalFinance, displayCurrency, 2)} + {formatBalance(maxAvailable, displayCurrency, 2)} - - {poolFees.renderSummary()} - + + + + Principal amount + + {formatBalance(totalFinance, displayCurrency, 2)} + + + {poolFees.renderSummary()} + - {source === 'reserve' ? ( - - - Stablecoins will be transferred to the specified withdrawal addresses, on the specified networks. A - delay until the transfer is completed is to be expected. - - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + {source === 'reserve' ? ( + + + Stablecoins will be transferred to the designated withdrawal addresses on the specified networks. + A delay may occur before the transfer is completed. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx index 18e8b025e8..c286192fda 100644 --- a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx @@ -5,12 +5,23 @@ import { useCentrifugeUtils, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + InlineFeedback, + Shelf, + Stack, + Text, +} from '@centrifuge/fabric' import { BN } from 'bn.js' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { copyable } from '../../components/Report/utils' import { Tooltips } from '../../components/Tooltips' import { Dec } from '../../utils/Decimal' @@ -22,6 +33,8 @@ import { usePool } from '../../utils/usePools' import { combine, maxNotRequired, nonNegativeNumberNotRequired } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' +import { StyledSuccessButton } from './ExternalFinanceForm' +import { SourceSelect } from './SourceSelect' type RepayValues = { price: number | '' | Decimal @@ -36,7 +49,16 @@ const UNLIMITED = Dec(1000000000000000) /** * Repay form for loans with `valuationMethod === oracle */ -export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; destination: string }) { +export function ExternalRepayForm({ + loan, + destination, + setDestination, +}: { + loan: ExternalLoan + destination: string + setDestination: (destination: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) const account = useBorrower(loan.poolId, loan.id) const balances = useBalances(account?.actingAddress) @@ -46,6 +68,7 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d const destinationLoan = loans?.find((l) => l.id === destination) as ActiveLoan const displayCurrency = destination === 'reserve' ? pool.currency.symbol : 'USD' const utils = useCentrifugeUtils() + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doRepayTransaction, isLoading: isRepayLoading } = useCentrifugeTransaction( 'Sell asset', @@ -90,7 +113,7 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d }, { onSuccess: () => { - repayForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -187,94 +210,102 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d return ( - - + + + + + { + if (Dec(val || 0).gt(maxQuantity.toDecimal())) { + return `Quantity exeeds max (${maxQuantity.toString()})` + } + return '' + })} + name="quantity" + > + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + placeholder="0" + onSetMax={() => + form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber()) + } + /> + ) + }} + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + currency={displayCurrency} + /> + ) + }} + + + + + = {formatBalance(principal, displayCurrency, 2)} principal + + + {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && ( + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('interest', value)} + onSetMax={() => form.setFieldValue('interest', maxInterest.toNumber())} + /> + ) + }} + + )} { - if (Dec(val || 0).gt(maxQuantity.toDecimal())) { - return `Quantity exeeds max (${maxQuantity.toString()})` - } - return '' - })} - name="quantity" + name="amountAdditional" + validate={combine(nonNegativeNumberNotRequired(), maxNotRequired(maxAvailable.toNumber()))} > {({ field, form }: FieldProps) => { return ( form.setFieldValue('quantity', value)} - placeholder="0" - onSetMax={() => - form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber()) - } - /> - ) - }} - - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} currency={displayCurrency} + onChange={(value) => form.setFieldValue('amountAdditional', value)} /> ) }} - - - - = {formatBalance(principal, displayCurrency, 2)} principal - - - - - {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && ( - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('interest', value)} - onSetMax={() => form.setFieldValue('interest', maxInterest.toNumber())} - /> - ) - }} - - )} - - {({ field, form }: FieldProps) => { - return ( - } - disabled={isRepayLoading} - currency={displayCurrency} - onChange={(value) => form.setFieldValue('amountAdditional', value)} - /> - ) - }} - - {poolFees.render()} + {poolFees.render()} + + The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is insufficient. @@ -292,58 +323,68 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). - - + + Transaction summary - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - - - - + - - Sale amount + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - {formatBalance(totalRepay, displayCurrency, 2)} - - {poolFees.renderSummary()} + + + + Sale amount + + {formatBalance(totalRepay, displayCurrency, 2)} + + + + {poolFees.renderSummary()} + - {destination === 'reserve' ? ( - - Stablecoins will be transferred to the onchain reserve. - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + + {destination === 'reserve' ? ( + + + Stablecoins will be transferred to the onchain reserve. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/FinanceForm.tsx b/centrifuge-app/src/pages/Loan/FinanceForm.tsx index 016793aec3..78e8a8236e 100644 --- a/centrifuge-app/src/pages/Loan/FinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/FinanceForm.tsx @@ -41,6 +41,7 @@ import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useField, useFormik, useFormikContext } from 'formik' import * as React from 'react' import { combineLatest, map, of, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { AnchorTextLink } from '../../components/TextLink' import { parachainIcons, parachainNames } from '../../config' import { Dec, min } from '../../utils/Decimal' @@ -75,8 +76,7 @@ export function FinanceForm({ loan }: { loan: LoanType }) { return ( Purchase - - + ) } @@ -84,8 +84,7 @@ export function FinanceForm({ loan }: { loan: LoanType }) { return ( {isCashLoan(loan) ? 'Deposit' : 'Finance'} - - + ) } @@ -93,7 +92,16 @@ export function FinanceForm({ loan }: { loan: LoanType }) { /** * Finance form for loans with `valuationMethod: outstandingDebt, discountedCashflow, cash` */ -function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string }) { +function InternalFinanceForm({ + loan, + source, + onChange, +}: { + loan: LoanType + source: string + onChange: (source: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const api = useCentrifugeApi() @@ -189,104 +197,115 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string <> {!maturityDatePassed && ( - - { - const principalValue = typeof val === 'number' ? Dec(val) : (val as Decimal) - if (maxAvailable !== UNLIMITED && principalValue.gt(maxAvailable)) { - return `Principal exceeds available financing` - } - return '' - })} - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - onSetMax={ - maxAvailable !== UNLIMITED ? () => form.setFieldValue('principal', maxAvailable) : undefined - } - /> - ) - }} - - {source === 'other' && ( - - {({ field }: FieldProps) => { + + + + { + const principalValue = typeof val === 'number' ? Dec(val) : (val as Decimal) + if (maxAvailable !== UNLIMITED && principalValue.gt(maxAvailable)) { + return `Principal exceeds available financing` + } + return '' + })} + > + {({ field, form }: FieldProps) => { return ( - + ) + }} + + )} + {source === 'reserve' && withdraw.render()} - {poolFees.render()} + {poolFees.render()} - - {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} ( - {formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance ( - {formatBalance(maxAvailable, displayCurrency, 2)}). - - - - There is an additional{' '} - {formatBalance( - new CurrencyBalance(pool.reserve.total.sub(pool.reserve.available), pool.currency.decimals), - displayCurrency - )}{' '} - available from repayments or deposits. This requires first executing the orders on the{' '} - Liquidity tab. - - - - Transaction summary - + + {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} ( + {formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance ( + {formatBalance(maxAvailable, displayCurrency, 2)}). + + + + There is an additional{' '} + {formatBalance( + new CurrencyBalance(pool.reserve.total.sub(pool.reserve.available), pool.currency.decimals), + displayCurrency + )}{' '} + available from repayments or deposits. This requires first executing the orders on the{' '} + Liquidity tab. + + + + + + Transaction summary + + - - Available balance - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - + + + Available balance + + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - + {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} - {formatBalance(totalFinance, displayCurrency, 2)} + {formatBalance(totalFinance, displayCurrency, 2)} @@ -295,42 +314,42 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string {source === 'reserve' ? ( - - Stablecoins will be transferred to the specified withdrawal addresses, on the specified networks. A - delay until the transfer is completed is to be expected. + + Stablecoins will be transferred to the designated withdrawal addresses on the specified networks. A + delay may occur before the transfer is completed. ) : source === 'other' ? ( - + Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to an increase in the NAV of the pool. ) : ( - + Virtual accounting process. No onchain stablecoin transfers are expected. )} - + + - - - + + )} @@ -372,16 +391,18 @@ function WithdrawSelect({ withdrawAddresses, poolId }: { withdrawAddresses: With ) return ( - helpers.setValue(JSON.parse(event.target.value))} + onBlur={field.onBlur} + errorMessage={(meta.touched || form.submitCount > 0) && meta.error ? meta.error : undefined} + value={field.value ? JSON.stringify(field.value) : ''} + options={options} + disabled={withdrawAddresses.length === 1} + /> + ) } diff --git a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx index 8f9146f16a..d3ba229f84 100644 --- a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx +++ b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx @@ -74,11 +74,9 @@ export function HoldingsValues({ pool, transactions, currentFace, pricing }: Pro ] return ( - + - - Holdings - + Holdings diff --git a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx index 3d5bb53459..1f5e24548b 100644 --- a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx +++ b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx @@ -127,13 +127,13 @@ export function KeyMetrics({ pool, loan }: Props) { sumRealizedProfitFifo ? { label: 'Realized P&L', - value: formatBalance(sumRealizedProfitFifo, pool.currency.symbol), + value: formatBalance(sumRealizedProfitFifo, pool.currency.symbol, 2, 2), } : (null as never), unrealizedProfitAtMarketPrice ? { label: 'Unrealized P&L', - value: formatBalance(unrealizedProfitAtMarketPrice, pool.currency.symbol), + value: formatBalance(unrealizedProfitAtMarketPrice, pool.currency.symbol, 2, 2), } : (null as never), ].filter(Boolean) @@ -143,23 +143,21 @@ export function KeyMetrics({ pool, loan }: Props) { loan.pricing.valuationMethod === 'oracle' && loan.pricing.notional.gtn(0) && currentYTM - ? [{ label: , value: formatPercentage(currentYTM) }] + ? [{ label: , value: formatPercentage(currentYTM) }] : []), ...(loan.pricing.maturityDate && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && loan.pricing.notional.gtn(0) && averageWeightedYTM - ? [{ label: , value: formatPercentage(averageWeightedYTM) }] + ? [{ label: , value: formatPercentage(averageWeightedYTM) }] : []), ] return ( - + - - Key metrics - + Key metrics diff --git a/centrifuge-app/src/pages/Loan/MetricsTable.tsx b/centrifuge-app/src/pages/Loan/MetricsTable.tsx index 431ebc3425..dedc335393 100644 --- a/centrifuge-app/src/pages/Loan/MetricsTable.tsx +++ b/centrifuge-app/src/pages/Loan/MetricsTable.tsx @@ -12,7 +12,7 @@ type Props = { export function MetricsTable({ metrics }: Props) { return ( - + {metrics.map(({ label, value }, index) => { const multirow = value && value.length > 20 const asLink = value && /^(https?:\/\/[^\s]+)$/.test(value) @@ -33,25 +33,21 @@ export function MetricsTable({ metrics }: Props) { } : {} - const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle } + const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle, textAlign: 'right' } return ( - + {label} - + {asLink ? {value} : value} diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 4d095fa314..9ac7e84218 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -59,11 +59,9 @@ export function PricingValues({ loan, pool }: Props) { const accruedPrice = 'currentPrice' in loan && loan.currentPrice return ( - + - - Pricing - + Pricing , + label: , value: pricing.withLinearPricing ? 'Enabled' : 'Disabled', }, ...(loan.status === 'Active' && loan.outstandingDebt.toDecimal().lte(0) @@ -102,9 +100,7 @@ export function PricingValues({ loan, pool }: Props) { return ( - - Pricing - + Pricing Sell - - + ) } @@ -65,15 +77,23 @@ export function RepayForm({ loan }: { loan: CreatedLoan | ActiveLoan }) { return ( {isCashLoan(loan) ? 'Withdraw' : 'Repay'} - - + ) } /** * Repay form for loans with `valuationMethod: outstandingDebt, discountedCashflow, cash` */ -function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLoan; destination: string }) { +function InternalRepayForm({ + loan, + destination, + setDestination, +}: { + loan: ActiveLoan | CreatedLoan + destination: string + setDestination: (destination: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) const account = useBorrower(loan.poolId, loan.id) const balances = useBalances(account?.actingAddress) @@ -84,6 +104,7 @@ function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLo const destinationLoan = loans?.find((l) => l.id === destination) as Loan const displayCurrency = destination === 'reserve' ? pool.currency.symbol : 'USD' const utils = useCentrifugeUtils() + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doRepayTransaction, isLoading: isRepayLoading } = useCentrifugeTransaction( isCashLoan(loan) ? 'Withdraw funds' : 'Repay asset', @@ -129,7 +150,7 @@ function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLo }, { onSuccess: () => { - repayForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -225,169 +246,198 @@ function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLo <> - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - onSetMax={() => { - form.setFieldValue('principal', maxPrincipal.gte(0) ? maxPrincipal : 0) + + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('principal', value)} + onSetMax={() => { + form.setFieldValue('principal', maxPrincipal.gte(0) ? maxPrincipal : 0) + }} + secondaryLabel={`${formatBalance(maxPrincipal, displayCurrency)} outstanding`} + /> + ) + }} + + {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && !isCashLoan(loan) && ( + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('interest', value)} + onSetMax={() => form.setFieldValue('interest', maxInterest.gte(0) ? maxInterest : 0)} + /> + ) }} - secondaryLabel={`${formatBalance(maxPrincipal, displayCurrency)} outstanding`} - /> - ) - }} - - {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && !isCashLoan(loan) && ( - )} - name="interest" - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('interest', value)} - onSetMax={() => form.setFieldValue('interest', maxInterest.gte(0) ? maxInterest : 0)} - /> - ) - }} - - )} - {!isCashLoan(loan) && ( - + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('amountAdditional', value)} + /> + ) + }} + )} - > - {({ field, form }: FieldProps) => { - return ( - } - disabled={isRepayLoading} - currency={displayCurrency} - onChange={(value) => form.setFieldValue('amountAdditional', value)} - /> - ) - }} - - )} - {destination === 'other' && ( - - {({ field }: FieldProps) => { - return ( - + ) + }} + + )} + {poolFees.render()} - - {isCashLoan(loan) ? 'Amount' : 'Principal'} ( - {formatBalance(Dec(repayForm.values.principal || 0), displayCurrency, 2)}) is greater than the outstanding{' '} - {isCashLoan(loan) ? 'balance' : 'principal'} ({formatBalance(maxPrincipal, displayCurrency, 2)}). - + + {isCashLoan(loan) ? 'Amount' : 'Principal'} ( + {formatBalance(Dec(repayForm.values.principal || 0), displayCurrency, 2)}) is greater than the + outstanding {isCashLoan(loan) ? 'balance' : 'principal'} ( + {formatBalance(maxPrincipal, displayCurrency, 2)}). + - - Interest ({formatBalance(Dec(repayForm.values.interest || 0), displayCurrency, 2)}) is greater than the - outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). - + + Interest ({formatBalance(Dec(repayForm.values.interest || 0), displayCurrency, 2)}) is greater than the + outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). + - - The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is insufficient. - Transfer {formatBalance(totalRepay.sub(balance), displayCurrency, 2)} to{' '} - {copyable(utils.formatAddress(account?.actingAddress || ''))} on Centrifuge. - + + The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is + insufficient. Transfer {formatBalance(totalRepay.sub(balance), displayCurrency, 2)} to{' '} + {copyable(utils.formatAddress(account?.actingAddress || ''))} on Centrifuge. + + + - + Transaction summary - - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - - - + - - {isCashLoan(loan) ? 'Withdrawal amount' : 'Repayment amount'} + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - {formatBalance(totalRepay, displayCurrency, 2)} - - {poolFees.renderSummary()} - + + + + {isCashLoan(loan) ? 'Withdrawal amount' : 'Repayment amount'} + + {formatBalance(totalRepay, displayCurrency, 2)} + + - {destination === 'reserve' ? ( - - Stablecoins will be transferred to the onchain reserve. - - ) : destination === 'other' ? ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to a - decrease in the NAV of the pool. - - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + {poolFees.renderSummary()} + + + + {destination === 'reserve' ? ( + + + Stablecoins will be transferred to the onchain reserve. + + + ) : destination === 'other' ? ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to + a decrease in the NAV of the pool. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + + - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/TransactionTable.tsx b/centrifuge-app/src/pages/Loan/TransactionTable.tsx index 3ce9bcda10..5c92a876db 100644 --- a/centrifuge-app/src/pages/Loan/TransactionTable.tsx +++ b/centrifuge-app/src/pages/Loan/TransactionTable.tsx @@ -19,6 +19,7 @@ type Props = { poolType?: string maturityDate?: Date originationDate: Date | undefined + loanStatus: string } type Row = { @@ -31,6 +32,7 @@ type Row = { position: Decimal yieldToMaturity: Decimal | null realizedProfitFifo: CurrencyBalance | null + unrealizedProfitAtMarketPrice: CurrencyBalance | null } export const TransactionTable = ({ @@ -41,6 +43,7 @@ export const TransactionTable = ({ pricing, poolType, maturityDate, + loanStatus, }: Props) => { const assetTransactions = useMemo(() => { const sortedTransactions = transactions?.sort((a, b) => { @@ -120,6 +123,7 @@ export const TransactionTable = ({ return sum }, Dec(0)), realizedProfitFifo: transaction.realizedProfitFifo, + unrealizedProfitAtMarketPrice: transaction.unrealizedProfitAtMarketPrice, } }) }, [transactions, maturityDate, pricing, decimals]) @@ -189,10 +193,15 @@ export const TransactionTable = ({ }, { align: 'left', - header: `Realized P&L`, + header: loanStatus === 'Closed' || loanStatus === 'Repaid' ? 'Realized P&L' : 'Unrealized P&L', cell: (row: Row) => - row.realizedProfitFifo - ? `${row.type !== 'REPAID' ? '-' : ''}${formatBalance(row.realizedProfitFifo, undefined, 2, 2)}` + row.realizedProfitFifo || row.unrealizedProfitAtMarketPrice + ? formatBalance( + loanStatus === 'Closed' ? row.unrealizedProfitAtMarketPrice ?? 0 : row.realizedProfitFifo ?? 0, + undefined, + 2, + 2 + ) : '-', }, { @@ -204,7 +213,7 @@ export const TransactionTable = ({ : [ { align: 'left', - header: `Amount (${currency})`, + header: `Quantity (${currency})`, cell: (row: Row) => (row.amount ? `${formatBalance(row.amount, undefined, 2, 2)}` : '-'), }, { @@ -214,10 +223,15 @@ export const TransactionTable = ({ }, { align: 'left', - header: `Realized P&L`, + header: loanStatus === 'Closed' || loanStatus === 'Repaid' ? 'Realized P&L' : 'Unrealized P&L', cell: (row: Row) => - row.realizedProfitFifo - ? `${row.type !== 'REPAID' ? '-' : ''}${formatBalance(row.realizedProfitFifo, undefined, 2, 2)}` + row.realizedProfitFifo || row.unrealizedProfitAtMarketPrice + ? formatBalance( + loanStatus === 'Closed' ? row.unrealizedProfitAtMarketPrice ?? 0 : row.realizedProfitFifo ?? 0, + undefined, + 2, + 2 + ) : '-', }, ]), diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 78bb4547fa..527e1eea8b 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -1,28 +1,36 @@ -import { ActiveLoan, Loan as LoanType, Pool, PricingInfo, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { + ActiveLoan, + AssetTransaction, + ExternalPricingInfo, + Loan as LoanType, + Pool, + PricingInfo, + TinlakeLoan, +} from '@centrifuge/centrifuge-js' import { Box, Button, Card, Drawer, Grid, - IconChevronLeft, + IconArrowLeft, Shelf, Spinner, Stack, Text, - TextWithPlaceholder, truncate, } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' -import styled, { useTheme } from 'styled-components' -import { AssetSummary } from '../../components/AssetSummary' +import styled from 'styled-components' +import { AssetSummary } from '../../../src/components/AssetSummary' +import { SimpleLineChart } from '../../../src/components/Charts/SimpleLineChart' +import { LoanLabel, getLoanLabelStatus } from '../../../src/components/LoanLabel' +import { Dec } from '../../../src/utils/Decimal' import AssetPerformanceChart from '../../components/Charts/AssetPerformanceChart' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' import { LoadBoundary } from '../../components/LoadBoundary' -import { LoanLabel } from '../../components/LoanLabel' -import { PageHeader } from '../../components/PageHeader' import { PageSection } from '../../components/PageSection' import { TransactionHistoryTable } from '../../components/PoolOverview/TransactionHistory' import { RouterLinkButton } from '../../components/RouterLinkButton' @@ -47,13 +55,6 @@ import { RepayForm } from './RepayForm' import { TransactionTable } from './TransactionTable' import { formatNftAttribute, isCashLoan, isExternalLoan } from './utils' -const FullHeightStack = styled(Stack)` - flex: 1; - display: flex; - flex-direction: column; - height: 100%; -` - export default function LoanPage() { return } @@ -61,6 +62,30 @@ function isTinlakeLoan(loan: LoanType | TinlakeLoan): loan is TinlakeLoan { return loan.poolId.startsWith('0x') } +const StyledRouterLinkButton = styled(RouterLinkButton)` + margin-left: 14px; + border-radius: 50%; + margin: 0px; + padding: 0px; + width: fit-content; + margin-left: 30px; + border: 4px solid transparent; + + > span { + width: 34px; + border: 4px solid transparent; + } + &:hover { + background-color: ${({ theme }) => theme.colors.backgroundSecondary}; + border: ${({ theme }) => `4px solid ${theme.colors.backgroundTertiary}`}; + span { + color: ${({ theme }) => theme.colors.textPrimary}; + } + } +` + +const positiveNetflows = ['DEPOSIT_FROM_INVESTMENTS', 'INCREASE_DEBT'] + function ActionButtons({ loan }: { loan: LoanType }) { const canBorrow = useCanBorrowAsset(loan.poolId, loan.id) const [financeShown, setFinanceShown] = React.useState(false) @@ -68,7 +93,7 @@ function ActionButtons({ loan }: { loan: LoanType }) { const [correctionShown, setCorrectionShown] = React.useState(false) if (!loan || !canBorrow || isTinlakeLoan(loan) || !canBorrow || loan.status === 'Closed') return null return ( - <> + setFinanceShown(false)} innerPaddingTop={2}> @@ -92,27 +117,26 @@ function ActionButtons({ loan }: { loan: LoanType }) { {!(loan.pricing.maturityDate && new Date() > new Date(loan.pricing.maturityDate)) || !loan.pricing.maturityDate ? ( - ) : null} {loan.outstandingDebt.gtn(0) && ( - )} {loan.outstandingDebt.gtn(0) && ( - )} - + ) } function Loan() { - const theme = useTheme() const { pid: poolId, aid: loanId } = useParams<{ pid: string; aid: string }>() if (!poolId || !loanId) throw new Error('Loan no found') const basePath = useBasePath() @@ -124,6 +148,32 @@ function Loan() { const { data: nftMetadata, isLoading: nftMetadataIsLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const borrowerAssetTransactions = useBorrowerAssetTransactions(`${poolId}`, `${loanId}`) + const isOracle = loan && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' + const loanStatus = loan && getLoanLabelStatus(loan)[1] + + const getNetflow = (value: Number, type: string) => { + if (positiveNetflows.includes(type)) return value + else return -value + } + + const onchainReserveChart = React.useMemo(() => { + return borrowerAssetTransactions?.map((transaction) => { + return { + name: transaction.timestamp.toString(), + yAxis: getNetflow(transaction.amount?.toNumber() ?? 0, transaction.type), + } + }) + }, [borrowerAssetTransactions]) + + const sumRealizedProfitFifo = borrowerAssetTransactions?.reduce( + (sum, tx) => sum.add(tx.realizedProfitFifo?.toDecimal() ?? Dec(0)), + Dec(0) + ) + + const unrealizedProfitAtMarketPrice = borrowerAssetTransactions?.reduce( + (sum, tx) => sum.add(tx.unrealizedProfitAtMarketPrice?.toDecimal() ?? Dec(0)), + Dec(0) + ) const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing @@ -149,34 +199,77 @@ function Loan() { const originationDate = loan && 'originationDate' in loan ? new Date(loan?.originationDate).toISOString() : undefined + const getCurrentValue = () => { + if (loanId === '0') return pool.reserve.total + if (loan && 'presentValue' in loan) return loan.presentValue + return 0 + } + + const getCurrentPrice = () => { + if (loan && 'currentPrice' in loan) return loan.currentPrice + return 0 + } + + const getValueProfit = () => { + if (loanStatus === 'Closed' || loanStatus === 'Repaid') return sumRealizedProfitFifo ?? 0 + else return unrealizedProfitAtMarketPrice ?? 0 + } + + if (metadataIsLoading) return + return ( - - - - {poolMetadata?.pool?.name ?? 'Pool assets'} - + + + + + + {name} + + {loan && } + - - - {name} - - {loan && } - + + } - /> + > + {loan && !isTinlakeLoan(loan) && } + + {loanId === '0' && ( <> - + + + + + )} {loan && pool && ( - - + + }> - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> @@ -206,13 +299,13 @@ function Loan() { gridAutoRows="minContent" gap={[2, 2, 2]} > - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> )} @@ -228,11 +321,9 @@ function Loan() { if (!isPublic) return null return ( }> - + - - {section.name} - + {section.name} - - + ) : ( - - - - Transaction history - - - - - + + Transaction history + + ) ) : null} @@ -319,6 +400,6 @@ function Loan() { ) : null} - + ) } diff --git a/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx new file mode 100644 index 0000000000..9b4d574087 --- /dev/null +++ b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx @@ -0,0 +1,87 @@ +import { Loan } from '@centrifuge/centrifuge-js' +import { + Box, + IconChevronDown, + IconChevronRight, + IconChevronUp, + Menu, + MenuItem, + MenuItemGroup, + Popover, + Text, +} from '@centrifuge/fabric' +import { useLocation, useNavigate } from 'react-router' +import styled from 'styled-components' +import { nftMetadataSchema } from '../../../../src/schemas' +import { useMetadata } from '../../../../src/utils/useMetadata' +import { useCentNFT } from '../../../../src/utils/useNFTs' + +type Props = { + value: string + loans: Loan[] +} + +type LoanOptionProps = { + loan: Loan +} + +const StyledButton = styled(Box)` + background: transparent; + border: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + font-family: Inter, sans-serif; + &:hover { + text-decoration: underline; + cursor: pointer; + } +` + +const LoanOption = ({ loan }: LoanOptionProps) => { + const navigate = useNavigate() + const location = useLocation() + const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, loan.poolId.startsWith('0x')) + const { data } = useMetadata(nft?.metadataUri, nftMetadataSchema) + + const handleNavigate = (id: string) => { + navigate(`${location.pathname}/${id}`) + } + + return ( + + } + onClick={() => handleNavigate(loan.id)} + /> + + ) +} + +export const OffchainMenu = ({ value, loans }: Props) => { + if (!loans.length) return null + return ( + ( + + + {value} + {state.isOpen ? : } + + + )} + renderContent={(props, ref) => ( + + + {loans.map((loan) => ( + + ))} + + + )} + /> + ) +} diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index e7345950b7..41c735f452 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -1,12 +1,12 @@ -import { ActiveLoan, Loan } from '@centrifuge/centrifuge-js' -import { Box, Shelf, Text } from '@centrifuge/fabric' +import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js' +import { Box, IconChevronRight, IconPlus, Shelf, Text } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' -import currencyDollar from '../../../assets/images/currency-dollar.svg' -import daiLogo from '../../../assets/images/dai-logo.svg' -import usdcLogo from '../../../assets/images/usdc-logo.svg' +import styled from 'styled-components' +import { RouterTextLink } from '../../../../src/components/TextLink' +import { useBasePath } from '../../../../src/utils/useBasePath' import { LoadBoundary } from '../../../components/LoadBoundary' -import { LoanList } from '../../../components/LoanList' +import { LoanList, getAmount } from '../../../components/LoanList' import { PageSummary } from '../../../components/PageSummary' import { RouterLinkButton } from '../../../components/RouterLinkButton' import { Tooltips } from '../../../components/Tooltips' @@ -16,6 +16,16 @@ import { useLoans } from '../../../utils/useLoans' import { useSuitableAccounts } from '../../../utils/usePermissions' import { usePool } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' +import { OffchainMenu } from './OffchainMenu' + +const StyledRouterTextLink = styled(RouterTextLink)` + text-decoration: unset; + display: flex; + align-items: center; + &:hover { + text-decoration: underline; + } +` export function PoolDetailAssetsTab() { return ( @@ -36,6 +46,10 @@ export function PoolDetailAssets() { const pool = usePool(poolId) const loans = useLoans(poolId) const isTinlakePool = poolId.startsWith('0x') + const basePath = useBasePath() + const cashLoans = (loans ?? []).filter( + (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' + ) if (!pool) return null @@ -48,18 +62,18 @@ export function PoolDetailAssets() { ) } - function hasValuationMethod(pricing: any): pricing is { valuationMethod: string } { + const hasValuationMethod = (pricing: any): pricing is { valuationMethod: string; presentValue: CurrencyBalance } => { return pricing && typeof pricing.valuationMethod === 'string' } - const ongoingAssets = (loans && - [...loans].filter( - (loan) => - loan.status === 'Active' && - hasValuationMethod(loan.pricing) && - loan.pricing.valuationMethod !== 'cash' && - !loan.outstandingDebt.isZero() - )) as ActiveLoan[] + const totalAssets = loans.reduce((sum, loan) => { + const amount = + hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash' + ? new CurrencyBalance(getAmount(loan as any, pool), pool.currency.decimals).toDecimal() + : 0 + + return sum.add(amount) + }, Dec(0)) const offchainAssets = !isTinlakePool ? loans.filter( @@ -71,48 +85,41 @@ export function PoolDetailAssets() { Dec(0) ) - const overdueAssets = loans.filter( - (loan) => - loan.status === 'Active' && - loan.outstandingDebt.gtn(0) && - loan.pricing.maturityDate && - new Date(loan.pricing.maturityDate).getTime() < Date.now() - ) + const total = isTinlakePool ? pool.nav.total : pool.reserve.total.toDecimal().add(offchainReserve).add(totalAssets) + const totalNAV = isTinlakePool ? pool.nav.total : Dec(total).sub(pool.fees.totalPaid.toDecimal()) - const pageSummaryData: { label: React.ReactNode; value: React.ReactNode }[] = [ + const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { - label: , - value: formatBalance(pool.nav.total.toDecimal(), pool.currency.symbol), + label: `Total NAV (${pool.currency.symbol})`, + value: formatBalance(totalNAV), + heading: true, }, { - label: ( - - - - + label: , + value: ( + + {formatBalance(pool.reserve.total || 0)} + + ), - value: formatBalance(pool.reserve.total || 0, pool.currency.symbol), + heading: false, }, - ...(!isTinlakePool + ...(!isTinlakePool && cashLoans.length ? [ { - label: ( - - - - - ), - value: formatBalance(offchainReserve, 'USD'), + label: , + value: , + heading: false, }, { - label: 'Total assets', - value: loans.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash') - .length, + label: `Total Assets (${pool.currency.symbol})`, + value: formatBalance(totalAssets), + heading: false, }, - { label: , value: ongoingAssets.length || 0 }, { - label: 'Overdue assets', - value: 0 ? 'statusCritical' : 'inherit'}>{overdueAssets.length}, + label: `Accrued fees (${pool.currency.symbol})`, + value: `-${formatBalance(pool.fees.totalPaid)}`, + heading: false, }, ] : []), @@ -123,7 +130,7 @@ export function PoolDetailAssets() { - + @@ -134,8 +141,8 @@ function CreateAssetButton({ poolId }: { poolId: string }) { const canCreateAssets = useSuitableAccounts({ poolId, poolRole: ['Borrower'], proxyType: ['Borrow'] }).length > 0 return canCreateAssets ? ( - - Create asset + }> + Create assets ) : null } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index e8aa7c9efd..cc995d9d89 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -849,6 +849,7 @@ export type AssetTransaction = { interestAmount: CurrencyBalance | undefined hash: string realizedProfitFifo: CurrencyBalance | undefined + unrealizedProfitAtMarketPrice: CurrencyBalance | undefined asset: { id: string metadata: string diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 5f781c7f0c..520f3e28bd 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -129,6 +129,7 @@ export type SubqueryAssetTransaction = { type: AssetType sumRealizedProfitFifo: string unrealizedProfitAtMarketPrice: string + currentPrice: string } fromAsset?: { id: string diff --git a/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx b/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx index 0190f0cc7e..4cb6592e25 100644 --- a/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx +++ b/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx @@ -6,7 +6,7 @@ type Props = WalletButtonProps & { label?: string } -export function ConnectButton({ label = 'Connect', ...rest }: Props) { +export function ConnectButton({ label = 'Connect wallet', ...rest }: Props) { const { showNetworks, pendingConnect } = useWallet() return ( diff --git a/fabric/src/components/Button/WalletButton.tsx b/fabric/src/components/Button/WalletButton.tsx index a09a120c0c..2ced79aab2 100644 --- a/fabric/src/components/Button/WalletButton.tsx +++ b/fabric/src/components/Button/WalletButton.tsx @@ -22,7 +22,6 @@ export type WalletButtonProps = Omit< const StyledButton = styled.button` display: inline-block; - width: 100%; padding: 0; border: none; appearance: none; diff --git a/fabric/src/components/Card/index.ts b/fabric/src/components/Card/index.ts index 52f34e9528..79e5894cdf 100644 --- a/fabric/src/components/Card/index.ts +++ b/fabric/src/components/Card/index.ts @@ -3,7 +3,7 @@ import styled from 'styled-components' import { Box, BoxProps } from '../Box' type Props = { - variant?: 'default' | 'interactive' | 'overlay' + variant?: 'default' | 'interactive' | 'overlay' | 'secondary' backgroundColor?: string } @@ -14,9 +14,9 @@ export const Card = styled(Box)(({ variant = 'default', backgroundColor } css({ bg: backgroundColor ?? 'white', borderRadius: 'card', - borderWidth: variant === 'default' && !backgroundColor ? 1 : 0, + borderWidth: variant === 'default' || (variant === 'secondary' && !backgroundColor) ? 1 : 0, borderStyle: 'solid', - borderColor: 'borderPrimary', + borderColor: variant === 'secondary' ? 'borderSecondary' : 'borderPrimary', boxShadow: variant === 'interactive' ? 'cardInteractive' : variant === 'overlay' ? 'cardOverlay' : undefined, transition: 'box-shadow 100ms ease', diff --git a/fabric/src/components/InlineFeedback/index.tsx b/fabric/src/components/InlineFeedback/index.tsx index d7d6f4d0ab..fc08b4a0cd 100644 --- a/fabric/src/components/InlineFeedback/index.tsx +++ b/fabric/src/components/InlineFeedback/index.tsx @@ -26,7 +26,7 @@ export function InlineFeedback({ status = 'default', children }: InlineFeedbackP - + {children} diff --git a/fabric/src/components/InputUnit/index.tsx b/fabric/src/components/InputUnit/index.tsx index f5f25902f4..0b6d13bd4f 100644 --- a/fabric/src/components/InputUnit/index.tsx +++ b/fabric/src/components/InputUnit/index.tsx @@ -42,7 +42,7 @@ export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElemen {inputElement} {secondaryLabel && ( - + {secondaryLabel} )} @@ -63,7 +63,7 @@ export function InputLabel({ }) { return ( + - {label} - {sublabel} + + {label} + + + {sublabel} + {action} diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx index 725f0e8455..e21423e7f1 100644 --- a/fabric/src/components/Tooltip/index.tsx +++ b/fabric/src/components/Tooltip/index.tsx @@ -60,10 +60,18 @@ const placements: { const Container = styled(Stack)<{ pointer: PlacementAxis }>` background-color: ${({ theme }) => theme.colors.backgroundInverted}; filter: ${({ theme }) => `drop-shadow(${theme.shadows.cardInteractive})`}; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.2s ease, transform 0.2s ease; + will-change: opacity, transform; + + &.show { + opacity: 1; + transform: translateY(0); + } &::before { --size: 5px; - content: ''; position: absolute; ${({ pointer }) => placements[pointer!]} @@ -77,7 +85,7 @@ export function Tooltip({ body, children, disabled, - delay = 1000, + delay = 200, bodyWidth, bodyPadding, triggerStyle, @@ -108,6 +116,7 @@ export function Tooltip({ {...tooltipElementProps} {...rest} ref={overlayRef} + className={state.isOpen ? 'show' : ''} backgroundColor="backgroundPrimary" p={bodyPadding ?? 1} borderRadius="tooltip" @@ -116,7 +125,7 @@ export function Tooltip({ pointer={pointer} > {!!title && ( - + {title} )} diff --git a/fabric/src/icon-svg/icon-chevron-down.svg b/fabric/src/icon-svg/icon-chevron-down.svg index f87d1768a3..e70d2ea0d0 100644 --- a/fabric/src/icon-svg/icon-chevron-down.svg +++ b/fabric/src/icon-svg/icon-chevron-down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/fabric/src/icon-svg/icon-download.svg b/fabric/src/icon-svg/icon-download.svg index b1b7145f29..62e0c852be 100644 --- a/fabric/src/icon-svg/icon-download.svg +++ b/fabric/src/icon-svg/icon-download.svg @@ -1,5 +1,3 @@ - - - + diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts index 50fde892a4..73f8be47db 100644 --- a/fabric/src/theme/tokens/theme.ts +++ b/fabric/src/theme/tokens/theme.ts @@ -2,14 +2,14 @@ import { black, blueScale, gold, grayScale, yellowScale } from './colors' const statusDefault = grayScale[800] const statusInfo = yellowScale[800] -const statusOk = '#519b10' +const statusOk = '#277917' const statusWarning = yellowScale[800] const statusCritical = '#d43f2b' const statusPromote = '#f81071' const statusDefaultBg = grayScale[100] const statusInfoBg = yellowScale[100] -const statusOkBg = '#f1f7ec' +const statusOkBg = '#DCEBCF' const statusWarningBg = yellowScale[50] const statusCriticalBg = '#fcf0ee' const statusPromoteBg = '#f8107114' diff --git a/fabric/src/theme/tokens/typography.ts b/fabric/src/theme/tokens/typography.ts index 73ab39c964..24389f3994 100644 --- a/fabric/src/theme/tokens/typography.ts +++ b/fabric/src/theme/tokens/typography.ts @@ -1,6 +1,12 @@ import { ThemeTypography } from '../types' const typography: ThemeTypography = { + heading: { + fontSize: 36, + lineHeight: 1.4, + fontWeight: 500, + color: 'textPrimary', + }, heading1: { fontSize: [20, 24], lineHeight: 1.4, diff --git a/fabric/src/theme/types.ts b/fabric/src/theme/types.ts index e8c36c0074..52d0023606 100644 --- a/fabric/src/theme/types.ts +++ b/fabric/src/theme/types.ts @@ -90,6 +90,7 @@ type ThemeSpace = SpaceValue[] & { // Typography export type TextVariantName = + | 'heading' | 'heading1' | 'heading2' | 'heading3'