From 13d097e99e932e7b606bdf46d07f5271877a2ea3 Mon Sep 17 00:00:00 2001 From: alex <123092072+AlexRLT@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:53:43 +0200 Subject: [PATCH 1/6] Revert "Master" From 3f46b2d89096bb3cacb8369c4bd6893ac7875542 Mon Sep 17 00:00:00 2001 From: alex <123092072+AlexRLT@users.noreply.github.com> Date: Sun, 25 Aug 2024 07:56:53 +0200 Subject: [PATCH 2/6] merge preprod <> master (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add dropdown * simplify selector and add all token option * feat: change allPage value to Infinity * feat: estimate the fully rented rent * feat: add fully rented estimation to asset cards * refactor: move hook calls to top of the page * fix: propInfo definition * fix: last rent condition * feat: add estimation for property with not fully rented history * feat: get fully rented APR * apply max APR method * rename variable and functions * feat: add RWA token * feat: re-enable property onClick * feat: add rwa valuation on the rwa card * fix: missing property * feat: add rwa to summary card * define useRWA * take into account user currency * feat: add RWA value to net value calculation * remove comment * refactor: clean imports * feat: include RWA on Ethereum * fix: en communs * feat: update filter to support RWA token * fix: prettier * fix: other prettier errors * let prettier add strange semi-column * fix: imports * use hook * add fallback * switch for a useMemo * feat: add real time fully rented APR * feat: add gloabl metric fully rented APR * feat: add disclaimer * feat: add disclaimer * feat: update disclaimer message * fix: disclaimer message * improve message * feat: create yam statics stics page * feat: add yam statistics for all RealT Tokens on Gnosis (who have Gnosis chain contract prop) * feat: mask tokens with no volume * fix: add token name * feat: add pagination * feat: improve style * feat: change token per page to 100 * feat: add fully rented APR to asset grid * refactor: remove logs * feat: add fully rented APR to property details * fix: reset current page when tokens changed * fix: reset current page when user change page size * feat: add translation for YAM statistics hearder label * fix: yamStatistics: use selected currency for token price * feat: yamStatistics: add owned | all filter * feat: yamStatistics: add subsidized, fullySubsidized and notSubsidized filters * add additional fallbacks RPC URLs * fix: RPC initialization on currencies file --------- Co-authored-by: Nandy Bâ --- .prettierrc.js | 2 +- next.config.js | 6 +- postcss.config.cjs | 2 +- .../assetPage/assetPagePropertyTab.tsx | 7 + src/components/assetsView/AssetsView.tsx | 10 +- .../assetsView/AssetsViewSearch.tsx | 11 +- .../filters/AssetsViewRentStatusFilter.tsx | 14 +- .../filters/AssetsViewRmmStatusFilter.tsx | 12 +- .../assetsView/filters/AssetsViewSort.tsx | 47 ++-- .../filters/AssetsViewSubsidyFilter.tsx | 24 +- .../filters/AssetsViewUserProtocolFilter.tsx | 18 +- .../filters/AssetsViewUserStatusFilter.tsx | 18 +- .../assetsView/filters/useFilters.ts | 9 +- src/components/assetsView/views/AssetGrid.tsx | 83 +++++- .../assetsView/views/AssetTable.tsx | 32 ++- src/components/cards/AssetCard.tsx | 41 ++- src/components/cards/RWACard.tsx | 76 +++++ src/components/cards/main/RentsCard.tsx | 9 + src/components/cards/main/SummaryCard.tsx | 7 +- .../others/FullyRentedAPRDisclaimer.tsx | 14 + src/components/layouts/Header.tsx | 6 + src/hooks/useFullyRentedAPR.ts | 98 +++++++ src/hooks/useRWA.ts | 82 ++++++ src/i18next/locales/en/common.json | 26 +- src/i18next/locales/fr/common.json | 23 +- src/pages/asset/[assetId].tsx | 2 + src/pages/yamStatistics.tsx | 259 ++++++++++++++++++ src/repositories/RpcProvider.ts | 40 ++- src/repositories/currencies.repository.ts | 19 +- src/repositories/rmm.repository.ts | 27 +- src/repositories/yamStatistics.repository.ts | 13 +- src/store/features/wallets/walletsSelector.ts | 13 + 32 files changed, 945 insertions(+), 105 deletions(-) create mode 100644 src/components/cards/RWACard.tsx create mode 100644 src/components/commons/others/FullyRentedAPRDisclaimer.tsx create mode 100644 src/hooks/useFullyRentedAPR.ts create mode 100644 src/hooks/useRWA.ts create mode 100644 src/pages/yamStatistics.tsx diff --git a/.prettierrc.js b/.prettierrc.js index 18c42b99..c3ab6c8b 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -14,7 +14,7 @@ module.exports = { '^i18next(.*)', '^(?!(src|../|./))(.*)', '^src(.*)$', - '^(.*)$' + '^(.*)$', ], importOrderSeparation: true, importOrderSortSpecifiers: true, diff --git a/next.config.js b/next.config.js index ca1d6f71..369bee80 100644 --- a/next.config.js +++ b/next.config.js @@ -11,11 +11,11 @@ const nextConfig = { outputStandalone: true, }, images: { - domains: ['realt.co'] + domains: ['realt.co'], }, publicRuntimeConfig: { version, }, -}; +} -module.exports = nextConfig; +module.exports = nextConfig diff --git a/postcss.config.cjs b/postcss.config.cjs index c759b743..6a683623 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -11,4 +11,4 @@ module.exports = { }, }, }, -}; \ No newline at end of file +} diff --git a/src/components/assetPage/assetPagePropertyTab.tsx b/src/components/assetPage/assetPagePropertyTab.tsx index 49f6dfb7..b3f58a6b 100644 --- a/src/components/assetPage/assetPagePropertyTab.tsx +++ b/src/components/assetPage/assetPagePropertyTab.tsx @@ -2,6 +2,7 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import { useCurrencyValue } from 'src/hooks/useCurrencyValue' +import { useFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' import { RealTokenRentalType } from 'src/types/RealToken' @@ -44,6 +45,8 @@ export const AssetPagePropertyTab: FC<{ const propertyStories = realtoken.propertyStories ? tNumbers('integer', { value: realtoken.propertyStories }) : '-' + const fullyRentedAPR = useFullyRentedAPR(realtoken) + const fullyRentedAPRValue = tNumbers('percent', { value: fullyRentedAPR }) return ( <> @@ -106,6 +109,10 @@ export const AssetPagePropertyTab: FC<{ label: t('annualYield'), value: annualYield, }, + { + label: t('fullyRentedAPR'), + value: fullyRentedAPRValue, + }, ]} /> diff --git a/src/components/assetsView/AssetsView.tsx b/src/components/assetsView/AssetsView.tsx index c8b81d76..fff92fbc 100644 --- a/src/components/assetsView/AssetsView.tsx +++ b/src/components/assetsView/AssetsView.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux' import { Grid } from '@mantine/core' +import { useRWA } from 'src/hooks/useRWA' import { selectUserRealtokens } from 'src/store/features/wallets/walletsSelector' import { AssetsViewSearch, useAssetsViewSearch } from './AssetsViewSearch' @@ -19,11 +20,12 @@ export const AssetsView: FC = () => { const { choosenAssetView } = useAssetsViewSelect() const realtokens = useSelector(selectUserRealtokens) + const rwa = useRWA() - const data = useMemo( - () => assetsViewFilterFunction(realtokens.filter(assetSearchFunction)), - [realtokens, assetSearchFunction, assetsViewFilterFunction], - ) + const data = useMemo(() => { + const assets = rwa ? [...realtokens, rwa] : realtokens + return assetsViewFilterFunction(assets.filter(assetSearchFunction)) + }, [realtokens, rwa, assetSearchFunction, assetsViewFilterFunction]) return realtokens.length ? ( <> diff --git a/src/components/assetsView/AssetsViewSearch.tsx b/src/components/assetsView/AssetsViewSearch.tsx index fb1403c7..8b6bd419 100644 --- a/src/components/assetsView/AssetsViewSearch.tsx +++ b/src/components/assetsView/AssetsViewSearch.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { TextInput } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../inputs/useInputStyles' @@ -36,11 +39,11 @@ export function useAssetsViewSearch() { [assetSearch], ) - function assetSearchFunction(realtoken: UserRealtoken) { + function assetSearchFunction(asset: UserRealtoken | RWARealtoken) { return ( !cleanSearch || - realtoken.shortName.toLowerCase().includes(cleanSearch) || - realtoken.fullName.toLowerCase().includes(cleanSearch) + asset.shortName.toLowerCase().includes(cleanSearch) || + asset.fullName.toLowerCase().includes(cleanSearch) ) } diff --git a/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx index e72e802b..5ac96507 100644 --- a/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetRentStatusType } from '../types' @@ -58,16 +61,17 @@ AssetsViewRentStatusFilter.displayName = 'AssetsViewRentStatusFilter' export function useAssetsViewRentStatusFilter( filter: AssetsViewRentStatusFilterModel, ) { - function assetRentStatusFilterFunction(asset: UserRealtoken) { + function assetRentStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + const Asset = asset as UserRealtoken switch (filter.rentStatus) { case AssetRentStatusType.ALL: return true case AssetRentStatusType.RENTED: - return asset.rentStatus === 'full' + return Asset.rentStatus === 'full' case AssetRentStatusType.PARTIALLY_RENTED: - return asset.rentStatus === 'partial' + return Asset.rentStatus === 'partial' case AssetRentStatusType.NOT_RENTED: - return asset.rentStatus === 'none' + return Asset.rentStatus === 'none' } } diff --git a/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx index c01742b9..f73949da 100644 --- a/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetRmmStatusType } from '../types' @@ -53,14 +56,15 @@ AssetsViewRmmStatusFilter.displayName = 'AssetsViewRmmStatusFilter' export function useAssetsViewRmmStatusFilter( filter: AssetsViewRmmStatusFilterModel, ) { - function assetRmmStatusFilterFunction(asset: UserRealtoken) { + function assetRmmStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + const Asset = asset as UserRealtoken switch (filter.rmmStatus) { case AssetRmmStatusType.ALL: return true case AssetRmmStatusType.AVAILABLE: - return asset.isRmmAvailable + return Asset.isRmmAvailable case AssetRmmStatusType.NOT_AVAILABLE: - return !asset.isRmmAvailable + return !Asset.isRmmAvailable } } diff --git a/src/components/assetsView/filters/AssetsViewSort.tsx b/src/components/assetsView/filters/AssetsViewSort.tsx index 1ffeabf1..08f5a6c9 100644 --- a/src/components/assetsView/filters/AssetsViewSort.tsx +++ b/src/components/assetsView/filters/AssetsViewSort.tsx @@ -5,7 +5,10 @@ import { useSelector } from 'react-redux' import { Grid, Select, Switch } from '@mantine/core' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetSortType } from '../types' @@ -90,42 +93,50 @@ export const AssetsViewSort: FC = ({ AssetsViewSort.displayName = 'AssetsViewSort' export function useAssetsViewSort(filter: AssetsViewSortFilter) { - function assetSortFunction(a: UserRealtoken, b: UserRealtoken) { + function assetSortFunction( + a: UserRealtoken | RWARealtoken, + b: UserRealtoken | RWARealtoken, + ) { const value = getAssetSortValue(a, b) return filter.sortReverse ? value * -1 : value } - function getAssetSortValue(a: UserRealtoken, b: UserRealtoken) { + function getAssetSortValue( + a: UserRealtoken | RWARealtoken, + b: UserRealtoken | RWARealtoken, + ) { + const A = a as UserRealtoken + const B = b as UserRealtoken switch (filter.sortBy) { case AssetSortType.VALUE: - return b.value - a.value + return B.value - A.value case AssetSortType.APR: - return b.annualPercentageYield - a.annualPercentageYield + return B.annualPercentageYield - A.annualPercentageYield case AssetSortType.RENT: - return b.amount * b.netRentDayPerToken - a.amount * a.netRentDayPerToken + return B.amount * B.netRentDayPerToken - A.amount * A.netRentDayPerToken case AssetSortType.RENT_START: - return b.rentStartDate.date.localeCompare(a.rentStartDate.date) + return B.rentStartDate.date.localeCompare(A.rentStartDate.date) case AssetSortType.NAME: - return a.shortName.localeCompare(b.shortName) + return A.shortName.localeCompare(b.shortName) case AssetSortType.SUPPLY: - return b.totalInvestment - a.totalInvestment + return B.totalInvestment - A.totalInvestment case AssetSortType.TOKEN: - return b.amount - a.amount + return B.amount - A.amount case AssetSortType.TOTAL_UNIT: - return b.totalUnits - a.totalUnits + return B.totalUnits - A.totalUnits case AssetSortType.RENTED_UNIT: - return b.rentedUnits - a.rentedUnits + return B.rentedUnits - A.rentedUnits case AssetSortType.OCCUPANCY: - return b.rentedUnits / b.totalUnits - a.rentedUnits / a.totalUnits + return B.rentedUnits / B.totalUnits - A.rentedUnits / A.totalUnits case AssetSortType.INITIAL_LAUNCH: - return b.initialLaunchDate?.date.localeCompare( - a.initialLaunchDate?.date, + return B.initialLaunchDate?.date.localeCompare( + A.initialLaunchDate?.date, ) case AssetSortType.UNIT_PRICE_COST: - return (b.unitPriceCost ?? 0) - (a.unitPriceCost ?? 0) + return (B.unitPriceCost ?? 0) - (A.unitPriceCost ?? 0) case AssetSortType.UNREALIZED_CAPITAL_GAIN: - return (b.unrealizedCapitalGain ?? 0) - (a.unrealizedCapitalGain ?? 0) + return (B.unrealizedCapitalGain ?? 0) - (A.unrealizedCapitalGain ?? 0) case AssetSortType.LAST_CHANGE: - return b.lastChanges.localeCompare(a.lastChanges) ?? 0 + return B.lastChanges.localeCompare(A.lastChanges) ?? 0 } } diff --git a/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx b/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx index b38c5575..fb465867 100644 --- a/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetSubsidyType } from '../types' @@ -73,27 +76,28 @@ AssetsViewSubsidyFilter.displayName = 'AssetsViewSubsidyFilter' export function useAssetsViewSubsidyFilter( filter: AssetsViewSubsidyFilterModel, ) { - function assetSubsidyFilterFunction(asset: UserRealtoken) { + function assetSubsidyFilterFunction(asset: UserRealtoken | RWARealtoken) { + const Asset = asset as UserRealtoken switch (filter.subsidy) { case AssetSubsidyType.ALL: return true case AssetSubsidyType.SUBSIDIZED: - return asset.subsidyStatus !== 'no' + return Asset.subsidyStatus !== 'no' case AssetSubsidyType.FULLY_SUBSIDIZED: - return asset.subsidyStatus === 'yes' && !!asset.subsidyStatusValue + return Asset.subsidyStatus === 'yes' && !!Asset.subsidyStatusValue case AssetSubsidyType.PARTIALLY_SUBSIDIZED: - return asset.subsidyStatus !== 'no' && !!asset.subsidyStatusValue + return Asset.subsidyStatus !== 'no' && !!Asset.subsidyStatusValue case AssetSubsidyType.SECTION_8: - return asset.subsidyStatus !== 'no' && asset.subsidyBy === 'Section 8' + return Asset.subsidyStatus !== 'no' && Asset.subsidyBy === 'Section 8' case AssetSubsidyType.SECTION_42: - return asset.subsidyStatus !== 'no' && asset.subsidyBy === 'Section 42' + return Asset.subsidyStatus !== 'no' && Asset.subsidyBy === 'Section 42' case AssetSubsidyType.OTHER_SUBSIDY: return ( - asset.subsidyStatus !== 'no' && - !['Section 8', 'Section 42'].includes(asset.subsidyBy ?? '') + Asset.subsidyStatus !== 'no' && + !['Section 8', 'Section 42'].includes(Asset.subsidyBy ?? '') ) case AssetSubsidyType.NOT_SUBSIDIZED: - return !asset.subsidyStatus || asset.subsidyStatus === 'no' + return !Asset.subsidyStatus || Asset.subsidyStatus === 'no' } } diff --git a/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx b/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx index 329b24e7..8072b35e 100644 --- a/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetUserProtocolType } from '../types' @@ -62,18 +65,21 @@ AssetsViewUserProtocolFilter.displayName = 'AssetsViewUserProtocolFilter' export function useAssetsViewUserProtocolFilter( filter: AssetsViewUserProtocolFilterModel, ) { - function assetUserProtocolFilterFunction(asset: UserRealtoken) { + function assetUserProtocolFilterFunction( + asset: UserRealtoken | RWARealtoken, + ) { + const Asset = asset as UserRealtoken switch (filter.userProtocol) { case AssetUserProtocolType.ALL: return true case AssetUserProtocolType.ETHEREUM: - return asset.balance.ethereum.amount > 0 + return Asset.balance.ethereum.amount > 0 case AssetUserProtocolType.GNOSIS: - return asset.balance.gnosis.amount > 0 + return Asset.balance.gnosis.amount > 0 case AssetUserProtocolType.RMM: - return asset.balance.rmm.amount > 0 + return Asset.balance.rmm.amount > 0 case AssetUserProtocolType.LEVINSWAP: - return asset.balance.levinSwap.amount > 0 + return Asset.balance.levinSwap.amount > 0 } } diff --git a/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx index 8a3d9303..635ff06f 100644 --- a/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useInputStyles } from '../../inputs/useInputStyles' import { AssetUserStatusType } from '../types' @@ -66,20 +69,21 @@ AssetsViewUserStatusFilter.displayName = 'AssetsViewUserStatusFilter' export function useAssetsViewUserStatusFilter( filter: AssetsViewUserStatusFilterModel, ) { - function assetUserStatusFilterFunction(asset: UserRealtoken) { + function assetUserStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + const Asset = asset as UserRealtoken switch (filter.userStatus) { case AssetUserStatusType.ALL: return true case AssetUserStatusType.OWNED: - return asset.amount > 0 + return Asset.amount > 0 case AssetUserStatusType.WHITELISTED: - return asset.isWhitelisted + return Asset.isWhitelisted case AssetUserStatusType.WHITELISTED_NOT_OWNED: - return asset.isWhitelisted && asset.amount === 0 + return Asset.isWhitelisted && Asset.amount === 0 case AssetUserStatusType.NOT_OWNED: - return asset.amount === 0 + return Asset.amount === 0 case AssetUserStatusType.NOT_WHITELISTED: - return !asset.isWhitelisted + return !Asset.isWhitelisted } } diff --git a/src/components/assetsView/filters/useFilters.ts b/src/components/assetsView/filters/useFilters.ts index 2cb2a05c..dae6dc0a 100644 --- a/src/components/assetsView/filters/useFilters.ts +++ b/src/components/assetsView/filters/useFilters.ts @@ -1,7 +1,10 @@ import { useAtom } from 'jotai' import { assetsViewDefaultFilter, assetsViewFilterAtom } from 'src/states' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { useAssetsViewRentStatusFilter } from './AssetsViewRentStatusFilter' import { useAssetsViewRmmStatusFilter } from './AssetsViewRmmStatusFilter' @@ -26,7 +29,9 @@ export function useAssetsViewFilters() { const { assetUserProtocolFilterFunction } = useAssetsViewUserProtocolFilter(activeFilter) - function assetsViewFilterFunction(tokenList: UserRealtoken[]) { + function assetsViewFilterFunction( + tokenList: (UserRealtoken | RWARealtoken)[], + ) { return tokenList .filter(assetUserStatusFilterFunction) .filter(assetUserProtocolFilterFunction) diff --git a/src/components/assetsView/views/AssetGrid.tsx b/src/components/assetsView/views/AssetGrid.tsx index c794c381..167d1240 100644 --- a/src/components/assetsView/views/AssetGrid.tsx +++ b/src/components/assetsView/views/AssetGrid.tsx @@ -2,16 +2,29 @@ import { FC, useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/router' -import { Grid, Group, Pagination } from '@mantine/core' +import { + Combobox, + Grid, + Group, + InputBase, + Pagination, + useCombobox, +} from '@mantine/core' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import FullyRentedAPRDisclaimer from 'src/components/commons/others/FullyRentedAPRDisclaimer' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { AssetCard } from '../../cards' -export const AssetGrid: FC<{ realtokens: UserRealtoken[] }> = (props) => { +export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( + props, +) => { const router = useRouter() const [page, setPage] = useState(1) - const pageSize = 24 + const [pageSize, setPageSize] = useState(20) function onPageChange(page: number) { setPage(page) @@ -19,14 +32,32 @@ export const AssetGrid: FC<{ realtokens: UserRealtoken[] }> = (props) => { document.getElementsByClassName('asset-grid')[0]?.scrollIntoView() } - const paginationOffers: UserRealtoken[] = useMemo(() => { + const paginationOffers: (UserRealtoken | RWARealtoken)[] = useMemo(() => { + if (pageSize === Infinity) return props.realtokens const start = (page - 1) * pageSize const end = start + pageSize return props.realtokens.slice(start, end) }, [props.realtokens, page, pageSize]) // Go to first page when data changes (e.g. search, filter, order, ...) - useEffect(() => setPage(1), [props.realtokens]) + useEffect(() => setPage(1), [props.realtokens, pageSize]) + + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption(), + }) + + const values = [20, 40, 100, 200] + + const options = [ + + {'All'} + , + ...values.map((item) => ( + + {item} + + )), + ] return ( <> @@ -49,12 +80,50 @@ export const AssetGrid: FC<{ realtokens: UserRealtoken[] }> = (props) => { > + { + if (val === 'All') return setPageSize(Infinity) + setPageSize(Number(val)) + + combobox.closeDropdown() + }} + > + + } + value={pageSize == Infinity ? 'All' : pageSize} + type={'button'} + onChange={() => { + combobox.openDropdown() + combobox.updateSelectedOptionIndex() + }} + onClick={() => combobox.openDropdown()} + onFocus={() => combobox.openDropdown()} + onBlur={() => { + combobox.closeDropdown() + }} + placeholder={'Search value'} + rightSectionPointerEvents={'none'} + /> + + + + {options} + + + ) diff --git a/src/components/assetsView/views/AssetTable.tsx b/src/components/assetsView/views/AssetTable.tsx index 98f2a685..f125cc54 100644 --- a/src/components/assetsView/views/AssetTable.tsx +++ b/src/components/assetsView/views/AssetTable.tsx @@ -9,10 +9,16 @@ import { Anchor, ScrollArea, Table } from '@mantine/core' import moment from 'moment' import { useCurrencyValue } from 'src/hooks/useCurrencyValue' +import { useFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' -export const AssetTable: FC<{ realtokens: UserRealtoken[] }> = (props) => { +export const AssetTable: FC<{ + realtokens: (UserRealtoken | RWARealtoken)[] +}> = (props) => { return ( @@ -21,9 +27,13 @@ export const AssetTable: FC<{ realtokens: UserRealtoken[] }> = (props) => { - {props.realtokens.map((item) => ( - - ))} + {props.realtokens.map((item, index) => { + const isAProperty = item.hasOwnProperty('rentStatus') + if (!isAProperty) { + return + } + return + })}
@@ -53,6 +63,7 @@ const AssetTableHeader: FC = () => { ) : null} {t('ownedTokens')} {t('apr')} + {t('fullyRentedAPR')} {t('weeklyRents')} {t('yearlyRents')} {t('rentedUnits')} @@ -76,6 +87,10 @@ const AssetTableRow: FC<{ value: UserRealtoken }> = (props) => { const weeklyAmount = props.value.amount * props.value.netRentDayPerToken * 7 const yearlyAmount = props.value.amount * props.value.netRentYearPerToken const totalInvestment = props.value.totalInvestment + const isAProperty = props.value.hasOwnProperty('rentStatus') + const fullyRentedAPR = isAProperty + ? useFullyRentedAPR(props.value as UserRealtoken) + : null return ( @@ -112,6 +127,13 @@ const AssetTableRow: FC<{ value: UserRealtoken }> = (props) => { {t('percent', { value: props.value.annualPercentageYield })} + {isAProperty ? ( + + {t('percent', { value: fullyRentedAPR })} + + ) : ( + + )} {useCurrencyValue(weeklyAmount)} diff --git a/src/components/cards/AssetCard.tsx b/src/components/cards/AssetCard.tsx index 9d3dc841..6a659a84 100644 --- a/src/components/cards/AssetCard.tsx +++ b/src/components/cards/AssetCard.tsx @@ -1,4 +1,4 @@ -import { FC, memo } from 'react' +import { FC } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -9,8 +9,12 @@ import { Badge, Card, Group } from '@mantine/core' import moment from 'moment' import { useCurrencyValue } from 'src/hooks/useCurrencyValue' +import { useFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { selectUserRentCalculation } from 'src/store/features/settings/settingsSelector' -import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' +import { + RWARealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { RentCalculationState } from 'src/types/RentCalculation' import { @@ -20,13 +24,19 @@ import { SubsidyStatusTag, } from '../commons' import styles from './AssetCard.module.sass' +import { RWACard } from './RWACard' interface AssetCardProps { + value: UserRealtoken | RWARealtoken + onClick?: (id: string) => unknown +} + +interface PropertyCardProps { value: UserRealtoken onClick?: (id: string) => unknown } -const AssetCardComponent: FC = (props) => { +const PropertyCardComponent: FC = (props) => { const { t: tNumbers } = useTranslation('common', { keyPrefix: 'numbers' }) const { t } = useTranslation('common', { keyPrefix: 'assetCard' }) @@ -48,6 +58,8 @@ const AssetCardComponent: FC = (props) => { const yearlyAmount = props.value.amount * props.value.netRentYearPerToken const totalInvestment = props.value.totalInvestment + const fullyRentedAPR = useFullyRentedAPR(props.value) + return ( = (props) => { +
+
{t('fullyRentedEstimation')}*
+
+ {fullyRentedAPR + ? tNumbers('percent', { value: fullyRentedAPR }) + : '-'} +
+
+
@@ -158,4 +179,16 @@ const AssetCardComponent: FC = (props) => { ) } -export const AssetCard = memo(AssetCardComponent) +export const AssetCard: FC = (props) => { + const isAProperty = props.value && props.value.hasOwnProperty('rentStatus') + if (isAProperty) { + return ( + + ) + } else { + return + } +} diff --git a/src/components/cards/RWACard.tsx b/src/components/cards/RWACard.tsx new file mode 100644 index 00000000..2367578b --- /dev/null +++ b/src/components/cards/RWACard.tsx @@ -0,0 +1,76 @@ +import { FC, memo } from 'react' +import { useTranslation } from 'react-i18next' + +import Image from 'next/image' + +import { Badge, Card, Group } from '@mantine/core' + +import { useCurrencyValue } from 'src/hooks/useCurrencyValue' +import { RWARealtoken } from 'src/store/features/wallets/walletsSelector' + +import { Divider } from '../commons' +import styles from './AssetCard.module.sass' + +interface RWACardProps { + value: RWARealtoken + onClick?: (id: string) => unknown +} + +const RWACardComponent: FC = (props) => { + const { t: tNumbers } = useTranslation('common', { keyPrefix: 'numbers' }) + const { t } = useTranslation('common', { keyPrefix: 'assetCard' }) + + const value = props.value.value + const totalInvestment = props.value.totalInvestment + + return ( + props.onClick?.(props.value.id)} + > + +
+ {props.value.fullName} +
+
+ + +
{props.value.shortName}
+ {useCurrencyValue(value)} +
+ + + +
+
{t('tokens')}
+
+ {tNumbers('decimal', { value: props.value.amount })} + {' / '} + {tNumbers('integer', { value: props.value.totalTokens })} +
+
+ +
+
{t('propertyValue')}
+
{useCurrencyValue(totalInvestment)}
+
+ +
+ + +
{props.value.fullName}
+ + ) +} + +export const RWACard = memo(RWACardComponent) diff --git a/src/components/cards/main/RentsCard.tsx b/src/components/cards/main/RentsCard.tsx index 2f1f1887..20df43ef 100644 --- a/src/components/cards/main/RentsCard.tsx +++ b/src/components/cards/main/RentsCard.tsx @@ -4,7 +4,9 @@ import { useSelector } from 'react-redux' import { Box, Card, Title } from '@mantine/core' +import { useGeneralFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { + selectOwnedRealtokens, selectOwnedRealtokensAPY, selectOwnedRealtokensRents, } from 'src/store/features/wallets/walletsSelector' @@ -16,6 +18,7 @@ export const RentsCard: FC = () => { const rents = useSelector(selectOwnedRealtokensRents) const apy = useSelector(selectOwnedRealtokensAPY) + const realtokens = useSelector(selectOwnedRealtokens) // In Dollars const dailyRents = rents.daily @@ -23,11 +26,17 @@ export const RentsCard: FC = () => { const monthlyRents = rents.monthly const yearlyRents = rents.yearly + const fullyRentedAPR = useGeneralFullyRentedAPR(realtokens) + return ( {t('title')} + diff --git a/src/components/cards/main/SummaryCard.tsx b/src/components/cards/main/SummaryCard.tsx index 6fb4ed0c..a500d048 100644 --- a/src/components/cards/main/SummaryCard.tsx +++ b/src/components/cards/main/SummaryCard.tsx @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux' import { Box, Card, Text, Title } from '@mantine/core' +import { useRWA } from 'src/hooks/useRWA' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' import { selectOwnedRealtokensValue, @@ -19,10 +20,13 @@ export const SummaryCard: FC = () => { const rmmDetails = useSelector(selectRmmDetails) const transfersIsLoaded = useSelector(selectTransfersIsLoaded) + const rwa = useRWA() + const stableDepositValue = rmmDetails.stableDeposit const stableDebtValue = rmmDetails.stableDebt + const rwaValue = rwa?.value ?? 0 const totalNetValue = - realtokensValue.total + stableDepositValue - stableDebtValue + realtokensValue.total + stableDepositValue + rwaValue - stableDebtValue return ( @@ -43,6 +47,7 @@ export const SummaryCard: FC = () => { ) : null} + ) diff --git a/src/components/commons/others/FullyRentedAPRDisclaimer.tsx b/src/components/commons/others/FullyRentedAPRDisclaimer.tsx new file mode 100644 index 00000000..8a977cd7 --- /dev/null +++ b/src/components/commons/others/FullyRentedAPRDisclaimer.tsx @@ -0,0 +1,14 @@ +import { useTranslation } from 'react-i18next' + +import { Text } from '@mantine/core' + +const FullyRentedAPRDisclaimer = () => { + const { t } = useTranslation('common', { keyPrefix: 'disclaimer' }) + return ( + + *{t('fullyRentedAPR')} + + ) +} + +export default FullyRentedAPRDisclaimer diff --git a/src/components/layouts/Header.tsx b/src/components/layouts/Header.tsx index 0fedc021..c76fc3cb 100644 --- a/src/components/layouts/Header.tsx +++ b/src/components/layouts/Header.tsx @@ -78,6 +78,12 @@ export const Header: FC = () => { onClick={() => router.push('/histories').then(() => close())} /> + } + onClick={() => router.push('/yamStatistics').then(() => close())} + /> +
{ + // Case of fully rented property + if (token.rentedUnits === token.totalUnits) { + return token.annualPercentageYield + } + + // Case of property with no rented unit but with APR (e.g. RMM or rent paid by inssurance) + if (token.rentedUnits === 0 && token.annualPercentageYield !== 0) { + return token.annualPercentageYield + } + + if (token.history.length > 0) { + let propInfo = token.history[0].values + const history = token.history.map((h) => { + propInfo = { ...propInfo, ...h.values } + return propInfo + }) + + const previousAPR = history + .map((h) => { + if ( + h.rentedUnits && + h.rentedUnits !== 0 && + h.netRentYear && + h.tokenPrice + ) { + return ( + ((h.netRentYear * token.totalUnits) / + (token.totalTokens * h.tokenPrice * h.rentedUnits)) * + 100 + ) + } + return 0 + }) + .filter((apr) => apr !== undefined) + + // Assuming the highest APR is the most accurate + return Math.max(...previousAPR, token.annualPercentageYield) + } + + return Math.max(token.annualPercentageYield, 0) +} + +export const useFullyRentedAPR = (token: UserRealtoken) => { + const rentCalculation = useSelector(selectUserRentCalculation) + + const fullyRentedAPR = useMemo(() => { + const isDisabled = APRDisabled(rentCalculation, token) + if (isDisabled) return 0 + return fullyRentedAPREstimation(token) + }, [token, rentCalculation]) + + return fullyRentedAPR +} + +export const useGeneralFullyRentedAPR = (tokens: UserRealtoken[]) => { + const rentCalculation = useSelector(selectUserRentCalculation) + // Fully rented APR average using valuation ponderation + const fullyRentedAPR = useMemo(() => { + const totalValue = tokens.reduce((acc, token) => { + const isDisabled = APRDisabled(rentCalculation, token) + if (isDisabled) return acc + return acc + token.value + }, 0) + const totalAPR = tokens.reduce((acc, token) => { + const isDisabled = APRDisabled(rentCalculation, token) + if (isDisabled) return acc + return acc + token.value * fullyRentedAPREstimation(token) + }, 0) + return totalAPR / totalValue + }, [tokens, rentCalculation]) + + return fullyRentedAPR +} + +const APRDisabled = ( + rentCalculation: RentCalculation, + token: UserRealtoken, +) => { + const realtimeDate = moment(new Date(rentCalculation.date)) + const rentStartDate = new Date(token.rentStartDate.date) + const isDisabled = + rentCalculation.state === RentCalculationState.Realtime && + rentStartDate > realtimeDate.toDate() + return isDisabled +} diff --git a/src/hooks/useRWA.ts b/src/hooks/useRWA.ts new file mode 100644 index 00000000..076d7101 --- /dev/null +++ b/src/hooks/useRWA.ts @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { ethers } from 'ethers' + +import { initializeProviders } from 'src/repositories/RpcProvider' +import { selectUserCurrency } from 'src/store/features/currencies/currenciesSelector' +import { + selectUserAddressList, + selectUserIncludesEth, +} from 'src/store/features/settings/settingsSelector' +import { RWARealtoken } from 'src/store/features/wallets/walletsSelector' + +const tokenDecimals = 9 + +const getRWA = async ( + addressList: string[], + rate: number, + includeETH: boolean = false, +): Promise => { + let totalAmount = 0 + const { GnosisRpcProvider, EthereumRpcProvider } = await initializeProviders() + + let providers = [GnosisRpcProvider] + + if (includeETH) { + providers.push(EthereumRpcProvider) + } + + for (let i = 0; i < addressList.length; i++) { + for (let j = 0; j < providers.length; j++) { + const RPCProvider = providers[j] + const RWAContract = new ethers.Contract( + '0x0675e8F4A52eA6c845CB6427Af03616a2af42170', + ['function balanceOf(address) view returns (uint)'], + RPCProvider, + ) + const RWAContractBalance = await RWAContract.balanceOf(addressList[i]) + totalAmount += Number(RWAContractBalance) + } + } + + const totalTokens = 100_000 + const amount = totalAmount / 10 ** tokenDecimals + const unitPriceCost = 50 / rate + + const value = unitPriceCost * amount + const totalInvestment = totalTokens * unitPriceCost + + return { + id: '0', + fullName: 'RWA Holdings SA, Neuchatel, NE, Suisse', + shortName: 'RWA', + amount, + totalTokens, + imageLink: [ + 'https://realt.co/wp-content/uploads/2024/02/Equity_FinalDesign-2000px-800x542.png', + ], + isRmmAvailable: false, + value, + totalInvestment, + unitPriceCost, + } +} + +export const useRWA = () => { + const [rwa, setRwa] = useState(null) + const addressList = useSelector(selectUserAddressList) + + const { rate } = useSelector(selectUserCurrency) + const includeETH = useSelector(selectUserIncludesEth) + + useEffect(() => { + ;(async () => { + const rwa_ = await getRWA(addressList, rate, includeETH) + + setRwa(rwa_) + })() + }, [addressList]) + + return rwa +} diff --git a/src/i18next/locales/en/common.json b/src/i18next/locales/en/common.json index 87059e27..32ee90b8 100644 --- a/src/i18next/locales/en/common.json +++ b/src/i18next/locales/en/common.json @@ -3,6 +3,7 @@ "title": "Realtoken Dashboard", "transactions": "My transactions", "histories": "Changes history", + "yamStatistics": "Secondary market statistics (Yam)", "documentation": "Community Wiki", "home": "Home", "realt": "RealT", @@ -68,7 +69,8 @@ "realtokenValue": "RealTokens", "totalPriceCost": "Estimated price cost", "stableDeposit": "RMM deposit", - "stableBorrow": "RMM borrow" + "stableBorrow": "RMM borrow", + "rwa": "RWA" }, "worthCard": { "title": "RealTokens", @@ -84,6 +86,7 @@ "rentsCard": { "title": "Rents", "apr": "APR", + "fullyRentedAPR": "Fully rented APR *", "daily": "Daily", "weekly": "Weekly", "monthly": "Monthly", @@ -190,6 +193,7 @@ "rentedUnits": "Rented units", "propertyValue": "Property value", "rentStartDate": "Rent Start", + "fullyRentedEstimation": "Fully rented APR", "rentNotStarted": "Rent not started yet", "isRmmAvailable": "RMM", "rentStatus": { @@ -212,6 +216,7 @@ "unitPriceCost": "Unit price cost", "unrealizedCapitalGain": "Capital gain", "apr": "Yield", + "fullyRentedAPR":"Fully rented APR *", "weeklyRents": "Weekly rents", "yearlyRents": "Yearly rents", "rentedUnits": "Rented units", @@ -260,6 +265,7 @@ "grossRentMonth": "Gross monthly rent", "netRentMonth": "Net monthly rent", "annualYield": "Annual yield", + "fullyRentedAPR": "Fully rented APR *", "constructionYear": "Construction year", "propertyStories": "Number of stories", "propertyUnits": "Number of units", @@ -358,7 +364,8 @@ "netRentMonth": "Net monthly", "grossRentMonth": "Gross monthly", "yield": "Yield", - "rentedUnits": "Rented units" + "rentedUnits": "Rented units", + "rwa": "RWA value" }, "filter": { "field": "Filter", @@ -370,8 +377,23 @@ "ownedRent": "My rents" } }, + "yamStatisticsPage": { + "home": "Home", + "title": "Secondary market statistics (Yam)", + "filter": { + "field": "Filter", + "all": "All", + "owned": "My properties", + "subsidized": "Subsidized", + "fullySubsidized": "Fully subsidized", + "notSubsidized": "Not subsidized" + } + }, "initialTransfersLoader": { "title": "Initial data loading", "description": "Retrieving your transactions in progress. This loading can take some time depending on the number of transactions performed (10-20 seconds / 1000 transactions). On your next visits, only new transactions will be retrieved." + }, + "disclaimer":{ + "fullyRentedAPR": "This is a beta estimation done by RealT community. Please report any issues. Please note that is an indicative value and not a guarantee. RealT community or RealT does not take any responsibility for user actions based on this value." } } diff --git a/src/i18next/locales/fr/common.json b/src/i18next/locales/fr/common.json index e7fb8522..096f2d46 100644 --- a/src/i18next/locales/fr/common.json +++ b/src/i18next/locales/fr/common.json @@ -3,6 +3,7 @@ "title": "Realtoken Dashboard", "transactions": "Mes transactions", "histories": "Historique des changements", + "yamStatistics": "Statistiques marché secondaire (Yam)", "documentation": "Wiki communautaire", "home": "Accueil", "realt": "RealT", @@ -68,7 +69,8 @@ "realtokenValue": "RealTokens", "totalPriceCost": "Prix d'achat estimé", "stableDeposit": "Dépôt RMM", - "stableBorrow": "Emprunt RMM" + "stableBorrow": "Emprunt RMM", + "rwa": "RWA" }, "worthCard": { "title": "RealTokens", @@ -84,6 +86,7 @@ "rentsCard": { "title": "Loyers", "apr": "Rendement annuel", + "fullyRentedAPR": "Rendement 100% loué *", "daily": "Journaliers", "weekly": "Hebdomadaires", "monthly": "Mensuels", @@ -190,6 +193,7 @@ "rentedUnits": "Logements loués", "propertyValue": "Valeur de la propriété", "rentStartDate": "Date du premier loyer", + "fullyRentedEstimation": "Rendement 100% loué", "rentNotStarted": "Le loyer n'a pas débuté", "isRmmAvailable": "RMM", "rentStatus": { @@ -212,6 +216,7 @@ "tokenPrice": "Prix du token", "unitPriceCost": "Prix de revient", "apr": "Rendement annuel", + "fullyRentedAPR":"Rendement 100% loué *", "weeklyRents": "Loyer hebdo", "yearlyRents": "Loyer annuel", "rentedUnits": "Logements loués", @@ -260,6 +265,7 @@ "grossRentMonth": "Loyer brut mensuel", "netRentMonth": "Loyer net mensuel", "annualYield": "Rendement annuel", + "fullyRentedAPR": "Rendement 100% loué *", "constructionYear": "Année de construction", "propertyStories": "Nombre d'étages", "propertyUnits": "Nombre de logements", @@ -372,8 +378,23 @@ "ownedRent": "Mes changements de loyers" } }, + "yamStatisticsPage": { + "home": "Accueil", + "title": "Statistiques marché secondaire (Yam)", + "filter": { + "field": "Filtre", + "all": "Toutes les propriétés", + "owned": "Mes propriétés", + "subsidized": "Subventionnés", + "fullySubsidized": "Entièrement subventionnés", + "notSubsidized": "Non subventionnés" + } + }, "initialTransfersLoader": { "title": "Chargement initial des données", "description": "Récupération de vos transactions en cours. Ce chargement peut prendre un certain temps en fonction du nombre de transactions effecutées (10-20 secondes / 1000 transactions). Lors de vos prochaines visites, seul les nouvelles transactions seront récupérées." + }, + "disclaimer":{ + "fullyRentedAPR": "Cette estimation est en phase bêta et a été développée par la communauté RealT. Nous vous invitons à signaler tout problème éventuel. Les informations fournies sont à titre indicatif uniquement. La communauté RealT ou RealT ne peut être tenu responsable en cas de décision prise à partir de données inexactites." } } diff --git a/src/pages/asset/[assetId].tsx b/src/pages/asset/[assetId].tsx index 2d2a1ada..9c93d541 100644 --- a/src/pages/asset/[assetId].tsx +++ b/src/pages/asset/[assetId].tsx @@ -14,6 +14,7 @@ import { AssetPageMainTab } from 'src/components/assetPage/assetPageMainTab' import { AssetPagePropertyTab } from 'src/components/assetPage/assetPagePropertyTab' import { AssetPageTransfersTab } from 'src/components/assetPage/assetPageTransfersTab' import { AssetPageYamStatisticsTab } from 'src/components/assetPage/assetPageYamStatisticsTab' +import FullyRentedAPRDisclaimer from 'src/components/commons/others/FullyRentedAPRDisclaimer' import { selectIsLoading } from 'src/store/features/settings/settingsSelector' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' import { selectAllUserRealtokens } from 'src/store/features/wallets/walletsSelector' @@ -158,6 +159,7 @@ const AssetPage: NextPage = () => {
+ ) } diff --git a/src/pages/yamStatistics.tsx b/src/pages/yamStatistics.tsx new file mode 100644 index 00000000..dba6596b --- /dev/null +++ b/src/pages/yamStatistics.tsx @@ -0,0 +1,259 @@ +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { useRouter } from 'next/router' + +import { + Anchor, + Breadcrumbs, + Divider, + Flex, + Group, + Pagination, + Select, +} from '@mantine/core' + +import { AssetSubsidyType } from 'src/components/assetsView' +import { useInputStyles } from 'src/components/inputs/useInputStyles' +import { useCurrencyValue } from 'src/hooks/useCurrencyValue' +import { GetYamStatistics, YamStatistics } from 'src/repositories' +import { + UserRealtoken, + selectAllUserRealtokens, +} from 'src/store/features/wallets/walletsSelector' + +const YamStatisticsRow: React.FC<{ + statistics: YamStatistics + realtoken: UserRealtoken | null +}> = ({ statistics, realtoken }) => { + if (!realtoken) return null + const { t: tNumbers } = useTranslation('common', { keyPrefix: 'numbers' }) + const yamPrice = statistics.volume / statistics.quantity + const yamDifference = yamPrice - realtoken.tokenPrice + const yamDifferencePercent = (yamDifference / realtoken.tokenPrice) * 100 + + const fallback = '-' + const tokenPriceValue = useCurrencyValue(realtoken.tokenPrice, fallback) + const yamPriceValue = useCurrencyValue(yamPrice, fallback) + const yamDifferenceValue = useCurrencyValue(yamDifference, fallback) + const volumeValue = useCurrencyValue(statistics.volume, fallback) + + return yamPriceValue !== fallback ? ( + <> + + {realtoken.shortName} + {tokenPriceValue} + {yamPriceValue} + + {yamDifferenceValue} ( + {tNumbers('percent', { value: yamDifferencePercent })}) + + {volumeValue} + + + + + + + + + ) : null +} + +const YamStatisticsPage = () => { + const { t } = useTranslation('common', { keyPrefix: 'yamStatisticsPage' }) + const router = useRouter() + const realtokens = useSelector(selectAllUserRealtokens) + const realtokensWithYam = useMemo(() => { + return realtokens.filter( + (realtoken) => realtoken.blockchainAddresses.xDai.contract, + ) + }, [realtokens]) + + const [yamStatistics, setYamStatistics] = useState( + realtokensWithYam.map(() => { + return { + quantity: 0, + volume: 0, + days: [], + } + }), + ) + const [page, setPage] = useState(1) + const pageSize = 100 + + const [currentFilter, setCurrentFilter] = + useState('owned') + const filteredRealtokens = useMemo(() => { + return getFilteredRealtokens(currentFilter, realtokensWithYam) + }, [realtokensWithYam, currentFilter]) + + const [isLoading, setIsLoading] = useState(true) + + function onPageChange(page: number) { + setPage(page) + // Scroll to top of grid + document.getElementsByClassName('history-list')[0]?.scrollIntoView() + } + + const yamStatisticsPromise: Promise = useMemo(async () => { + if (!filteredRealtokens.length) return Promise.resolve([]) + + const statsPromises = filteredRealtokens.map((realtoken) => + GetYamStatistics({ realtoken }), + ) + const data = await Promise.all(statsPromises) + return data + }, [filteredRealtokens, currentFilter]) + + useEffect(() => { + setIsLoading(true) + yamStatisticsPromise.then((data) => { + setYamStatistics(data) + setIsLoading(false) + }) + }, [yamStatisticsPromise]) + + const paginationYamStatistics: YamStatistics[] = useMemo(() => { + const start = (page - 1) * pageSize + const end = start + pageSize + return yamStatistics.slice(start, end) + }, [yamStatistics, page, pageSize]) + + if (isLoading) { + return
Loading...
+ } + + return ( + +
+ + router.push('/')}>{t('home')} + {t('title')} + +

{`${t('title')}`}

+ + + +
+ + + + + + + + + {paginationYamStatistics.map((statistics, index) => ( + + ))} +
TokenToken PriceYam PriceYam Difference (30 days)Yam Volume
+ + + +
+
+
+ ) +} + +const getFilteredRealtokens = ( + filter: YamStatisticsPageFilterValue, + realtokens: UserRealtoken[], +) => { + switch (filter) { + case 'all': + return realtokens + case 'owned': + return realtokens.filter((realtoken) => realtoken.amount > 0) + case AssetSubsidyType.SUBSIDIZED: + return realtokens.filter((realtoken) => realtoken.subsidyStatus !== 'no') + case AssetSubsidyType.FULLY_SUBSIDIZED: + return realtokens.filter( + (realtoken) => + realtoken.subsidyStatus === 'yes' && !!realtoken.subsidyStatusValue, + ) + default: + return realtokens + } +} + +type YamStatisticsPageFilterValue = + | 'all' + | 'owned' + | AssetSubsidyType.SUBSIDIZED + | AssetSubsidyType.FULLY_SUBSIDIZED + | AssetSubsidyType.NOT_SUBSIDIZED + +const YamStatisticsPageFilter = ({ + currentFilter, + setCurrentFilter, +}: { + currentFilter: YamStatisticsPageFilterValue + setCurrentFilter: (value: YamStatisticsPageFilterValue) => void +}) => { + const { t } = useTranslation('common', { + keyPrefix: 'yamStatisticsPage.filter', + }) + const { classes: inputClasses } = useInputStyles() + + const filterOptions: { + value: YamStatisticsPageFilterValue + label: string + }[] = [ + { value: 'all', label: t('all') }, + { value: 'owned', label: t('owned') }, + { value: AssetSubsidyType.SUBSIDIZED, label: t('subsidized') }, + { value: AssetSubsidyType.FULLY_SUBSIDIZED, label: t('fullySubsidized') }, + { + value: AssetSubsidyType.NOT_SUBSIDIZED, + label: t('notSubsidized'), + }, + ] + + return ( + +
+