fix: add liquidity flow polish (#2017)
* addressed feedback * set initial, min and max zoom levels * better handle 0 * avoid formatting range selector * polish `not created` state * remove unused import
This commit is contained in:
parent
77fbccd3f1
commit
8d567e4d4a
@ -75,7 +75,7 @@ export const Brush = ({
|
||||
const previousBrushExtent = usePrevious(brushExtent)
|
||||
|
||||
const brushed = useCallback(
|
||||
({ mode, type, selection }: D3BrushEvent<unknown>) => {
|
||||
({ type, selection }: D3BrushEvent<unknown>) => {
|
||||
if (!selection) {
|
||||
setLocalBrushExtent(null)
|
||||
return
|
||||
@ -83,15 +83,14 @@ export const Brush = ({
|
||||
|
||||
const scaled = (selection as [number, number]).map(xScale.invert) as [number, number]
|
||||
|
||||
// undefined `mode` means brush was programatically moved
|
||||
// skip calling the handler to avoid a loop
|
||||
if (type === 'end' && mode !== undefined) {
|
||||
// avoid infinite render loop by checking for change
|
||||
if (type === 'end' && (brushExtent[0] !== scaled[0] || brushExtent[1] !== scaled[1])) {
|
||||
setBrushExtent(scaled)
|
||||
}
|
||||
|
||||
setLocalBrushExtent(scaled)
|
||||
},
|
||||
[xScale.invert, setBrushExtent]
|
||||
[xScale.invert, brushExtent, setBrushExtent]
|
||||
)
|
||||
|
||||
// keep local and external brush extent in sync
|
||||
|
@ -20,7 +20,7 @@ export function Chart({
|
||||
brushDomain,
|
||||
brushLabels,
|
||||
onBrushDomainChange,
|
||||
initialZoom,
|
||||
zoomLevels,
|
||||
}: LiquidityChartRangeInputProps) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null)
|
||||
|
||||
@ -34,7 +34,7 @@ export function Chart({
|
||||
const { xScale, yScale } = useMemo(() => {
|
||||
const scales = {
|
||||
xScale: scaleLinear()
|
||||
.domain([(1 - initialZoom) * current, (1 + initialZoom) * current] as number[])
|
||||
.domain([(1 - zoomLevels.initial) * current, (1 + zoomLevels.initial) * current] as number[])
|
||||
.range([0, innerWidth]),
|
||||
yScale: scaleLinear()
|
||||
.domain([0, max(series, yAccessor)] as number[])
|
||||
@ -47,7 +47,7 @@ export function Chart({
|
||||
}
|
||||
|
||||
return scales
|
||||
}, [initialZoom, current, innerWidth, series, innerHeight, zoom])
|
||||
}, [zoomLevels.initial, current, innerWidth, series, innerHeight, zoom])
|
||||
|
||||
useEffect(() => {
|
||||
if (!brushDomain) {
|
||||
@ -67,6 +67,7 @@ export function Chart({
|
||||
innerWidth={innerWidth}
|
||||
innerHeight={innerHeight}
|
||||
showClear={Boolean(zoom && zoom.k !== 1)}
|
||||
zoomLevels={zoomLevels}
|
||||
/>
|
||||
<svg ref={svgRef} width="100%" height="100%" viewBox={`0 0 ${width} ${height}`} style={{ overflow: 'visible' }}>
|
||||
<defs>
|
||||
|
@ -3,6 +3,7 @@ import { ButtonGray } from 'components/Button'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ScaleLinear, select, ZoomBehavior, zoom, ZoomTransform } from 'd3'
|
||||
import { RefreshCcw, ZoomIn, ZoomOut } from 'react-feather'
|
||||
import { ZoomLevels } from './types'
|
||||
|
||||
const Wrapper = styled.div<{ count: number }>`
|
||||
display: grid;
|
||||
@ -32,6 +33,7 @@ export default function Zoom({
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
showClear,
|
||||
zoomLevels,
|
||||
}: {
|
||||
svg: SVGSVGElement | null
|
||||
xScale: ScaleLinear<number, number>
|
||||
@ -39,6 +41,7 @@ export default function Zoom({
|
||||
innerWidth: number
|
||||
innerHeight: number
|
||||
showClear: boolean
|
||||
zoomLevels: ZoomLevels
|
||||
}) {
|
||||
const zoomBehavior = useRef<ZoomBehavior<Element, unknown>>()
|
||||
|
||||
@ -71,7 +74,7 @@ export default function Zoom({
|
||||
|
||||
// zoom
|
||||
zoomBehavior.current = zoom()
|
||||
.scaleExtent([0.3, 10])
|
||||
.scaleExtent([zoomLevels.min, zoomLevels.max])
|
||||
.translateExtent([
|
||||
[0, 0],
|
||||
[innerWidth, innerHeight],
|
||||
@ -85,7 +88,7 @@ export default function Zoom({
|
||||
select(svg as Element)
|
||||
.call(zoomBehavior.current)
|
||||
.on('mousedown.zoom', null)
|
||||
}, [innerHeight, innerWidth, setZoom, svg, xScale, zoomBehavior])
|
||||
}, [innerHeight, innerWidth, setZoom, svg, xScale, zoomBehavior, zoomLevels.max, zoomLevels.min])
|
||||
|
||||
return (
|
||||
<Wrapper count={showClear ? 3 : 2}>
|
||||
|
@ -36,7 +36,9 @@ export function useDensityChartData({
|
||||
price0: parseFloat(t.price0),
|
||||
}
|
||||
|
||||
newData.push(chartEntry)
|
||||
if (chartEntry.activeLiquidity > 0) {
|
||||
newData.push(chartEntry)
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
|
@ -16,6 +16,25 @@ import { format } from 'd3'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import ReactGA from 'react-ga'
|
||||
import { ZoomLevels } from './types'
|
||||
|
||||
const ZOOM_LEVELS: Record<FeeAmount, ZoomLevels> = {
|
||||
[FeeAmount.LOW]: {
|
||||
initial: 0.002,
|
||||
min: 0.001,
|
||||
max: 2,
|
||||
},
|
||||
[FeeAmount.MEDIUM]: {
|
||||
initial: 0.3,
|
||||
min: 0.01,
|
||||
max: 20,
|
||||
},
|
||||
[FeeAmount.HIGH]: {
|
||||
initial: 0.3,
|
||||
min: 0.01,
|
||||
max: 20,
|
||||
},
|
||||
}
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
position: relative;
|
||||
@ -51,7 +70,7 @@ export default function LiquidityChartRangeInput({
|
||||
}: {
|
||||
currencyA: Currency | undefined
|
||||
currencyB: Currency | undefined
|
||||
feeAmount?: number
|
||||
feeAmount?: FeeAmount
|
||||
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
|
||||
price: number | undefined
|
||||
priceLower?: Price<Token, Token>
|
||||
@ -73,7 +92,7 @@ export default function LiquidityChartRangeInput({
|
||||
|
||||
const onBrushDomainChangeEnded = useCallback(
|
||||
(domain) => {
|
||||
const leftRangeValue = Number(domain[0])
|
||||
let leftRangeValue = Number(domain[0])
|
||||
const rightRangeValue = Number(domain[1])
|
||||
|
||||
ReactGA.event({
|
||||
@ -81,6 +100,10 @@ export default function LiquidityChartRangeInput({
|
||||
action: 'Chart brushed',
|
||||
})
|
||||
|
||||
if (leftRangeValue <= 0) {
|
||||
leftRangeValue = 1 / 10 ** 6
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
// simulate user input for auto-formatting and other validations
|
||||
leftRangeValue > 0 && onLeftRangeInput(leftRangeValue.toFixed(6))
|
||||
@ -165,7 +188,7 @@ export default function LiquidityChartRangeInput({
|
||||
brushLabels={brushLabelValue}
|
||||
brushDomain={brushDomain}
|
||||
onBrushDomainChange={onBrushDomainChangeEnded}
|
||||
initialZoom={feeAmount === FeeAmount.LOW ? 0.02 : 0.3}
|
||||
zoomLevels={ZOOM_LEVELS[feeAmount ?? FeeAmount.MEDIUM]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
)}
|
||||
|
@ -15,6 +15,12 @@ export interface Margins {
|
||||
left: number
|
||||
}
|
||||
|
||||
export interface ZoomLevels {
|
||||
initial: number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface LiquidityChartRangeInputProps {
|
||||
// to distringuish between multiple charts in the DOM
|
||||
id?: string
|
||||
@ -47,5 +53,5 @@ export interface LiquidityChartRangeInputProps {
|
||||
brushDomain: [number, number] | undefined
|
||||
onBrushDomainChange: (domain: [number, number]) => void
|
||||
|
||||
initialZoom: number
|
||||
zoomLevels: ZoomLevels
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import StepCounter from 'components/InputStepCounter/InputStepCounter'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
|
||||
// currencyA is the base token
|
||||
export default function RangeSelector({
|
||||
@ -45,7 +44,7 @@ export default function RangeSelector({
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<StepCounter
|
||||
value={formatTickPrice(leftPrice, ticksAtLimit, Bound.LOWER, '')}
|
||||
value={ticksAtLimit[Bound.LOWER] ? '0' : leftPrice?.toSignificant(5) ?? ''}
|
||||
onUserInput={onLeftRangeInput}
|
||||
width="48%"
|
||||
decrement={isSorted ? getDecrementLower : getIncrementUpper}
|
||||
@ -59,7 +58,7 @@ export default function RangeSelector({
|
||||
tokenB={currencyB?.symbol}
|
||||
/>
|
||||
<StepCounter
|
||||
value={formatTickPrice(rightPrice, ticksAtLimit, Bound.UPPER, '')}
|
||||
value={ticksAtLimit[Bound.UPPER] ? '∞' : rightPrice?.toSignificant(5) ?? ''}
|
||||
onUserInput={onRightRangeInput}
|
||||
width="48%"
|
||||
decrement={isSorted ? getDecrementUpper : getIncrementLower}
|
||||
|
@ -68,7 +68,6 @@ import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { AddRemoveTabs } from 'components/NavigationTabs'
|
||||
import HoverInlineText from 'components/HoverInlineText'
|
||||
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
|
||||
import PresetsButtons from 'components/RangeSelector/PresetsButtons'
|
||||
import LiquidityChartRangeInput from 'components/LiquidityChartRangeInput'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import OptimismDowntimeWarning from 'components/OptimismDowntimeWarning'
|
||||
@ -451,7 +450,7 @@ export default function AddLiquidity({
|
||||
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks
|
||||
const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = pricesAtTicks
|
||||
|
||||
const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetRange, getSetFullRange } =
|
||||
const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetFullRange } =
|
||||
useRangeHopCallbacks(baseCurrency ?? undefined, quoteCurrency ?? undefined, feeAmount, tickLower, tickUpper, pool)
|
||||
|
||||
// we need an existence check on parsed amounts for single-asset deposits
|
||||
@ -656,32 +655,6 @@ export default function AddLiquidity({
|
||||
token0={currencyA?.wrapped}
|
||||
token1={currencyB?.wrapped}
|
||||
/>
|
||||
|
||||
{noLiquidity && (
|
||||
<BlueCard
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '1rem 1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: '12px', width: '30px', height: '30px' }}>
|
||||
<AlertCircle color={theme.primaryText1} size={30} />
|
||||
</div>
|
||||
<TYPE.body
|
||||
fontSize={14}
|
||||
style={{ marginBottom: 8, fontWeight: 500 }}
|
||||
textAlign="center"
|
||||
color={theme.primaryText1}
|
||||
>
|
||||
<Trans>
|
||||
You are the first liquidity provider for this Uniswap V3 pool.The transaction cost will be
|
||||
much higher as it includes the gas to create the pool.
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
</BlueCard>
|
||||
)}
|
||||
</AutoColumn>{' '}
|
||||
</>
|
||||
)}
|
||||
@ -743,65 +716,54 @@ export default function AddLiquidity({
|
||||
</HideMedium>
|
||||
<RightContainer gap="lg">
|
||||
<DynamicSection gap="md" disabled={!feeAmount || invalidPool}>
|
||||
<RowBetween>
|
||||
<TYPE.label>
|
||||
<Trans>Set your Price Range</Trans>
|
||||
</TYPE.label>
|
||||
</RowBetween>
|
||||
{!noLiquidity ? (
|
||||
<>
|
||||
<RowBetween>
|
||||
<TYPE.label>
|
||||
<Trans>Set Price Range</Trans>
|
||||
</TYPE.label>
|
||||
</RowBetween>
|
||||
|
||||
{price && baseCurrency && quoteCurrency && !noLiquidity && (
|
||||
<AutoRow gap="4px" justify="center" style={{ marginTop: '0.5rem' }}>
|
||||
<Trans>
|
||||
<TYPE.main fontWeight={500} textAlign="center" fontSize={12} color="text1">
|
||||
Current Price:
|
||||
</TYPE.main>
|
||||
<TYPE.body fontWeight={500} textAlign="center" fontSize={12} color="text1">
|
||||
<HoverInlineText
|
||||
maxCharacters={20}
|
||||
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
|
||||
/>
|
||||
</TYPE.body>
|
||||
<TYPE.body color="text2" fontSize={12}>
|
||||
{quoteCurrency?.symbol} per {baseCurrency.symbol}
|
||||
</TYPE.body>
|
||||
</Trans>
|
||||
</AutoRow>
|
||||
)}
|
||||
{price && baseCurrency && quoteCurrency && !noLiquidity && (
|
||||
<AutoRow gap="4px" justify="center" style={{ marginTop: '0.5rem' }}>
|
||||
<Trans>
|
||||
<TYPE.main fontWeight={500} textAlign="center" fontSize={12} color="text1">
|
||||
Current Price:
|
||||
</TYPE.main>
|
||||
<TYPE.body fontWeight={500} textAlign="center" fontSize={12} color="text1">
|
||||
<HoverInlineText
|
||||
maxCharacters={20}
|
||||
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
|
||||
/>
|
||||
</TYPE.body>
|
||||
<TYPE.body color="text2" fontSize={12}>
|
||||
{quoteCurrency?.symbol} per {baseCurrency.symbol}
|
||||
</TYPE.body>
|
||||
</Trans>
|
||||
</AutoRow>
|
||||
)}
|
||||
|
||||
<LiquidityChartRangeInput
|
||||
currencyA={baseCurrency ?? undefined}
|
||||
currencyB={quoteCurrency ?? undefined}
|
||||
feeAmount={feeAmount}
|
||||
ticksAtLimit={ticksAtLimit}
|
||||
price={price ? parseFloat((invertPrice ? price.invert() : price).toSignificant(8)) : undefined}
|
||||
priceLower={priceLower}
|
||||
priceUpper={priceUpper}
|
||||
onLeftRangeInput={onLeftRangeInput}
|
||||
onRightRangeInput={onRightRangeInput}
|
||||
interactive={!hasExistingPosition}
|
||||
/>
|
||||
|
||||
{noLiquidity && (
|
||||
<LiquidityChartRangeInput
|
||||
currencyA={baseCurrency ?? undefined}
|
||||
currencyB={quoteCurrency ?? undefined}
|
||||
feeAmount={feeAmount}
|
||||
ticksAtLimit={ticksAtLimit}
|
||||
price={
|
||||
price ? parseFloat((invertPrice ? price.invert() : price).toSignificant(8)) : undefined
|
||||
}
|
||||
priceLower={priceLower}
|
||||
priceUpper={priceUpper}
|
||||
onLeftRangeInput={onLeftRangeInput}
|
||||
onRightRangeInput={onRightRangeInput}
|
||||
interactive={!hasExistingPosition}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<TYPE.label>
|
||||
<Trans>Set Starting Price</Trans>
|
||||
</TYPE.label>
|
||||
{baseCurrency && quoteCurrency ? (
|
||||
<RateToggle
|
||||
currencyA={baseCurrency}
|
||||
currencyB={quoteCurrency}
|
||||
handleRateToggle={() => {
|
||||
onLeftRangeInput('')
|
||||
onRightRangeInput('')
|
||||
history.push(
|
||||
`/add/${currencyIdB as string}/${currencyIdA as string}${
|
||||
feeAmount ? '/' + feeAmount : ''
|
||||
}`
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</RowBetween>
|
||||
|
||||
<OutlineCard padding="12px">
|
||||
@ -842,19 +804,6 @@ export default function AddLiquidity({
|
||||
<StackedContainer>
|
||||
<StackedItem style={{ opacity: showCapitalEfficiencyWarning ? '0.05' : 1 }}>
|
||||
<AutoColumn gap="md">
|
||||
{!noLiquidity && (
|
||||
<PresetsButtons
|
||||
feeAmount={feeAmount}
|
||||
setRange={(numTicks: number) => {
|
||||
const [range1, range2] = getSetRange(numTicks)
|
||||
onLeftRangeInput(invertPrice ? range2 : range1)
|
||||
onRightRangeInput(invertPrice ? range1 : range2)
|
||||
}}
|
||||
setFullRange={() => {
|
||||
setShowCapitalEfficiencyWarning(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<RangeSelector
|
||||
priceLower={priceLower}
|
||||
priceUpper={priceUpper}
|
||||
@ -954,6 +903,32 @@ export default function AddLiquidity({
|
||||
) : null}
|
||||
</DynamicSection>
|
||||
|
||||
{noLiquidity && (
|
||||
<BlueCard
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '1rem 1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: '12px', width: '30px', height: '30px' }}>
|
||||
<AlertCircle color={theme.primaryText1} size={30} />
|
||||
</div>
|
||||
<TYPE.body
|
||||
fontSize={14}
|
||||
style={{ marginBottom: 8, fontWeight: 500 }}
|
||||
textAlign="center"
|
||||
color={theme.primaryText1}
|
||||
>
|
||||
<Trans>
|
||||
You are the first liquidity provider for this Uniswap V3 pool.The transaction cost will be
|
||||
much higher as it includes the gas to create the pool.
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
</BlueCard>
|
||||
)}
|
||||
|
||||
<MediumOnly>
|
||||
<Buttons />
|
||||
</MediumOnly>
|
||||
|
Loading…
Reference in New Issue
Block a user