From 719c0776f57451cb05d6007a4cbdca8d4997e9d8 Mon Sep 17 00:00:00 2001 From: Katty Barroso <51223655+kattylucy@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:14:55 +0200 Subject: [PATCH 1/6] Fix gnosis chart (#2472) Y axis values should update accordingly to show an increasing value instead of flat values Ticks should be month only --- .../Charts/PoolPerformanceChart.tsx | 27 ++--------- centrifuge-app/src/components/Charts/utils.ts | 26 +++++++++++ .../Portfolio/CardPortfolioValue.tsx | 8 +--- .../components/Portfolio/PortfolioValue.tsx | 46 +++++++++++-------- 4 files changed, 58 insertions(+), 49 deletions(-) diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index ce2bc15c37..b7a2203956 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -10,10 +10,10 @@ import { useLoans } from '../../utils/useLoans' import { useDailyPoolStates, usePool } from '../../utils/usePools' import { Tooltips } from '../Tooltips' import { TooltipContainer, TooltipTitle } from './Tooltip' -import { getRangeNumber } from './utils' +import { getOneDayPerMonth, getRangeNumber } from './utils' type ChartData = { - day: Date + day: Date | string nav: number price: number | null } @@ -120,23 +120,6 @@ function PoolPerformanceChart() { if (truncatedPoolStates && truncatedPoolStates?.length < 1 && poolAge > 0) return No data available - const getOneDayPerMonth = (): any[] => { - const seenMonths = new Set() - const result: any[] = [] - - chartData.forEach((item) => { - const date = new Date(item.day) - const monthYear = date.toLocaleString('default', { month: 'short', year: 'numeric' }) - - if (!seenMonths.has(monthYear)) { - seenMonths.add(monthYear) - result.push(item.day) - } - }) - - return result - } - return ( @@ -194,8 +177,8 @@ function PoolPerformanceChart() { minTickGap={100000} tickLine={false} type="category" - tick={} - ticks={getOneDayPerMonth()} + tick={} + ticks={getOneDayPerMonth(chartData, 'day')} /> { +export const CustomTick = ({ x, y, payload }: any) => { const theme = useTheme() return ( diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts index f69ef6b21d..f65faeee38 100644 --- a/centrifuge-app/src/components/Charts/utils.ts +++ b/centrifuge-app/src/components/Charts/utils.ts @@ -1,3 +1,7 @@ +type ChartDataProps = { + [key: string]: any +} + export const getRangeNumber = (rangeValue: string, poolAge?: number) => { if (rangeValue === '30d') { return 30 @@ -21,3 +25,25 @@ export const getRangeNumber = (rangeValue: string, poolAge?: number) => { return 30 } + +export const getOneDayPerMonth = (chartData: ChartDataProps[], key: string): (string | number)[] => { + const seenMonths = new Set() + const result: (string | number)[] = [] + + chartData.forEach((item) => { + const value = item[key] + if (value) { + const date = new Date(value) + if (!isNaN(date.getTime())) { + const monthYear = date.toLocaleString('default', { month: 'short', year: 'numeric' }) + + if (!seenMonths.has(monthYear)) { + seenMonths.add(monthYear) + result.push(date.getTime()) + } + } + } + }) + + return result +} diff --git a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx index 37c2e33137..ba32d1c806 100644 --- a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx @@ -71,7 +71,7 @@ export function CardPortfolioValue({ Overview - + Current portfolio value @@ -79,12 +79,6 @@ export function CardPortfolioValue({ {formatBalance(currentPortfolioValue || 0, config.baseCurrency)} - {/* - Profit - - + {formatBalance(Dec(portfolioValue || 0), config.baseCurrency)} - - */} diff --git a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx index c723b6942a..3ad4704465 100644 --- a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx @@ -1,8 +1,10 @@ import { Card, Stack, Text } from '@centrifuge/fabric' +import { useMemo } from 'react' import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' import { formatDate } from '../../utils/date' -import { formatBalance } from '../../utils/formatting' -import { getRangeNumber } from '../Charts/utils' +import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { CustomTick } from '../Charts/PoolPerformanceChart' +import { getOneDayPerMonth, getRangeNumber } from '../Charts/utils' import { useDailyPortfolioValue } from './usePortfolio' const chartColor = '#006ef5' @@ -31,17 +33,19 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad const rangeNumber = getRangeNumber(rangeValue) const dailyPortfolioValue = useDailyPortfolioValue(address, rangeNumber) - const getXAxisInterval = () => { - if (!rangeNumber) return dailyPortfolioValue ? Math.floor(dailyPortfolioValue.length / 10) : 45 - if (rangeNumber <= 30) return 5 - if (rangeNumber > 30 && rangeNumber <= 90) { - return 14 - } - if (rangeNumber > 90 && rangeNumber <= 180) { - return 30 + const chartData = dailyPortfolioValue?.map((value) => ({ + ...value, + portfolioValue: value.portfolioValue.toNumber(), + })) + + const yAxisDomain = useMemo(() => { + if (chartData?.length) { + const values = chartData.map((data) => data.portfolioValue) + return [Math.min(...values), Math.max(...values)] } - return 45 - } + }, [chartData]) + + if (!chartData?.length) return return ( @@ -51,7 +55,7 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad right: 20, bottom: 0, }} - data={dailyPortfolioValue?.reverse()} + data={chartData?.reverse()} > @@ -60,28 +64,30 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad - - `${dateInMilliseconds?.toLocaleString('default', { month: 'short' })} ${dateInMilliseconds?.getDate()}` - } + dataKey="dateInMilliseconds" tickLine={false} axisLine={false} style={{ fontSize: '10px', }} dy={4} - interval={getXAxisInterval()} + interval={0} + minTickGap={100000} + type="category" + ticks={getOneDayPerMonth(chartData, 'dateInMilliseconds')} + tick={} /> portfolioValue} + dataKey="portfolioValue" tickCount={10} tickLine={false} axisLine={false} - tickFormatter={(value) => value.toLocaleString()} + tickFormatter={(value) => formatBalanceAbbreviated(value, '', 2)} style={{ fontSize: '10px', }} + domain={yAxisDomain} label={{ value: 'USD', position: 'top', From ebcfd1bfc00a0536164cb8c04b3eb31c02b06cfc Mon Sep 17 00:00:00 2001 From: Sophia Date: Mon, 7 Oct 2024 10:53:17 -0400 Subject: [PATCH 2/6] Clean up onboarding UI after redesign (#2475) --- .../src/components/Onboarding/Header.tsx | 22 ++----------------- .../Onboarding/CompleteExternalOnboarding.tsx | 4 +--- .../src/pages/Onboarding/EmailVerified.tsx | 2 +- .../pages/Onboarding/UpdateInvestorStatus.tsx | 2 +- centrifuge-app/src/pages/Onboarding/index.tsx | 6 ++--- .../src/controllers/kyb/verifyBusiness.ts | 6 ++--- .../src/emails/sendApproveInvestorMessage.ts | 2 +- .../src/emails/sendDocumentsMessage.ts | 6 ++--- .../src/emails/sendVerifyEmailMessage.ts | 4 ++-- 9 files changed, 16 insertions(+), 38 deletions(-) diff --git a/centrifuge-app/src/components/Onboarding/Header.tsx b/centrifuge-app/src/components/Onboarding/Header.tsx index faadce6d37..a67b02fb63 100644 --- a/centrifuge-app/src/components/Onboarding/Header.tsx +++ b/centrifuge-app/src/components/Onboarding/Header.tsx @@ -1,34 +1,16 @@ -import { WalletMenu } from '@centrifuge/centrifuge-react' -import { Box, Shelf } from '@centrifuge/fabric' +import { Shelf } from '@centrifuge/fabric' import * as React from 'react' -import { Link } from 'react-router-dom' -import { config } from '../../config' -import { OnboardingStatus } from '../OnboardingStatus' type HeaderProps = { children?: React.ReactNode - walletMenu?: boolean } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const [_, WordMark] = config.logo - -export function Header({ children, walletMenu = true }: HeaderProps) { +export function Header({ children }: HeaderProps) { return ( - - - - {children} - - {walletMenu && ( - - ]} /> - - )} ) } diff --git a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx index 48e82fdd03..cc65cacbbb 100644 --- a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx +++ b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx @@ -27,9 +27,7 @@ export const CompleteExternalOnboarding = ({ openNewTab, poolId, poolSymbol }: P return ( -
- {!!poolId && } -
+
{!!poolId && }
<> diff --git a/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx b/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx index 8fd683afe8..d82796a3a8 100644 --- a/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx +++ b/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx @@ -11,7 +11,7 @@ export default function EmailVerified() { return ( -
+
diff --git a/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx b/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx index e837ee8ba5..98cb13a416 100644 --- a/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx +++ b/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx @@ -25,7 +25,7 @@ export default function UpdateInvestorStatus() { : null return ( -
+
{data && poolMetadata && data && token ? ( diff --git a/centrifuge-app/src/pages/Onboarding/index.tsx b/centrifuge-app/src/pages/Onboarding/index.tsx index 5db356fb5f..6d7171ae69 100644 --- a/centrifuge-app/src/pages/Onboarding/index.tsx +++ b/centrifuge-app/src/pages/Onboarding/index.tsx @@ -81,7 +81,7 @@ export default function OnboardingPage() { } setPool(null) - return navigate('/onboarding') + return navigate('#/onboarding') }, [ poolId, setPool, @@ -118,9 +118,7 @@ export default function OnboardingPage() { return ( -
- {!!poolId && } -
+
{!!poolId && }
reference: MANUAL_KYB_REFERENCE, email: body.email, country: body.jurisdictionCode, - redirect_url: `${origin}/manual-kyb-redirect.html`, - callback_url: `${callbackBaseUrl}/manualKybCallback?${searchParams}`, + redirect_url: `${origin}#/manual-kyb-redirect.html`, + callback_url: `${callbackBaseUrl}#/manualKybCallback?${searchParams}`, } const manualKyb = await shuftiProRequest(payloadmanualKYB) diff --git a/onboarding-api/src/emails/sendApproveInvestorMessage.ts b/onboarding-api/src/emails/sendApproveInvestorMessage.ts index 28f41948d9..55e6b35caf 100644 --- a/onboarding-api/src/emails/sendApproveInvestorMessage.ts +++ b/onboarding-api/src/emails/sendApproveInvestorMessage.ts @@ -18,7 +18,7 @@ export const sendApproveInvestorMessage = async ( ], dynamic_template_data: { trancheName: tranche?.currency.name, - poolUrl: `${process.env.REDIRECT_URL}/pools/${poolId}`, + poolUrl: `${process.env.REDIRECT_URL}#/pools/${poolId}`, }, }, ], diff --git a/onboarding-api/src/emails/sendDocumentsMessage.ts b/onboarding-api/src/emails/sendDocumentsMessage.ts index e9d73fd3be..b307948d0e 100644 --- a/onboarding-api/src/emails/sendDocumentsMessage.ts +++ b/onboarding-api/src/emails/sendDocumentsMessage.ts @@ -54,13 +54,13 @@ export const sendDocumentsMessage = async ( }, ], dynamic_template_data: { - rejectLink: `${process.env.REDIRECT_URL}/onboarding/updateInvestorStatus?token=${encodeURIComponent( + rejectLink: `${process.env.REDIRECT_URL}#/onboarding/updateInvestorStatus?token=${encodeURIComponent( token )}&status=rejected&metadata=${pool?.metadata}`, - approveLink: `${process.env.REDIRECT_URL}/onboarding/updateInvestorStatus?token=${encodeURIComponent( + approveLink: `${process.env.REDIRECT_URL}#/onboarding/updateInvestorStatus?token=${encodeURIComponent( token )}&status=approved&metadata=${pool?.metadata}&network=${wallet.network}`, - disclaimerLink: `${process.env.REDIRECT_URL}/disclaimer`, + disclaimerLink: `${process.env.REDIRECT_URL}#/disclaimer`, trancheName: tranche?.currency.name, investorEmail, }, diff --git a/onboarding-api/src/emails/sendVerifyEmailMessage.ts b/onboarding-api/src/emails/sendVerifyEmailMessage.ts index 8dcb06aa38..38f83309e5 100644 --- a/onboarding-api/src/emails/sendVerifyEmailMessage.ts +++ b/onboarding-api/src/emails/sendVerifyEmailMessage.ts @@ -26,8 +26,8 @@ export const sendVerifyEmailMessage = async (user: OnboardingUser, wallet: Reque }, ], dynamic_template_data: { - verifyLink: `${process.env.REDIRECT_URL}/onboarding/verifyEmail?token=${encodeURIComponent(token)}`, - disclaimerLink: `${process.env.REDIRECT_URL}/disclaimer`, + verifyLink: `${process.env.REDIRECT_URL}#/onboarding/verifyEmail?token=${encodeURIComponent(token)}`, + disclaimerLink: `${process.env.REDIRECT_URL}#/disclaimer`, }, }, ], From 4a888938506bc8b046984a8e5ca3b059e4913f65 Mon Sep 17 00:00:00 2001 From: Sophia Date: Mon, 7 Oct 2024 11:43:15 -0400 Subject: [PATCH 3/6] Fix invalid character in purchase form (#2466) * Fix invalid character when quantity is a decimal * Fix invalid character --- centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index a81ce0a66f..51ec17336b 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -53,8 +53,8 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour if (source === 'reserve') { financeTx = cent.pools.financeExternalLoan([poolId, loanId, quantity, price], { batch: true }) } else { - const principal = new CurrencyBalance( - price.mul(new BN(quantity.toDecimal().toString())), + const principal = CurrencyBalance.fromFloat( + price.toDecimal().mul(quantity.toDecimal()).toString(), pool.currency.decimals ) const repay = { principal, interest: new BN(0), unscheduled: new BN(0) } @@ -93,7 +93,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour }, onSubmit: (values, actions) => { const price = CurrencyBalance.fromFloat(values.price.toString(), pool.currency.decimals) - const quantity = Price.fromFloat(values.quantity) + const quantity = Price.fromFloat(values.quantity.toString()) doFinanceTransaction([loan.poolId, loan.id, quantity, price], { account, }) From a183e21509a93d1563ae9bf9267c813a55d86417 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 8 Oct 2024 10:39:11 -0400 Subject: [PATCH 4/6] Add ConnectionGuard to swaps fulfillment (#2476) --- .../src/components/Swaps/Orders.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/centrifuge-app/src/components/Swaps/Orders.tsx b/centrifuge-app/src/components/Swaps/Orders.tsx index c0ddbed895..4a8ecdb90c 100644 --- a/centrifuge-app/src/components/Swaps/Orders.tsx +++ b/centrifuge-app/src/components/Swaps/Orders.tsx @@ -12,6 +12,7 @@ import { WithdrawAddress, } from '@centrifuge/centrifuge-js' import { + ConnectionGuard, truncateAddress, useBalances, useCentrifuge, @@ -110,15 +111,17 @@ export function Orders({ buyOrSell }: OrdersProps) { align: 'left', header: '', cell: (row: SwapOrder) => ( - + + + ), flex: '1', }, From 2a2ea427e87d14fae4a5fe82eb5047dedd362e40 Mon Sep 17 00:00:00 2001 From: Sophia Date: Wed, 9 Oct 2024 13:55:59 -0400 Subject: [PATCH 5/6] Update token price of all deployed domains (#2478) --- .../NavManagement/NavManagementAssetTable.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx index dc15c94997..792f501e4a 100644 --- a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx +++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx @@ -102,16 +102,18 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) { const { execute, isLoading } = useCentrifugeTransaction( 'Update NAV', (cent) => (args: [values: FormValues], options) => { - const domain = domains?.find((domain) => domain.isActive && domain.hasDeployedLp) - const updateTokenPrices = domain - ? Object.entries(domain.liquidityPools).flatMap(([tid, poolsByCurrency]) => { - return domain.currencies - .filter((cur) => !!poolsByCurrency[cur.address]) - .map((cur) => [tid, cur.key] satisfies [string, CurrencyKey]) - .map(([tid, curKey]) => - cent.liquidityPools.updateTokenPrice([poolId, tid, curKey, domain.chainId], { batch: true }) - ) - }) + const deployedDomains = domains?.filter((domain) => domain.hasDeployedLp) + const updateTokenPrices = deployedDomains + ? deployedDomains.flatMap((domain) => + Object.entries(domain.liquidityPools).flatMap(([tid, poolsByCurrency]) => { + return domain.currencies + .filter((cur) => !!poolsByCurrency[cur.address]) + .map((cur) => [tid, cur.key] satisfies [string, CurrencyKey]) + .map(([tid, curKey]) => + cent.liquidityPools.updateTokenPrice([poolId, tid, curKey, domain.chainId], { batch: true }) + ) + }) + ) : [] return combineLatest([cent.pools.closeEpoch([poolId, false], { batch: true }), ...updateTokenPrices]).pipe( From 180f9ec1eb664a6bbbedb80cdb19e9f8793791f8 Mon Sep 17 00:00:00 2001 From: Katty Barroso <51223655+kattylucy@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:01:36 +0200 Subject: [PATCH 6/6] Pool Overview redesign (#2435) * Pool overview redesign * Fix types * Fix linter warnings --- .../Charts/PoolPerformanceChart.tsx | 456 ++++++++++++---- .../src/components/Charts/PriceChart.tsx | 66 ++- .../src/components/Charts/SimpleBarChart.tsx | 101 ++++ .../src/components/Charts/Tooltip.tsx | 6 +- centrifuge-app/src/components/DataTable.tsx | 59 +- .../components/InvestRedeem/InvestForm.tsx | 1 + .../components/InvestRedeem/InvestRedeem.tsx | 69 +-- .../InvestRedeem/InvestRedeemDrawer.tsx | 106 ++-- .../components/InvestRedeem/RedeemForm.tsx | 3 +- .../src/components/IssuerSection.tsx | 294 ++++++---- .../src/components/LayoutBase/index.tsx | 32 +- .../src/components/LayoutBase/styles.tsx | 38 +- centrifuge-app/src/components/PillButton.tsx | 11 +- centrifuge-app/src/components/PoolList.tsx | 51 +- .../components/PoolOverview/KeyMetrics.tsx | 359 ++++++++----- .../PoolOverview/PoolPerfomance.tsx | 2 +- .../components/PoolOverview/PoolStructure.tsx | 102 ---- .../PoolOverview/TrancheTokenCards.tsx | 203 ++++--- .../PoolOverview/TransactionHistory.tsx | 54 +- .../src/components/Portfolio/Transactions.tsx | 4 +- .../src/components/Report/BalanceSheet.tsx | 11 +- .../components/Report/CashflowStatement.tsx | 16 +- .../src/components/Report/DataFilter.tsx | 355 ++++++++++++ .../src/components/Report/PoolReportPage.tsx | 21 +- .../src/components/Report/ProfitAndLoss.tsx | 19 +- .../src/components/Report/ReportContext.tsx | 12 +- .../src/components/Report/ReportFilter.tsx | 503 ++++++------------ .../src/components/Report/index.tsx | 21 +- centrifuge-app/src/components/Tooltips.tsx | 16 +- .../IssuerCreatePool/CustomCategories.tsx | 128 +++++ .../pages/IssuerCreatePool/IssuerInput.tsx | 4 + .../src/pages/IssuerCreatePool/index.tsx | 2 + .../pages/IssuerPool/Configuration/Issuer.tsx | 7 +- .../src/pages/IssuerPool/Header.tsx | 3 +- centrifuge-app/src/pages/IssuerPool/index.tsx | 2 + .../NavManagement/NavManagementAssetTable.tsx | 2 +- .../Onboarding/CompleteExternalOnboarding.tsx | 2 +- centrifuge-app/src/pages/Pool/Header.tsx | 13 +- .../src/pages/Pool/Overview/index.tsx | 124 ++--- centrifuge-app/src/pages/Pool/index.tsx | 2 + centrifuge-app/src/pages/Pools.tsx | 17 +- centrifuge-app/src/utils/formatting.ts | 9 +- centrifuge-js/src/modules/pools.ts | 30 +- fabric/.storybook/preview.jsx | 4 - fabric/src/components/Button/WalletButton.tsx | 6 + fabric/src/components/InputUnit/index.tsx | 31 +- fabric/src/components/Select/index.tsx | 10 +- fabric/src/components/Tabs/index.tsx | 40 +- fabric/src/components/TextInput/index.tsx | 24 +- fabric/src/components/Tooltip/index.tsx | 7 +- fabric/src/icon-svg/Icon-balance-sheet.svg | 3 + fabric/src/icon-svg/Icon-cashflow.svg | 3 + fabric/src/icon-svg/Icon-profit-and-loss.svg | 3 + fabric/src/icon-svg/IconMoody.svg | 9 + fabric/src/icon-svg/IconSp.svg | 10 + .../src/icon-svg/icon-arrow-right-white.svg | 10 + fabric/src/theme/altairDark.ts | 36 -- fabric/src/theme/altairLight.ts | 28 - fabric/src/theme/centrifugeTheme.ts | 18 +- fabric/src/theme/index.ts | 2 - fabric/src/theme/tokens/baseTheme.ts | 2 +- fabric/src/theme/tokens/colors.ts | 1 + fabric/src/theme/tokens/theme.ts | 50 +- fabric/src/theme/tokens/typography.ts | 2 +- 64 files changed, 2215 insertions(+), 1420 deletions(-) create mode 100644 centrifuge-app/src/components/Charts/SimpleBarChart.tsx delete mode 100644 centrifuge-app/src/components/PoolOverview/PoolStructure.tsx create mode 100644 centrifuge-app/src/components/Report/DataFilter.tsx create mode 100644 centrifuge-app/src/pages/IssuerCreatePool/CustomCategories.tsx create mode 100644 fabric/src/icon-svg/Icon-balance-sheet.svg create mode 100644 fabric/src/icon-svg/Icon-cashflow.svg create mode 100644 fabric/src/icon-svg/Icon-profit-and-loss.svg create mode 100644 fabric/src/icon-svg/IconMoody.svg create mode 100644 fabric/src/icon-svg/IconSp.svg create mode 100644 fabric/src/icon-svg/icon-arrow-right-white.svg delete mode 100644 fabric/src/theme/altairDark.ts delete mode 100644 fabric/src/theme/altairLight.ts diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index b7a2203956..62c839d35e 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -1,39 +1,96 @@ -import { AnchorButton, Box, Grid, IconDownload, Shelf, Stack, Text } from '@centrifuge/fabric' +import { DailyPoolState, DailyTrancheState, Pool } from '@centrifuge/centrifuge-js' +import { AnchorButton, Box, IconDownload, Select, Shelf, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' import { Bar, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import styled, { useTheme } from 'styled-components' +import { ValueType } from 'recharts/types/component/DefaultTooltipContent' +import { useTheme } from 'styled-components' import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl' import { daysBetween, formatDate } from '../../utils/date' -import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting' import { useLoans } from '../../utils/useLoans' import { useDailyPoolStates, usePool } from '../../utils/usePools' -import { Tooltips } from '../Tooltips' +import { Tooltips, tooltipText } from '../Tooltips' import { TooltipContainer, TooltipTitle } from './Tooltip' import { getOneDayPerMonth, getRangeNumber } from './utils' type ChartData = { day: Date | string nav: number - price: number | null + juniorTokenPrice: number | null + seniorTokenPrice?: number | null + currency?: string + seniorAPY: number | null | undefined + juniorAPY: number | null + isToday: boolean } -const RangeFilterButton = styled(Stack)` - &:hover { - cursor: pointer; +type GraphDataItemWithType = { + show: boolean + color: string + type: keyof typeof tooltipText + label: string + value: string | number +} + +type GraphDataItemWithoutType = { + show: boolean + color: string + label: string + value: string | number +} + +type GraphDataItem = GraphDataItemWithType | GraphDataItemWithoutType + +type CustomTickProps = { + x: number + y: number + payload: { + value: ValueType } -` +} const rangeFilters = [ + { value: 'all', label: 'All' }, { value: '30d', label: '30 days' }, { value: '90d', label: '90 days' }, { value: 'ytd', label: 'Year to date' }, - { value: 'all', label: 'All' }, -] as const +] + +function calculateTranchePrices(pool: Pool) { + if (!pool?.tranches) return { juniorTokenPrice: 0, seniorTokenPrice: null } + + const juniorTranche = pool.tranches.find((t) => t.seniority === 0) + const seniorTranche = pool.tranches.length > 1 ? pool.tranches.find((t) => t.seniority === 1) : null + + const juniorTokenPrice = + juniorTranche && juniorTranche.tokenPrice ? Number(formatBalance(juniorTranche.tokenPrice, undefined, 5, 5)) : 0 + + const seniorTokenPrice = + seniorTranche && seniorTranche.tokenPrice ? Number(formatBalance(seniorTranche.tokenPrice, undefined, 5, 5)) : null + + return { juniorTokenPrice, seniorTokenPrice } +} + +function getYieldFieldForFilter(tranche: DailyTrancheState, filter: string) { + switch (filter) { + case '30d': + return tranche.yield30DaysAnnualized || 0 + case '90d': + return tranche.yield90DaysAnnualized || 0 + case 'ytd': + return tranche.yieldYTD || 0 + case 'all': + return tranche.yieldSinceInception || 0 + default: + return 0 + } +} function PoolPerformanceChart() { const theme = useTheme() - const chartColor = theme.colors.accentPrimary + const [selectedTabIndex, setSelectedTabIndex] = React.useState(0) + const chartColor = theme.colors.textGold const { pid: poolId } = useParams<{ pid: string }>() if (!poolId) throw new Error('Pool not found') @@ -51,7 +108,7 @@ function PoolPerformanceChart() { return acc }, '') - const truncatedPoolStates = poolStates?.filter((poolState) => { + const truncatedPoolStates = poolStates?.filter((poolState: DailyPoolState) => { if (firstOriginationDate) { return new Date(poolState.timestamp) >= new Date(firstOriginationDate) } @@ -61,30 +118,67 @@ function PoolPerformanceChart() { const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'all', label: 'All' }) const rangeNumber = getRangeNumber(range.value, poolAge) ?? 100 - const isSingleTranche = pool?.tranches.length === 1 - // querying chain for more accurate data, since data for today from subquery is not necessarily up to date const todayAssetValue = pool?.nav.total.toDecimal().toNumber() || 0 const todayPrice = pool?.tranches ? formatBalance(pool?.tranches[pool.tranches.length - 1].tokenPrice || 0, undefined, 5, 5) : null + const trancheTodayPrice = calculateTranchePrices(pool as Pool) + const data: ChartData[] = React.useMemo( () => truncatedPoolStates?.map((day) => { const nav = day.poolState.netAssetValue.toDecimal().toNumber() - const price = (isSingleTranche && Object.values(day.tranches)[0].price?.toFloat()) || null + + const trancheKeys = Object.keys(day.tranches) + const juniorTrancheKey = trancheKeys[0] + const seniorTrancheKey = trancheKeys[1] || null + + const juniorTokenPrice = day.tranches[juniorTrancheKey]?.price?.toFloat() ?? 0 + const seniorTokenPrice = seniorTrancheKey ? day.tranches[seniorTrancheKey]?.price?.toFloat() ?? null : null + + const juniorAPY = getYieldFieldForFilter(day.tranches[juniorTrancheKey], range.value) + const formattedJuniorAPY = juniorAPY !== 0 ? juniorAPY.toPercent().toNumber() : 0 + const seniorAPY = seniorTrancheKey ? getYieldFieldForFilter(day.tranches[seniorTrancheKey], range.value) : null + const formattedSeniorAPY = seniorAPY !== 0 ? seniorAPY?.toPercent().toNumber() : null + if (day.timestamp && new Date(day.timestamp).toDateString() === new Date().toDateString()) { - return { day: new Date(day.timestamp), nav: todayAssetValue, price: Number(todayPrice) } + const tranchePrices = calculateTranchePrices(pool as Pool) + + return { + day: new Date(day.timestamp), + nav: todayAssetValue, + juniorTokenPrice: tranchePrices.juniorTokenPrice ?? 0, + seniorTokenPrice: tranchePrices.seniorTokenPrice ?? null, + juniorAPY: formattedJuniorAPY, + seniorAPY: formattedSeniorAPY, + isToday: true, + } + } + + return { + day: new Date(day.timestamp), + nav: Number(nav), + juniorTokenPrice: juniorTokenPrice !== 0 ? juniorTokenPrice : null, + seniorTokenPrice: seniorTokenPrice !== 0 ? seniorTokenPrice : null, + juniorAPY: formattedJuniorAPY, + seniorAPY: formattedSeniorAPY, + isToday: false, } - return { day: new Date(day.timestamp), nav: Number(nav), price: Number(price) } }) || [], - [isSingleTranche, truncatedPoolStates, todayAssetValue, todayPrice] + [truncatedPoolStates, todayAssetValue, pool, range] ) + const todayData = data.find((day) => day.isToday) + const today = { nav: todayAssetValue, price: todayPrice, + currency: pool.currency.symbol, + juniorAPY: todayData?.juniorAPY, + seniorAPY: todayData?.seniorAPY, + ...trancheTodayPrice, } const chartData = data.slice(-rangeNumber) @@ -94,73 +188,54 @@ function PoolPerformanceChart() { return undefined } - const filteredData = chartData.map((data) => ({ - day: data.day, - tokenPrice: data.price, - })) + const filteredData = chartData.map((data) => { + const base = { + day: data.day, + nav: data.nav, + juniorTokenPrice: data.juniorTokenPrice ?? 0, + juniorAPY: data.juniorAPY, + } + if (data.seniorTokenPrice && data.seniorAPY) { + return { + ...base, + seniorTokenPrice: data.seniorTokenPrice, + seniorAPY: data.seniorAPY, + } + } else return { ...base } + }) return getCSVDownloadUrl(filteredData as any) }, [chartData]) - const priceRange = React.useMemo(() => { - if (!chartData) return [0, 100] - - const min = - chartData?.reduce((prev, curr) => { - return prev.price! < curr.price! ? prev : curr - }, chartData[0])?.price || 0 - - const max = - chartData?.reduce((prev, curr) => { - return prev.price! > curr.price! ? prev : curr - }, chartData[0])?.price || 1 - return [min, max] - }, [chartData]) - if (truncatedPoolStates && truncatedPoolStates?.length < 1 && poolAge > 0) return No data available return ( - - - + + + Pool performance + setSelectedTabIndex(index)}> + + Price + + + APY + + Download - - - - {chartData.length > 0 && - rangeFilters.map((rangeFilter, index) => ( - - setRange(rangeFilter)}> - - {rangeFilter.label} - - - - {index !== rangeFilters.length - 1 && ( - - )} - - ))} - - - - + + {chartData?.length ? ( @@ -177,13 +252,13 @@ function PoolPerformanceChart() { minTickGap={100000} tickLine={false} type="category" - tick={} + tick={(props) => } ticks={getOneDayPerMonth(chartData, 'day')} /> formatBalanceAbbreviated(tick, '', 0)} yAxisId="left" width={80} @@ -191,11 +266,11 @@ function PoolPerformanceChart() { formatBalanceAbbreviated(tick, '', 6)} + style={{ fontSize: '10px', fill: theme.colors.textPrimary }} + tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 2)} yAxisId="right" orientation="right" - domain={priceRange} + domain={selectedTabIndex === 0 ? ['dataMin - 0.25', 'dataMax + 0.25'] : [0, 'dataMax + 0.25']} /> {formatDate(payload[0].payload.day)} - {payload.map(({ name, value }, index) => ( - - - {name === 'nav' ? 'NAV' : name === 'price' ? 'Token price' : 'Cash'} - - - {name === 'nav' && typeof value === 'number' - ? formatBalance(value, 'USD') - : typeof value === 'number' - ? formatBalance(value, 'USD', 6) - : '-'} - - - ))} + {payload.map(({ name, value }, index) => { + const labelMap: Record = { + nav: 'NAV', + juniorTokenPrice: 'Junior Token Price', + seniorTokenPrice: 'Senior Token Price', + juniorAPY: 'Junior APY', + seniorAPY: 'Senior APY', + default: 'Cash', + } + + const label = typeof name === 'string' ? labelMap[name] ?? labelMap.default : labelMap.default + + const formattedValue = (() => { + if (typeof value === 'undefined' || Array.isArray(value)) { + return '-' + } + + if (name === 'juniorAPY' || name === 'seniorAPY') { + return formatPercentage(value) + } + + return formatBalance( + Number(value), + name === 'nav' ? pool.currency.symbol ?? 'USD' : '', + name === 'juniorTokenPrice' || name === 'seniorTokenPrice' ? 6 : 0 + ) + })() + + return ( + + + {label} + + + {formattedValue} + + + ) + })} ) } return null }} /> - - + + + {chartData.some((d) => d.seniorTokenPrice !== null) && ( + + )} + + ) : ( @@ -238,50 +387,131 @@ function PoolPerformanceChart() { function CustomLegend({ data, + setRange, + selectedTabIndex, }: { data: { + currency: string nav: number - price: number | null + juniorTokenPrice: number + seniorTokenPrice?: number | null + juniorAPY: number | undefined | null + seniorAPY: number | undefined | null } + setRange: (value: { value: string; label: string }) => void + selectedTabIndex: number }) { - const theme = useTheme() + const juniorAPY = data.juniorAPY ?? 0 + + const Dot = ({ color }: { color: string }) => ( + + ) + + const navData = { + color: 'backgroundTertiary', + label: `NAV ${data.currency}`, + value: formatBalance(data.nav), + type: 'nav', + show: true, + } + + const tokenData = [ + navData, + { + color: 'textGold', + label: 'Junior token price', + value: formatBalance(data.juniorTokenPrice ?? 0, '', 3), + type: 'singleTrancheTokenPrice', + show: true, + }, + { + color: 'textPrimary', + label: 'Senior token price', + value: formatBalance(data.seniorTokenPrice ?? 0, '', 3), + type: 'singleTrancheTokenPrice', + show: !!data.seniorTokenPrice, + }, + ] + + const apyData = [ + navData, + { + color: 'textGold', + label: 'Junior APY', + value: formatPercentage(juniorAPY), + show: !!data.juniorAPY, + }, + { + color: 'textPrimary', + label: 'Senior APY', + value: formatPercentage(data.seniorAPY ?? 0), + show: !!data.seniorAPY, + }, + ] + + const graphData = selectedTabIndex === 0 ? tokenData : apyData + + const toggleRange = (e: React.ChangeEvent) => { + const value = e.target.value + const range = rangeFilters.find((range) => range.value === value) + setRange(range ?? rangeFilters[0]) + } return ( - - - - - {formatBalance(data.nav, 'USD')} - - {data.price && ( - - - {data.price ? formatBalance(data.price, 'USD', 6) : '-'} - - )} - - + + + {graphData.map((item: GraphDataItem, index: number) => { + if (!item.show) return null + + const hasType = (item: GraphDataItem): item is GraphDataItemWithType => { + return (item as GraphDataItemWithType).type !== undefined + } + + return ( + + + + {hasType(item) ? ( + + ) : ( + + {item.label} + + )} + + {item.value} + + ) + })} + + + ) => { + const { value } = event.target + if (value) { + navigate(`${basePath}/${pool.id}/data/${value}`) + } + }} + /> + + + {['pool-balance', 'token-price'].includes(report) && ( + + { + setLoanStatus(event.target.value) + }} + /> + + )} + + {(report === 'investor-list' || report === 'investor-tx') && ( + + { + setLoan(event.target.value) + }} + value={loan} + options={[ + { label: 'All', value: 'all' }, + ...(loans?.map((l) => ({ value: l.id, label: })) ?? []), + ]} + /> + + )} + + {['investor-tx', 'asset-tx', 'fee-tx'].includes(report) && ( + + { + return { + label: getNetworkName(domain.chainId), + value: String(domain.chainId), + } + }), + ]} + value={network} + onChange={(e) => { + const { value } = e.target + if (value) { + setNetwork(isNaN(Number(value)) ? value : Number(value)) + } + }} + /> + + + setAddress(e.target.value)} + /> + + + )} + + + {!['investor-list', 'asset-list'].includes(report) && ( + <> + + setStartDate(e.target.value)} /> + + setEndDate(e.target.value)} /> + + )} + {report === 'asset-list' && ( + setStartDate(e.target.value)} /> + )} + + } + small + variant="inverted" + style={{ marginTop: 28, marginLeft: 12 }} + > + CSV + + + + ) +} + +function LoanOption({ loan }: { loan: Loan }) { + const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, false) + const { data: metadata } = useMetadata(nft?.metadataUri, nftMetadataSchema) + return ( + + ) +} diff --git a/centrifuge-app/src/components/Report/PoolReportPage.tsx b/centrifuge-app/src/components/Report/PoolReportPage.tsx index 820f62c750..c2f0124750 100644 --- a/centrifuge-app/src/components/Report/PoolReportPage.tsx +++ b/centrifuge-app/src/components/Report/PoolReportPage.tsx @@ -1,34 +1,37 @@ import { Pool } from '@centrifuge/centrifuge-js' import * as React from 'react' -import { useParams } from 'react-router' +import { useLocation, useParams } from 'react-router' import { ReportComponent } from '.' -import { usePool } from '../../utils/usePools' +import { usePool } from '../../../src/utils/usePools' import { LoadBoundary } from '../LoadBoundary' import { Spinner } from '../Spinner' +import { DataFilter } from './DataFilter' import { ReportContextProvider } from './ReportContext' import { ReportFilter } from './ReportFilter' export function PoolReportPage({ header }: { header: React.ReactNode }) { - const { pid: poolId } = useParams<{ pid: string }>() - if (!poolId) throw new Error('Pool not found') + const params = useParams<{ pid: string; '*': string }>() + const location = useLocation() + const { pid: poolId } = params - const pool = usePool(poolId) as Pool + if (!poolId) throw new Error('Pool not found') return ( {header} - {pool && } + {location.pathname.includes('reporting') ? : } - + ) } -function PoolDetailReporting({ pool }: { pool: Pool }) { - if (!pool) { +function PoolDetailReporting({ poolId }: { poolId: string }) { + const pool = usePool(poolId) as Pool + if (!poolId || !pool) { return } diff --git a/centrifuge-app/src/components/Report/ProfitAndLoss.tsx b/centrifuge-app/src/components/Report/ProfitAndLoss.tsx index cadd57ca52..c8b21bf9b3 100644 --- a/centrifuge-app/src/components/Report/ProfitAndLoss.tsx +++ b/centrifuge-app/src/components/Report/ProfitAndLoss.tsx @@ -1,5 +1,5 @@ import { CurrencyBalance } from '@centrifuge/centrifuge-js' -import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' +import { DailyPoolState, Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' import { formatBalance } from '@centrifuge/centrifuge-react' import { Text, Tooltip } from '@centrifuge/fabric' import * as React from 'react' @@ -25,7 +25,7 @@ type Row = TableDataRow & { } export function ProfitAndLoss({ pool }: { pool: Pool }) { - const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext) + const { startDate, endDate, groupBy, setCsvData, setReportData } = React.useContext(ReportContext) const { data: poolMetadata } = usePoolMetadata(pool) const [adjustedStartDate, adjustedEndDate] = React.useMemo(() => { @@ -89,6 +89,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) { {row.name} ), width: '240px', + isLabel: true, }, ] .concat( @@ -102,6 +103,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) { ), width: '170px', + isLabel: false, })) ) .concat({ @@ -109,6 +111,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) { header: '', cell: () => , width: '1fr', + isLabel: false, }) }, [poolStates, groupBy]) @@ -334,6 +337,18 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [profitAndLossRecords, feesRecords, totalProfitRecords]) + React.useEffect(() => { + if (poolStates && Object.keys(poolStates).length > 0) { + const fullPoolStates: DailyPoolState[] = Object.values(poolStates).map((partialState) => { + return { + ...partialState, + } as DailyPoolState + }) + + setReportData(fullPoolStates) + } + }, [poolStates, setReportData]) + if (!poolStates) { return } diff --git a/centrifuge-app/src/components/Report/ReportContext.tsx b/centrifuge-app/src/components/Report/ReportContext.tsx index e9a9876c1c..7edcb7ed7a 100644 --- a/centrifuge-app/src/components/Report/ReportContext.tsx +++ b/centrifuge-app/src/components/Report/ReportContext.tsx @@ -1,5 +1,6 @@ +import { DailyPoolState } from '@centrifuge/centrifuge-js' import * as React from 'react' -import { useParams } from 'react-router' +import { useLocation, useParams } from 'react-router' import { useSearchParams } from 'react-router-dom' export type GroupBy = 'day' | 'month' | 'quarter' | 'year' | 'daily' @@ -49,6 +50,9 @@ export type ReportContextType = { loan: string setLoan: (type: string) => void + + reportData: DailyPoolState[] + setReportData: (type: DailyPoolState[]) => void } export type CsvDataProps = { @@ -63,9 +67,10 @@ export function ReportContextProvider({ children }: { children: React.ReactNode // Global filters const { report: reportParam } = useParams<{ report: Report }>() + const location = useLocation() const [searchParams, setSearchParams] = useSearchParams() - const report = reportParam || 'balance-sheet' + const report = reportParam ? reportParam : location.pathname.includes('reporting') ? 'balance-sheet' : 'investor-tx' const [startDate, setStartDate] = React.useState('') const [endDate, setEndDate] = React.useState(new Date().toISOString().slice(0, 10)) @@ -78,6 +83,7 @@ export function ReportContextProvider({ children }: { children: React.ReactNode const [address, setAddress] = React.useState(searchParams.get('address') || '') const [network, setNetwork] = React.useState(searchParams.get('network') || 'all') const [loan, setLoan] = React.useState(searchParams.get('loan') || '') + const [reportData, setReportData] = React.useState([]) React.useEffect(() => { const startDate = searchParams.get('from') @@ -170,6 +176,8 @@ export function ReportContextProvider({ children }: { children: React.ReactNode setNetwork: (value: any) => updateParamValues('network', value), loan, setLoan: (value: string) => updateParamValues('asset', value), + reportData, + setReportData, }} > {children} diff --git a/centrifuge-app/src/components/Report/ReportFilter.tsx b/centrifuge-app/src/components/Report/ReportFilter.tsx index 72193d6306..e4fcb9c62e 100644 --- a/centrifuge-app/src/components/Report/ReportFilter.tsx +++ b/centrifuge-app/src/components/Report/ReportFilter.tsx @@ -1,356 +1,197 @@ -import { Loan, Pool } from '@centrifuge/centrifuge-js' -import { useGetNetworkName } from '@centrifuge/centrifuge-react' -import { AnchorButton, Box, DateInput, SearchInput, Select, Shelf } from '@centrifuge/fabric' +import { CurrencyBalance, DailyPoolState, Pool } from '@centrifuge/centrifuge-js' +import { + AnchorButton, + Box, + Button, + DateInput, + IconBalanceSheet, + IconCashflow, + IconDownload, + IconProfitAndLoss, + Select, + Shelf, +} from '@centrifuge/fabric' import * as React from 'react' import { useNavigate } from 'react-router' -import { nftMetadataSchema } from '../../schemas' +import styled from 'styled-components' +import { usePool, usePoolMetadata } from '../../../src/utils/usePools' import { useBasePath } from '../../utils/useBasePath' -import { useActiveDomains } from '../../utils/useLiquidityPools' -import { useLoans } from '../../utils/useLoans' -import { useMetadata } from '../../utils/useMetadata' -import { useCentNFT } from '../../utils/useNFTs' -import { useDebugFlags } from '../DebugFlags' -import { GroupBy, Report, ReportContext } from './ReportContext' -import { formatPoolFeeTransactionType } from './utils' +import { SimpleBarChart } from '../Charts/SimpleBarChart' +import { GroupBy, ReportContext } from './ReportContext' + +interface StyledButtonProps { + selected?: boolean +} + +const StyledButton = styled(Button)` + margin-bottom: 12px; + margin-right: 12px; + @media (min-width: ${({ theme }) => theme.breakpoints['M']}) { + margin-bottom: 0; + } + & > span { + border-color: ${({ selected, theme }) => (selected ? 'transparent' : theme.colors.backgroundInverted)}; + } + &:hover > span { + border-color: ${({ selected, theme }) => (selected ? 'transparent' : theme.colors.backgroundInverted)}; + color: ${({ selected, theme }) => (!selected ? theme.colors.textPrimary : theme.colors.textInverted)}; + } +` type ReportFilterProps = { - pool: Pool + poolId: string } -export function ReportFilter({ pool }: ReportFilterProps) { - const { - csvData, - setStartDate, - startDate, - endDate, - setEndDate, - report, - loanStatus, - setLoanStatus, - txType, - setTxType, - groupBy, - setGroupBy, - activeTranche, - setActiveTranche, - address, - setAddress, - network, - setNetwork, - loan, - setLoan, - } = React.useContext(ReportContext) +export function ReportFilter({ poolId }: ReportFilterProps) { + const { csvData, setStartDate, startDate, endDate, setEndDate, groupBy, setGroupBy, report, reportData } = + React.useContext(ReportContext) const navigate = useNavigate() const basePath = useBasePath() + const pool = usePool(poolId) as Pool + const metadata = usePoolMetadata(pool as Pool) - const { data: domains } = useActiveDomains(pool.id) - const getNetworkName = useGetNetworkName() - const loans = useLoans(pool.id) as Loan[] | undefined - - const { showOracleTx } = useDebugFlags() - - const reportOptions: { label: string; value: Report }[] = [ - { label: 'Balance sheet', value: 'balance-sheet' }, - { label: 'Profit & loss', value: 'profit-and-loss' }, - { label: 'Cash flow statement', value: 'cash-flow-statement' }, - { label: 'Investor transactions', value: 'investor-tx' }, - { label: 'Asset transactions', value: 'asset-tx' }, - { label: 'Fee transactions', value: 'fee-tx' }, - ...(showOracleTx === true ? [{ label: 'Oracle transactions', value: 'oracle-tx' as Report }] : []), - // { label: 'Pool balance', value: 'pool-balance' }, - { label: 'Token price', value: 'token-price' }, - { label: 'Asset list', value: 'asset-list' }, - { label: 'Investor list', value: 'investor-list' }, - ] + const transformDataChart = React.useMemo(() => { + if (!reportData.length) return + if (report === 'balance-sheet') { + return reportData.map((data: DailyPoolState) => ({ + name: data.timestamp, + yAxis: new CurrencyBalance(data.poolState.netAssetValue, pool.currency.decimals).toNumber(), + })) + } else if (report === 'profit-and-loss') { + return reportData.map((data: DailyPoolState) => { + return { + name: data.timestamp, + yAxis: (metadata?.data?.pool?.asset.class === 'Private credit' + ? data.poolState.sumInterestRepaidAmountByPeriod + .add(data.poolState.sumInterestAccruedByPeriod) + .add(data.poolState.sumUnscheduledRepaidAmountByPeriod) + .sub(data.poolState.sumDebtWrittenOffByPeriod) + : data.poolState.sumUnrealizedProfitByPeriod + .add(data.poolState.sumInterestRepaidAmountByPeriod) + .add(data.poolState.sumUnscheduledRepaidAmountByPeriod) + ) + .sub(data.poolState.sumPoolFeesChargedAmountByPeriod) + .sub(data.poolState.sumPoolFeesAccruedAmountByPeriod) + .toNumber(), + } + }) + } else { + return reportData.map((data: DailyPoolState) => { + return { + name: data.timestamp, + yAxis: data.poolState.sumPrincipalRepaidAmountByPeriod + .sub(data.poolState.sumBorrowedAmountByPeriod) + .add(data.poolState.sumInterestRepaidAmountByPeriod) + .add(data.poolState.sumUnscheduledRepaidAmountByPeriod) + .sub(data.poolState.sumPoolFeesPaidAmountByPeriod) + .add(data.poolState.sumInvestedAmountByPeriod) + .sub(data.poolState.sumRedeemedAmountByPeriod) + .toNumber(), + } + }) + } + }, [report, reportData, metadata?.data?.pool?.asset.class, pool.currency.decimals]) return ( - { - if (event.target.value) { - setGroupBy(event.target.value as GroupBy) - } - }} - /> - )} - - {report === 'asset-list' && ( - <> - { - return { - label: token.currency.name, - value: token.id, - } - }), - ]} - value={activeTranche} - onChange={(event) => { - if (event.target.value) { - setActiveTranche(event.target.value) - } - }} - /> - )} - {report === 'asset-tx' && ( - { - setGroupBy(event.target.value as GroupBy) - }} - value={groupBy} - options={[ - { label: 'Day', value: 'day' }, - { label: 'Daily', value: 'daily' }, - { label: 'Monthly', value: 'month' }, - { label: 'Quarterly', value: 'quarter' }, - { label: 'Yearly', value: 'year' }, - ]} - /> - {groupBy === 'day' && ( - setStartDate(e.target.value)} /> - )} + + + { - if (event.target.value) { - setTxType(event.target.value) - } - }} - /> + } + small + variant="inverted" + > + CSV + + + + {transformDataChart?.length && ( + + + )} - {['investor-tx', 'investor-list'].includes(report) && ( - <> - { + const selectedValue = event.target.value + fmk.setFieldValue(`issuerCategories.${index}.type`, selectedValue) + if (selectedValue !== 'other') { + fmk.setFieldValue(`issuerCategories.${index}.customType`, '') + } + }} + onBlur={field.onBlur} + errorMessage={meta.touched && meta.error ? meta.error : undefined} + value={field.value} + options={OPTIONS} + label="Type" + /> + + {category.type === 'other' && ( + + ) => { + fmk.setFieldValue(`issuerCategories.${index}.customType`, event.target.value) + }} + /> + + )} + + ) => { + fmk.setFieldValue(`issuerCategories.${index}.value`, event.target.value) + }} + onBlur={field.onBlur} + value={category.value} + label="Value" + /> + + + + + + ) + }} + + ) + })} + + )} + + ) +} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx b/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx index 456a24e85e..69fbb682e1 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx @@ -3,6 +3,7 @@ import { Field, FieldProps } from 'formik' import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' import { Tooltips } from '../../components/Tooltips' import { isTestEnv } from '../../config' +import { CustomCategories } from './CustomCategories' import { CustomDetails } from './CustomDetails' import { validate } from './validate' @@ -132,6 +133,9 @@ export function IssuerInput({ waitingForStoredIssuer = false }: Props) { + + + ) } diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 1d896377eb..33eddcef88 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -114,6 +114,7 @@ export type CreatePoolValues = Omit< poolType: 'open' | 'closed' investorType: string issuerShortDescription: string + issuerCategories: { type: string; value: string }[] ratingAgency: string ratingValue: string ratingReportUrl: string @@ -137,6 +138,7 @@ const initialValues: CreatePoolValues = { issuerLogo: null, issuerDescription: '', issuerShortDescription: '', + issuerCategories: [], executiveSummary: null, website: '', diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx index 5f04058d27..e7edcf09d0 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import { useParams } from 'react-router' import { lastValueFrom } from 'rxjs' import { ButtonGroup } from '../../../components/ButtonGroup' -import { IssuerDetails, RatingDetails, ReportDetails } from '../../../components/IssuerSection' +import { IssuerDetails, PoolAnalysis, RatingDetails } from '../../../components/IssuerSection' import { PageSection } from '../../../components/PageSection' import { getFileDataURI } from '../../../utils/getFileDataURI' import { useFile } from '../../../utils/useFile' @@ -25,6 +25,7 @@ type Values = Pick< | 'issuerLogo' | 'issuerDescription' | 'issuerShortDescription' + | 'issuerCategories' | 'executiveSummary' | 'website' | 'forum' @@ -60,6 +61,7 @@ export function Issuer() { issuerLogo: logoFile ?? null, issuerDescription: metadata?.pool?.issuer?.description ?? '', issuerShortDescription: metadata?.pool?.issuer?.shortDescription ?? '', + issuerCategories: metadata?.pool?.issuer?.categories ?? [], executiveSummary: metadata?.pool?.links?.executiveSummary ? 'executiveSummary.pdf' : ('' as any), website: metadata?.pool?.links?.website ?? '', forum: metadata?.pool?.links?.forum ?? '', @@ -120,6 +122,7 @@ export function Issuer() { logo: logoChanged && logoUri ? { uri: logoUri, mime: values.issuerLogo!.type } : oldMetadata.pool.issuer.logo, shortDescription: values.issuerShortDescription, + categories: values.issuerCategories, }, links: { executiveSummary: execSummaryUri @@ -216,7 +219,7 @@ export function Issuer() { ) : ( - {metadata?.pool?.reports?.[0] && } + {metadata?.pool?.reports?.[0] && } {metadata?.pool?.rating && } )} diff --git a/centrifuge-app/src/pages/IssuerPool/Header.tsx b/centrifuge-app/src/pages/IssuerPool/Header.tsx index e484bf85bb..334d64e2ec 100644 --- a/centrifuge-app/src/pages/IssuerPool/Header.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Header.tsx @@ -77,7 +77,8 @@ export function IssuerPoolHeader({ actions }: Props) { Overview Assets Liquidity - {!isTinlakePool && Reporting} + {!isTinlakePool && Reports} + {!isTinlakePool && Data} Investors Configuration Access diff --git a/centrifuge-app/src/pages/IssuerPool/index.tsx b/centrifuge-app/src/pages/IssuerPool/index.tsx index e08846e39f..8bfe1a8c47 100644 --- a/centrifuge-app/src/pages/IssuerPool/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/index.tsx @@ -32,6 +32,8 @@ export default function IssuerPoolPage() { } /> } /> } /> + } /> + } /> } /> } /> diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx index 792f501e4a..0cea78c852 100644 --- a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx +++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx @@ -218,7 +218,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) { pool.currency.decimals ) }, new CurrencyBalance(0, pool.currency.decimals)) - }, [externalLoans, pool?.nav, form.values.feed]) + }, [externalLoans, form.values.feed, pool.currency.decimals]) const pendingNav = totalAum.add(changeInValuation.toDecimal()).sub(pendingFees.toDecimal()) diff --git a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx index cc65cacbbb..3fa2277083 100644 --- a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx +++ b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx @@ -10,7 +10,7 @@ type Props = { } export const CompleteExternalOnboarding = ({ openNewTab, poolId, poolSymbol }: Props) => { - const { refetchOnboardingUser, isOnboardingExternally } = useOnboarding() + const { refetchOnboardingUser } = useOnboarding() const onFocus = () => { refetchOnboardingUser() diff --git a/centrifuge-app/src/pages/Pool/Header.tsx b/centrifuge-app/src/pages/Pool/Header.tsx index 8c6eba054d..c9d1a7e996 100644 --- a/centrifuge-app/src/pages/Pool/Header.tsx +++ b/centrifuge-app/src/pages/Pool/Header.tsx @@ -3,7 +3,6 @@ import { Box, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric' import * as React from 'react' import { useLocation, useParams } from 'react-router' import { useTheme } from 'styled-components' -import { Eththumbnail } from '../../components/EthThumbnail' import { BASE_PADDING } from '../../components/LayoutBase/BasePadding' import { NavigationTabs, NavigationTabsItem } from '../../components/NavigationTabs' import { PageHeader } from '../../components/PageHeader' @@ -30,14 +29,11 @@ export function PoolDetailHeader({ actions }: Props) { return ( {metadata?.pool?.name ?? 'Unnamed pool'}} - subtitle={ - by {metadata?.pool?.issuer.name ?? 'Unknown'} - } parent={{ to: `/pools${state?.token ? '/tokens' : ''}`, label: state?.token ? 'Tokens' : 'Pools' }} icon={ - + <> {metadata?.pool?.icon ? ( - + ) : ( {(isLoading ? '' : metadata?.pool?.name ?? 'U')[0]} )} - + } border={false} actions={actions} @@ -67,7 +63,8 @@ export function PoolDetailHeader({ actions }: Props) { Overview Assets Liquidity - {!isTinlakePool && Reporting} + {!isTinlakePool && Reports} + {!isTinlakePool && Data} {!isTinlakePool && Fees} diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 9ab15172a9..6a7b3fb4bd 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,5 +1,5 @@ -import { CurrencyBalance, Price, Rate } from '@centrifuge/centrifuge-js' -import { Box, Button, Card, Grid, IconFileText, Stack, Text, TextWithPlaceholder } from '@centrifuge/fabric' +import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js' +import { Box, Button, Card, Grid, TextWithPlaceholder } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import * as React from 'react' import { useParams } from 'react-router' @@ -9,10 +9,8 @@ import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedee import { IssuerDetails, ReportDetails } from '../../../components/IssuerSection' import { LayoutSection } from '../../../components/LayoutBase/LayoutSection' import { LoadBoundary } from '../../../components/LoadBoundary' -import { Cashflows } from '../../../components/PoolOverview/Cashflows' import { KeyMetrics } from '../../../components/PoolOverview/KeyMetrics' import { PoolPerformance } from '../../../components/PoolOverview/PoolPerfomance' -import { PoolStructure } from '../../../components/PoolOverview/PoolStructure' import { TrancheTokenCards } from '../../../components/PoolOverview/TrancheTokenCards' import { TransactionHistory } from '../../../components/PoolOverview/TransactionHistory' import { Spinner } from '../../../components/Spinner' @@ -23,8 +21,7 @@ import { getPoolValueLocked } from '../../../utils/getPoolValueLocked' import { useAverageMaturity } from '../../../utils/useAverageMaturity' import { useConnectBeforeAction } from '../../../utils/useConnectBeforeAction' import { useIsAboveBreakpoint } from '../../../utils/useIsAboveBreakpoint' -import { useLoans } from '../../../utils/useLoans' -import { usePool, usePoolFees, usePoolMetadata } from '../../../utils/usePools' +import { usePool, usePoolMetadata } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' export type Token = { @@ -71,11 +68,7 @@ export function PoolDetailOverview() { const isTinlakePool = poolId.startsWith('0x') const pool = usePool(poolId) - const poolFees = usePoolFees(poolId) const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool) - const averageMaturity = useAverageMaturity(poolId) - const loans = useLoans(poolId) - const isMedium = useIsAboveBreakpoint('M') const pageSummaryData = [ { @@ -109,100 +102,43 @@ export function PoolDetailOverview() { id: tranche.id, capacity: tranche.capacity, tokenPrice: tranche.tokenPrice, - yield30DaysAnnualized: tranche?.yield30DaysAnnualized, + yield30DaysAnnualized: tranche?.yield30DaysAnnualized?.toString() || '', } }) .reverse() return ( - - - - }> - - - - {tokens.length > 0 && ( + + + + + }> + + + + {tokens.length > 0 && ( + }> + + + )} }> - - - )} - }> - {metadata?.pool?.reports?.length || !isTinlakePool ? ( - - - - - - - Reports - - - - - - Issuer details - - - - - ) : null} - {isTinlakePool && ( - - - Issuer details + + - - - )} - - {!isTinlakePool && ( - <> - - }> - { - return { - fee: poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0), - name: fee.name, - id: fee.id, - } - }) || [] - } - /> - - {/* }> - - */} - - {isMedium && ( - }> - - + + {metadata?.pool?.reports?.length || !isTinlakePool ? ( + + - - )} + ) : null} + + + {!isTinlakePool && ( }> - - - + - - )} + )} + ) } diff --git a/centrifuge-app/src/pages/Pool/index.tsx b/centrifuge-app/src/pages/Pool/index.tsx index ff51232aac..ffc8233867 100644 --- a/centrifuge-app/src/pages/Pool/index.tsx +++ b/centrifuge-app/src/pages/Pool/index.tsx @@ -10,7 +10,9 @@ export default function PoolDetailPage() { } /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/centrifuge-app/src/pages/Pools.tsx b/centrifuge-app/src/pages/Pools.tsx index cd7f41d52f..08fd0188b3 100644 --- a/centrifuge-app/src/pages/Pools.tsx +++ b/centrifuge-app/src/pages/Pools.tsx @@ -1,7 +1,7 @@ import { formatBalance } from '@centrifuge/centrifuge-react' -import { Box, IconArrowDown, IconArrowUpRight, Stack, StatusChip, Text } from '@centrifuge/fabric' +import { Box, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' -import { useListedPools, useYearOverYearGrowth } from '../../src/utils/useListedPools' +import { useListedPools } from '../../src/utils/useListedPools' import { LayoutSection } from '../components/LayoutBase/LayoutSection' import { PoolList } from '../components/PoolList' import { prefetchRoute } from '../components/Root' @@ -10,9 +10,6 @@ import { Dec } from '../utils/Decimal' export default function PoolsPage() { const [, listedTokens] = useListedPools() - const { totalYoyGrowth, isLoading } = useYearOverYearGrowth() - const isPositiveYoy = totalYoyGrowth > 0 - const IconComponent = isPositiveYoy ? IconArrowUpRight : IconArrowDown const totalValueLocked = React.useMemo(() => { return ( @@ -44,16 +41,6 @@ export default function PoolsPage() { Total value locked (TVL) - {!isLoading && ( - - - - - {formatBalance(totalYoyGrowth ?? 0, '', 2)} YoY - - - - )} {formatBalance(totalValueLocked ?? 0, config.baseCurrency)} diff --git a/centrifuge-app/src/utils/formatting.ts b/centrifuge-app/src/utils/formatting.ts index 4db8c2d82a..b301b0e91c 100644 --- a/centrifuge-app/src/utils/formatting.ts +++ b/centrifuge-app/src/utils/formatting.ts @@ -35,15 +35,18 @@ export function formatBalanceAbbreviated( ? amount.toNumber() : amount let formattedAmount = '' - if (amountNumber >= 1e9) { + const absAmount = Math.abs(amountNumber) + + if (absAmount >= 1e9) { formattedAmount = `${(amountNumber / 1e9).toFixed(decimals)}B` - } else if (amountNumber >= 1e6) { + } else if (absAmount >= 1e6) { formattedAmount = `${(amountNumber / 1e6).toFixed(decimals)}M` - } else if (amountNumber > 999) { + } else if (absAmount > 999) { formattedAmount = `${(amountNumber / 1e3).toFixed(decimals)}K` } else { formattedAmount = `${amountNumber.toFixed(decimals)}` } + return currency ? `${formattedAmount} ${currency}` : formattedAmount } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 2a07f549c4..9dd6e73b51 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -684,6 +684,7 @@ export interface PoolMetadataInput { issuerLogo?: FileType | null issuerDescription: string issuerShortDescription: string + issuerCategories: { type: string; value: string; customType?: string }[] poolReport?: { authorName: string @@ -748,6 +749,7 @@ export type PoolMetadata = { email: string logo?: FileType | null shortDescription: string + categories: { type: string; value: string; customType?: string }[] } links: { executiveSummary: FileType | null @@ -1129,6 +1131,7 @@ export function getPoolsModule(inst: Centrifuge) { email: metadata.email, logo: metadata.issuerLogo, shortDescription: metadata.issuerShortDescription, + categories: metadata.issuerCategories, }, poolStructure: metadata.poolStructure, investorType: metadata.investorType, @@ -2469,13 +2472,19 @@ export function getPoolsModule(inst: Centrifuge) { }), takeLast(1), map(({ trancheSnapshots }) => { - const trancheStates: Record = {} + const trancheStates: Record< + string, + { timestamp: string; tokenPrice: Price; yield30DaysAnnualized: Perquintill }[] + > = {} trancheSnapshots?.forEach((state) => { const tid = state.tranche.trancheId const entry = { timestamp: state.timestamp, tokenPrice: new Price(state.tokenPrice), pool: state.tranche.poolId, + yield30DaysAnnualized: state.yield30DaysAnnualized + ? new Perquintill(state.yield30DaysAnnualized) + : new Perquintill(0), } if (trancheStates[tid]) { trancheStates[tid].push(entry) @@ -2665,26 +2674,25 @@ export function getPoolsModule(inst: Centrifuge) { poolCurrency.decimals ), yield7DaysAnnualized: tranche.yield7DaysAnnualized - ? new Perquintill(hexToBN(tranche.yield7DaysAnnualized)) + ? new Perquintill(tranche.yield7DaysAnnualized) : new Perquintill(0), yield30DaysAnnualized: tranche.yield30DaysAnnualized - ? new Perquintill(hexToBN(tranche.yield30DaysAnnualized)) + ? new Perquintill(tranche.yield30DaysAnnualized) : new Perquintill(0), yield90DaysAnnualized: tranche.yield90DaysAnnualized - ? new Perquintill(hexToBN(tranche.yield90DaysAnnualized)) + ? new Perquintill(tranche.yield90DaysAnnualized) : new Perquintill(0), yieldSinceInception: tranche.yieldSinceInception - ? new Perquintill(hexToBN(tranche.yieldSinceInception)) + ? new Perquintill(tranche.yieldSinceInception) : new Perquintill(0), - yieldMTD: tranche.yieldMTD ? new Perquintill(hexToBN(tranche.yieldMTD)) : new Perquintill(0), - yieldQTD: tranche.yieldQTD ? new Perquintill(hexToBN(tranche.yieldQTD)) : new Perquintill(0), - yieldYTD: tranche.yieldYTD ? new Perquintill(hexToBN(tranche.yieldYTD)) : new Perquintill(0), + yieldMTD: tranche.yieldMTD ? new Perquintill(tranche.yieldMTD) : new Perquintill(0), + yieldQTD: tranche.yieldQTD ? new Perquintill(tranche.yieldQTD) : new Perquintill(0), + yieldYTD: tranche.yieldYTD ? new Perquintill(tranche.yieldYTD) : new Perquintill(0), yieldSinceLastPeriod: tranche.yieldSinceLastPeriod - ? new Perquintill(hexToBN(tranche.yieldSinceLastPeriod)) + ? new Perquintill(tranche.yieldSinceLastPeriod) : new Perquintill(0), } }) - return { ...state, poolState, poolValue, tranches } }) || [], trancheStates, @@ -4629,7 +4637,7 @@ export function getPoolsModule(inst: Centrifuge) { } } -function hexToBN(value?: string | number | null) { +export function hexToBN(value?: string | number | null) { if (typeof value === 'number' || value == null) return new BN(value ?? 0) return new BN(value.toString().substring(2), 'hex') } diff --git a/fabric/.storybook/preview.jsx b/fabric/.storybook/preview.jsx index 24da97cb2a..3bfd399ddc 100644 --- a/fabric/.storybook/preview.jsx +++ b/fabric/.storybook/preview.jsx @@ -1,13 +1,9 @@ import * as React from 'react' import { ThemeProvider } from 'styled-components' import { Box, GlobalStyle } from '../src' -import altairDark from '../src/theme/altairDark' -import altairLight from '../src/theme/altairLight' import centrifugeTheme from '../src/theme/centrifugeTheme' const themes = { - altairDark, - altairLight, centrifugeTheme, } diff --git a/fabric/src/components/Button/WalletButton.tsx b/fabric/src/components/Button/WalletButton.tsx index e89aab6536..47a32bd635 100644 --- a/fabric/src/components/Button/WalletButton.tsx +++ b/fabric/src/components/Button/WalletButton.tsx @@ -30,6 +30,12 @@ const StyledButton = styled.button` outline: 0; border-radius: 40px; white-space: nowrap; + & > span { + border-color: ${({ theme }) => theme.colors.backgroundPrimary}; + :hover { + border-color: ${({ theme }) => theme.colors.backgroundPrimary}; + } + } ` const IdenticonWrapper = styled(Flex)({ diff --git a/fabric/src/components/InputUnit/index.tsx b/fabric/src/components/InputUnit/index.tsx index 0de1fd4e19..831969820d 100644 --- a/fabric/src/components/InputUnit/index.tsx +++ b/fabric/src/components/InputUnit/index.tsx @@ -15,15 +15,21 @@ export type InputUnitProps = { errorMessage?: string inputElement?: React.ReactNode disabled?: boolean + row?: boolean } -export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElement, disabled }: InputUnitProps) { +export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElement, disabled, row }: InputUnitProps) { const defaultId = React.useId() id ??= defaultId + return ( - - {label && {label}} + + {label && ( + + {label} + + )} {secondaryLabel && ( - + {secondaryLabel} )} @@ -46,9 +52,22 @@ export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElemen ) } -export function InputLabel({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) { +export function InputLabel({ + children, + disabled, + row, +}: { + children: React.ReactNode + disabled?: boolean + row?: boolean +}) { return ( - + {children} ) diff --git a/fabric/src/components/Select/index.tsx b/fabric/src/components/Select/index.tsx index 38fa43e539..e6e1eb8afa 100644 --- a/fabric/src/components/Select/index.tsx +++ b/fabric/src/components/Select/index.tsx @@ -17,6 +17,7 @@ export type SelectProps = React.SelectHTMLAttributes & { placeholder?: string errorMessage?: string small?: boolean + hideBorder?: boolean } const StyledSelect = styled.select` @@ -31,14 +32,11 @@ const StyledSelect = styled.select` cursor: pointer; line-height: inherit; text-overflow: ellipsis; + font-weight: 500; &:disabled { cursor: default; } - - &:focus { - color: ${({ theme }) => theme.colors.textSelected}; - } ` export function SelectInner({ @@ -79,7 +77,7 @@ export function SelectInner({ ) } -export function Select({ label, errorMessage, id, ...rest }: SelectProps) { +export function Select({ label, errorMessage, id, hideBorder, ...rest }: SelectProps) { const defaultId = React.useId() id ??= defaultId return ( @@ -89,7 +87,7 @@ export function Select({ label, errorMessage, id, ...rest }: SelectProps) { disabled={rest.disabled} errorMessage={errorMessage} inputElement={ - + } diff --git a/fabric/src/components/Tabs/index.tsx b/fabric/src/components/Tabs/index.tsx index 0e7a40e828..1d5ff62d13 100644 --- a/fabric/src/components/Tabs/index.tsx +++ b/fabric/src/components/Tabs/index.tsx @@ -29,7 +29,7 @@ export function Tabs({ selectedIndex, onChange, children }: TabsProps) { ) } -const StyledTabsItem = styled.button<{ $active?: boolean }>( +const StyledTabsItem = styled.button<{ $active?: boolean; styleOverrides?: React.CSSProperties, showBorder?: boolean }>( { display: 'flex', alignItems: 'center', @@ -43,34 +43,56 @@ const StyledTabsItem = styled.button<{ $active?: boolean }>( appearance: 'none', background: 'transparent', }, - ({ $active, theme }) => { + ({ $active, theme, styleOverrides, showBorder }) => { return css({ paddingTop: 1, paddingLeft: 2, paddingRight: 2, paddingBottom: 2, - color: 'textPrimary', - boxShadow: $active ? `inset 0 -2px 0 ${theme.colors.textGold}` : 'none', + color: $active ? 'textPrimary' : 'textSecondary', + boxShadow: $active ? `inset 0 -2px 0 ${theme.colors.textGold}` : showBorder ? `inset 0 -2px 0 ${theme.colors.textDisabled}` : 'none', + fontWeight: 400, '&:hover, &:active, &:focus-visible': { color: 'textGold', }, + ...styleOverrides, }) } ) -export type TabsItemProps = Omit, '$active' | 'ariaLabel'> - +export type TabsItemProps = Omit, '$active' | 'ariaLabel'> & { + styleOverrides?: React.CSSProperties + showBorder?: boolean +} type TabsItemPrivateProps = TabsItemProps & { active?: boolean onClick?: () => void ariaLabel?: string + styleOverrides?: React.CSSProperties + showBorder?: boolean } -export function TabsItem({ children, active, onClick, ariaLabel, ...rest }: TabsItemPrivateProps) { +export function TabsItem({ + children, + active, + onClick, + ariaLabel, + styleOverrides, + showBorder, + ...rest +}: TabsItemPrivateProps) { return ( - - + + {children} diff --git a/fabric/src/components/TextInput/index.tsx b/fabric/src/components/TextInput/index.tsx index e48312d350..9f91c83a5a 100644 --- a/fabric/src/components/TextInput/index.tsx +++ b/fabric/src/components/TextInput/index.tsx @@ -11,6 +11,7 @@ export type TextInputProps = React.InputHTMLAttributes & InputUnitProps & { action?: React.ReactNode symbol?: React.ReactNode + row?: boolean } export type TextAreaInputProps = React.InputHTMLAttributes & InputUnitProps & { @@ -49,13 +50,12 @@ export const StyledTextInput = styled.input` margin: 0; } ` - -export const StyledInputBox = styled(Shelf)` +export const StyledInputBox = styled(Shelf)<{ hideBorder?: boolean }>` width: 100%; position: relative; background: ${({ theme }) => theme.colors.backgroundPage}; - border: 1px solid ${({ theme }) => theme.colors.borderPrimary}; - border-radius: ${({ theme }) => theme.radii.input}px; + border: ${({ hideBorder, theme }) => (hideBorder ? 'none' : `1px solid ${theme.colors.borderPrimary}`)}; + border-radius: ${({ hideBorder, theme }) => (hideBorder ? 'none' : `${theme.radii.input}px`)}; &::before { content: ''; @@ -80,7 +80,7 @@ export const StyledInputAction = styled.button` cursor: pointer; appearance: none; border: none; - background: ${(props) => props.theme.colors.backgroundButtonSecondary}; + background: ${(props) => props.theme.colors.backgroundButtonInverted}; display: flex; justify-content: center; align-items: center; @@ -103,7 +103,7 @@ export const StyledInputAction = styled.button` export function InputAction({ children, ...props }: React.ButtonHTMLAttributes) { return ( - + {children} @@ -114,11 +114,12 @@ export function TextInputBox( props: Omit & { error?: boolean inputRef?: React.Ref + row?: boolean } ) { - const { error, disabled, action, symbol, inputRef, inputElement, ...inputProps } = props + const { error, disabled, action, symbol, inputRef, inputElement, row, ...inputProps } = props return ( - + {inputElement ?? } {symbol && ( @@ -160,7 +161,7 @@ export function SearchInput({ label, secondaryLabel, disabled, errorMessage, id, type="search" disabled={disabled} error={!!errorMessage} - symbol={} + symbol={} {...inputProps} /> } @@ -168,9 +169,10 @@ export function SearchInput({ label, secondaryLabel, disabled, errorMessage, id, ) } -export function DateInput({ label, secondaryLabel, disabled, errorMessage, id, ...inputProps }: TextInputProps) { +export function DateInput({ label, secondaryLabel, disabled, errorMessage, id, row, ...inputProps }: TextInputProps) { const defaultId = React.useId() id ??= defaultId + return ( } diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx index 9eb97c49a5..ae0b10090f 100644 --- a/fabric/src/components/Tooltip/index.tsx +++ b/fabric/src/components/Tooltip/index.tsx @@ -57,6 +57,7 @@ const placements: { } const Container = styled(Stack)<{ pointer: PlacementAxis }>` + background-color: ${({ theme }) => theme.colors.backgroundInverted}; filter: ${({ theme }) => `drop-shadow(${theme.shadows.cardInteractive})`}; &::before { @@ -65,7 +66,7 @@ const Container = styled(Stack)<{ pointer: PlacementAxis }>` content: ''; position: absolute; ${({ pointer }) => placements[pointer!]} - border: ${({ theme }) => `var(--size) solid ${theme.colors.backgroundPrimary}`}; + border: ${({ theme }) => `var(--size) solid ${theme.colors.backgroundInverted}`}; transform: rotate(-45deg); } ` @@ -117,7 +118,9 @@ export function Tooltip({ {title} )} - {body} + + {body} + )} /> diff --git a/fabric/src/icon-svg/Icon-balance-sheet.svg b/fabric/src/icon-svg/Icon-balance-sheet.svg new file mode 100644 index 0000000000..ec8f772b96 --- /dev/null +++ b/fabric/src/icon-svg/Icon-balance-sheet.svg @@ -0,0 +1,3 @@ + + + diff --git a/fabric/src/icon-svg/Icon-cashflow.svg b/fabric/src/icon-svg/Icon-cashflow.svg new file mode 100644 index 0000000000..b254ea2001 --- /dev/null +++ b/fabric/src/icon-svg/Icon-cashflow.svg @@ -0,0 +1,3 @@ + + + diff --git a/fabric/src/icon-svg/Icon-profit-and-loss.svg b/fabric/src/icon-svg/Icon-profit-and-loss.svg new file mode 100644 index 0000000000..f544e30502 --- /dev/null +++ b/fabric/src/icon-svg/Icon-profit-and-loss.svg @@ -0,0 +1,3 @@ + + + diff --git a/fabric/src/icon-svg/IconMoody.svg b/fabric/src/icon-svg/IconMoody.svg new file mode 100644 index 0000000000..2499a357a6 --- /dev/null +++ b/fabric/src/icon-svg/IconMoody.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fabric/src/icon-svg/IconSp.svg b/fabric/src/icon-svg/IconSp.svg new file mode 100644 index 0000000000..fd64483382 --- /dev/null +++ b/fabric/src/icon-svg/IconSp.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fabric/src/icon-svg/icon-arrow-right-white.svg b/fabric/src/icon-svg/icon-arrow-right-white.svg new file mode 100644 index 0000000000..d56f3c1f14 --- /dev/null +++ b/fabric/src/icon-svg/icon-arrow-right-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fabric/src/theme/altairDark.ts b/fabric/src/theme/altairDark.ts deleted file mode 100644 index a5090a6e90..0000000000 --- a/fabric/src/theme/altairDark.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { baseTheme } from './tokens/baseTheme' -import { brandAltair } from './tokens/brandAltair' -import { black, blueScale, gold, grayScale, yellowScale } from './tokens/colors' -import { colorTheme } from './tokens/theme' -import { FabricTheme } from './types' - -export const altairDark: FabricTheme = { - ...baseTheme, - scheme: 'dark', - colors: { - ...brandAltair, - ...colorTheme.colors, - primarySelectedBackground: yellowScale[500], - secondarySelectedBackground: yellowScale[800], - focus: yellowScale[500], - borderFocus: yellowScale[500], - borderSelected: yellowScale[500], - textSelected: yellowScale[500], - textInteractive: yellowScale[500], - textInteractiveHover: yellowScale[500], - accentScale: blueScale, - blueScale, - yellowScale, - grayScale, - backgroundInverted: black, - textGold: gold, - }, - shadows: { - ...baseTheme.shadows, - cardInteractive: '0 1px 5px rgba(255, 255, 255, .8)', - cardActive: '0 0 0 1px var(--fabric-focus), 0 1px 5px rgba(255, 255, 255, .8)', - cardOverlay: '4px 8px 24px rgba(255, 255, 255, .4)', - }, -} - -export default altairDark diff --git a/fabric/src/theme/altairLight.ts b/fabric/src/theme/altairLight.ts deleted file mode 100644 index 6493140241..0000000000 --- a/fabric/src/theme/altairLight.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { baseTheme } from './tokens/baseTheme' -import { brandAltair } from './tokens/brandAltair' -import { blueScale, grayScale, yellowScale } from './tokens/colors' -import { colorTheme } from './tokens/theme' -import { FabricTheme } from './types' - -export const altairLight: FabricTheme = { - ...baseTheme, - scheme: 'light', - colors: { - ...brandAltair, - ...colorTheme.colors, - primarySelectedBackground: blueScale[500], - secondarySelectedBackground: blueScale[50], - focus: blueScale[500], - borderFocus: blueScale[500], - borderSelected: blueScale[500], - textSelected: blueScale[500], - textInteractive: blueScale[500], - textInteractiveHover: blueScale[500], - accentScale: blueScale, - blueScale, - yellowScale, - grayScale, - }, -} - -export default altairLight diff --git a/fabric/src/theme/centrifugeTheme.ts b/fabric/src/theme/centrifugeTheme.ts index faac679481..932b205f80 100644 --- a/fabric/src/theme/centrifugeTheme.ts +++ b/fabric/src/theme/centrifugeTheme.ts @@ -10,15 +10,15 @@ export const centrifugeTheme: FabricTheme = { colors: { ...brandCentrifuge, ...colorTheme.colors, - primarySelectedBackground: blueScale[500], - secondarySelectedBackground: blueScale[50], - focus: blueScale[500], - borderFocus: blueScale[500], - borderSelected: blueScale[500], - textSelected: blueScale[500], - textInteractive: blueScale[500], - textInteractiveHover: blueScale[500], - accentScale: blueScale, + primarySelectedBackground: yellowScale[500], + secondarySelectedBackground: yellowScale[50], + focus: grayScale[600], + borderFocus: grayScale[500], + borderSelected: grayScale[500], + textSelected: grayScale[500], + textInteractive: grayScale[500], + textInteractiveHover: grayScale[500], + accentScale: yellowScale, blueScale, yellowScale, grayScale, diff --git a/fabric/src/theme/index.ts b/fabric/src/theme/index.ts index 7e00d5fcfe..65cb46c4ed 100644 --- a/fabric/src/theme/index.ts +++ b/fabric/src/theme/index.ts @@ -1,4 +1,2 @@ -export * from './altairDark' -export * from './altairLight' export * from './centrifugeTheme' export * from './types' diff --git a/fabric/src/theme/tokens/baseTheme.ts b/fabric/src/theme/tokens/baseTheme.ts index 3144752ddd..9853222081 100644 --- a/fabric/src/theme/tokens/baseTheme.ts +++ b/fabric/src/theme/tokens/baseTheme.ts @@ -20,7 +20,7 @@ export const baseTheme: Omit = { radii: { tooltip: 4, card: 8, - input: 2, + input: 8, button: 4, chip: 4, }, diff --git a/fabric/src/theme/tokens/colors.ts b/fabric/src/theme/tokens/colors.ts index a6bc15e96f..2a6386261b 100644 --- a/fabric/src/theme/tokens/colors.ts +++ b/fabric/src/theme/tokens/colors.ts @@ -6,6 +6,7 @@ export const grayScale = { 100: '#E7E7E7', 300: '#CFCFCF', 500: '#91969B', + 600: '#667085', 800: '#252B34', 900: '#0F1115', } diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts index 87824b8bee..c24d2aa394 100644 --- a/fabric/src/theme/tokens/theme.ts +++ b/fabric/src/theme/tokens/theme.ts @@ -1,4 +1,4 @@ -import { black, blackScale, blueScale, centrifugeBlue, gold, grayScale, yellowScale } from './colors' +import { black, blueScale, gold, grayScale, yellowScale } from './colors' const statusDefault = grayScale[800] const statusInfo = blueScale[500] @@ -65,32 +65,32 @@ const colors = { shadowButtonPrimary: 'transparent', backgroundButtonSecondary: black, - backgroundButtonSecondaryFocus: blackScale[500], - backgroundButtonSecondaryHover: blackScale[500], - backgroundButtonSecondaryPressed: blackScale[500], + backgroundButtonSecondaryFocus: black, + backgroundButtonSecondaryHover: black, + backgroundButtonSecondaryPressed: black, backgroundButtonSecondaryDisabled: grayScale[300], textButtonSecondary: 'white', - textButtonSecondaryFocus: gold, - textButtonSecondaryHover: gold, - textButtonSecondaryPressed: gold, + textButtonSecondaryFocus: 'white', + textButtonSecondaryHover: 'white', + textButtonSecondaryPressed: 'white', textButtonSecondaryDisabled: grayScale[500], - borderButtonSecondary: grayScale[300], - borderButtonSecondaryFocus: gold, - borderButtonSecondaryHover: gold, - borderButtonSecondaryPressed: gold, + borderButtonSecondary: black, + borderButtonSecondaryFocus: black, + borderButtonSecondaryHover: black, + borderButtonSecondaryPressed: black, borderButtonSecondaryDisabled: 'transparent', - shadowButtonSecondary: '#A8BFFD35', + shadowButtonSecondary: 'transparent', backgroundButtonTertiary: 'transparent', backgroundButtonTertiaryFocus: 'transparent', backgroundButtonTertiaryHover: 'tranparent', backgroundButtonTertiaryPressed: 'transparent', backgroundButtonTertiaryDisabled: 'transparent', - textButtonTertiary: centrifugeBlue, - textButtonTertiaryFocus: centrifugeBlue, - textButtonTertiaryHover: grayScale[800], - textButtonTertiaryPressed: centrifugeBlue, - textButtonTertiaryDisabled: grayScale[500], + textButtonTertiary: grayScale[800], + textButtonTertiaryFocus: gold, + textButtonTertiaryHover: gold, + textButtonTertiaryPressed: gold, + textButtonTertiaryDisabled: grayScale[300], borderButtonTertiary: 'transparent', borderButtonTertiaryFocus: 'transparent', borderButtonTertiaryHover: 'transparent', @@ -102,17 +102,17 @@ const colors = { backgroundButtonInvertedHover: grayScale[100], backgroundButtonInvertedPressed: grayScale[100], backgroundButtonInvertedDisabled: grayScale[100], - textButtonInverted: centrifugeBlue, - textButtonInvertedFocus: centrifugeBlue, - textButtonInvertedHover: centrifugeBlue, - textButtonInvertedPressed: centrifugeBlue, + textButtonInverted: black, + textButtonInvertedFocus: black, + textButtonInvertedHover: black, + textButtonInvertedPressed: black, textButtonInvertedDisabled: grayScale[500], borderButtonInverted: grayScale[100], - borderButtonInvertedFocus: centrifugeBlue, - borderButtonInvertedHover: centrifugeBlue, - borderButtonInvertedPressed: centrifugeBlue, + borderButtonInvertedFocus: black, + borderButtonInvertedHover: black, + borderButtonInvertedPressed: black, borderButtonInvertedDisabled: 'transparent', - shadowButtonInverted: '#E0E7FF', + shadowButtonInverted: 'transparent', } export const colorTheme = { diff --git a/fabric/src/theme/tokens/typography.ts b/fabric/src/theme/tokens/typography.ts index fb917350a5..73ab39c964 100644 --- a/fabric/src/theme/tokens/typography.ts +++ b/fabric/src/theme/tokens/typography.ts @@ -80,7 +80,7 @@ const typography: ThemeTypography = { label2: { fontSize: 12, lineHeight: 1.375, - fontWeight: 400, + fontWeight: 500, color: 'textSecondary', }, }