diff --git a/src/components/Modals/PromotedPoolPopover/PromotedPoolPopover.tsx b/src/components/Modals/PromotedPoolPopover/PromotedPoolPopover.tsx index 5b953e86..749bb389 100644 --- a/src/components/Modals/PromotedPoolPopover/PromotedPoolPopover.tsx +++ b/src/components/Modals/PromotedPoolPopover/PromotedPoolPopover.tsx @@ -1,15 +1,21 @@ import { BN } from '@coral-xyz/anchor' import useStyles from './style' import { Popover, Typography } from '@mui/material' -import { formatNumberWithCommas, printBN } from '@utils/utils' +import { formatNumberWithCommas, printBN, removeAdditionalDecimals } from '@utils/utils' +import { LEADERBOARD_DECIMAL } from '@pages/LeaderboardPage/config' +import { useRef, useCallback, useEffect } from 'react' export interface IPromotedPoolPopover { open: boolean anchorEl: HTMLElement | null onClose: () => void - apr: number - apy: number + apr?: BN + apy?: number + estPoints?: BN points: BN + headerText?: string | React.ReactNode + pointsLabel?: string | React.ReactNode + showEstPointsFirst?: boolean } export const PromotedPoolPopover = ({ @@ -18,16 +24,72 @@ export const PromotedPoolPopover = ({ anchorEl, apr, apy, - points + estPoints, + points, + headerText = 'The pool distributes points:', + pointsLabel = 'Points per 24H', + showEstPointsFirst = false }: IPromotedPoolPopover) => { const { classes } = useStyles() + const timeoutRef = useRef() + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + }, []) + + const handleMouseLeave = useCallback(() => { + timeoutRef.current = setTimeout(() => { + onClose() + }, 100) + }, [onClose]) + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) if (!anchorEl) return null + const TotalPointsSection = ( +
+ + {typeof pointsLabel !== 'string' ? pointsLabel : null} + + + {formatNumberWithCommas(printBN(points, 0))} + +
+ ) + + const EstPointsSection = estPoints ? ( +
+ Points earned by this position per 24H: + + {points.isZero() + ? '<0.01' + : removeAdditionalDecimals( + formatNumberWithCommas(printBN(estPoints, LEADERBOARD_DECIMAL)), + 2 + )} + +
+ ) : null + return ( e.stopPropagation()} open={open} anchorEl={anchorEl} + className='promoted-pool-popover' classes={{ paper: classes.paper, root: classes.popover @@ -40,7 +102,8 @@ export const PromotedPoolPopover = ({ disableRestoreFocus slotProps={{ paper: { - onMouseLeave: onClose + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave } }} transformOrigin={{ @@ -50,25 +113,43 @@ export const PromotedPoolPopover = ({ marginThreshold={16}>
- This pool distributes points: -
- Points per 24H - - {formatNumberWithCommas(printBN(points, 0))} - -
-
- - APY - APR - {' '} - - {`${apy > 1000 ? '>1000%' : apy === 0 ? '' : apy.toFixed(2) + '%'}`} - - {`${apr > 1000 ? '>1000%' : apr === 0 ? '-' : apr.toFixed(2) + '%'}`} - - -
+ {/* Content remains the same */} + + {typeof headerText !== 'string' ? headerText : null} + + + {showEstPointsFirst ? ( + <> + {EstPointsSection} + {TotalPointsSection} + + ) : ( + <> + {TotalPointsSection} + {EstPointsSection} + + )} + + {apr && apy ? ( + <> +
+ + APY + APR + {' '} + + {`${apy > 1000 ? '>1000%' : apy === 0 ? '' : apy.toFixed(2) + '%'}`} + + {`${apr > 1000 ? '>1000%' : apr === 0 ? '-' : apr.toFixed(2) + '%'}`} + + +
+ + ) : null}
diff --git a/src/components/Modals/PromotedPoolPopover/style.ts b/src/components/Modals/PromotedPoolPopover/style.ts index e9860f34..bf8a2cbd 100644 --- a/src/components/Modals/PromotedPoolPopover/style.ts +++ b/src/components/Modals/PromotedPoolPopover/style.ts @@ -13,7 +13,7 @@ const useStyles = makeStyles()(() => { }, root: { background: colors.invariant.component, - width: 217, + width: 'fit-content', height: 'fit-content', borderRadius: 14, paddingTop: 16, diff --git a/src/components/PositionsList/PositionItem/PositionItem.stories.tsx b/src/components/PositionsList/PositionItem/PositionItem.stories.tsx index 6dac29e7..2d16749b 100644 --- a/src/components/PositionsList/PositionItem/PositionItem.stories.tsx +++ b/src/components/PositionsList/PositionItem/PositionItem.stories.tsx @@ -1,12 +1,14 @@ import { NetworkType } from '@store/consts/static' -import { PositionItem } from './PositionItem' import type { Meta, StoryObj } from '@storybook/react' +import { Keypair } from '@solana/web3.js' +import { BN } from '@coral-xyz/anchor' +import { PositionItemDesktop } from './variants/PositionItemDesktop' const meta = { title: 'Components/PositionItem', - component: PositionItem -} satisfies Meta + component: PositionItemDesktop +} satisfies Meta export default meta type Story = StoryObj @@ -23,7 +25,53 @@ export const Primary: Story = { max: 149.6, fee: 0.05, valueX: 10000.45, + position: { + bump: 0, + feeGrowthInsideX: new BN(0), + feeGrowthInsideY: new BN(0), + id: 0, + lastSlot: new BN(0), + lowerTickIndex: new BN(0), + owner: Keypair.generate().publicKey, + pool: Keypair.generate().publicKey, + secondsPerLiquidityInside: new BN(0), + tokensOwedX: new BN(0), + tokensOwedY: new BN(0), + upperTickIndex: new BN(0), + liquidity: new BN(0) + }, valueY: 2137.4, + liquidity: new BN(0), + poolAddress: Keypair.generate().publicKey, + + poolData: { + address: Keypair.generate().publicKey, + bump: 0, + currentTickIndex: 0, + fee: new BN(0), + feeGrowthGlobalX: new BN(0), + feeProtocolTokenX: new BN(0), + feeProtocolTokenY: new BN(0), + feeReceiver: Keypair.generate().publicKey, + lastTimestamp: new BN(0), + oracleAddress: Keypair.generate().publicKey, + oracleInitialized: true, + liquidity: new BN(0), + poolIndex: 0, + positionIterator: new BN(0), + protocolFee: new BN(0), + secondsPerLiquidityGlobal: new BN(0), + sqrtPrice: new BN(0), + startTimestamp: new BN(0), + tickmap: Keypair.generate().publicKey, + tickSpacing: 0, + tokenX: Keypair.generate().publicKey, + tokenY: Keypair.generate().publicKey, + tokenXReserve: Keypair.generate().publicKey, + tokenYReserve: Keypair.generate().publicKey, + feeGrowthGlobalY: new BN(0) + }, + id: '0', address: '', tokenXLiq: 5000, diff --git a/src/components/PositionsList/PositionItem/PositionItem.tsx b/src/components/PositionsList/PositionItem/PositionItem.tsx deleted file mode 100644 index 87d6375c..00000000 --- a/src/components/PositionsList/PositionItem/PositionItem.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Grid, Hidden, Tooltip, Typography, useMediaQuery } from '@mui/material' -import SwapList from '@static/svg/swap-list.svg' -import { theme } from '@static/theme' -import { formatNumber } from '@utils/utils' -import classNames from 'classnames' -import { useMemo, useState } from 'react' -import { useStyles } from './style' -import { TooltipHover } from '@components/TooltipHover/TooltipHover' -import { initialXtoY, tickerToAddress } from '@utils/utils' -import { NetworkType } from '@store/consts/static' -import lockIcon from '@static/svg/lock.svg' -import unlockIcon from '@static/svg/unlock.svg' - -export interface IPositionItem { - tokenXName: string - tokenYName: string - tokenXIcon: string - tokenYIcon: string - tokenXLiq: number - tokenYLiq: number - fee: number - min: number - max: number - valueX: number - valueY: number - id: string - address: string - isActive?: boolean - currentPrice: number - network: NetworkType - isFullRange: boolean - isLocked: boolean -} - -export const PositionItem: React.FC = ({ - tokenXName, - tokenYName, - tokenXIcon, - tokenYIcon, - fee, - min, - max, - valueX, - valueY, - isActive = false, - currentPrice, - tokenXLiq, - tokenYLiq, - network, - isFullRange, - isLocked -}) => { - const { classes } = useStyles() - - const isXs = useMediaQuery(theme.breakpoints.down('xs')) - const isDesktop = useMediaQuery(theme.breakpoints.up('lg')) - - const [xToY, setXToY] = useState( - initialXtoY(tickerToAddress(network, tokenXName), tickerToAddress(network, tokenYName)) - ) - - const getPercentageRatio = () => { - const firstTokenPercentage = - ((tokenXLiq * currentPrice) / (tokenYLiq + tokenXLiq * currentPrice)) * 100 - - const tokenXPercentageFloat = xToY ? firstTokenPercentage : 100 - firstTokenPercentage - const tokenXPercentage = - tokenXPercentageFloat > 50 - ? Math.floor(tokenXPercentageFloat) - : Math.ceil(tokenXPercentageFloat) - - const tokenYPercentage = 100 - tokenXPercentage - - return { tokenXPercentage, tokenYPercentage } - } - - const { tokenXPercentage, tokenYPercentage } = getPercentageRatio() - - const feeFragment = useMemo( - () => ( - e.stopPropagation()} - title={ - isActive ? ( - <> - The position is active and currently earning a fee as long as the - current price is within the position's price range. - - ) : ( - <> - The position is inactive and not earning a fee as long as the current - price is outside the position's price range. - - ) - } - placement='top' - classes={{ - tooltip: classes.tooltip - }}> - - - {fee}% fee - - - - ), - [fee, classes, isActive] - ) - - const valueFragment = useMemo( - () => ( - - Value - - - {formatNumber(xToY ? valueX : valueY)} {xToY ? tokenXName : tokenYName} - - - - ), - [valueX, valueY, tokenXName, classes, isXs, isDesktop, tokenYName, xToY] - ) - - return ( - - - - - {xToY - - Arrow { - e.stopPropagation() - setXToY(!xToY) - }} - /> - - {xToY - - - - {xToY ? tokenXName : tokenYName} - {xToY ? tokenYName : tokenXName} - - - - {feeFragment} - - - - {feeFragment} - - - {tokenXPercentage === 100 && ( - - {tokenXPercentage} - {'%'} {xToY ? tokenXName : tokenYName} - - )} - {tokenYPercentage === 100 && ( - - {tokenYPercentage} - {'%'} {xToY ? tokenYName : tokenXName} - - )} - - {tokenYPercentage !== 100 && tokenXPercentage !== 100 && ( - - {tokenXPercentage} - {'%'} {xToY ? tokenXName : tokenYName} {' - '} {tokenYPercentage} - {'%'} {xToY ? tokenYName : tokenXName} - - )} - - - {valueFragment} - - - <> - - MIN - MAX - - - {isFullRange ? ( - FULL RANGE - ) : ( - - {formatNumber(xToY ? min : 1 / max)} - {formatNumber(xToY ? max : 1 / min)}{' '} - {xToY ? tokenYName : tokenXName} per {xToY ? tokenXName : tokenYName} - - )} - - - - - {valueFragment} - - {isLocked && ( - - {isLocked ? ( - - Lock - - ) : ( - - Lock - - )} - - )} - - - ) -} diff --git a/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/InactivePoolsPopover.tsx b/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/InactivePoolsPopover.tsx new file mode 100644 index 00000000..c8abfdf3 --- /dev/null +++ b/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/InactivePoolsPopover.tsx @@ -0,0 +1,58 @@ +import useStyles from './style' +import { Popover } from '@mui/material' +import PositionStatusTooltip from '../PositionStatusTooltip' + +export interface IPromotedPoolPopover { + open: boolean + anchorEl: HTMLElement | null + onClose: () => void + + isActive: boolean + isPromoted: boolean +} + +export const InactivePoolsPopover = ({ + open, + onClose, + anchorEl, + isActive, + isPromoted +}: IPromotedPoolPopover) => { + const { classes } = useStyles() + + if (!anchorEl) return null + + return ( + e.stopPropagation()} + open={open} + anchorEl={anchorEl} + className='promoted-pool-inactive-popover' + classes={{ + paper: classes.paper, + root: classes.popover + }} + onClose={onClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center' + }} + disableRestoreFocus + slotProps={{ + paper: { + onMouseLeave: onClose + } + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center' + }} + marginThreshold={16}> +
+
+ +
+
+
+ ) +} diff --git a/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/style.ts b/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/style.ts new file mode 100644 index 00000000..f9272dd5 --- /dev/null +++ b/src/components/PositionsList/PositionItem/components/InactivePoolsPopover/style.ts @@ -0,0 +1,63 @@ +import { colors, theme, typography } from '@static/theme' +import { makeStyles } from 'tss-react/mui' + +const useStyles = makeStyles()(() => { + return { + popover: { + marginTop: '8px', + pointerEvents: 'none', + [theme.breakpoints.down('sm')]: { + width: '100%', + padding: '16px' + } + }, + root: { + width: '300px', + height: 'fit-content', + + color: colors.invariant.textGrey, + ...typography.caption4, + lineHeight: '24px', + background: colors.black.full, + borderRadius: 12, + padding: 10, + fontSize: 14 + }, + paper: { + background: 'transparent', + boxShadow: 'none', + borderRadius: '14px', + border: '1px solid transparent', + // backgroundImage: 'linear-gradient(#2A365C, #2A365C), linear-gradient(0deg, #2EE09A, #EF84F5)', + backgroundOrigin: 'border-box', + backgroundClip: 'padding-box, border-box' + }, + container: { + display: 'flex', + flexDirection: 'column', + width: '100%', + justifyContent: 'flex-start', + alignItems: 'flex-start', + gap: 16 + }, + insideBox: { + display: 'flex', + flexDirection: 'column', + width: '100%', + justifyContent: 'flex-start', + alignItems: 'flex-start' + }, + greyText: { + ...typography.body2, + color: colors.invariant.textGrey + }, + whiteText: { ...typography.heading4, color: colors.invariant.text }, + apr: { + ...typography.tiny2, + color: colors.invariant.textGrey, + marginLeft: 8 + } + } +}) + +export default useStyles diff --git a/src/components/PositionsList/PositionItem/components/PositionStatusTooltip.tsx b/src/components/PositionsList/PositionItem/components/PositionStatusTooltip.tsx new file mode 100644 index 00000000..b7368696 --- /dev/null +++ b/src/components/PositionsList/PositionItem/components/PositionStatusTooltip.tsx @@ -0,0 +1,50 @@ +const PositionStatusTooltip: React.FC<{ isActive: boolean; isPromoted: boolean }> = ({ + isActive, + isPromoted +}) => { + if (!isActive && !isPromoted) { + return ( +

+ This position isn't earning points for two reasons: +
+
+ 1. Your position's liquidity remains inactive and won't earn points as long as + the current price is outside its specified price range. +
+
+ 2. This position was opened on a pool that doesn't generate points. If you were + expecting to earn points, make sure you selected a pool with a fee tier that generates + points. +

+ ) + } + + if (!isActive) { + return ( +

+ This position isn't earning points, even though the pool is generating them. Your + position's liquidity remains inactive and won't earn points as long as the + current price is outside its specified price range.
+
To start earning points again, close the current position and open a new one with a + price range that includes the pool's current price. +

+ ) + } + + if (!isPromoted) { + return ( +

+ This position isn't earning points because it was opened on a pool that + doesn't generate them. +
+
If you were expecting to earn points, make sure you selected a pool with a fee tier + that generates points, as not all pools do. Only pools with the specified fee tier + can generate points. +

+ ) + } + + return null +} + +export default PositionStatusTooltip diff --git a/src/components/PositionsList/PositionItem/components/PromotedIcons.tsx b/src/components/PositionsList/PositionItem/components/PromotedIcons.tsx new file mode 100644 index 00000000..fbe3a673 --- /dev/null +++ b/src/components/PositionsList/PositionItem/components/PromotedIcons.tsx @@ -0,0 +1,93 @@ +import PromotedPoolPopover from '@components/Modals/PromotedPoolPopover/PromotedPoolPopover' +import { BN } from '@coral-xyz/anchor' +import { Tooltip } from '@mui/material' +import icons from '@static/icons' + +interface IPromotedIconProps { + isPromoted: boolean + isActive: boolean + pointsPerSecond: string + estimated24hPoints: BN + onOpenChange: (isOpen: boolean) => void + isOpen: boolean + iconRef: React.RefObject + isDesktop?: boolean +} + +export const PromotedIcon: React.FC = ({ + isPromoted, + isActive, + pointsPerSecond, + estimated24hPoints, + onOpenChange, + isOpen, + iconRef, + isDesktop +}) => { + if (!isPromoted || !isActive) { + return ( + e.stopPropagation()} + title={ + !isActive ? ( +

+ This position isn't earning points, even though the pool is generating them. + Your position's liquidity remains inactive and won't earn points as long + as the current price is outside its specified price range. +

+ ) : !isPromoted ? ( +

+ This position isn't earning points because it was opened on a pool that + doesn't generate them. +

+ ) : null + } + placement='top'> + {'Airdrop'} +
+ ) + } + + return ( + <> +
e.stopPropagation()} + onPointerLeave={() => onOpenChange(false)} + onPointerEnter={() => onOpenChange(true)}> + {'Airdrop'} +
+ onOpenChange(false)} + headerText={ + <> + This position is currently earning points + + } + pointsLabel={'Total points distributed across the pool per 24H:'} + estPoints={estimated24hPoints} + points={new BN(pointsPerSecond, 'hex').muln(24).muln(60).muln(60)} + /> + + ) +} diff --git a/src/components/PositionsList/PositionItem/hooks/usePromotedPool.ts b/src/components/PositionsList/PositionItem/hooks/usePromotedPool.ts new file mode 100644 index 00000000..91ce124a --- /dev/null +++ b/src/components/PositionsList/PositionItem/hooks/usePromotedPool.ts @@ -0,0 +1,42 @@ +import { BN } from '@coral-xyz/anchor' +import { estimatePointsForUserPositions } from '@invariant-labs/points-sdk' +import { Position, PoolStructure } from '@invariant-labs/sdk-eclipse/lib/market' +import { LEADERBOARD_DECIMAL } from '@pages/LeaderboardPage/config' +import { PublicKey } from '@solana/web3.js' +import { leaderboardSelectors } from '@store/selectors/leaderboard' +import { PoolWithAddressAndIndex } from '@store/selectors/positions' +import { useMemo } from 'react' +import { useSelector } from 'react-redux' + +export const usePromotedPool = ( + poolAddress: PublicKey, + position: Position, + poolData: PoolWithAddressAndIndex +) => { + const { promotedPools } = useSelector(leaderboardSelectors.config) + + const { isPromoted, pointsPerSecond } = useMemo(() => { + if (!poolAddress) return { isPromoted: false, pointsPerSecond: '00' } + const promotedPool = promotedPools.find(pool => pool.address === poolAddress.toString()) + if (!promotedPool) return { isPromoted: false, pointsPerSecond: '00' } + return { isPromoted: true, pointsPerSecond: promotedPool.pointsPerSecond } + }, [promotedPools, poolAddress]) + + const estimated24hPoints = useMemo(() => { + if (!promotedPools.some(pool => pool.address === poolAddress.toString())) { + return new BN(0) + } + + const poolPointsPerSecond = promotedPools.find( + pool => pool.address === poolAddress.toString() + )!.pointsPerSecond + + return estimatePointsForUserPositions( + [position], + poolData as PoolStructure, + new BN(poolPointsPerSecond, 'hex').mul(new BN(10).pow(new BN(LEADERBOARD_DECIMAL))) + ) + }, [poolAddress, position, poolData, promotedPools]) + + return { isPromoted, pointsPerSecond, estimated24hPoints } +} diff --git a/src/components/PositionsList/PositionItem/utils/calculations.ts b/src/components/PositionsList/PositionItem/utils/calculations.ts new file mode 100644 index 00000000..1561c8d8 --- /dev/null +++ b/src/components/PositionsList/PositionItem/utils/calculations.ts @@ -0,0 +1,19 @@ +export const calculatePercentageRatio = ( + tokenXLiq: number, + tokenYLiq: number, + currentPrice: number, + xToY: boolean +) => { + const firstTokenPercentage = + ((tokenXLiq * currentPrice) / (tokenYLiq + tokenXLiq * currentPrice)) * 100 + const tokenXPercentageFloat = xToY ? firstTokenPercentage : 100 - firstTokenPercentage + const tokenXPercentage = + tokenXPercentageFloat > 50 + ? Math.floor(tokenXPercentageFloat) + : Math.ceil(tokenXPercentageFloat) + + return { + tokenXPercentage, + tokenYPercentage: 100 - tokenXPercentage + } +} diff --git a/src/components/PositionsList/PositionItem/variants/PositionItemDesktop.tsx b/src/components/PositionsList/PositionItem/variants/PositionItemDesktop.tsx new file mode 100644 index 00000000..6c4fe2f5 --- /dev/null +++ b/src/components/PositionsList/PositionItem/variants/PositionItemDesktop.tsx @@ -0,0 +1,400 @@ +import { Box, Grid, Hidden, Tooltip, Typography, useMediaQuery } from '@mui/material' +import SwapList from '@static/svg/swap-list.svg' +import { theme } from '@static/theme' +import { formatNumber } from '@utils/utils' +import classNames from 'classnames' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useDesktopStyles } from './style/desktop' +import { TooltipHover } from '@components/TooltipHover/TooltipHover' +import { initialXtoY, tickerToAddress } from '@utils/utils' +import lockIcon from '@static/svg/lock.svg' +import unlockIcon from '@static/svg/unlock.svg' +import icons from '@static/icons' +import PromotedPoolPopover from '@components/Modals/PromotedPoolPopover/PromotedPoolPopover' +import { BN } from '@coral-xyz/anchor' +import { usePromotedPool } from '../hooks/usePromotedPool' +import { calculatePercentageRatio } from '../utils/calculations' +import { IPositionItem } from '@components/PositionsList/types' +import { useSharedStyles } from './style/shared' +import PositionStatusTooltip from '../components/PositionStatusTooltip' + +export const PositionItemDesktop: React.FC = ({ + tokenXName, + tokenYName, + tokenXIcon, + poolAddress, + tokenYIcon, + fee, + min, + max, + valueX, + valueY, + position, + // liquidity, + poolData, + isActive = false, + currentPrice, + tokenXLiq, + tokenYLiq, + network, + isFullRange, + isLocked +}) => { + const { classes } = useDesktopStyles() + const { classes: sharedClasses } = useSharedStyles() + const airdropIconRef = useRef(null) + const [isPromotedPoolPopoverOpen, setIsPromotedPoolPopoverOpen] = useState(false) + const timeoutRef = useRef() + + const isXs = useMediaQuery(theme.breakpoints.down('xs')) + const isDesktop = useMediaQuery(theme.breakpoints.up('lg')) + + const [xToY, setXToY] = useState( + initialXtoY(tickerToAddress(network, tokenXName), tickerToAddress(network, tokenYName)) + ) + + const { tokenXPercentage, tokenYPercentage } = calculatePercentageRatio( + tokenXLiq, + tokenYLiq, + currentPrice, + xToY + ) + + const { isPromoted, pointsPerSecond, estimated24hPoints } = usePromotedPool( + poolAddress, + position, + poolData + ) + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + setIsPromotedPoolPopoverOpen(true) + }, []) + + const handleMouseLeave = useCallback(() => { + timeoutRef.current = setTimeout(() => { + setIsPromotedPoolPopoverOpen(false) + }, 100) + }, []) + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + const [isTooltipOpen, setIsTooltipOpen] = useState(false) + const tooltipTimeoutRef = useRef() + + const handleTooltipEnter = useCallback(() => { + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current) + } + setIsTooltipOpen(true) + }, []) + + const handleTooltipLeave = useCallback(() => { + tooltipTimeoutRef.current = setTimeout(() => { + setIsTooltipOpen(false) + }, 100) + }, []) + + const feeFragment = useMemo( + () => ( + e.stopPropagation()} + title={ + isActive ? ( + <> + The position is active and currently earning a fee as long as the + current price is within the position's price range. + + ) : ( + <> + The position is inactive and not earning a fee as long as the current + price is outside the position's price range. + + ) + } + placement='top' + classes={{ + tooltip: sharedClasses.tooltip + }}> + + + {fee}% fee + + + + ), + [fee, classes, isActive] + ) + + const valueFragment = useMemo( + () => ( + + + Value + + + + {formatNumber(xToY ? valueX : valueY)} {xToY ? tokenXName : tokenYName} + + + + ), + [valueX, valueY, tokenXName, classes, isXs, isDesktop, tokenYName, xToY] + ) + + const PromotedIcon = () => + isPromoted && isActive ? ( + <> +
e.stopPropagation()} + className={classes.actionButton} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave}> + {'Airdrop'} +
+ { + setIsPromotedPoolPopoverOpen(false) + }} + headerText={ + <> + This position is currently earning points + + } + pointsLabel={'Total points distributed across the pool per 24H:'} + estPoints={estimated24hPoints} + points={new BN(pointsPerSecond, 'hex').muln(24).muln(60).muln(60)} + /> + + ) : ( + <> + setIsTooltipOpen(true)} + onClose={() => setIsTooltipOpen(false)} + enterTouchDelay={0} + leaveTouchDelay={0} + onClick={e => e.stopPropagation()} + title={ +
+ +
+ } + placement='top' + classes={{ + tooltip: sharedClasses.tooltip + }}> +
+ {'Airdrop'} +
+
+ + ) + + return ( + + + + + {xToY + + Arrow { + e.stopPropagation() + setXToY(!xToY) + }} + /> + + {xToY + + + + {xToY ? tokenXName : tokenYName} - {xToY ? tokenYName : tokenXName} + + + + + + + + + {feeFragment} + + + {tokenXPercentage === 100 && ( + + {tokenXPercentage} + {'%'} {xToY ? tokenXName : tokenYName} + + )} + {tokenYPercentage === 100 && ( + + {tokenYPercentage} + {'%'} {xToY ? tokenYName : tokenXName} + + )} + + {tokenYPercentage !== 100 && tokenXPercentage !== 100 && ( + + {tokenXPercentage} + {'%'} {xToY ? tokenXName : tokenYName} {' - '} {tokenYPercentage} + {'%'} {xToY ? tokenYName : tokenXName} + + )} + + + + + <> + + MIN - MAX + + + {isFullRange ? ( + FULL RANGE + ) : ( + + {formatNumber(xToY ? min : 1 / max)} - {formatNumber(xToY ? max : 1 / min)}{' '} + {xToY ? tokenYName : tokenXName} per {xToY ? tokenXName : tokenYName} + + )} + + + + + {valueFragment} + + {isLocked && ( + + {isLocked ? ( + + Lock + + ) : ( + + Lock + + )} + + )} + + + ) +} diff --git a/src/components/PositionsList/PositionItem/variants/PositionItemMobile.tsx b/src/components/PositionsList/PositionItem/variants/PositionItemMobile.tsx new file mode 100644 index 00000000..d66120d5 --- /dev/null +++ b/src/components/PositionsList/PositionItem/variants/PositionItemMobile.tsx @@ -0,0 +1,439 @@ +import { Box, Grid, Tooltip, Typography, useMediaQuery } from '@mui/material' +import SwapList from '@static/svg/swap-list.svg' +import { theme } from '@static/theme' +import { formatNumber } from '@utils/utils' +import classNames from 'classnames' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useMobileStyles } from './style/mobile' +import { TooltipHover } from '@components/TooltipHover/TooltipHover' +import { initialXtoY, tickerToAddress } from '@utils/utils' +import lockIcon from '@static/svg/lock.svg' +import unlockIcon from '@static/svg/unlock.svg' +import icons from '@static/icons' +import PromotedPoolPopover from '@components/Modals/PromotedPoolPopover/PromotedPoolPopover' +import { BN } from '@coral-xyz/anchor' +import { usePromotedPool } from '../hooks/usePromotedPool' +import { calculatePercentageRatio } from '../utils/calculations' +import { IPositionItem } from '@components/PositionsList/types' +import { useSharedStyles } from './style/shared' +import { InactivePoolsPopover } from '../components/InactivePoolsPopover/InactivePoolsPopover' + +interface IPositionItemMobile extends IPositionItem { + setAllowPropagation: React.Dispatch> +} + +export const PositionItemMobile: React.FC = ({ + tokenXName, + tokenYName, + tokenXIcon, + poolAddress, + tokenYIcon, + fee, + min, + max, + valueX, + valueY, + position, + setAllowPropagation, + // liquidity, + poolData, + isActive = false, + currentPrice, + tokenXLiq, + tokenYLiq, + network, + isFullRange, + isLocked +}) => { + const { classes } = useMobileStyles() + const { classes: sharedClasses } = useSharedStyles() + const airdropIconRef = useRef(null) + const [isPromotedPoolPopoverOpen, setIsPromotedPoolPopoverOpen] = useState(false) + const [isPromotedPoolInactive, setIsPromotedPoolInactive] = useState(false) + + const isXs = useMediaQuery(theme.breakpoints.down('xs')) + const isDesktop = useMediaQuery(theme.breakpoints.up('lg')) + + const [xToY, setXToY] = useState( + initialXtoY(tickerToAddress(network, tokenXName), tickerToAddress(network, tokenYName)) + ) + + const { tokenXPercentage, tokenYPercentage } = calculatePercentageRatio( + tokenXLiq, + tokenYLiq, + currentPrice, + xToY + ) + + const { isPromoted, pointsPerSecond, estimated24hPoints } = usePromotedPool( + poolAddress, + position, + poolData + ) + + // const isAnyPopoverActive = isPromotedPoolPopoverOpen || isPromotedPoolInactive + + // const handleGridClick = (e: React.MouseEvent) => { + // // if (isAnyPopoverActive) { + // // e.preventDefault() + + // return + // // } + // // e.stopPropagation() + // } + + const feeFragment = useMemo( + () => ( + e.stopPropagation()} + title={ + isActive ? ( + <> + The position is active and currently earning a fee as long as the + current price is within the position's price range. + + ) : ( + <> + The position is inactive and not earning a fee as long as the current + price is outside the position's price range. + + ) + } + placement='top' + classes={{ + tooltip: sharedClasses.tooltip + }}> + + + {fee}% fee + + + + ), + [fee, classes, isActive] + ) + + const valueFragment = useMemo( + () => ( + + + Value + + + + {formatNumber(xToY ? valueX : valueY)} {xToY ? tokenXName : tokenYName} + + + + ), + [valueX, valueY, tokenXName, classes, isXs, isDesktop, tokenYName, xToY] + ) + + const handleInteraction = (event: React.MouseEvent | React.TouchEvent) => { + event.stopPropagation() + + if (event.type === 'touchstart') { + setIsPromotedPoolPopoverOpen(!isPromotedPoolPopoverOpen) + setAllowPropagation(false) + } + } + + const PromotedIcon = () => { + const PROPAGATION_ALLOW_TIME = 500 + useEffect(() => { + const handleClickOutside = (event: TouchEvent | MouseEvent) => { + if ( + airdropIconRef.current && + !(airdropIconRef.current as HTMLElement).contains(event.target as Node) && + !document.querySelector('.promoted-pool-popover')?.contains(event.target as Node) && + !document.querySelector('.promoted-pool-inactive-popover')?.contains(event.target as Node) + ) { + setIsPromotedPoolPopoverOpen(false) + setIsPromotedPoolInactive(false) + setTimeout(() => { + setAllowPropagation(true) + }, PROPAGATION_ALLOW_TIME) + } + } + + if (isPromotedPoolPopoverOpen || isPromotedPoolInactive) { + document.addEventListener('click', handleClickOutside) + document.addEventListener('touchstart', handleClickOutside) + } + + return () => { + document.removeEventListener('click', handleClickOutside) + document.removeEventListener('touchstart', handleClickOutside) + } + }, [isPromotedPoolPopoverOpen, isPromotedPoolInactive]) + + return isPromoted && isActive ? ( + <> +
{ + if (window.matchMedia('(hover: hover)').matches) { + setIsPromotedPoolPopoverOpen(true) + } + }} + onPointerLeave={() => { + if (window.matchMedia('(hover: hover)').matches) { + setIsPromotedPoolPopoverOpen(false) + } + }} + onTouchStart={handleInteraction}> + {'Airdrop'} +
+ { + setIsPromotedPoolPopoverOpen(false) + }} + headerText={ + <> + This position is currently earning points + + } + pointsLabel={'Total points distributed across the pool per 24H:'} + estPoints={estimated24hPoints} + points={new BN(pointsPerSecond, 'hex').muln(24).muln(60).muln(60)} + /> + + ) : ( + <> + { + setIsPromotedPoolInactive(false) + }} + isActive={isActive} + isPromoted={isPromoted} + /> + +
{ + event.stopPropagation() + + if (event.type === 'touchstart') { + setIsPromotedPoolInactive(!isPromotedPoolInactive) + } + }} + onPointerEnter={() => { + if (window.matchMedia('(hover: hover)').matches) { + setIsPromotedPoolInactive(true) + } + }} + onPointerLeave={() => { + if (window.matchMedia('(hover: hover)').matches) { + setIsPromotedPoolInactive(false) + } + }} + onTouchStart={event => { + event.stopPropagation() + + if (event.type === 'touchstart') { + setIsPromotedPoolInactive(!isPromotedPoolInactive) + setAllowPropagation(false) + } + }}> + {'Airdrop'} +
+ + ) + } + + return ( + + + + + + {xToY + + Arrow { + e.stopPropagation() + setXToY(!xToY) + }} + /> + + {xToY + + + + {xToY ? tokenXName : tokenYName} - {xToY ? tokenYName : tokenXName} + + + + + + + + + + + + + + {tokenXPercentage === 100 && ( + + {tokenXPercentage} + {'%'} {xToY ? tokenXName : tokenYName} + + )} + {tokenYPercentage === 100 && ( + + {tokenYPercentage} + {'%'} {xToY ? tokenYName : tokenXName} + + )} + + {tokenYPercentage !== 100 && tokenXPercentage !== 100 && ( + + {tokenXPercentage} + {'%'} {xToY ? tokenXName : tokenYName} {' - '} {tokenYPercentage} + {'%'} {xToY ? tokenYName : tokenXName} + + )} + + + + + + + {feeFragment} + + {/* {feeFragment} */} + + {valueFragment} + + + + <> + + MIN - MAX + + + {isFullRange ? ( + FULL RANGE + ) : ( + + {formatNumber(xToY ? min : 1 / max)} - {formatNumber(xToY ? max : 1 / min)}{' '} + {xToY ? tokenYName : tokenXName} per {xToY ? tokenXName : tokenYName} + + )} + + + + + {isLocked && ( + + {isLocked ? ( + + Lock + + ) : ( + + Lock + + )} + + )} + + + ) +} diff --git a/src/components/PositionsList/PositionItem/variants/style/desktop.ts b/src/components/PositionsList/PositionItem/variants/style/desktop.ts new file mode 100644 index 00000000..53df499b --- /dev/null +++ b/src/components/PositionsList/PositionItem/variants/style/desktop.ts @@ -0,0 +1,293 @@ +// import { Theme } from '@mui/material' +// import { colors, typography } from '@static/theme' +// import { makeStyles } from 'tss-react/mui' +// //desktop style +// export const useStyles = makeStyles()((theme: Theme) => ({ +// root: { +// background: colors.invariant.component, +// borderRadius: 24, +// padding: 20, +// flexWrap: 'nowrap', +// '&:not(:last-child)': { +// marginBottom: 20 +// }, + +// '&:hover': { +// background: `${colors.invariant.component}B0` +// }, + +// [theme.breakpoints.down('lg')]: { +// padding: 16, +// flexWrap: 'wrap' +// } +// }, +// icons: { +// marginRight: 12, +// width: 'fit-content', + +// [theme.breakpoints.down('lg')]: { +// marginRight: 12 +// } +// }, +// tokenIcon: { +// width: 40, +// borderRadius: '100%', + +// [theme.breakpoints.down('sm')]: { +// width: 28 +// } +// }, +// actionButton: { +// background: 'none', +// padding: 0, +// margin: 0, +// border: 'none', +// display: 'inline-flex', +// position: 'relative', +// color: colors.invariant.black, +// textTransform: 'none', + +// transition: 'filter 0.2s linear', + +// '&:hover': { +// filter: 'brightness(1.2)', +// cursor: 'pointer', +// '@media (hover: none)': { +// filter: 'none' +// } +// } +// }, +// arrows: { +// width: 36, +// marginLeft: 4, +// marginRight: 4, + +// [theme.breakpoints.down('lg')]: { +// width: 30 +// }, + +// [theme.breakpoints.down('sm')]: { +// width: 24 +// }, + +// '&:hover': { +// filter: 'brightness(2)' +// } +// }, +// names: { +// textOverflow: 'ellipsis', +// overflow: 'hidden', +// ...typography.heading2, +// color: colors.invariant.text, +// lineHeight: '40px', +// whiteSpace: 'nowrap', +// width: 180, +// [theme.breakpoints.down('xl')]: { +// ...typography.heading2 +// }, +// [theme.breakpoints.down('lg')]: { +// lineHeight: '32px', +// width: 'unset' +// }, +// [theme.breakpoints.down('sm')]: { +// ...typography.heading3, +// lineHeight: '25px' +// } +// }, +// infoText: { +// ...typography.body1, +// color: colors.invariant.lightGrey, +// whiteSpace: 'nowrap', +// textOverflow: 'ellipsis', +// overflow: 'hidden', +// [theme.breakpoints.down('sm')]: { +// ...typography.caption1, +// padding: '0 4px' +// } +// }, +// activeInfoText: { +// color: colors.invariant.black +// }, +// greenText: { +// ...typography.body1, +// color: colors.invariant.green, +// whiteSpace: 'nowrap', +// textOverflow: 'ellipsis', +// overflow: 'hidden', +// [theme.breakpoints.down('sm')]: { +// ...typography.caption1 +// } +// }, +// liquidity: { +// background: colors.invariant.light, +// borderRadius: 11, +// height: 36, +// width: 170, +// marginRight: 8, +// lineHeight: 20, +// paddingInline: 10, +// [theme.breakpoints.down('lg')]: { +// flex: '1 1 0%' +// } +// }, +// fee: { +// background: colors.invariant.light, +// borderRadius: 11, +// height: 36, +// width: 90, +// marginRight: 8, + +// [theme.breakpoints.down('md')]: { +// marginRight: 0 +// } +// }, +// activeFee: { +// background: colors.invariant.greenLinearGradient +// }, +// infoCenter: { +// flex: '1 1 0%' +// }, +// minMax: { +// background: colors.invariant.light, +// borderRadius: 11, +// height: 36, +// width: 320, +// paddingInline: 10, +// marginRight: 8, + +// [theme.breakpoints.down('md')]: { +// width: '100%', +// marginRight: 0, +// marginTop: 8 +// } +// }, +// dropdown: { +// background: colors.invariant.greenLinearGradient, +// borderRadius: 11, +// height: 36, +// width: 57, +// paddingInline: 10, +// marginRight: 8, + +// [theme.breakpoints.down(1029)]: { +// width: '100%', +// marginRight: 0, +// marginTop: 8 +// } +// }, +// dropdownLocked: { +// background: colors.invariant.lightHover +// }, +// dropdownText: { +// color: colors.invariant.black, +// width: '100%' +// }, +// value: { +// background: colors.invariant.light, +// borderRadius: 11, +// height: 36, +// width: 160, +// paddingInline: 12, +// marginRight: 8, + +// [theme.breakpoints.down(1029)]: { +// marginRight: 0 +// }, +// [theme.breakpoints.down('sm')]: { +// width: 144, +// paddingInline: 6 +// } +// }, +// mdInfo: { +// width: 'fit-content', +// flexWrap: 'nowrap', + +// [theme.breakpoints.down('lg')]: { +// flexWrap: 'nowrap', +// marginTop: 16, +// width: '100%' +// }, + +// [theme.breakpoints.down(1029)]: { +// flexWrap: 'wrap' +// } +// }, +// mdTop: { +// width: 'fit-content', + +// [theme.breakpoints.down('lg')]: { +// width: '100%', +// justifyContent: 'space-between' +// } +// }, +// iconsAndNames: { +// width: 'fit-content' +// }, +// label: { +// marginRight: 2 +// }, +// tooltip: { +// color: colors.invariant.textGrey, +// ...typography.caption4, +// lineHeight: '24px', +// background: colors.black.full, +// borderRadius: 12, +// fontSize: 14 +// } +// })) +import { Theme } from '@mui/material' +import { colors } from '@static/theme' +import { makeStyles } from 'tss-react/mui' + +export const useDesktopStyles = makeStyles()((theme: Theme) => ({ + root: { + padding: 20, + flexWrap: 'nowrap', + background: colors.invariant.component, + borderRadius: 24, + '&:not(:last-child)': { + marginBottom: 20 + }, + '&:hover': { + background: `${colors.invariant.component}B0` + } + }, + actionButton: { + display: 'inline-flex' + }, + minMax: { + background: colors.invariant.light, + borderRadius: 11, + height: 36, + width: 320, + paddingInline: 10, + marginRight: 8, + [theme.breakpoints.down('md')]: { + width: '100%', + marginRight: 0, + marginTop: 8 + } + }, + mdInfo: { + width: 'fit-content', + flexWrap: 'nowrap', + [theme.breakpoints.down('lg')]: { + flexWrap: 'nowrap', + marginTop: 16, + width: '100%' + }, + [theme.breakpoints.down(1029)]: { + flexWrap: 'wrap' + } + }, + mdTop: { + width: 'fit-content', + [theme.breakpoints.down('lg')]: { + width: '100%', + justifyContent: 'space-between' + } + }, + iconsAndNames: { + width: 'fit-content' + } +})) diff --git a/src/components/PositionsList/PositionItem/variants/style/mobile.tsx b/src/components/PositionsList/PositionItem/variants/style/mobile.tsx new file mode 100644 index 00000000..57ff8922 --- /dev/null +++ b/src/components/PositionsList/PositionItem/variants/style/mobile.tsx @@ -0,0 +1,46 @@ +import { Theme } from '@mui/material' +import { colors } from '@static/theme' +import { makeStyles } from 'tss-react/mui' + +export const useMobileStyles = makeStyles()((theme: Theme) => ({ + root: { + padding: 16, + flexWrap: 'wrap', + [theme.breakpoints.down('sm')]: { + padding: 8 + }, + background: colors.invariant.component, + borderRadius: 24, + '&:not(:last-child)': { + marginBottom: 20 + }, + '&:hover': { + background: `${colors.invariant.component}B0` + } + }, + actionButton: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + minMax: { + background: colors.invariant.light, + borderRadius: 11, + height: 36, + paddingInline: 10, + width: '100%', + marginRight: 0, + marginTop: '8px' + }, + mdInfo: { + flexWrap: 'wrap', + width: '100%' + }, + mdTop: { + justifyContent: 'space-between', + width: '100%' + }, + iconsAndNames: { + display: 'flex' + } +})) diff --git a/src/components/PositionsList/PositionItem/style.tsx b/src/components/PositionsList/PositionItem/variants/style/shared.ts similarity index 65% rename from src/components/PositionsList/PositionItem/style.tsx rename to src/components/PositionsList/PositionItem/variants/style/shared.ts index 4fda7dab..33849f5e 100644 --- a/src/components/PositionsList/PositionItem/style.tsx +++ b/src/components/PositionsList/PositionItem/variants/style/shared.ts @@ -2,32 +2,10 @@ import { Theme } from '@mui/material' import { colors, typography } from '@static/theme' import { makeStyles } from 'tss-react/mui' -export const useStyles = makeStyles()((theme: Theme) => ({ - root: { - background: colors.invariant.component, - borderRadius: 24, - padding: 20, - flexWrap: 'nowrap', - '&:not(:last-child)': { - marginBottom: 20 - }, - - '&:hover': { - background: `${colors.invariant.component}B0` - }, - - [theme.breakpoints.down('lg')]: { - padding: 16, - flexWrap: 'wrap' - }, - [theme.breakpoints.down('sm')]: { - padding: 8 - } - }, +export const useSharedStyles = makeStyles()((theme: Theme) => ({ icons: { marginRight: 12, width: 'fit-content', - [theme.breakpoints.down('lg')]: { marginRight: 12 } @@ -35,24 +13,37 @@ export const useStyles = makeStyles()((theme: Theme) => ({ tokenIcon: { width: 40, borderRadius: '100%', - [theme.breakpoints.down('sm')]: { width: 28 } }, + actionButton: { + background: 'none', + padding: 0, + margin: 0, + border: 'none', + position: 'relative', + color: colors.invariant.black, + textTransform: 'none', + transition: 'filter 0.2s linear', + '&:hover': { + filter: 'brightness(1.2)', + cursor: 'pointer', + '@media (hover: none)': { + filter: 'none' + } + } + }, arrows: { width: 36, marginLeft: 4, marginRight: 4, - [theme.breakpoints.down('lg')]: { width: 30 }, - [theme.breakpoints.down('sm')]: { width: 24 }, - '&:hover': { filter: 'brightness(2)' } @@ -105,7 +96,6 @@ export const useStyles = makeStyles()((theme: Theme) => ({ background: colors.invariant.light, borderRadius: 11, height: 36, - width: 170, marginRight: 8, lineHeight: 20, paddingInline: 10, @@ -117,9 +107,7 @@ export const useStyles = makeStyles()((theme: Theme) => ({ background: colors.invariant.light, borderRadius: 11, height: 36, - width: 90, marginRight: 8, - [theme.breakpoints.down('md')]: { marginRight: 0 } @@ -130,33 +118,12 @@ export const useStyles = makeStyles()((theme: Theme) => ({ infoCenter: { flex: '1 1 0%' }, - minMax: { - background: colors.invariant.light, - borderRadius: 11, - height: 36, - width: 320, - paddingInline: 10, - marginRight: 8, - - [theme.breakpoints.down('md')]: { - width: '100%', - marginRight: 0, - marginTop: 8 - } - }, dropdown: { background: colors.invariant.greenLinearGradient, borderRadius: 11, height: 36, - width: 57, paddingInline: 10, - marginRight: 8, - - [theme.breakpoints.down(1029)]: { - width: '100%', - marginRight: 0, - marginTop: 8 - } + marginRight: 8 }, dropdownLocked: { background: colors.invariant.lightHover @@ -169,42 +136,8 @@ export const useStyles = makeStyles()((theme: Theme) => ({ background: colors.invariant.light, borderRadius: 11, height: 36, - width: 160, paddingInline: 12, - marginRight: 8, - - [theme.breakpoints.down(1029)]: { - marginRight: 0 - }, - [theme.breakpoints.down('sm')]: { - width: 144, - paddingInline: 6 - } - }, - mdInfo: { - width: 'fit-content', - flexWrap: 'nowrap', - - [theme.breakpoints.down('lg')]: { - flexWrap: 'nowrap', - marginTop: 16, - width: '100%' - }, - - [theme.breakpoints.down(1029)]: { - flexWrap: 'wrap' - } - }, - mdTop: { - width: 'fit-content', - - [theme.breakpoints.down('lg')]: { - width: '100%', - justifyContent: 'space-between' - } - }, - iconsAndNames: { - width: 'fit-content' + marginRight: 8 }, label: { marginRight: 2 diff --git a/src/components/PositionsList/PositionsList.stories.tsx b/src/components/PositionsList/PositionsList.stories.tsx index 8b019c28..8c45043a 100644 --- a/src/components/PositionsList/PositionsList.stories.tsx +++ b/src/components/PositionsList/PositionsList.stories.tsx @@ -2,7 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react' import { BrowserRouter } from 'react-router-dom' import { PositionsList } from './PositionsList' import { NetworkType } from '@store/consts/static' -import { IPositionItem } from './PositionItem/PositionItem' +import { Keypair } from '@solana/web3.js' +import { BN } from '@coral-xyz/anchor' +import { IPositionItem } from './types' const meta = { title: 'Components/PositionsList', @@ -27,12 +29,56 @@ const data: IPositionItem[] = [ network: NetworkType.Testnet, tokenXName: 'BTC', tokenYName: 'SNY', + position: { + bump: 0, + feeGrowthInsideX: new BN(0), + feeGrowthInsideY: new BN(0), + id: 0, + lastSlot: new BN(0), + lowerTickIndex: new BN(0), + owner: Keypair.generate().publicKey, + pool: Keypair.generate().publicKey, + secondsPerLiquidityInside: new BN(0), + tokensOwedX: new BN(0), + tokensOwedY: new BN(0), + upperTickIndex: new BN(0), + liquidity: new BN(0) + }, tokenXIcon: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', tokenYIcon: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', min: 2149.6, max: 149.6, + liquidity: 453.5, + poolAddress: Keypair.generate().publicKey, + poolData: { + address: Keypair.generate().publicKey, + bump: 0, + currentTickIndex: 0, + fee: new BN(0), + feeGrowthGlobalX: new BN(0), + feeProtocolTokenX: new BN(0), + feeProtocolTokenY: new BN(0), + feeReceiver: Keypair.generate().publicKey, + lastTimestamp: new BN(0), + oracleAddress: Keypair.generate().publicKey, + oracleInitialized: true, + liquidity: new BN(0), + poolIndex: 0, + positionIterator: new BN(0), + protocolFee: new BN(0), + secondsPerLiquidityGlobal: new BN(0), + sqrtPrice: new BN(0), + startTimestamp: new BN(0), + tickmap: Keypair.generate().publicKey, + tickSpacing: 0, + tokenX: Keypair.generate().publicKey, + tokenY: Keypair.generate().publicKey, + tokenXReserve: Keypair.generate().publicKey, + tokenYReserve: Keypair.generate().publicKey, + feeGrowthGlobalY: new BN(0) + }, fee: 0.05, tokenXLiq: 5000, tokenYLiq: 300.2, @@ -48,6 +94,21 @@ const data: IPositionItem[] = [ network: NetworkType.Testnet, tokenXName: 'BTC', tokenYName: 'SNY', + position: { + bump: 0, + feeGrowthInsideX: new BN(0), + feeGrowthInsideY: new BN(0), + id: 0, + lastSlot: new BN(0), + lowerTickIndex: new BN(0), + owner: Keypair.generate().publicKey, + pool: Keypair.generate().publicKey, + secondsPerLiquidityInside: new BN(0), + tokensOwedX: new BN(0), + tokensOwedY: new BN(0), + upperTickIndex: new BN(0), + liquidity: new BN(0) + }, tokenXIcon: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', tokenYIcon: @@ -60,7 +121,36 @@ const data: IPositionItem[] = [ valueX: 10000.45, valueY: 21370.4, id: '2', - isLocked: false + isLocked: false, + liquidity: new BN(0), + poolAddress: Keypair.generate().publicKey, + poolData: { + address: Keypair.generate().publicKey, + bump: 0, + currentTickIndex: 0, + fee: new BN(0), + feeGrowthGlobalX: new BN(0), + feeProtocolTokenX: new BN(0), + feeProtocolTokenY: new BN(0), + feeReceiver: Keypair.generate().publicKey, + lastTimestamp: new BN(0), + oracleAddress: Keypair.generate().publicKey, + oracleInitialized: true, + liquidity: new BN(0), + poolIndex: 0, + positionIterator: new BN(0), + protocolFee: new BN(0), + secondsPerLiquidityGlobal: new BN(0), + sqrtPrice: new BN(0), + startTimestamp: new BN(0), + tickmap: Keypair.generate().publicKey, + tickSpacing: 0, + tokenX: Keypair.generate().publicKey, + tokenY: Keypair.generate().publicKey, + tokenXReserve: Keypair.generate().publicKey, + tokenYReserve: Keypair.generate().publicKey, + feeGrowthGlobalY: new BN(0) + } }, { address: 'So11111111111111111111111111111111111111112', @@ -69,6 +159,21 @@ const data: IPositionItem[] = [ network: NetworkType.Testnet, tokenXName: 'BTC', tokenYName: 'SNY', + position: { + bump: 0, + feeGrowthInsideX: new BN(0), + feeGrowthInsideY: new BN(0), + id: 0, + lastSlot: new BN(0), + lowerTickIndex: new BN(0), + owner: Keypair.generate().publicKey, + pool: Keypair.generate().publicKey, + secondsPerLiquidityInside: new BN(0), + tokensOwedX: new BN(0), + tokensOwedY: new BN(0), + upperTickIndex: new BN(0), + liquidity: new BN(0) + }, tokenXIcon: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', tokenYIcon: @@ -81,7 +186,36 @@ const data: IPositionItem[] = [ valueX: 10000.45, valueY: 21370.4, id: '3', - isLocked: false + isLocked: false, + liquidity: new BN(0), + poolAddress: Keypair.generate().publicKey, + poolData: { + address: Keypair.generate().publicKey, + bump: 0, + currentTickIndex: 0, + fee: new BN(0), + feeGrowthGlobalX: new BN(0), + feeProtocolTokenX: new BN(0), + feeProtocolTokenY: new BN(0), + feeReceiver: Keypair.generate().publicKey, + lastTimestamp: new BN(0), + oracleAddress: Keypair.generate().publicKey, + oracleInitialized: true, + liquidity: new BN(0), + poolIndex: 0, + positionIterator: new BN(0), + protocolFee: new BN(0), + secondsPerLiquidityGlobal: new BN(0), + sqrtPrice: new BN(0), + startTimestamp: new BN(0), + tickmap: Keypair.generate().publicKey, + tickSpacing: 0, + tokenX: Keypair.generate().publicKey, + tokenY: Keypair.generate().publicKey, + tokenXReserve: Keypair.generate().publicKey, + tokenYReserve: Keypair.generate().publicKey, + feeGrowthGlobalY: new BN(0) + } }, { address: 'So11111111111111111111111111111111111111112', @@ -90,6 +224,21 @@ const data: IPositionItem[] = [ network: NetworkType.Testnet, tokenXName: 'BTC', tokenYName: 'SNY', + position: { + bump: 0, + feeGrowthInsideX: new BN(0), + feeGrowthInsideY: new BN(0), + id: 0, + lastSlot: new BN(0), + lowerTickIndex: new BN(0), + owner: Keypair.generate().publicKey, + pool: Keypair.generate().publicKey, + secondsPerLiquidityInside: new BN(0), + tokensOwedX: new BN(0), + tokensOwedY: new BN(0), + upperTickIndex: new BN(0), + liquidity: new BN(0) + }, tokenXIcon: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', tokenYIcon: @@ -102,7 +251,36 @@ const data: IPositionItem[] = [ valueX: 10000.45, valueY: 21370.4, id: '4', - isLocked: false + isLocked: false, + liquidity: new BN(0), + poolAddress: Keypair.generate().publicKey, + poolData: { + address: Keypair.generate().publicKey, + bump: 0, + currentTickIndex: 0, + fee: new BN(0), + feeGrowthGlobalX: new BN(0), + feeProtocolTokenX: new BN(0), + feeProtocolTokenY: new BN(0), + feeReceiver: Keypair.generate().publicKey, + lastTimestamp: new BN(0), + oracleAddress: Keypair.generate().publicKey, + oracleInitialized: true, + liquidity: new BN(0), + poolIndex: 0, + positionIterator: new BN(0), + protocolFee: new BN(0), + secondsPerLiquidityGlobal: new BN(0), + sqrtPrice: new BN(0), + startTimestamp: new BN(0), + tickmap: Keypair.generate().publicKey, + tickSpacing: 0, + tokenX: Keypair.generate().publicKey, + tokenY: Keypair.generate().publicKey, + tokenXReserve: Keypair.generate().publicKey, + tokenYReserve: Keypair.generate().publicKey, + feeGrowthGlobalY: new BN(0) + } } ] diff --git a/src/components/PositionsList/PositionsList.tsx b/src/components/PositionsList/PositionsList.tsx index 7d8597ce..fc3b8c9a 100644 --- a/src/components/PositionsList/PositionsList.tsx +++ b/src/components/PositionsList/PositionsList.tsx @@ -8,17 +8,22 @@ import { InputBase, ToggleButton, ToggleButtonGroup, - Typography + Typography, + useMediaQuery } from '@mui/material' import loader from '@static/gif/loader.gif' import SearchIcon from '@static/svg/lupaDark.svg' import refreshIcon from '@static/svg/refresh.svg' import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { IPositionItem, PositionItem } from './PositionItem/PositionItem' import { useStyles } from './style' import { TooltipHover } from '@components/TooltipHover/TooltipHover' import { PaginationList } from '@components/Pagination/Pagination' +import { useDispatch } from 'react-redux' +import { actions } from '@store/reducers/leaderboard' +import { PositionItemDesktop } from './PositionItem/variants/PositionItemDesktop' +import { PositionItemMobile } from './PositionItem/variants/PositionItemMobile' +import { IPositionItem } from './types' export enum LiquidityPools { Standard = 'Standard', @@ -69,8 +74,10 @@ export const PositionsList: React.FC = ({ const { classes } = useStyles() const navigate = useNavigate() const [defaultPage] = useState(initialPage) + const dispatch = useDispatch() const [page, setPage] = useState(initialPage) const [alignment, setAlignment] = useState(LiquidityPools.Standard) + const isLg = useMediaQuery('@media (max-width: 1360px)') const currentData = useMemo(() => { if (alignment === LiquidityPools.Standard) { @@ -135,9 +142,11 @@ export const PositionsList: React.FC = ({ handleChangePagination(initialPage) }, [initialPage]) - // useEffect(() => { - // pageChanged(page) - // }, [page]) + useEffect(() => { + dispatch(actions.getLeaderboardConfig()) + }, [dispatch]) + + const [allowPropagation, setAllowPropagation] = useState(true) return ( @@ -227,11 +236,21 @@ export const PositionsList: React.FC = ({ paginator(page).data.map((element, index) => ( { - navigate(`/position/${element.id}`) + if (allowPropagation) { + navigate(`/position/${element.id}`) + } }} key={element.id} className={classes.itemLink}> - + {isLg ? ( + + ) : ( + + )} )) ) : showNoConnected ? ( diff --git a/src/components/PositionsList/style.ts b/src/components/PositionsList/style.ts index 6a5fcc07..fa3beb46 100644 --- a/src/components/PositionsList/style.ts +++ b/src/components/PositionsList/style.ts @@ -65,6 +65,7 @@ export const useStyles = makeStyles()((theme: Theme) => ({ height: 48 } }, + button: { color: colors.invariant.dark, ...typography.body1, diff --git a/src/components/PositionsList/types.d.ts b/src/components/PositionsList/types.d.ts new file mode 100644 index 00000000..6830c40c --- /dev/null +++ b/src/components/PositionsList/types.d.ts @@ -0,0 +1,30 @@ +import { BN } from '@coral-xyz/anchor' +import { Position } from '@invariant-labs/sdk-eclipse/lib/market' +import { PublicKey } from '@solana/web3.js' +import { NetworkType } from '@store/consts/static' +import { PoolWithAddressAndIndex } from '@store/selectors/positions' + +export interface IPositionItem { + tokenXName: string + tokenYName: string + tokenXIcon: string + tokenYIcon: string + tokenXLiq: number + poolAddress: PublicKey + position: Position + tokenYLiq: number + fee: number + min: number + max: number + valueX: number + valueY: number + id: string + address: string + isActive?: boolean + currentPrice: number + network: NetworkType + isFullRange: boolean + isLocked: boolean + poolData: PoolWithAddressAndIndex + liquidity: BN +} diff --git a/src/containers/WrappedPositionsList/WrappedPositionsList.tsx b/src/containers/WrappedPositionsList/WrappedPositionsList.tsx index c2ae3648..8a2d8938 100644 --- a/src/containers/WrappedPositionsList/WrappedPositionsList.tsx +++ b/src/containers/WrappedPositionsList/WrappedPositionsList.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { calcYPerXPriceBySqrtPrice, printBN } from '@utils/utils' -import { IPositionItem } from '@components/PositionsList/PositionItem/PositionItem' +import { IPositionItem } from '@components/PositionsList/types' export const WrappedPositionsList: React.FC = () => { const walletAddress = useSelector(address) @@ -115,9 +115,13 @@ export const WrappedPositionsList: React.FC = () => { tokenYName: position.tokenY.symbol, tokenXIcon: position.tokenX.logoURI, tokenYIcon: position.tokenY.logoURI, + poolAddress: position.poolData.address, + liquidity: position.liquidity, + poolData: position.poolData, fee: +printBN(position.poolData.fee, DECIMAL - 2), min, max, + position, valueX, valueY, address: walletAddress.toString(), @@ -205,7 +209,11 @@ export const WrappedPositionsList: React.FC = () => { min, max, valueX, + position, valueY, + poolAddress: position.poolData.address, + liquidity: position.liquidity, + poolData: position.poolData, address: walletAddress.toString(), id: position.id.toString() + '_' + position.pool.toString(), isActive: currentPrice >= min && currentPrice <= max, diff --git a/src/pages/LeaderboardPage/components/YourProgress/YourProgress.tsx b/src/pages/LeaderboardPage/components/YourProgress/YourProgress.tsx index ac35b4e1..9f079ca9 100644 --- a/src/pages/LeaderboardPage/components/YourProgress/YourProgress.tsx +++ b/src/pages/LeaderboardPage/components/YourProgress/YourProgress.tsx @@ -55,6 +55,14 @@ export const YourProgress: React.FC = ({ // return '0' // } // } + const pointsPerDayFormat: string | number = userStats + ? estimated24hPoints.isZero() + ? '<0.01' + : removeAdditionalDecimals( + formatNumberWithCommas(printBN(estimated24hPoints, LEADERBOARD_DECIMAL)), + 2 + ) + : 0 return ( = ({ desktopLabelAligment='right' label='Points Per Day' isLoading={isLoadingList} - value={ - userStats - ? removeAdditionalDecimals( - formatNumberWithCommas(printBN(estimated24hPoints, LEADERBOARD_DECIMAL)), - 2 - ) - : 0 - } + value={pointsPerDayFormat} />