diff --git a/src/components/AccordionContent.tsx b/src/components/AccordionContent.tsx index c54d432d..8adb021e 100644 --- a/src/components/AccordionContent.tsx +++ b/src/components/AccordionContent.tsx @@ -12,29 +12,39 @@ export interface Item { title: string renderContent: () => React.ReactNode isOpen?: boolean + subTitle?: string | React.ReactNode toggleOpen: (index: number) => void } export default function AccordionContent(props: Props) { + const { title, renderContent, isOpen, subTitle, toggleOpen } = props.item + + const shouldShowSubTitle = subTitle && !isOpen + return ( -
+
props.item.toggleOpen(props.index)} + onClick={() => toggleOpen(props.index)} className={classNames( 'mb-0 flex cursor-pointer items-center justify-between border-t border-white/10 bg-white/10 p-4 text-white', 'group-[&:first-child]:border-t-0 group-[[open]]:border-b', '[&::marker]:hidden [&::marker]:content-[""]', - props.item.isOpen && 'border-b [&:first-child]:border-t-0', + isOpen && 'border-b [&:first-child]:border-t-0', )} > - {props.item.title} +
+ {title} + {shouldShowSubTitle && ( + + {subTitle} + + )} +
- {props.item.isOpen ? : } + {isOpen ? : }
- {props.item.isOpen && ( -
{props.item.renderContent()}
- )} + {isOpen &&
{renderContent()}
}
) } diff --git a/src/components/Account/AccountDetails.tsx b/src/components/Account/AccountDetails.tsx index dc5c5e9f..32665d52 100644 --- a/src/components/Account/AccountDetails.tsx +++ b/src/components/Account/AccountDetails.tsx @@ -15,7 +15,7 @@ export default function AccountDetails() { className='fixed right-4 top-[89px] w-16 rounded-base border border-white/20 bg-white/5 backdrop-blur-sticky' >
- } /> + } /> Health diff --git a/src/components/Gauge.tsx b/src/components/Gauge.tsx index dde19b0b..fe1fe7d8 100644 --- a/src/components/Gauge.tsx +++ b/src/components/Gauge.tsx @@ -3,33 +3,42 @@ import { ReactElement, ReactNode } from 'react' import { Tooltip } from 'components/Tooltip' import useStore from 'store' +import { FormattedNumber } from 'components/FormattedNumber' interface Props { tooltip: string | ReactNode + strokeColor?: string strokeWidth?: number background?: string diameter?: number - value: number - label?: string + percentage: number + labelClassName?: string icon?: ReactElement } export const Gauge = ({ background = '#FFFFFF22', + strokeColor, + strokeWidth = 4, diameter = 40, - value = 0, + percentage = 0, tooltip, icon, + labelClassName, }: Props) => { const enableAnimations = useStore((s) => s.enableAnimations) const radius = 16 - const percentage = value * 100 const percentageValue = percentage > 100 ? 100 : percentage < 0 ? 0 : percentage const circlePercent = 100 - percentageValue return ( -
+
- - - - - + {!strokeColor && ( + + + + + + )} )} +
) diff --git a/src/components/Modals/vault/VaultDeposit.tsx b/src/components/Modals/vault/VaultDeposit.tsx index a24766b4..4a1d8a04 100644 --- a/src/components/Modals/vault/VaultDeposit.tsx +++ b/src/components/Modals/vault/VaultDeposit.tsx @@ -1,10 +1,10 @@ import BigNumber from 'bignumber.js' -import { useState } from 'react' +import { useMemo, useState } from 'react' import Button from 'components/Button' +import DisplayCurrency from 'components/DisplayCurrency' import Divider from 'components/Divider' -import { FormattedNumber } from 'components/FormattedNumber' -import { ArrowRight } from 'components/Icons' +import { ArrowRight, ExclamationMarkCircled } from 'components/Icons' import Slider from 'components/Slider' import Switch from 'components/Switch' import Text from 'components/Text' @@ -12,89 +12,117 @@ import TokenInput from 'components/TokenInput' import usePrice from 'hooks/usePrice' import { getAmount } from 'utils/accounts' import { BN } from 'utils/helpers' +import { Gauge } from 'components/Gauge' +import useStore from 'store' interface Props { + primaryAmount: BigNumber + secondaryAmount: BigNumber primaryAsset: Asset secondaryAsset: Asset account: Account - onChangeDeposits: (deposits: Map) => void + isCustomRatio: boolean + onChangeIsCustomRatio: (isCustomRatio: boolean) => void + onChangePrimaryAmount: (amount: BigNumber) => void + onChangeSecondaryAmount: (amount: BigNumber) => void toggleOpen: (index: number) => void } export default function VaultDeposit(props: Props) { - const [isCustomAmount, setIsCustomAmount] = useState(false) - const [percentage, setPercentage] = useState(0) - const [deposits, setDeposits] = useState>(new Map()) + const baseCurrency = useStore((s) => s.baseCurrency) const availablePrimaryAmount = getAmount(props.primaryAsset.denom, props.account.deposits) const availableSecondaryAmount = getAmount(props.secondaryAsset.denom, props.account.deposits) const primaryPrice = usePrice(props.primaryAsset.denom) const secondaryPrice = usePrice(props.secondaryAsset.denom) - const maxAssetValueNonCustom = BN( - Math.min(availablePrimaryAmount.toNumber(), availableSecondaryAmount.toNumber()), + const primaryValue = useMemo( + () => props.primaryAmount.times(primaryPrice), + [props.primaryAmount, primaryPrice], ) - const primaryMax = isCustomAmount - ? availablePrimaryAmount - : maxAssetValueNonCustom.dividedBy(primaryPrice) - const secondaryMax = isCustomAmount - ? availableSecondaryAmount - : maxAssetValueNonCustom.dividedBy(secondaryPrice) + const secondaryValue = useMemo( + () => props.secondaryAmount.times(secondaryPrice), + [props.secondaryAmount, secondaryPrice], + ) + const totalValue = useMemo( + () => primaryValue.plus(secondaryValue), + [primaryValue, secondaryValue], + ) + + const primaryValuePercentage = useMemo( + () => primaryValue.div(totalValue).times(100).decimalPlaces(2).toNumber() || 50, + [primaryValue, totalValue], + ) + const secondaryValuePercentage = useMemo( + () => new BigNumber(100).minus(primaryValuePercentage).decimalPlaces(2).toNumber() || 50, + [primaryValuePercentage], + ) + + const maxAssetValueNonCustom = useMemo( + () => + BN( + Math.min( + availablePrimaryAmount.times(primaryPrice).toNumber(), + availableSecondaryAmount.times(secondaryPrice).toNumber(), + ), + ), + [availablePrimaryAmount, primaryPrice, availableSecondaryAmount, secondaryPrice], + ) + const primaryMax = useMemo( + () => + props.isCustomRatio ? availablePrimaryAmount : maxAssetValueNonCustom.dividedBy(primaryPrice), + [props.isCustomRatio, availablePrimaryAmount, primaryPrice, maxAssetValueNonCustom], + ) + const secondaryMax = useMemo( + () => + props.isCustomRatio + ? availableSecondaryAmount + : maxAssetValueNonCustom.dividedBy(secondaryPrice), + [props.isCustomRatio, availableSecondaryAmount, secondaryPrice, maxAssetValueNonCustom], + ) + + const [percentage, setPercentage] = useState( + primaryValue.dividedBy(maxAssetValueNonCustom).times(100).decimalPlaces(0).toNumber(), + ) + const disableInput = + (availablePrimaryAmount.isZero() || availableSecondaryAmount.isZero()) && !props.isCustomRatio function handleSwitch() { - const isCustomAmountNew = !isCustomAmount - if (!isCustomAmountNew) { - setDeposits((deposits) => { - deposits.clear() - return new Map(deposits) - }) + const isCustomRatioNew = !props.isCustomRatio + if (!isCustomRatioNew) { + props.onChangePrimaryAmount(BN(0)) + props.onChangeSecondaryAmount(BN(0)) setPercentage(0) } - setIsCustomAmount(isCustomAmountNew) + props.onChangeIsCustomRatio(isCustomRatioNew) } function onChangePrimaryDeposit(amount: BigNumber) { - onChangeDeposit(props.primaryAsset.denom, amount) - if (!isCustomAmount) { - onChangeDeposit( - props.secondaryAsset.denom, - secondaryMax.multipliedBy(amount.dividedBy(primaryMax)), - ) + if (amount.isGreaterThan(primaryMax)) { + amount = primaryMax + } + props.onChangePrimaryAmount(amount) + setPercentage(amount.dividedBy(primaryMax).times(100).decimalPlaces(0).toNumber()) + if (!props.isCustomRatio) { + props.onChangeSecondaryAmount(secondaryMax.multipliedBy(amount.dividedBy(primaryMax))) } } function onChangeSecondaryDeposit(amount: BigNumber) { - onChangeDeposit(props.secondaryAsset.denom, amount) - if (!isCustomAmount) { - onChangeDeposit( - props.primaryAsset.denom, - primaryMax.multipliedBy(amount.dividedBy(secondaryMax)), - ) + if (amount.isGreaterThan(secondaryMax)) { + amount = secondaryMax } - } - - function onChangeDeposit(denom: string, amount: BigNumber) { - if (amount.isZero()) { - return setDeposits((deposits) => { - deposits.delete(denom) - return new Map(deposits) - }) + props.onChangeSecondaryAmount(amount) + setPercentage(amount.dividedBy(secondaryMax).times(100).decimalPlaces(0).toNumber()) + if (!props.isCustomRatio) { + props.onChangePrimaryAmount(primaryMax.multipliedBy(amount.dividedBy(secondaryMax))) } - - setDeposits((deposits) => { - deposits.set(denom, amount) - props.onChangeDeposits(deposits) - return new Map(deposits) - }) } function onChangeSlider(value: number) { setPercentage(value) - setDeposits((deposits) => { - deposits.set(props.primaryAsset.denom, primaryMax.multipliedBy(value / 100)) - deposits.set(props.secondaryAsset.denom, secondaryMax.multipliedBy(value / 100)) - return new Map(deposits) - }) + props.onChangePrimaryAmount(primaryMax.multipliedBy(value / 100)) + props.onChangeSecondaryAmount(secondaryMax.multipliedBy(value / 100)) } function getWarningText(asset: Asset) { @@ -102,39 +130,89 @@ export default function VaultDeposit(props: Props) { } return ( -
- - {!isCustomAmount && } - - -
- Custom amount - +
+
+
+ +
+ +
+
+ + {!props.isCustomRatio && ( + + )} + +
-
- {`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Position Value`} - +
+ {disableInput ? ( +
+ +
+
+ +
+ + You currently have little to none of one asset. Toggle custom ratio to supply your + assets asymmetrically. + +
+ +
+ ) : ( + + )} + +
+ Custom ratio + +
+
+ {`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Deposit Value`} + +
+
-
) } diff --git a/src/components/Modals/vault/VaultDepositSubTitle.tsx b/src/components/Modals/vault/VaultDepositSubTitle.tsx new file mode 100644 index 00000000..d0bda4b9 --- /dev/null +++ b/src/components/Modals/vault/VaultDepositSubTitle.tsx @@ -0,0 +1,49 @@ +import BigNumber from 'bignumber.js' + +import DisplayCurrency from 'components/DisplayCurrency' +import usePrice from 'hooks/usePrice' +import useStore from 'store' +import { formatAmountWithSymbol } from 'utils/formatters' + +interface Props { + primaryAmount: BigNumber + secondaryAmount: BigNumber + primaryAsset: Asset + secondaryAsset: Asset +} + +export default function VaultDepositSubTitle(props: Props) { + const baseCurrency = useStore((s) => s.baseCurrency) + const primaryPrice = usePrice(props.primaryAsset.denom) + const secondaryPrice = usePrice(props.secondaryAsset.denom) + const primaryText = formatAmountWithSymbol({ + denom: props.primaryAsset.denom, + amount: props.primaryAmount.toString(), + }) + const secondaryText = formatAmountWithSymbol({ + denom: props.secondaryAsset.denom, + amount: props.secondaryAmount.toString(), + }) + + const positionValue = props.primaryAmount + .times(primaryPrice) + .plus(props.secondaryAmount.times(secondaryPrice)) + .toNumber() + + const showPrimaryText = !props.primaryAmount.isZero() + const showSecondaryText = !props.secondaryAmount.isZero() + + return ( + <> + {showPrimaryText && primaryText} + {showPrimaryText && showSecondaryText && ' + '} + {showSecondaryText && secondaryText} + {(showPrimaryText || showSecondaryText) && ( + <> + {` = `} + + + )} + + ) +} diff --git a/src/components/Modals/vault/VaultModalContent.tsx b/src/components/Modals/vault/VaultModalContent.tsx index 45108289..e926e2b8 100644 --- a/src/components/Modals/vault/VaultModalContent.tsx +++ b/src/components/Modals/vault/VaultModalContent.tsx @@ -1,12 +1,13 @@ import BigNumber from 'bignumber.js' -import { useState } from 'react' +import { useCallback, useState } from 'react' import Accordion from 'components/Accordion' import AccountSummary from 'components/Account/AccountSummary' +import VaultBorrowings from 'components/Modals/vault/VaultBorrowings' +import VaultDeposit from 'components/Modals/vault/VaultDeposit' +import VaultDepositSubTitle from 'components/Modals/vault/VaultDepositSubTitle' import useIsOpenArray from 'hooks/useIsOpenArray' - -import VaultDeposit from './VaultDeposit' -import VaultBorrowings from './VaultBorrowings' +import { BN } from 'utils/helpers' interface Props { vault: Vault @@ -17,7 +18,23 @@ interface Props { export default function VaultModalContent(props: Props) { const [isOpen, toggleOpen] = useIsOpenArray(2, false) - const [deposits, setDeposits] = useState>(new Map()) + const [primaryAmount, setPrimaryAmount] = useState(BN(0)) + const [secondaryAmount, setSecondaryAmount] = useState(BN(0)) + const [isCustomRatio, setIsCustomRatio] = useState(false) + + const onChangePrimaryAmount = useCallback( + (amount: BigNumber) => setPrimaryAmount(amount), + [setPrimaryAmount], + ) + const onChangeSecondaryAmount = useCallback( + (amount: BigNumber) => setSecondaryAmount(amount), + [setSecondaryAmount], + ) + + const onChangeIsCustomRatio = useCallback( + (isCustomRatio: boolean) => setIsCustomRatio(isCustomRatio), + [setIsCustomRatio], + ) return (
@@ -26,14 +43,27 @@ export default function VaultModalContent(props: Props) { { renderContent: () => ( setDeposits(deposits)} + primaryAmount={primaryAmount} + secondaryAmount={secondaryAmount} + onChangePrimaryAmount={onChangePrimaryAmount} + onChangeSecondaryAmount={onChangeSecondaryAmount} primaryAsset={props.primaryAsset} secondaryAsset={props.secondaryAsset} account={props.account} toggleOpen={toggleOpen} + isCustomRatio={isCustomRatio} + onChangeIsCustomRatio={onChangeIsCustomRatio} /> ), title: 'Deposit', + subTitle: ( + + ), isOpen: isOpen[0], toggleOpen: (index: number) => toggleOpen(index), }, diff --git a/src/components/NumberInput.tsx b/src/components/NumberInput.tsx index 1006c406..1eec0951 100644 --- a/src/components/NumberInput.tsx +++ b/src/components/NumberInput.tsx @@ -25,16 +25,18 @@ interface Props { export default function NumberInput(props: Props) { const inputRef = React.useRef(null) const cursorRef = React.useRef(0) - // const max = props.max ? demagnify(props.max, props.asset) : undefined const [formattedAmount, setFormattedAmount] = useState( props.amount.shiftedBy(-1 * props.asset.decimals).toString(), ) useEffect(() => { + if (props.amount.isZero()) return setFormattedAmount('') + setFormattedAmount( formatValue(props.amount.toNumber(), { decimals: props.asset.decimals, + minDecimals: 0, maxDecimals: props.asset.decimals, thousandSeparator: false, }), @@ -95,6 +97,11 @@ export default function NumberInput(props: Props) { const isTooLong = props.maxLength !== undefined && numberCount > props.maxLength const exceedsMaxDecimals = props.maxDecimals !== undefined && decimals > props.maxDecimals + if (formattedAmount === '') { + updateValues('0', BN(0)) + return + } + if (isNegative && !props.allowNegative) return if (isSeparator && formattedAmount.length === 1) { @@ -140,7 +147,7 @@ export default function NumberInput(props: Props) { onInputChange(e.target.value)} onBlur={props.onBlur} diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx index a4daac11..e4dc242a 100644 --- a/src/components/Slider.tsx +++ b/src/components/Slider.tsx @@ -83,7 +83,7 @@ export default function Slider(props: Props) { className={classNames( 'relative min-h-3 w-full transition-opacity', props.className, - props.disabled && 'pointer-events-none opacity-50', + props.disabled && 'pointer-events-none', )} onMouseEnter={handleSliderRect} > @@ -95,15 +95,40 @@ export default function Slider(props: Props) { className='absolute z-2 w-full cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none' />
- + - + - + - + - +
void + disabled?: boolean } function Mark(props: MarkProps) { @@ -148,6 +174,7 @@ function Mark(props: MarkProps) { className={`z-20 h-3 w-3 rotate-45 rounded-xs border-[1px] border-white/20 hover:border-[2px] hover:border-white ${ props.sliderValue >= props.value ? 'bg-martian-red hover:border-white' : 'bg-grey-medium' }`} + disabled={props.disabled} > ) } diff --git a/src/components/TokenInput.tsx b/src/components/TokenInput.tsx index 8b8d4ad2..06bc60ed 100644 --- a/src/components/TokenInput.tsx +++ b/src/components/TokenInput.tsx @@ -45,11 +45,7 @@ export default function TokenInput(props: Props) { return (
MAX