diff --git a/public/images/3d-syrup-bunnies.png b/public/images/3d-syrup-bunnies.png new file mode 100644 index 0000000000000..02a47cf120915 Binary files /dev/null and b/public/images/3d-syrup-bunnies.png differ diff --git a/src/components/ApyCalculatorModal/index.tsx b/src/components/ApyCalculatorModal/index.tsx index da92cb4bf5a09..39fa9d3981b68 100644 --- a/src/components/ApyCalculatorModal/index.tsx +++ b/src/components/ApyCalculatorModal/index.tsx @@ -11,6 +11,7 @@ interface ApyCalculatorModalProps { linkLabel: string linkHref: string earningTokenSymbol?: string + roundingDecimals?: number } const Grid = styled.div` @@ -36,6 +37,7 @@ const ApyCalculatorModal: React.FC = ({ linkLabel, linkHref, earningTokenSymbol = 'CAKE', + roundingDecimals = 2, }) => { const TranslateString = useI18n() const oneThousandDollarsWorthOfToken = 1000 / tokenPrice @@ -44,21 +46,25 @@ const ApyCalculatorModal: React.FC = ({ numberOfDays: 1, farmApr: apr, tokenPrice, + roundingDecimals, }) const tokenEarnedPerThousand7D = tokenEarnedPerThousandDollarsCompounding({ numberOfDays: 7, farmApr: apr, tokenPrice, + roundingDecimals, }) const tokenEarnedPerThousand30D = tokenEarnedPerThousandDollarsCompounding({ numberOfDays: 30, farmApr: apr, tokenPrice, + roundingDecimals, }) const tokenEarnedPerThousand365D = tokenEarnedPerThousandDollarsCompounding({ numberOfDays: 365, farmApr: apr, tokenPrice, + roundingDecimals, }) return ( diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 3ca503f261619..02cde9922cab7 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -6,6 +6,7 @@ interface TextProps { isDisabled?: boolean fontSize?: string color?: string + bold?: boolean } interface BalanceProps extends TextProps { @@ -14,7 +15,7 @@ interface BalanceProps extends TextProps { unit?: string } -const Balance: React.FC = ({ value, fontSize, color, decimals, isDisabled, unit }) => { +const Balance: React.FC = ({ value, fontSize, color, decimals, isDisabled, unit, bold }) => { const previousValue = useRef(0) useEffect(() => { @@ -22,7 +23,7 @@ const Balance: React.FC = ({ value, fontSize, color, decimals, isD }, [value]) return ( - + {value && unit && {unit}} diff --git a/src/config/index.ts b/src/config/index.ts index 3b22ae895c4c2..abf088c4cca05 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -21,6 +21,7 @@ export const BASE_URL = 'https://pancakeswap.finance' export const BASE_EXCHANGE_URL = 'https://exchange.pancakeswap.finance' export const BASE_ADD_LIQUIDITY_URL = `${BASE_EXCHANGE_URL}/#/add` export const BASE_LIQUIDITY_POOL_URL = `${BASE_EXCHANGE_URL}/#/pool` +export const BASE_BSC_SCAN_URL = 'https://bscscan.com' export const LOTTERY_MAX_NUMBER_OF_TICKETS = 50 export const LOTTERY_TICKET_PRICE = 1 export const DEFAULT_TOKEN_DECIMAL = new BigNumber(10).pow(18) diff --git a/src/utils/compoundApyHelpers.ts b/src/utils/compoundApyHelpers.ts index 6bf6b5890e4b8..01a013b361db4 100644 --- a/src/utils/compoundApyHelpers.ts +++ b/src/utils/compoundApyHelpers.ts @@ -1,6 +1,9 @@ -const roundToTwoDp = (number) => Math.round(number * 100) / 100 - -export const tokenEarnedPerThousandDollarsCompounding = ({ numberOfDays, farmApr, tokenPrice }) => { +export const tokenEarnedPerThousandDollarsCompounding = ({ + numberOfDays, + farmApr, + tokenPrice, + roundingDecimals = 2, +}) => { // Everything here is worked out relative to a year, with the asset compounding daily const timesCompounded = 365 // We use decimal values rather than % in the math for both APY and the number of days being calculates as a proportion of the year @@ -12,10 +15,10 @@ export const tokenEarnedPerThousandDollarsCompounding = ({ numberOfDays, farmApr const finalAmount = principal * (1 + aprAsDecimal / timesCompounded) ** (timesCompounded * daysAsDecimalOfYear) // To get the TOKEN amount earned, deduct the amount after compounding (finalAmount) from the starting TOKEN balance (principal) const interestEarned = finalAmount - principal - return roundToTwoDp(interestEarned) + return parseFloat(interestEarned.toFixed(roundingDecimals)) } -export const getRoi = ({ amountEarned, amountInvested }) => { +export const getRoi = ({ amountEarned, amountInvested, roundingDecimals = 2 }) => { const percentage = (amountEarned / amountInvested) * 100 - return percentage.toFixed(2) + return percentage.toFixed(roundingDecimals) } diff --git a/src/views/Pools/components/CardContent.tsx b/src/views/Pools/components/CardContent.tsx deleted file mode 100644 index 92bbf6ea5ccd1..0000000000000 --- a/src/views/Pools/components/CardContent.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -const CardContent: React.FC = ({ children }) => {children} - -const StyledCardContent = styled.div` - display: flex; - flex: 1; - flex-direction: column; - padding: 0; -` - -export default CardContent diff --git a/src/views/Pools/components/CardFooter.tsx b/src/views/Pools/components/CardFooter.tsx deleted file mode 100644 index 7e48df9274ede..0000000000000 --- a/src/views/Pools/components/CardFooter.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react' -import BigNumber from 'bignumber.js' -import styled from 'styled-components' -import { getBalanceNumber } from 'utils/formatBalance' -import useI18n from 'hooks/useI18n' -import { ChevronDown, ChevronUp } from 'react-feather' -import { Flex, MetamaskIcon } from '@pancakeswap-libs/uikit' -import Balance from 'components/Balance' -import { CommunityTag, CoreTag, BinanceTag } from 'components/Tags' -import { useBlock } from 'state/hooks' -import { PoolCategory } from 'config/constants/types' -import { registerToken } from 'utils/wallet' -import { BASE_URL } from 'config' - -const tags = { - [PoolCategory.BINANCE]: BinanceTag, - [PoolCategory.CORE]: CoreTag, - [PoolCategory.COMMUNITY]: CommunityTag, -} - -interface Props { - projectLink: string - decimals: number - totalStaked: BigNumber - tokenName: string - tokenAddress: string - tokenDecimals: number - startBlock: number - endBlock: number - isFinished: boolean - poolCategory: PoolCategory -} - -const StyledFooter = styled.div<{ isFinished: boolean }>` - border-top: 1px solid ${({ theme }) => (theme.isDark ? '#524B63' : '#E9EAEB')}; - color: ${({ isFinished, theme }) => theme.colors[isFinished ? 'textDisabled2' : 'primary2']}; - padding: 24px; -` - -const StyledDetailsButton = styled.button` - align-items: center; - background-color: transparent; - border: 0; - color: ${(props) => props.theme.colors.primary}; - cursor: pointer; - display: inline-flex; - font-size: 16px; - font-weight: 600; - height: 32px; - justify-content: center; - outline: 0; - padding: 0; - &:hover { - opacity: 0.9; - } - - & > svg { - margin-left: 4px; - } -` - -const Details = styled.div` - margin-top: 24px; -` - -const Row = styled(Flex)` - align-items: center; -` - -const FlexFull = styled.div` - flex: 1; -` -const Label = styled.div` - font-size: 14px; -` -const TokenLink = styled.a` - font-size: 14px; - text-decoration: none; - color: ${(props) => props.theme.colors.primary}; - cursor: pointer; -` - -const CardFooter: React.FC = ({ - projectLink, - decimals, - tokenAddress, - totalStaked, - tokenName, - tokenDecimals, - isFinished, - startBlock, - endBlock, - poolCategory, -}) => { - const { currentBlock } = useBlock() - const [isOpen, setIsOpen] = useState(false) - const TranslateString = useI18n() - const Icon = isOpen ? ChevronUp : ChevronDown - - const handleClick = () => setIsOpen(!isOpen) - const Tag = tags[poolCategory] - - const blocksUntilStart = Math.max(startBlock - currentBlock, 0) - const blocksRemaining = Math.max(endBlock - currentBlock, 0) - - const imageSrc = `${BASE_URL}/images/tokens/${tokenName.toLowerCase()}.png` - - const isMetaMaskInScope = !!(window as WindowChain).ethereum?.isMetaMask - - return ( - - - - - - - {isOpen ? TranslateString(1066, 'Hide') : TranslateString(658, 'Details')} - - - {isOpen && ( -
- - - - - - - {blocksUntilStart > 0 && ( - - - - - - - )} - {blocksUntilStart === 0 && blocksRemaining > 0 && ( - - - - - - - )} - {isMetaMaskInScope && tokenAddress && ( - - registerToken(tokenAddress, tokenName, tokenDecimals, imageSrc)}> - Add {tokenName} to MetaMask - - - - )} - - {TranslateString(412, 'View project site')} - -
- )} -
- ) -} - -export default React.memo(CardFooter) diff --git a/src/views/Pools/components/CardTitle.tsx b/src/views/Pools/components/CardTitle.tsx deleted file mode 100644 index 817c0bf3254b7..0000000000000 --- a/src/views/Pools/components/CardTitle.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components' - -interface StyledTitleProps { - isFinished?: boolean -} - -const CardTitle = styled.div` - color: ${({ isFinished, theme }) => theme.colors[isFinished ? 'textDisabled' : 'text']}; - font-weight: 600; - font-size: 24px; - line-height: 1.1; - margin-bottom: 14px; -` - -export default CardTitle diff --git a/src/views/Pools/components/Coming.tsx b/src/views/Pools/components/Coming.tsx deleted file mode 100644 index f27e727cad0b9..0000000000000 --- a/src/views/Pools/components/Coming.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { Image, Button } from '@pancakeswap-libs/uikit' -import { CommunityTag } from 'components/Tags' -import useI18n from 'hooks/useI18n' -import Card from './Card' -import CardTitle from './CardTitle' - -const Balance = styled.div` - color: ${({ theme }) => theme.colors.text}; - font-size: 40px; - font-weight: 600; -` - -const Label = styled.div` - color: ${({ theme }) => theme.colors.textSubtle}; - font-size: 14px; - margin-bottom: 16px; -` - -const DetailPlaceholder = styled.div` - display: flex; - font-size: 14px; -` -const Value = styled.div` - color: ${({ theme }) => theme.colors.text}; - font-size: 14px; -` - -const Footer = styled.div` - border-top: 1px solid ${({ theme }) => (theme.isDark ? '#524B63' : '#E9EAEB')}; - padding: 24px; -` -const Coming: React.FC = () => { - const TranslateString = useI18n() - - return ( - -
- - {TranslateString(414, 'Your Project?')}{' '} - - 👀 - - - Your project here - ??? - - - -
{TranslateString(736, 'APR')}:
- ?? -
- -
- - 🥞{' '} - - {TranslateString(384, 'Your Stake')}: -
- ??? CAKE -
-
-
- -
-
- ) -} - -export default Coming diff --git a/src/views/Pools/components/CompoundModal.tsx b/src/views/Pools/components/CompoundModal.tsx deleted file mode 100644 index 80cadc018ac84..0000000000000 --- a/src/views/Pools/components/CompoundModal.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import BigNumber from 'bignumber.js' -import styled from 'styled-components' -import React, { useMemo, useState } from 'react' -import { Button, Modal } from '@pancakeswap-libs/uikit' -import ModalActions from 'components/ModalActions' -import Balance from 'components/Balance' -import useI18n from 'hooks/useI18n' -import { getFullDisplayBalance } from 'utils/formatBalance' - -interface DepositModalProps { - earnings: BigNumber - onConfirm: (amount: string, decimals: number) => void - onDismiss?: () => void - tokenName?: string - stakingTokenDecimals?: number -} - -const CompoundModal: React.FC = ({ - earnings, - onConfirm, - onDismiss, - tokenName = '', - stakingTokenDecimals = 18, -}) => { - const [pendingTx, setPendingTx] = useState(false) - const TranslateString = useI18n() - const fullBalance = useMemo(() => { - return getFullDisplayBalance(earnings, stakingTokenDecimals) - }, [earnings, stakingTokenDecimals]) - - return ( - - - - - - - - - - ) -} - -export default CompoundModal - -const BalanceRow = styled.div` - display: flex; - align-items: center; - justify-content: center; - flex-direction: row; -` diff --git a/src/views/Pools/components/DepositModal.tsx b/src/views/Pools/components/DepositModal.tsx deleted file mode 100644 index 78c37ea7ad5f2..0000000000000 --- a/src/views/Pools/components/DepositModal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import BigNumber from 'bignumber.js' -import React, { useCallback, useMemo, useState } from 'react' -import { Button, Modal } from '@pancakeswap-libs/uikit' -import ModalActions from 'components/ModalActions' -import TokenInput from 'components/TokenInput' -import useI18n from 'hooks/useI18n' -import { getFullDisplayBalance } from 'utils/formatBalance' - -interface DepositModalProps { - max: BigNumber - onConfirm: (amount: string, decimals: number) => void - onDismiss?: () => void - tokenName?: string - stakingTokenDecimals?: number -} - -const DepositModal: React.FC = ({ - max, - onConfirm, - onDismiss, - tokenName = '', - stakingTokenDecimals = 18, -}) => { - const [val, setVal] = useState('') - const [pendingTx, setPendingTx] = useState(false) - const TranslateString = useI18n() - const fullBalance = useMemo(() => { - return getFullDisplayBalance(max, stakingTokenDecimals) - }, [max, stakingTokenDecimals]) - - const handleChange = useCallback( - (e: React.FormEvent) => { - setVal(e.currentTarget.value) - }, - [setVal], - ) - - const handleSelectMax = useCallback(() => { - setVal(fullBalance) - }, [fullBalance, setVal]) - - return ( - - - - - - - - ) -} - -export default DepositModal diff --git a/src/views/Pools/components/Divider.tsx b/src/views/Pools/components/Divider.tsx deleted file mode 100644 index b71d204c32ca0..0000000000000 --- a/src/views/Pools/components/Divider.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import styled from 'styled-components' - -export default styled.div` - background-color: ${({ theme }) => theme.colors.textSubtle}; - height: 1px; - margin: 0 auto 32px; - width: 100%; -` diff --git a/src/views/Pools/components/HarvestButton.tsx b/src/views/Pools/components/HarvestButton.tsx deleted file mode 100644 index d623409af55c0..0000000000000 --- a/src/views/Pools/components/HarvestButton.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useContext, useMemo } from 'react' -import styled, { ThemeContext } from 'styled-components' - -import { Link } from 'react-router-dom' - -interface ButtonProps { - children?: React.ReactNode - disabled?: boolean - href?: string - onClick?: () => void - size?: 'sm' | 'md' | 'lg' - text?: string - to?: string - variant?: 'default' | 'secondary' | 'tertiary' -} - -const Button: React.FC = ({ children, disabled, href, onClick, size, text, to }) => { - const { colors, spacing } = useContext(ThemeContext) - const buttonColor = colors.background - - let boxShadow: string - let buttonSize: number - let buttonPadding: number - let fontSize: number - switch (size) { - case 'sm': - buttonPadding = spacing[3] - buttonSize = 36 - fontSize = 14 - break - case 'lg': - buttonPadding = spacing[4] - buttonSize = 72 - fontSize = 16 - break - case 'md': - default: - buttonPadding = spacing[4] - buttonSize = 56 - fontSize = 16 - } - - const ButtonChild = useMemo(() => { - if (to) { - return {text} - } - if (href) { - return ( - - {text} - - ) - } - return text - }, [href, text, to]) - - return ( - - {children} - {ButtonChild} - - ) -} - -interface StyledButtonProps { - boxShadow: string - color: string - disabled?: boolean - fontSize: number - padding: number - size: number -} - -const StyledButton = styled.button` - align-items: center; - background: ${(props) => (!props.disabled ? props.theme.card.background : `#ddd`)}; - border: 0; - border-radius: 12px; - color: ${(props) => (!props.disabled ? `#32cad7` : `#acaaaf`)}; - cursor: pointer; - display: flex; - font-size: ${(props) => props.fontSize}px; - font-weight: 700; - height: ${(props) => props.size}px; - justify-content: center; - outline: none; - padding-left: ${(props) => props.padding}px; - padding-right: ${(props) => props.padding}px; - pointer-events: ${(props) => (!props.disabled ? undefined : 'none')}; - width: 100%; - border: 2px solid ${(props) => (!props.disabled ? `#33cbd7` : `#eee`)}; - width: 100px; - height: 30px; - font-size: 14px; - padding: 0px; -` - -const StyledLink = styled(Link)` - align-items: center; - color: inherit; - display: flex; - flex: 1; - height: 56px; - justify-content: center; - margin: 0 ${(props) => -props.theme.spacing[4]}px; - padding: 0 ${(props) => props.theme.spacing[4]}px; - text-decoration: none; -` - -const StyledExternalLink = styled.a` - align-items: center; - color: inherit; - display: flex; - flex: 1; - height: 56px; - justify-content: center; - margin: 0 ${(props) => -props.theme.spacing[4]}px; - padding: 0 ${(props) => props.theme.spacing[4]}px; - text-decoration: none; -` - -export default Button diff --git a/src/views/Pools/components/OldSyrupTitle.tsx b/src/views/Pools/components/OldSyrupTitle.tsx deleted file mode 100644 index 61ad11eb1ecb9..0000000000000 --- a/src/views/Pools/components/OldSyrupTitle.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import CardTitle from './CardTitle' - -interface Props { - hasBalance?: boolean -} - -const Title = styled.div` - color: #ed4b9e; - font-size: 24px; - font-weight: 600; - text-transform: uppercase; -` - -const ActionLink = styled.a` - color: #ed4b9e; - font-size: 14px; -` - -const OldSyrupTitle: React.FC = ({ hasBalance = false }) => { - if (hasBalance) { - return ( -
- Action Required - - What do I need to do? - -
- ) - } - - return ( -
- FINISHED -
- ) -} - -export default OldSyrupTitle diff --git a/src/views/Pools/components/PoolCard.tsx b/src/views/Pools/components/PoolCard.tsx deleted file mode 100644 index decb6956db9af..0000000000000 --- a/src/views/Pools/components/PoolCard.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import BigNumber from 'bignumber.js' -import React, { useCallback, useState } from 'react' -import styled from 'styled-components' -import { Button, IconButton, useModal, AddIcon, Image } from '@pancakeswap-libs/uikit' -import { useWeb3React } from '@web3-react/core' -import UnlockButton from 'components/UnlockButton' -import Label from 'components/Label' -import { useERC20 } from 'hooks/useContract' -import { useSousApprove } from 'hooks/useApprove' -import useI18n from 'hooks/useI18n' -import { useSousStake } from 'hooks/useStake' -import { useSousUnstake } from 'hooks/useUnstake' -import { getBalanceNumber } from 'utils/formatBalance' -import { getPoolApr } from 'utils/apr' -import { getAddress } from 'utils/addressHelpers' -import { useSousHarvest } from 'hooks/useHarvest' -import Balance from 'components/Balance' -import { PoolCategory } from 'config/constants/types' -import tokens from 'config/constants/tokens' -import { Pool } from 'state/types' -import { useGetApiPrice } from 'state/hooks' -import DepositModal from './DepositModal' -import WithdrawModal from './WithdrawModal' -import CompoundModal from './CompoundModal' -import CardTitle from './CardTitle' -import Card from './Card' -import OldSyrupTitle from './OldSyrupTitle' -import HarvestButton from './HarvestButton' -import CardFooter from './CardFooter' - -interface HarvestProps { - pool: Pool -} - -const PoolCard: React.FC = ({ pool }) => { - const { - sousId, - stakingToken, - earningToken, - harvest, - poolCategory, - totalStaked, - startBlock, - endBlock, - isFinished, - userData, - stakingLimit, - } = pool - - // Pools using native BNB behave differently than pools using a token - const isBnbPool = poolCategory === PoolCategory.BINANCE - const TranslateString = useI18n() - const stakingTokenContract = useERC20(stakingToken.address ? getAddress(stakingToken.address) : '') - const { account } = useWeb3React() - const { onApprove } = useSousApprove(stakingTokenContract, sousId) - const { onStake } = useSousStake(sousId, isBnbPool) - const { onUnstake } = useSousUnstake(sousId) - const { onReward } = useSousHarvest(sousId, isBnbPool) - - // APR - const rewardTokenPrice = useGetApiPrice(earningToken.address ? getAddress(earningToken.address) : '') - const stakingTokenPrice = useGetApiPrice(stakingToken.address ? getAddress(stakingToken.address) : '') - const apr = getPoolApr( - stakingTokenPrice, - rewardTokenPrice, - getBalanceNumber(pool.totalStaked, stakingToken.decimals), - parseFloat(pool.tokenPerBlock), - ) - - const [requestedApproval, setRequestedApproval] = useState(false) - const [pendingTx, setPendingTx] = useState(false) - - const allowance = new BigNumber(userData?.allowance || 0) - const stakingTokenBalance = new BigNumber(userData?.stakingTokenBalance || 0) - const stakedBalance = new BigNumber(userData?.stakedBalance || 0) - const earnings = new BigNumber(userData?.pendingReward || 0) - - const isOldSyrup = stakingToken.symbol === tokens.syrup.symbol - const accountHasStakedBalance = stakedBalance?.toNumber() > 0 - const needsApproval = !accountHasStakedBalance && !allowance.toNumber() && !isBnbPool - const isCardActive = isFinished && accountHasStakedBalance - - const convertedLimit = new BigNumber(stakingLimit).multipliedBy(new BigNumber(10).pow(earningToken.decimals)) - const [onPresentDeposit] = useModal( - , - ) - - const [onPresentCompound] = useModal( - , - ) - const poolImage = `${pool.earningToken.symbol}-${pool.stakingToken.symbol}.svg`.toLocaleLowerCase() - const [onPresentWithdraw] = useModal( - , - ) - - const handleApprove = useCallback(async () => { - try { - setRequestedApproval(true) - const txHash = await onApprove() - // user rejected tx or didn't go thru - if (!txHash) { - setRequestedApproval(false) - } - } catch (e) { - console.error(e) - } - }, [onApprove, setRequestedApproval]) - - return ( - - {isFinished && sousId !== 0 && } -
- - {isOldSyrup && '[OLD]'} {earningToken.symbol} {TranslateString(348, 'Pool')} - -
-
- {earningToken.symbol} -
- {account && harvest && !isOldSyrup && ( - { - setPendingTx(true) - await onReward() - setPendingTx(false) - }} - /> - )} -
- {!isOldSyrup ? ( - - - {sousId === 0 && account && harvest && ( - - )} - - ) : ( - - )} -
- -
- ) -} - -const PoolFinishedSash = styled.div` - background-image: url('/images/pool-finished-sash.svg'); - background-position: top right; - background-repeat: not-repeat; - height: 135px; - position: absolute; - right: -24px; - top: -24px; - width: 135px; -` - -const StyledCardActions = styled.div` - display: flex; - justify-content: center; - margin: 16px 0; - width: 100%; - box-sizing: border-box; -` - -const BalanceAndCompound = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-direction: row; -` - -const StyledActionSpacer = styled.div` - height: ${(props) => props.theme.spacing[4]}px; - width: ${(props) => props.theme.spacing[4]}px; -` - -const StyledDetails = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; -` - -export default PoolCard diff --git a/src/views/Pools/components/PoolCard/AprRow.tsx b/src/views/Pools/components/PoolCard/AprRow.tsx new file mode 100644 index 0000000000000..dfa8b8c61f8f5 --- /dev/null +++ b/src/views/Pools/components/PoolCard/AprRow.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { Flex, Text, IconButton, useModal, CalculateIcon, Skeleton } from '@pancakeswap-libs/uikit' +import useI18n from 'hooks/useI18n' +import { getBalanceNumber } from 'utils/formatBalance' +import { getPoolApr } from 'utils/apr' +import { getAddress } from 'utils/addressHelpers' +import { useGetApiPrice } from 'state/hooks' +import Balance from 'components/Balance' +import ApyCalculatorModal from 'components/ApyCalculatorModal' +import { Pool } from 'state/types' +import { BASE_EXCHANGE_URL } from 'config' + +interface AprRowProps { + pool: Pool + stakingTokenPrice: number +} + +const AprRow: React.FC = ({ pool, stakingTokenPrice }) => { + const TranslateString = useI18n() + const { stakingToken, earningToken, totalStaked, isFinished, tokenPerBlock } = pool + + const earningTokenPrice = useGetApiPrice(earningToken.address ? getAddress(earningToken.address) : '') + const apr = getPoolApr( + stakingTokenPrice, + earningTokenPrice, + getBalanceNumber(totalStaked, stakingToken.decimals), + parseFloat(tokenPerBlock), + ) + // // special handling for tokens like tBTC or BIFI where the daily token rewards for $1000 dollars will be less than 0.001 of that token + const isHighValueToken = Math.round(earningTokenPrice / 1000) > 0 + + const apyModalLink = + stakingToken.address && + `${BASE_EXCHANGE_URL}/#/swap?outputCurrency=${stakingToken.address[process.env.REACT_APP_CHAIN_ID]}` + + const [onPresentApyModal] = useModal( + , + ) + + return ( + + {TranslateString(736, 'APR')}: + {isFinished || !apr ? ( + + ) : ( + + + + + + + )} + + ) +} + +export default AprRow diff --git a/src/views/Pools/components/PoolCard/CardActions/ApprovalAction.tsx b/src/views/Pools/components/PoolCard/CardActions/ApprovalAction.tsx new file mode 100644 index 0000000000000..b87f00dd4917b --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardActions/ApprovalAction.tsx @@ -0,0 +1,69 @@ +import React, { useState, useCallback } from 'react' +import { Button, AutoRenewIcon, Skeleton } from '@pancakeswap-libs/uikit' +import { useSousApprove } from 'hooks/useApprove' +import useI18n from 'hooks/useI18n' +import { useERC20 } from 'hooks/useContract' +import useToast from 'hooks/useToast' +import { getAddress } from 'utils/addressHelpers' +import { Pool } from 'state/types' + +interface ApprovalActionProps { + pool: Pool + isLoading?: boolean +} + +const ApprovalAction: React.FC = ({ pool, isLoading = false }) => { + const { sousId, stakingToken, earningToken, isFinished } = pool + const TranslateString = useI18n() + const stakingTokenContract = useERC20(stakingToken.address ? getAddress(stakingToken.address) : '') + const [requestedApproval, setRequestedApproval] = useState(false) + const { onApprove } = useSousApprove(stakingTokenContract, sousId) + const { toastSuccess, toastError } = useToast() + + const handleApprove = useCallback(async () => { + try { + setRequestedApproval(true) + const txHash = await onApprove() + if (txHash) { + toastSuccess( + `${TranslateString(999, 'Contract Enabled')}`, + `${TranslateString(999, `You can now stake in the ${earningToken.symbol} pool!`)}`, + ) + setRequestedApproval(false) + } else { + // user rejected tx or didn't go thru + toastError( + `${TranslateString(999, 'Error')}`, + `${TranslateString( + 999, + `Please try again. Confirm the transaction and make sure you are paying enough gas!`, + )}`, + ) + setRequestedApproval(false) + } + } catch (e) { + console.error(e) + toastError('Error', e?.message) + } + }, [onApprove, setRequestedApproval, toastSuccess, toastError, TranslateString, earningToken]) + + return ( + <> + {isLoading ? ( + + ) : ( + + )} + + ) +} + +export default ApprovalAction diff --git a/src/views/Pools/components/PoolCard/CardActions/HarvestActions.tsx b/src/views/Pools/components/PoolCard/CardActions/HarvestActions.tsx new file mode 100644 index 0000000000000..2779674f4e3f1 --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardActions/HarvestActions.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { Flex, Text, Button, Heading, useModal, Skeleton } from '@pancakeswap-libs/uikit' +import BigNumber from 'bignumber.js' +import { Token } from 'config/constants/types' +import { getAddress } from 'utils/addressHelpers' +import useI18n from 'hooks/useI18n' +import { getFullDisplayBalance, getBalanceNumber, formatNumber } from 'utils/formatBalance' +import { useGetApiPrice } from 'state/hooks' +import CollectModal from '../Modals/CollectModal' + +interface HarvestActionsProps { + earnings: BigNumber + earningToken: Token + sousId: number + isBnbPool: boolean + isLoading?: boolean +} + +const HarvestActions: React.FC = ({ + earnings, + earningToken, + sousId, + isBnbPool, + isLoading = false, +}) => { + const TranslateString = useI18n() + const earningTokenPrice = useGetApiPrice(earningToken.address ? getAddress(earningToken.address) : '') + const fullBalance = getFullDisplayBalance(earnings, earningToken.decimals) + const formattedBalance = formatNumber(getBalanceNumber(earnings, earningToken.decimals), 3, 3) + const earningsDollarValue = formatNumber( + getBalanceNumber(earnings.multipliedBy(earningTokenPrice), earningToken.decimals), + ) + const hasEarnings = earnings.toNumber() > 0 + const isCompoundPool = sousId === 0 + + const [onPresentCollect] = useModal( + , + ) + + return ( + + + + {isLoading ? ( + + ) : ( + <> + {hasEarnings ? formattedBalance : 0} + + {`~${hasEarnings ? earningsDollarValue : 0} USD`} + + + )} + + + + + + + ) +} + +export default HarvestActions diff --git a/src/views/Pools/components/PoolCard/CardActions/StakeActions.tsx b/src/views/Pools/components/PoolCard/CardActions/StakeActions.tsx new file mode 100644 index 0000000000000..683f5f725a134 --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardActions/StakeActions.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { + Flex, + Text, + Button, + IconButton, + AddIcon, + MinusIcon, + Heading, + useModal, + Skeleton, +} from '@pancakeswap-libs/uikit' +import BigNumber from 'bignumber.js' +import useI18n from 'hooks/useI18n' +import { getBalanceNumber, formatNumber, getDecimalAmount } from 'utils/formatBalance' +import { Pool } from 'state/types' +import NotEnoughTokensModal from '../Modals/NotEnoughTokensModal' +import StakeModal from '../Modals/StakeModal' + +interface StakeActionsProps { + pool: Pool + stakingTokenBalance: BigNumber + stakingTokenPrice: number + stakedBalance: BigNumber + isBnbPool: boolean + isStaked: ConstrainBoolean + isLoading?: boolean +} + +const StakeAction: React.FC = ({ + pool, + stakingTokenBalance, + stakingTokenPrice, + stakedBalance, + isBnbPool, + isStaked, + isLoading = false, +}) => { + const { stakingToken, earningToken, stakingLimit, isFinished } = pool + const TranslateString = useI18n() + const convertedLimit = getDecimalAmount(new BigNumber(stakingLimit), earningToken.decimals) + const stakingMax = + stakingLimit && stakingTokenBalance.isGreaterThan(convertedLimit) ? convertedLimit : stakingTokenBalance + const formattedBalance = formatNumber(getBalanceNumber(stakedBalance, stakingToken.decimals), 3, 3) + const stakingMaxDollarValue = formatNumber( + getBalanceNumber(stakedBalance.multipliedBy(stakingTokenPrice), stakingToken.decimals), + ) + + const [onPresentTokenRequired] = useModal() + + const [onPresentStake] = useModal( + , + ) + + const [onPresentUnstake] = useModal( + , + ) + + const renderStakeAction = () => { + return isStaked ? ( + + + {formattedBalance} + {`~${stakingMaxDollarValue || 0} USD`} + + + + + + + + + + + ) : ( + + ) + } + + return {isLoading ? : renderStakeAction()} +} + +export default StakeAction diff --git a/src/views/Pools/components/PoolCard/CardActions/index.tsx b/src/views/Pools/components/PoolCard/CardActions/index.tsx new file mode 100644 index 0000000000000..5e4c6cc1989ce --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardActions/index.tsx @@ -0,0 +1,88 @@ +import BigNumber from 'bignumber.js' +import React from 'react' +import styled from 'styled-components' +import { Flex, Text, Box } from '@pancakeswap-libs/uikit' +import useI18n from 'hooks/useI18n' +import { PoolCategory } from 'config/constants/types' +import { Pool } from 'state/types' +import ApprovalAction from './ApprovalAction' +import StakeActions from './StakeActions' +import HarvestActions from './HarvestActions' + +const InlineText = styled(Text)` + display: inline; +` + +interface CardActionsProps { + pool: Pool + stakedBalance: BigNumber + accountHasStakedBalance: boolean + stakingTokenPrice: number +} + +const CardActions: React.FC = ({ + pool, + stakedBalance, + accountHasStakedBalance, + stakingTokenPrice, +}) => { + const { sousId, stakingToken, earningToken, harvest, poolCategory, userData } = pool + // Pools using native BNB behave differently than pools using a token + const isBnbPool = poolCategory === PoolCategory.BINANCE + const TranslateString = useI18n() + const allowance = new BigNumber(userData?.allowance || 0) + const stakingTokenBalance = new BigNumber(userData?.stakingTokenBalance || 0) + const earnings = new BigNumber(userData?.pendingReward || 0) + const needsApproval = !accountHasStakedBalance && !allowance.gt(0) && !isBnbPool + const isStaked = stakedBalance.gt(0) + const isLoading = !userData + + return ( + + + {harvest && ( + <> + + + {`${earningToken.symbol} `} + + + {TranslateString(330, `earned`)} + + + + + )} + + + {isStaked ? stakingToken.symbol : TranslateString(1070, `stake`)}{' '} + + + {isStaked ? TranslateString(1074, `staked`) : `${stakingToken.symbol}`} + + + {needsApproval ? ( + + ) : ( + + )} + + + ) +} + +export default CardActions diff --git a/src/views/Pools/components/PoolCard/CardFooter/ExpandedFooter.tsx b/src/views/Pools/components/PoolCard/CardFooter/ExpandedFooter.tsx new file mode 100644 index 0000000000000..3cfbb31ba681b --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardFooter/ExpandedFooter.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import styled from 'styled-components' +import { getBalanceNumber } from 'utils/formatBalance' +import useI18n from 'hooks/useI18n' +import { Flex, MetamaskIcon, Text, LinkExternal, TimerIcon, Skeleton } from '@pancakeswap-libs/uikit' +import { BASE_BSC_SCAN_URL, BASE_URL } from 'config' +import { useBlock } from 'state/hooks' +import { Pool } from 'state/types' +import { getAddress } from 'utils/addressHelpers' +import { registerToken } from 'utils/wallet' +import Balance from 'components/Balance' + +interface ExpandedFooterProps { + pool: Pool + account: string +} + +const ExpandedWrapper = styled(Flex)` + svg { + height: 14px; + width: 14px; + } +` + +const ExpandedFooter: React.FC = ({ pool, account }) => { + const TranslateString = useI18n() + const { currentBlock } = useBlock() + const { stakingToken, earningToken, totalStaked, startBlock, endBlock, isFinished, contractAddress } = pool + + const tokenAddress = earningToken.address ? getAddress(earningToken.address) : '' + const poolContractAddress = getAddress(contractAddress) + const imageSrc = `${BASE_URL}/images/tokens/${earningToken.symbol.toLowerCase()}.png` + const isMetaMaskInScope = !!(window as WindowChain).ethereum?.isMetaMask + + const shouldShowBlockCountdown = Boolean(!isFinished && startBlock && endBlock) + const blocksUntilStart = Math.max(startBlock - currentBlock, 0) + const blocksRemaining = Math.max(endBlock - currentBlock, 0) + const hasPoolStarted = blocksUntilStart === 0 && blocksRemaining > 0 + + return ( + + + {TranslateString(999, 'Total staked:')} + + {totalStaked ? ( + <> + + + {stakingToken.symbol} + + + ) : ( + + )} + + + {shouldShowBlockCountdown && ( + + {hasPoolStarted ? TranslateString(410, 'End') : TranslateString(1212, 'Start')}: + + {blocksRemaining || blocksUntilStart ? ( + + ) : ( + + )} + + {TranslateString(999, 'blocks')} + + + + + )} + + + {TranslateString(412, 'View Project Site')} + + + {poolContractAddress && ( + + + {TranslateString(412, 'View Contract')} + + + )} + {account && isMetaMaskInScope && tokenAddress && ( + + registerToken(tokenAddress, earningToken.symbol, earningToken.decimals, imageSrc)} + > + Add to Metamask + + + + )} + + ) +} + +export default React.memo(ExpandedFooter) diff --git a/src/views/Pools/components/PoolCard/CardFooter/index.tsx b/src/views/Pools/components/PoolCard/CardFooter/index.tsx new file mode 100644 index 0000000000000..0e2f739a905d9 --- /dev/null +++ b/src/views/Pools/components/PoolCard/CardFooter/index.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import useI18n from 'hooks/useI18n' +import { Flex, CardFooter, ExpandableLabel } from '@pancakeswap-libs/uikit' +import { Pool } from 'state/types' +import ExpandedFooter from './ExpandedFooter' + +interface FooterProps { + pool: Pool + account: string +} + +const ExpandableButtonWrapper = styled(Flex)` + align-items: center; + justify-content: center; + button { + padding: 0; + } +` + +const Footer: React.FC = ({ pool, account }) => { + const TranslateString = useI18n() + const [isExpanded, setIsExpanded] = useState(false) + + return ( + + + setIsExpanded(!isExpanded)}> + {isExpanded ? TranslateString(1066, 'Hide') : TranslateString(658, 'Details')} + + + {isExpanded && } + + ) +} + +export default Footer diff --git a/src/views/Pools/components/PoolCard/Modals/CollectModal.tsx b/src/views/Pools/components/PoolCard/Modals/CollectModal.tsx new file mode 100644 index 0000000000000..90da153873f85 --- /dev/null +++ b/src/views/Pools/components/PoolCard/Modals/CollectModal.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react' +import { + Modal, + Text, + Button, + Heading, + Flex, + AutoRenewIcon, + ButtonMenu, + ButtonMenuItem, + HelpIcon, + useTooltip, + Box, +} from '@pancakeswap-libs/uikit' +import useI18n from 'hooks/useI18n' +import useTheme from 'hooks/useTheme' +import { useSousHarvest } from 'hooks/useHarvest' +import { useSousStake } from 'hooks/useStake' +import useToast from 'hooks/useToast' +import { Token } from 'config/constants/types' + +interface CollectModalProps { + formattedBalance: string + fullBalance: string + earningToken: Token + earningsDollarValue: string + sousId: number + isBnbPool: boolean + isCompoundPool?: boolean + onDismiss?: () => void +} + +const CollectModal: React.FC = ({ + formattedBalance, + fullBalance, + earningToken, + earningsDollarValue, + sousId, + isBnbPool, + isCompoundPool = false, + onDismiss, +}) => { + const TranslateString = useI18n() + const { theme } = useTheme() + const { toastSuccess, toastError } = useToast() + const { onReward } = useSousHarvest(sousId, isBnbPool) + const { onStake } = useSousStake(sousId, isBnbPool) + const [pendingTx, setPendingTx] = useState(false) + const [shouldCompound, setShouldCompound] = useState(isCompoundPool) + const { targetRef, tooltip, tooltipVisible } = useTooltip( + <> + {TranslateString(999, 'Compound: collect and restake CAKE into pool.')} + {TranslateString(999, 'Harvest: collect CAKE and send to wallet')} + , + 'bottom-end', + 'hover', + undefined, + undefined, + [20, 10], + ) + + const handleHarvestConfirm = async () => { + setPendingTx(true) + // compounding + if (shouldCompound) { + try { + await onStake(fullBalance, earningToken.decimals) + toastSuccess( + `${TranslateString(999, 'Compounded')}!`, + TranslateString(999, `Your ${earningToken.symbol} earnings have been re-invested into the pool!`), + ) + setPendingTx(false) + onDismiss() + } catch (e) { + toastError( + TranslateString(999, 'Canceled'), + TranslateString(999, 'Please try again and confirm the transaction.'), + ) + setPendingTx(false) + } + } else { + // harvesting + try { + await onReward() + toastSuccess( + `${TranslateString(999, 'Harvested')}!`, + TranslateString(999, `Your ${earningToken.symbol} earnings have been sent to your wallet!`), + ) + setPendingTx(false) + onDismiss() + } catch (e) { + toastError( + TranslateString(999, 'Canceled'), + TranslateString(999, 'Please try again and confirm the transaction.'), + ) + setPendingTx(false) + } + } + } + + return ( + + {isCompoundPool && ( + + setShouldCompound(!index)} + > + {TranslateString(704, 'Compound')} + {TranslateString(562, 'Harvest')} + + + + + {tooltipVisible && tooltip} + + )} + + + {shouldCompound ? TranslateString(999, 'Compounding') : TranslateString(999, 'Harvesting')}: + + + {formattedBalance} {earningToken.symbol} + + {`~${earningsDollarValue || 0} USD`} + + + + + + + ) +} + +export default CollectModal diff --git a/src/views/Pools/components/PoolCard/Modals/NotEnoughTokensModal.tsx b/src/views/Pools/components/PoolCard/Modals/NotEnoughTokensModal.tsx new file mode 100644 index 0000000000000..8a3533696e690 --- /dev/null +++ b/src/views/Pools/components/PoolCard/Modals/NotEnoughTokensModal.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import useI18n from 'hooks/useI18n' +import styled from 'styled-components' +import { Modal, Text, Button, OpenNewIcon, Link } from '@pancakeswap-libs/uikit' +import { BASE_EXCHANGE_URL } from 'config' +import useTheme from 'hooks/useTheme' + +interface NotEnoughTokensModalProps { + tokenSymbol: string + onDismiss?: () => void +} + +const StyledLink = styled(Link)` + width: 100%; +` + +const NotEnoughTokensModal: React.FC = ({ tokenSymbol, onDismiss }) => { + const TranslateString = useI18n() + const { theme } = useTheme() + + return ( + + + {TranslateString(999, 'Insufficient')} {tokenSymbol} {TranslateString(1120, 'balance')} + + {TranslateString(999, `You’ll need ${tokenSymbol} to stake in this pool!`)} + + {TranslateString(999, `Buy some ${tokenSymbol}, or make sure your ${tokenSymbol} isn’t in another pool or LP.`)} + + + + + + + + ) +} + +export default NotEnoughTokensModal diff --git a/src/views/Pools/components/PoolCard/Modals/PercentageButton.tsx b/src/views/Pools/components/PoolCard/Modals/PercentageButton.tsx new file mode 100644 index 0000000000000..7178dbccbe40a --- /dev/null +++ b/src/views/Pools/components/PoolCard/Modals/PercentageButton.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import styled from 'styled-components' +import { Button } from '@pancakeswap-libs/uikit' + +interface PercentageButtonProps { + onClick: () => void +} + +const StyledButton = styled(Button)` + flex-grow: 1; +` + +const PercentageButton: React.FC = ({ children, onClick }) => { + return ( + + {children} + + ) +} + +export default PercentageButton diff --git a/src/views/Pools/components/PoolCard/Modals/StakeModal.tsx b/src/views/Pools/components/PoolCard/Modals/StakeModal.tsx new file mode 100644 index 0000000000000..2dfb3d3ecfac1 --- /dev/null +++ b/src/views/Pools/components/PoolCard/Modals/StakeModal.tsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { Modal, Text, Flex, Image, Button, Slider, BalanceInput, AutoRenewIcon, Link } from '@pancakeswap-libs/uikit' +import useI18n from 'hooks/useI18n' +import { BASE_EXCHANGE_URL } from 'config' +import { useSousStake } from 'hooks/useStake' +import { useSousUnstake } from 'hooks/useUnstake' +import useTheme from 'hooks/useTheme' +import useToast from 'hooks/useToast' +import BigNumber from 'bignumber.js' +import { getFullDisplayBalance, formatNumber, getDecimalAmount } from 'utils/formatBalance' +import { Pool } from 'state/types' +import PercentageButton from './PercentageButton' + +interface StakeModalProps { + isBnbPool: boolean + pool: Pool + stakingMax: BigNumber + stakingTokenPrice: number + isRemovingStake?: boolean + onDismiss?: () => void +} + +const StyledLink = styled(Link)` + width: 100%; +` + +const StakeModal: React.FC = ({ + isBnbPool, + pool, + stakingMax, + stakingTokenPrice, + isRemovingStake = false, + onDismiss, +}) => { + const { sousId, stakingToken, earningToken } = pool + const TranslateString = useI18n() + const { theme } = useTheme() + + const { onStake } = useSousStake(sousId, isBnbPool) + const { onUnstake } = useSousUnstake(sousId) + const { toastSuccess, toastError } = useToast() + + const [pendingTx, setPendingTx] = useState(false) + const [stakeAmount, setStakeAmount] = useState('') + const [percent, setPercent] = useState(0) + + const usdValueStaked = stakeAmount && formatNumber(new BigNumber(stakeAmount).times(stakingTokenPrice).toNumber()) + + const handleStakeInputChange = (event: React.ChangeEvent) => { + const inputValue = event.target.value ? event.target.value : '0' + const convertedInput = getDecimalAmount(new BigNumber(inputValue), stakingToken.decimals) + const percentage = Math.floor(convertedInput.dividedBy(stakingMax).multipliedBy(100).toNumber()) + setStakeAmount(inputValue) + setPercent(Math.min(percentage, 100)) + } + + const handleChangePercent = (sliderPercent: number) => { + const percentageOfStakingMax = stakingMax.dividedBy(100).multipliedBy(sliderPercent) + const amountToStake = getFullDisplayBalance(percentageOfStakingMax, stakingToken.decimals, stakingToken.decimals) + setStakeAmount(amountToStake) + setPercent(sliderPercent) + } + + const handleConfirmClick = async () => { + setPendingTx(true) + + if (isRemovingStake) { + // unstaking + try { + await onUnstake(stakeAmount, stakingToken.decimals) + toastSuccess( + `${TranslateString(999, 'Unstaked')}!`, + TranslateString(999, `Your ${earningToken.symbol} earnings have also been harvested to your wallet!`), + ) + setPendingTx(false) + onDismiss() + } catch (e) { + toastError( + TranslateString(999, 'Canceled'), + TranslateString(999, 'Please try again and confirm the transaction.'), + ) + setPendingTx(false) + } + } else { + try { + // staking + await onStake(stakeAmount, stakingToken.decimals) + toastSuccess( + `${TranslateString(1074, 'Staked')}!`, + TranslateString(999, `Your ${stakingToken.symbol} funds have been staked in the pool!`), + ) + setPendingTx(false) + onDismiss() + } catch (e) { + toastError( + TranslateString(999, 'Canceled'), + TranslateString(999, 'Please try again and confirm the transaction.'), + ) + setPendingTx(false) + } + } + } + + return ( + + + {isRemovingStake ? TranslateString(588, 'Unstake') : TranslateString(316, 'Stake')}: + + {stakingToken.symbol} + + {stakingToken.symbol} + + + + + + Balance: {getFullDisplayBalance(stakingMax, stakingToken.decimals)} + + + + handleChangePercent(25)}>25% + handleChangePercent(50)}>50% + handleChangePercent(75)}>75% + handleChangePercent(100)}>MAX + + + {!isRemovingStake && ( + + + + )} + + ) +} + +export default StakeModal diff --git a/src/views/Pools/components/Card.tsx b/src/views/Pools/components/PoolCard/StyledCard.tsx similarity index 57% rename from src/views/Pools/components/Card.tsx rename to src/views/Pools/components/PoolCard/StyledCard.tsx index f24d465933712..425f244a09ece 100644 --- a/src/views/Pools/components/Card.tsx +++ b/src/views/Pools/components/PoolCard/StyledCard.tsx @@ -1,18 +1,25 @@ import styled from 'styled-components' +import { Card } from '@pancakeswap-libs/uikit' -const Card = styled.div<{ isActive?: boolean; isFinished?: boolean }>` +const StyledCard = styled(Card)<{ isStaking?: boolean; isFinished?: boolean }>` + max-width: 352px; + margin: 0 8px 24px; background: ${(props) => props.theme.card.background}; box-shadow: 0px 2px 12px -8px rgba(25, 19, 38, 0.1), 0px 1px 1px rgba(25, 19, 38, 0.05); border-radius: 32px; display: flex; color: ${({ isFinished, theme }) => theme.colors[isFinished ? 'textDisabled' : 'secondary']}; - box-shadow: ${({ isActive }) => - isActive - ? '0px 0px 0px 1px #0098A1, 0px 0px 4px 8px rgba(31, 199, 212, 0.4);' + box-shadow: ${({ isStaking }) => + isStaking + ? '0px 0px 0px 2px #53DEE9;' : '0px 2px 12px -8px rgba(25, 19, 38, 0.1), 0px 1px 1px rgba(25, 19, 38, 0.05)'}; flex-direction: column; align-self: baseline; position: relative; + + ${({ theme }) => theme.mediaQueries.sm} { + margin: 0 12px 46px; + } ` -export default Card +export default StyledCard diff --git a/src/views/Pools/components/PoolCard/StyledCardHeader.tsx b/src/views/Pools/components/PoolCard/StyledCardHeader.tsx new file mode 100644 index 0000000000000..de44dac784449 --- /dev/null +++ b/src/views/Pools/components/PoolCard/StyledCardHeader.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { CardHeader, Heading, Text, Flex, Image } from '@pancakeswap-libs/uikit' +import styled from 'styled-components' + +const Wrapper = styled(CardHeader)<{ isFinished?: boolean; activeBackground?: string }>` + background: ${({ isFinished, activeBackground, theme }) => + isFinished ? theme.colors.backgroundDisabled : theme.colors.gradients[activeBackground]}; +` + +const StyledCardHeader: React.FC<{ + earningTokenSymbol: string + stakingTokenSymbol: string + isFinished?: boolean +}> = ({ earningTokenSymbol, stakingTokenSymbol, isFinished = false }) => { + const poolImageSrc = `${earningTokenSymbol}-${stakingTokenSymbol}.svg`.toLocaleLowerCase() + const isPromoted = earningTokenSymbol === 'CAKE' && stakingTokenSymbol === 'CAKE' + const activeBackground = isPromoted ? 'bubblegum' : 'cardHeader' + + return ( + + + + + Earn {earningTokenSymbol} + + Stake {stakingTokenSymbol} + + {earningTokenSymbol} + + + ) +} + +export default StyledCardHeader diff --git a/src/views/Pools/components/PoolCard/index.tsx b/src/views/Pools/components/PoolCard/index.tsx new file mode 100644 index 0000000000000..2a8d2a08fb336 --- /dev/null +++ b/src/views/Pools/components/PoolCard/index.tsx @@ -0,0 +1,58 @@ +import BigNumber from 'bignumber.js' +import React from 'react' +import { CardBody, Flex, Text, CardRibbon } from '@pancakeswap-libs/uikit' +import UnlockButton from 'components/UnlockButton' +import useI18n from 'hooks/useI18n' +import { getAddress } from 'utils/addressHelpers' +import { useGetApiPrice } from 'state/hooks' +import { Pool } from 'state/types' +import AprRow from './AprRow' +import StyledCard from './StyledCard' +import CardFooter from './CardFooter' +import StyledCardHeader from './StyledCardHeader' +import CardActions from './CardActions' + +const PoolCard: React.FC<{ pool: Pool; account: string }> = ({ pool, account }) => { + const { sousId, stakingToken, earningToken, isFinished, userData } = pool + const TranslateString = useI18n() + const stakedBalance = new BigNumber(userData?.stakedBalance || 0) + const accountHasStakedBalance = stakedBalance?.toNumber() > 0 + const stakingTokenPrice = useGetApiPrice(stakingToken.address ? getAddress(stakingToken.address) : '') + + return ( + } + > + + + + + {account ? ( + + ) : ( + <> + + {TranslateString(999, 'Start earning')} + + + + )} + + + + + ) +} + +export default PoolCard diff --git a/src/views/Pools/components/PoolTabButtons.tsx b/src/views/Pools/components/PoolTabButtons.tsx new file mode 100644 index 0000000000000..6e4d5e0a4edb5 --- /dev/null +++ b/src/views/Pools/components/PoolTabButtons.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import styled from 'styled-components' +import { useRouteMatch, Link } from 'react-router-dom' +import { + ButtonMenu, + ButtonMenuItem, + Button, + HelpIcon, + Toggle, + Text, + Flex, + NotificationDot, + Link as UiKitLink, +} from '@pancakeswap-libs/uikit' +import useI18n from 'hooks/useI18n' + +const ButtonText = styled(Text)` + display: none; + ${({ theme }) => theme.mediaQueries.lg} { + display: block; + } +` + +const StyledLink = styled(UiKitLink)` + width: 100%; +` + +const PoolTabButtons = ({ stakedOnly, setStakedOnly, hasStakeInFinishedPools }) => { + const { url, isExact } = useRouteMatch() + const TranslateString = useI18n() + + return ( + + + + + {TranslateString(1198, 'Live')} + + + + {TranslateString(388, 'Finished')} + + + + + setStakedOnly((prev) => !prev)} /> + + {TranslateString(999, 'Staked only')} + + + + + + + + + + ) +} + +export default PoolTabButtons diff --git a/src/views/Pools/components/PoolTabButtons/index.tsx b/src/views/Pools/components/PoolTabButtons/index.tsx deleted file mode 100644 index 45e787b50b5fc..0000000000000 --- a/src/views/Pools/components/PoolTabButtons/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import { useRouteMatch, Link } from 'react-router-dom' -import { ButtonMenu, ButtonMenuItem, Toggle, Text, Flex, NotificationDot } from '@pancakeswap-libs/uikit' -import useI18n from 'hooks/useI18n' - -const PoolTabButtons = ({ stakedOnly, setStakedOnly, hasStakeInFinishedPools }) => { - const { url, isExact } = useRouteMatch() - const TranslateString = useI18n() - - return ( - - - - {TranslateString(1198, 'Live')} - - - - {TranslateString(388, 'Finished')} - - - - - setStakedOnly(!stakedOnly)} /> - - {TranslateString(999, 'Staked only')} - - - - ) -} - -export default PoolTabButtons diff --git a/src/views/Pools/components/WithdrawModal.tsx b/src/views/Pools/components/WithdrawModal.tsx deleted file mode 100644 index 85ab026373963..0000000000000 --- a/src/views/Pools/components/WithdrawModal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import BigNumber from 'bignumber.js' -import React, { useCallback, useMemo, useState } from 'react' -import { Button, Modal } from '@pancakeswap-libs/uikit' -import ModalActions from 'components/ModalActions' -import TokenInput from 'components/TokenInput' -import useI18n from 'hooks/useI18n' -import { getFullDisplayBalance } from 'utils/formatBalance' - -interface WithdrawModalProps { - max: BigNumber - onConfirm: (amount: string, decimals: number) => void - onDismiss?: () => void - tokenName?: string - stakingTokenDecimals?: number -} - -const WithdrawModal: React.FC = ({ - onConfirm, - onDismiss, - max, - tokenName = '', - stakingTokenDecimals = 18, -}) => { - const [val, setVal] = useState('') - const [pendingTx, setPendingTx] = useState(false) - const TranslateString = useI18n() - const fullBalance = useMemo(() => { - return getFullDisplayBalance(max, stakingTokenDecimals) - }, [max, stakingTokenDecimals]) - - const handleChange = useCallback( - (e: React.FormEvent) => { - setVal(e.currentTarget.value) - }, - [setVal], - ) - - const handleSelectMax = useCallback(() => { - setVal(fullBalance) - }, [fullBalance, setVal]) - - return ( - - - - - - - - ) -} - -export default WithdrawModal diff --git a/src/views/Pools/components/warningTag.tsx b/src/views/Pools/components/warningTag.tsx deleted file mode 100644 index f60e6cb573bcf..0000000000000 --- a/src/views/Pools/components/warningTag.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' - -const Icon = () => ( - - - -) - -const WarningTag = () => - -export default WarningTag diff --git a/src/views/Pools/index.ts b/src/views/Pools/index.ts deleted file mode 100644 index d03b7bc86c530..0000000000000 --- a/src/views/Pools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Syrup' diff --git a/src/views/Pools/Syrup.tsx b/src/views/Pools/index.tsx similarity index 69% rename from src/views/Pools/Syrup.tsx rename to src/views/Pools/index.tsx index a1f999c368e50..ded9d5a782fb6 100644 --- a/src/views/Pools/Syrup.tsx +++ b/src/views/Pools/index.tsx @@ -1,35 +1,20 @@ import React, { useMemo } from 'react' import { Route, useRouteMatch } from 'react-router-dom' import BigNumber from 'bignumber.js' -import styled from 'styled-components' import { useWeb3React } from '@web3-react/core' -import { Heading, Flex, Button, Text, HelpIcon, Link } from '@pancakeswap-libs/uikit' +import { Heading, Flex, Image } from '@pancakeswap-libs/uikit' import orderBy from 'lodash/orderBy' import partition from 'lodash/partition' import useI18n from 'hooks/useI18n' import usePersistState from 'hooks/usePersistState' import { usePools, useBlock } from 'state/hooks' -import PageHeader from 'components/PageHeader' import FlexLayout from 'components/layout/Flex' import Page from 'components/layout/Page' -import Coming from './components/Coming' +import PageHeader from 'components/PageHeader' import PoolCard from './components/PoolCard' import PoolTabButtons from './components/PoolTabButtons' -import Divider from './components/Divider' - -const ButtonText = styled(Text)` - display: none; - ${({ theme }) => theme.mediaQueries.lg} { - display: block; - } -` -const StyledLink = styled(Link)` - display: inline; - height: fit-content; -` - -const Syrup: React.FC = () => { +const Pools: React.FC = () => { const { path } = useRouteMatch() const TranslateString = useI18n() const { account } = useWeb3React() @@ -65,16 +50,6 @@ const Syrup: React.FC = () => { {TranslateString(999, 'High APR, low risk.')} - - - - - @@ -83,25 +58,35 @@ const Syrup: React.FC = () => { setStakedOnly={setStakedOnly} hasStakeInFinishedPools={hasStakeInFinishedPools} /> - <> {stakedOnly - ? orderBy(stakedOnlyPools, ['sortOrder']).map((pool) => ) - : orderBy(openPools, ['sortOrder']).map((pool) => )} - + ? orderBy(stakedOnlyPools, ['sortOrder']).map((pool) => ( + + )) + : orderBy(openPools, ['sortOrder']).map((pool) => ( + + ))} {orderBy(finishedPools, ['sortOrder']).map((pool) => ( - + ))} + Pancake illustration ) } -export default Syrup +export default Pools