diff --git a/app/(app)/vebal/manage/page.tsx b/app/(app)/vebal/manage/page.tsx new file mode 100644 index 000000000..d43d569c7 --- /dev/null +++ b/app/(app)/vebal/manage/page.tsx @@ -0,0 +1,12 @@ +'use client' + +import { VebalManage } from '@/lib/modules/vebal/VebalManage' +import { Stack } from '@chakra-ui/react' + +export default function VebalManagePage() { + return ( + + + + ) +} diff --git a/app/(app)/vebal/page.tsx b/app/(app)/vebal/page.tsx index df584b3c2..01974c04d 100644 --- a/app/(app)/vebal/page.tsx +++ b/app/(app)/vebal/page.tsx @@ -1,12 +1,5 @@ -'use client' - -import { VebalInfo } from '@/lib/modules/vebal/VebalInfo' import { Stack } from '@chakra-ui/react' -export default function VebalPage() { - return ( - - - - ) +export default function VeBALPage() { + return } diff --git a/lib/modules/vebal/VebalBreadcrumbs.tsx b/lib/modules/vebal/VebalBreadcrumbs.tsx new file mode 100644 index 000000000..0e19c66be --- /dev/null +++ b/lib/modules/vebal/VebalBreadcrumbs.tsx @@ -0,0 +1,31 @@ +import { Box, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button } from '@chakra-ui/react' +import { ChevronRight, Home } from 'react-feather' + +export function VebalBreadcrumbs() { + return ( + + + + } + > + + + + + + + veBAL + + + Manage + + + ) +} diff --git a/lib/modules/vebal/VebalInfo.tsx b/lib/modules/vebal/VebalInfo.tsx deleted file mode 100644 index a76ac2d14..000000000 --- a/lib/modules/vebal/VebalInfo.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useVebalLockInfo } from './useVebalLockInfo' -import { bn, fNum } from '@/lib/shared/utils/numbers' -import { differenceInDays, format } from 'date-fns' -import { useUserAccount } from '../web3/UserAccountProvider' -import { Stack, Text } from '@chakra-ui/react' -import { VeBALLocksChart } from './vebal-chart/VebalLocksChart' -import { useVebalUserData } from './useVebalUserData' -import { useTokenBalances } from '../tokens/TokenBalancesProvider' -import mainnetNetworkConfig from '@/lib/config/networks/mainnet' - -export function VebalInfo() { - const lockInfo = useVebalLockInfo() - const { isConnected } = useUserAccount() - - const { data } = useVebalUserData() - - const lockedUntil = !lockInfo.mainnetLockedInfo.lockedEndDate - ? '-' - : format(lockInfo.mainnetLockedInfo.lockedEndDate, 'yyyy-MM-dd') - - const percentOfAllSupply = bn(data?.veBalGetUser.balance || 0).div( - lockInfo.mainnetLockedInfo.totalSupply || 0 - ) - - const { balanceFor } = useTokenBalances() - const unlockedBalance = balanceFor(mainnetNetworkConfig.tokens.addresses.veBalBpt) - - const lockData = [ - { - title: 'Locked veBAL', - value: lockInfo.mainnetLockedInfo.lockedAmount, - }, - { - title: 'Unlocked veBAL', - value: unlockedBalance?.formatted, - }, - ] - - const vebalData = [ - { - title: 'My veBAL', - value: data?.veBalGetUser.balance, - }, - { - title: '', - value: lockedUntil - ? `Expires ${lockedUntil} (${differenceInDays(new Date(lockedUntil), new Date())} days)` - : '', - }, - { - title: '% of all supply', - value: fNum('feePercent', percentOfAllSupply), - }, - { - title: 'Rank', - value: data?.veBalGetUser.rank, - }, - ] - - if (!isConnected) { - return Not connected - } - - return ( - - {vebalData.map(({ title, value }) => ( - - - {title} - - {value} - - ))} - - {lockData.map(({ title, value }) => ( - - {title} - {value} - - ))} - - - - ) -} diff --git a/lib/modules/vebal/VebalManage.tsx b/lib/modules/vebal/VebalManage.tsx new file mode 100644 index 000000000..7284976bb --- /dev/null +++ b/lib/modules/vebal/VebalManage.tsx @@ -0,0 +1,54 @@ +import { useUserAccount } from '../web3/UserAccountProvider' +import { Button, Heading, Stack, Text, VStack } from '@chakra-ui/react' +import { VebalStatsLayout } from './VebalStats/VebalStatsLayout' +import { VebalBreadcrumbs } from '@/lib/modules/vebal/VebalBreadcrumbs' + +export function VebalManage() { + const { isConnected } = useUserAccount() + + if (!isConnected) { + return Not connected + } + + return ( + + + + + + Manage veBAL + + + + + + + + + + + ) +} diff --git a/lib/modules/vebal/VebalStats/AllVebalStatsValues.tsx b/lib/modules/vebal/VebalStats/AllVebalStatsValues.tsx new file mode 100644 index 000000000..3e6e4776c --- /dev/null +++ b/lib/modules/vebal/VebalStats/AllVebalStatsValues.tsx @@ -0,0 +1,14 @@ +'use client' +import { Stack } from '@chakra-ui/react' +import BigNumber from 'bignumber.js' + +export type VebalAllStatsValues = { + balance: string | undefined + rank: number | undefined + percentOfAllSupply: BigNumber | undefined + lockedUntil: string | undefined +} + +export function AllVebalStatsValues() { + return +} diff --git a/lib/modules/vebal/VebalStats/UserVebalStatsValues.tsx b/lib/modules/vebal/VebalStats/UserVebalStatsValues.tsx new file mode 100644 index 000000000..4a1892344 --- /dev/null +++ b/lib/modules/vebal/VebalStats/UserVebalStatsValues.tsx @@ -0,0 +1,115 @@ +'use client' + +import React, { useMemo } from 'react' +import { Heading, Skeleton, Text, Tooltip, VStack } from '@chakra-ui/react' +import { bn, fNum } from '@/lib/shared/utils/numbers' +import { useVebalUserData } from '@/lib/modules/vebal/useVebalUserData' +import { useVebalLockInfo } from '@/lib/modules/vebal/useVebalLockInfo' +import { differenceInDays, format } from 'date-fns' +import BigNumber from 'bignumber.js' + +export type VebalUserStatsValues = { + balance: string | undefined + rank: number | undefined + percentOfAllSupply: BigNumber | undefined + lockedUntil: string | undefined +} + +export function UserVebalStatsValues() { + const lockInfo = useVebalLockInfo() + const vebalUserData = useVebalUserData() + + const vebalUserStatsValues: VebalUserStatsValues | undefined = useMemo(() => { + if (vebalUserData.isConnected) { + const balance = vebalUserData.data?.veBalGetUser.balance + const rank = vebalUserData.data?.veBalGetUser.rank ?? undefined + const percentOfAllSupply = vebalUserData.data + ? bn(vebalUserData.data.veBalGetUser.balance || 0).div( + lockInfo.mainnetLockedInfo.totalSupply || 0 + ) + : undefined + const lockedUntil = + !lockInfo.mainnetLockedInfo.lockedEndDate || lockInfo.mainnetLockedInfo.isExpired + ? undefined + : format(lockInfo.mainnetLockedInfo.lockedEndDate, 'yyyy-MM-dd') + + return { + balance, + rank, + percentOfAllSupply, + lockedUntil, + } + } + }, [lockInfo.mainnetLockedInfo, vebalUserData.isConnected, vebalUserData.data]) + + return ( + <> + + + My veBAL + + {vebalUserData.loading ? ( + + ) : ( + + {typeof vebalUserStatsValues?.balance === 'string' ? ( + fNum('token', vebalUserStatsValues.balance) + ) : ( + <>— + )} + + )} + + + + My rank + + {vebalUserData.loading ? ( + + ) : ( + {vebalUserStatsValues?.rank ?? <>—} + )} + + + + My share of veBAL + + {vebalUserData.loading ? ( + + ) : ( + + {vebalUserStatsValues?.percentOfAllSupply ? ( + fNum('feePercent', vebalUserStatsValues.percentOfAllSupply) + ) : ( + <>— + )} + + )} + + + + Expiry date + + {lockInfo.isLoading ? ( + + ) : ( + + + {vebalUserStatsValues?.lockedUntil ? ( + `${differenceInDays(new Date(vebalUserStatsValues.lockedUntil), new Date())} days` + ) : ( + <>— + )} + + + )} + + + ) +} diff --git a/lib/modules/vebal/VebalStats/VebalStats.tsx b/lib/modules/vebal/VebalStats/VebalStats.tsx new file mode 100644 index 000000000..1a1216153 --- /dev/null +++ b/lib/modules/vebal/VebalStats/VebalStats.tsx @@ -0,0 +1,82 @@ +'use client' + +import React, { useState } from 'react' +import { Box, BoxProps, Card, CardProps, VStack } from '@chakra-ui/react' + +import { NoisyCard } from '@/lib/shared/components/containers/NoisyCard' +import { ZenGarden } from '@/lib/shared/components/zen/ZenGarden' +import ButtonGroup, { + ButtonGroupOption, +} from '@/lib/shared/components/btns/button-group/ButtonGroup' + +import { UserVebalStatsValues } from '@/lib/modules/vebal/VebalStats/UserVebalStatsValues' +import { AllVebalStatsValues } from '@/lib/modules/vebal/VebalStats/AllVebalStatsValues' + +const COMMON_NOISY_CARD_PROPS: { contentProps: BoxProps; cardProps: BoxProps } = { + contentProps: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderBottomLeftRadius: 'none', + borderTopLeftRadius: 'none', + borderBottomRightRadius: 'none', + }, + cardProps: { + position: 'relative', + height: 'full', + }, +} + +const TABS = [ + { + value: 'allStats', + label: 'All stats', + }, + { + value: 'myStats', + label: 'My stats', + }, +] as const + +export function VebalStats({ ...props }: CardProps) { + const [activeTab, setActiveTab] = useState(TABS[1]) + + function handleTabChanged(option: ButtonGroupOption) { + setActiveTab(option) + } + + return ( + + + + + + + + {activeTab.value === 'allStats' && } + {activeTab.value === 'myStats' && } + + + + ) +} diff --git a/lib/modules/vebal/VebalStats/VebalStatsLayout.tsx b/lib/modules/vebal/VebalStats/VebalStatsLayout.tsx new file mode 100644 index 000000000..122b08908 --- /dev/null +++ b/lib/modules/vebal/VebalStats/VebalStatsLayout.tsx @@ -0,0 +1,14 @@ +'use client' + +import { Stack } from '@chakra-ui/react' +import { VebalStats } from '@/lib/modules/vebal/VebalStats/VebalStats' +import { VeBALLocksChart } from '@/lib/modules/vebal/vebal-chart/VebalLocksChart' + +export function VebalStatsLayout() { + return ( + + + + + ) +} diff --git a/lib/modules/vebal/useVebalUserData.ts b/lib/modules/vebal/useVebalUserData.ts index 8d77927af..ee5ed2c85 100644 --- a/lib/modules/vebal/useVebalUserData.ts +++ b/lib/modules/vebal/useVebalUserData.ts @@ -5,7 +5,7 @@ import { useQuery } from '@apollo/experimental-nextjs-app-support/ssr' export function useVebalUserData() { const { userAddress, isConnected } = useUserAccount() - const { data, refetch } = useQuery(GetVeBalUserDocument, { + const { data, refetch, loading, error } = useQuery(GetVeBalUserDocument, { variables: { address: userAddress.toLowerCase(), chain: GqlChain.Mainnet, @@ -16,5 +16,7 @@ export function useVebalUserData() { data, refetch, isConnected, + loading, + error, } } diff --git a/lib/modules/vebal/vebal-chart/VebalLocksChart.tsx b/lib/modules/vebal/vebal-chart/VebalLocksChart.tsx index e5fde03d8..81d07319a 100644 --- a/lib/modules/vebal/vebal-chart/VebalLocksChart.tsx +++ b/lib/modules/vebal/vebal-chart/VebalLocksChart.tsx @@ -1,14 +1,19 @@ -import { Stack } from '@chakra-ui/react' +import { Card, CardProps } from '@chakra-ui/react' import ReactECharts from 'echarts-for-react' import { useVebalLocksChart } from './useVebalLocksChart' -export function VeBALLocksChart() { - const { options } = useVebalLocksChart() +export function VeBALLocksChart(props: CardProps) { + const { options, onChartReady, onEvents } = useVebalLocksChart() return ( - - - + + + ) } diff --git a/lib/modules/vebal/vebal-chart/test-locks.ts b/lib/modules/vebal/vebal-chart/test-locks.ts index 8f957c07d..453180d32 100644 --- a/lib/modules/vebal/vebal-chart/test-locks.ts +++ b/lib/modules/vebal/vebal-chart/test-locks.ts @@ -1,92 +1,254 @@ export const lockSnapshots = [ { - bias: '301.388808164359183128', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1648477572', - slope: '0.000009705929936826', - timestamp: 1648477572, + bias: '411.968613215338375488', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1648494832', + slope: '0.000013274422196916', + timestamp: 1648494832, }, { - bias: '618.950318966359818012', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1651885359', - slope: '0.000022389846730332', - timestamp: 1651885359, + bias: '426.768704596875629824', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1648914664', + slope: '0.000013939885570784', + timestamp: 1648914664, }, { - bias: '956.863613477739943196', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1654088236', - slope: '0.000037610546882539', - timestamp: 1654088236, + bias: '437.379758392016650735', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1649386739', + slope: '0.000014510227094635', + timestamp: 1649386739, }, { - bias: '1002.355111964593047795', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1654088295', - slope: '0.000037610546882539', - timestamp: 1654088295, + bias: '498.274642722178822178', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1650253669', + slope: '0.000017019941832838', + timestamp: 1650253669, }, { - bias: '1475.194475239244712888', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1656527262', - slope: '0.000060928393061276', - timestamp: 1656527262, + bias: '525.4719047945281344', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1651287630', + slope: '0.00001860606412352', + timestamp: 1651287630, }, { - bias: '1504.241981926209602436', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1658797166', - slope: '0.000068555266203954', - timestamp: 1658797166, + bias: '526.22180254040241455', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1652399425', + slope: '0.000019396181651626', + timestamp: 1652399425, }, { - bias: '428.293668367008540547', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1682703443', - slope: '0.000013683610015471', - timestamp: 1682703443, + bias: '537.78149185152830984', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1652671730', + slope: '0.000020023236833432', + timestamp: 1652671730, }, { - bias: '771.437205712039985286', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1685451467', - slope: '0.000027018927562542', - timestamp: 1685451467, + bias: '546.23608139662436016', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1652935896', + slope: '0.00002054005269054', + timestamp: 1652935896, }, { - bias: '836.800746817080090678', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1685451491', - slope: '0.000027018927562542', - timestamp: 1685451491, + bias: '645.68362210525044576', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1655956656', + slope: '0.00002054005269054', + timestamp: 1655956656, }, { - bias: '1145.006774769866674166', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1687828823', - slope: '0.000040044195057158', - timestamp: 1687828823, + bias: '597.063090614348179615', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1659069463', + slope: '0.000021080847757895', + timestamp: 1659069463, }, { - bias: '1241.880730391461935974', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1687828847', - slope: '0.000040044195057158', - timestamp: 1687828847, + bias: '570.212661942865193703', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1660773221', + slope: '0.000021421443182757', + timestamp: 1660773221, }, { - bias: '1894.53106627656545857', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1689381779', - slope: '0.00006430898090917', - timestamp: 1689381779, + bias: '578.856298996191034753', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1660960409', + slope: '0.000021900168589783', + timestamp: 1660960409, }, { - bias: '2011.21173782262168649', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1689381803', - slope: '0.00006430898090917', - timestamp: 1689381803, + bias: '873.31490731813230788', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1661367935', + slope: '0.000033557974410152', + timestamp: 1661367935, }, { - bias: '2486.14202915624302687', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1692387515', - slope: '0.000087947480353342', - timestamp: 1692387515, + bias: '784.567787366075541538', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1664163563', + slope: '0.000033776176475674', + timestamp: 1664163563, }, { - bias: '2752.093099005220754662', - id: '0x25b70c8050b7e327ce62cfd80a0c60cccf057fa6-1692387539', - slope: '0.000087947480353342', - timestamp: 1692387539, + bias: '1050.128381346061674874', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1664163599', + slope: '0.000033776176475674', + timestamp: 1664163599, + }, + { + bias: '931.328332517805007732', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1668044771', + slope: '0.000034227895298308', + timestamp: 1668044771, + }, + { + bias: '1076.234317848491017444', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1668044807', + slope: '0.000034227895298308', + timestamp: 1668044807, + }, + { + bias: '994.550288880754896919', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1671037031', + slope: '0.000034956640277551', + timestamp: 1671037031, + }, + { + bias: '1100.258330120702459695', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1671037055', + slope: '0.000034956640277551', + timestamp: 1671037055, + }, + { + bias: '986.354377972902335952', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1674874919', + slope: '0.000035689528064592', + timestamp: 1674874919, + }, + { + bias: '1122.554182554120910032', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1675897079', + slope: '0.000035689528064592', + timestamp: 1675897079, + }, + { + bias: '1103.901122090218030416', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1685491727', + slope: '0.000035689528064592', + timestamp: 1685491727, + }, + { + bias: '1163.452523590283058086', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1686367439', + slope: '0.000038710831253126', + timestamp: 1686367439, + }, + { + bias: '1210.27575148413915515', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1686367475', + slope: '0.000038710831253126', + timestamp: 1686367475, + }, + { + bias: '1185.891454125148391631', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1687746371', + slope: '0.000039680993634939', + timestamp: 1687746371, + }, + { + bias: '1233.888155510199748227', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1687746407', + slope: '0.000039680993634939', + timestamp: 1687746407, + }, + { + bias: '1211.693697205053065936', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1688699951', + slope: '0.000040199980339664', + timestamp: 1688699951, + }, + { + bias: '1260.3186286243824884', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1688699975', + slope: '0.000040199980339664', + timestamp: 1688699975, + }, + { + bias: '1167.088644074268523808', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1691292359', + slope: '0.000040581908153888', + timestamp: 1691292359, + }, + { + bias: '1265.26293533146083344', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1691292395', + slope: '0.000040581908153888', + timestamp: 1691292395, + }, + { + bias: '1282.359069389423530864', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1691721443', + slope: '0.000041704148514352', + timestamp: 1691721443, + }, + { + bias: '1226.553715113925634922', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1693644311', + slope: '0.000042550125863898', + timestamp: 1693644311, + }, + { + bias: '1329.48995840084694297', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1693644335', + slope: '0.000042550125863898', + timestamp: 1693644335, + }, + { + bias: '1311.815238816018343179', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1694491019', + slope: '0.000043153831384959', + timestamp: 1694491019, + }, + { + bias: '1051.61523535449844771', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1700936615', + slope: '0.000043903306220686', + timestamp: 1700936615, + }, + { + bias: '1370.244182704026623686', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1700936699', + slope: '0.000043903306220686', + timestamp: 1700936699, + }, + { + bias: '1207.52747382758520986', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1706411831', + slope: '0.00004692093102794', + timestamp: 1706411831, + }, + { + bias: '1462.92523339417887674', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1706411879', + slope: '0.00004692093102794', + timestamp: 1706411879, + }, + { + bias: '1389.499237660943196577', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1708406507', + slope: '0.000047611853485789', + timestamp: 1708406507, + }, + { + bias: '1130.568460169324406853', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1714278167', + slope: '0.000048496789654141', + timestamp: 1714278167, + }, + { + bias: '989.152639305655695838', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1717392263', + slope: '0.000048972469060174', + timestamp: 1717392263, + }, + { + bias: '1522.28006211641798647', + id: '0x5e4568c4d8343052a06ec8aab1e124af08b73248-1717392395', + slope: '0.000048972469060174', + timestamp: 1717392395, }, ] diff --git a/lib/modules/vebal/vebal-chart/useVebalLocksChart.tsx b/lib/modules/vebal/vebal-chart/useVebalLocksChart.tsx index b1ff9c68d..c33159e0d 100644 --- a/lib/modules/vebal/vebal-chart/useVebalLocksChart.tsx +++ b/lib/modules/vebal/vebal-chart/useVebalLocksChart.tsx @@ -1,14 +1,17 @@ -import { useMemo } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' + import { useTheme as useChakraTheme } from '@chakra-ui/react' import * as echarts from 'echarts/core' +import { EChartsOption, ECharts } from 'echarts' import { format, differenceInDays } from 'date-fns' import BigNumber from 'bignumber.js' import { lockSnapshots } from './test-locks' import { useVebalLockInfo } from '../../vebal/useVebalLockInfo' -import { bn } from '@/lib/shared/utils/numbers' +import { bn, fNum } from '@/lib/shared/utils/numbers' +import { useTheme as useNextTheme } from 'next-themes' -type ChartValueAcc = (readonly [string, number])[] +type ChartValueAcc = [string, number][] interface LockSnapshot { bias: string @@ -72,8 +75,14 @@ function filterAndFlattenValues(valuesByDates: Record) { }, []) } +const MAIN_SERIES_ID = 'main-series' +const FUTURE_SERIES_ID = 'future-series' + export function useVebalLocksChart() { const theme = useChakraTheme() + const { theme: nextTheme } = useNextTheme() + + const instanceRef = useRef() const userHistoricalLocks = lockSnapshots @@ -95,29 +104,97 @@ export function useVebalLocksChart() { const futureLockChartData = useMemo(() => { if (hasExistingLock && !isExpired) { return { + id: FUTURE_SERIES_ID, name: '', - type: 'line', + type: 'line' as const, data: [ chartValues[chartValues.length - 1], [format(new Date(mainnetLockedInfo.lockedEndDate).getTime(), 'yyyy/MM/dd'), 0], ], lineStyle: { - type: 'dashed', + type: [3, 15], + color: '#EAA879', + width: 5, + cap: 'round' as const, }, + showSymbol: false, } } return { name: '', - type: 'line', + type: 'line' as const, data: [], } }, [chartValues, mainnetLockedInfo.lockedEndDate, hasExistingLock, isExpired]) - const options = useMemo(() => { + const showStaticTooltip = useCallback(() => { + if (!mouseoverRef.current) { + if (instanceRef.current) { + // Show tooltip on a specific data point when chart is loaded + instanceRef.current.dispatchAction({ + type: 'showTip', + seriesIndex: 0, // Index of the series + dataIndex: chartValues.length - 1, // Index of the data point + }) + } + } + }, [chartValues]) + + const onChartReady = useCallback((instance: ECharts) => { + instanceRef.current = instance + }, []) + + // detect if "static" tooltip is showing + const mouseoverRef = useRef() + + useEffect(() => { + if (instanceRef.current) { + const handler = () => { + mouseoverRef.current = true + } + const element = instanceRef.current.getDom() + + // using "addEventListener" instead of "onEvents.mouseover" since "onEvents.mouseover" emits only when cursor crosses the line, not the entire chart... + element.addEventListener('mouseover', handler) + + return () => { + element.removeEventListener('mouseover', handler) + } + } + }, []) + + const onEvents = useMemo((): Partial< + Record< + echarts.ElementEvent['type'] | 'highlight' | 'finished', + (event: echarts.ElementEvent | any, instance: ECharts) => boolean | void + > + > => { + return { + click: () => { + mouseoverRef.current = true + }, + globalout: () => { + mouseoverRef.current = false + showStaticTooltip() + }, + finished: () => { + showStaticTooltip() + }, + } + }, [showStaticTooltip]) + + const options = useMemo((): EChartsOption => { const toolTipTheme = { heading: 'font-weight: bold; color: #E5D3BE', - container: `background: ${theme.colors.gray[800]};`, - text: theme.colors.gray[400], + container: `background: ${ + nextTheme === 'dark' + ? theme.semanticTokens.colors.background.level3._dark + : theme.semanticTokens.colors.background.default + };`, + text: + nextTheme === 'dark' + ? theme.semanticTokens.colors.font.primary._dark + : theme.semanticTokens.colors.font.primary.default, } return { grid: { @@ -139,26 +216,67 @@ export function useVebalLocksChart() { show: false, }, }, - extraCssText: `padding-right:2rem;border: none;${toolTipTheme.container}`, - formatter: (params: any) => { + extraCssText: `border: none;${toolTipTheme.container};max-width: 215px; z-index: 5`, + position: (point, params, dom, rect, size) => { + if (!mouseoverRef.current) { + return [point[0] - size.contentSize[0] / 2, 0] + } + return [point[0] + 15, point[1] + 15] + }, + formatter: params => { const firstPoint = Array.isArray(params) ? params[0] : params const secondPoint = Array.isArray(params) ? params[1] : null + const firstPointValue = firstPoint.value as number[] + const secondPointValue = secondPoint ? (secondPoint.value as number[]) : null + + if (!mouseoverRef.current) { + if (firstPoint.seriesId === MAIN_SERIES_ID) { + if (firstPoint.dataIndex === chartValues.length - 1) { + return ` +
+
+ Increase your lock to 1 year to maximize your veBAL to 30.346 (mocked text) +
+
+ ` + } + } + } + return ` -
-
- ${format(new Date(firstPoint.value[0]), 'yyyy/MM/dd')} -
-
- ${secondPoint ? `${secondPoint.value[1]} veBAL` : ''} - ${firstPoint.value[1]} veBAL -
+
+
+ ${format(new Date(firstPointValue[0]), 'yyyy/MM/dd')} +
+
+ ${ + secondPointValue + ? ` + + + + ${fNum('token', secondPointValue[1])} veBAL + + ` + : '' + } + + + + ${fNum('token', firstPointValue[1])} veBAL +
- ` +
+ ` }, }, xAxis: { @@ -202,8 +320,9 @@ export function useVebalLocksChart() { }, series: [ { + id: MAIN_SERIES_ID, name: '', - type: 'line', + type: 'line' as const, data: chartValues, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ @@ -211,15 +330,29 @@ export function useVebalLocksChart() { { offset: 1, color: 'rgba(68, 9, 236, 0)' }, ]), }, + lineStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [ + { offset: 0, color: '#B3AEF5' }, + { offset: 0.33, color: '#D7CBE7' }, + { offset: 0.66, color: '#E5C8C8' }, + { offset: 1, color: '#EAA879' }, + ]), + width: 5, + join: 'round' as const, + cap: 'round' as const, + }, + showSymbol: false, }, futureLockChartData, ], } - }, [chartValues, futureLockChartData, theme]) + }, [chartValues, futureLockChartData, theme, nextTheme]) return { lockedUntil, chartData: options, options, + onChartReady, + onEvents, } } diff --git a/lib/shared/components/navs/useNav.tsx b/lib/shared/components/navs/useNav.tsx index a4e87aaa5..8ede56793 100644 --- a/lib/shared/components/navs/useNav.tsx +++ b/lib/shared/components/navs/useNav.tsx @@ -31,8 +31,8 @@ export function useNav() { // To-do: Remove this when veBAL is live if (isDev || isStaging) { appLinks.push({ - href: '/vebal', - label: 'veBAL (wip)', + href: '/vebal/manage', + label: 'veBAL (manage)', }) }