Commit 1619386a authored by Callil Capuozzo's avatar Callil Capuozzo Committed by GitHub

Position styles (#55)

* Better position list layout WIP

* Position list updates

* add badge data and current price hover

* merge cleanup

* fix missing library

* position page improvements

* Clean up position page and overview

* layout and color updates

* Clean up page

* Clean up position page

* clean up errors

* Add icons

* Merge main

* Position styles tweaks
Co-authored-by: default avatarNoah Zinsmeister <noahwz@gmail.com>
parent 93d33947
......@@ -89,7 +89,7 @@
"toleranceExplanation": "Lowering this limit decreases your risk of frontrunning. However, this makes more likely that your transaction will fail due to normal price movements.",
"tokenSearchPlaceholder": "Search name or paste address",
"selectFee": "Select Fee",
"selectLiquidityRange": "Select Liquidity Range",
"selectLiquidityRange": "Select Price Range",
"selectPool": "Select Fee Tier",
"depositAmounts": "Deposit Amounts",
"fee": "fee",
......
import React from 'react'
import Badge, { BadgeVariant } from 'components/Badge'
import styled from 'styled-components'
import { MouseoverTooltip } from '../../components/Tooltip'
import { useTranslation } from 'react-i18next'
import { AlertCircle } from 'react-feather'
const BadgeWrapper = styled.div`
font-size: 14px;
display: flex;
justify-content: flex-end;
`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
`
const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success};
border-radius: 50%;
height: 8px;
width: 8px;
margin-right: 4px;
`
export const DarkBadge = styled.div`
width: fit-content;
border-radius: 8px;
background-color: ${({ theme }) => theme.bg0};
padding: 4px 6px;
`
export default function RangeBadge({ inRange }: { inRange?: boolean }) {
const { t } = useTranslation()
return (
<BadgeWrapper>
{inRange ? (
<MouseoverTooltip
text={`The price of this pair is within your selected range. Your positions is earning fees.`}
>
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<BadgeText>{t('In range')}</BadgeText>
</Badge>
</MouseoverTooltip>
) : (
<MouseoverTooltip
text={`The price of this pair is outside of your selected range. Your positions is not earning fees.`}
>
<Badge variant={BadgeVariant.WARNING}>
<AlertCircle width={14} height={14} />
&nbsp;
<BadgeText>{t('Out of range')}</BadgeText>
</Badge>
</MouseoverTooltip>
)}
</BadgeWrapper>
)
}
......@@ -37,6 +37,10 @@ const Base = styled(RebassButton)<{
> * {
user-select: none;
}
> a {
text-decoration: none;
}
`
export const ButtonPrimary = styled(Base)`
......
......@@ -9,19 +9,17 @@ const DesktopHeader = styled.div`
display: none;
font-size: 14px;
font-weight: 500;
opacity: 0.6;
padding: 8px 8px 0 8px;
padding: 8px 8px 8px 8px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
align-items: center;
display: flex;
margin: 0 0 8px 0;
& > div:first-child {
flex: 1 1 auto;
}
& > div:not(:first-child) {
display: grid;
grid-template-columns: 1fr 1fr;
& > div:last-child {
text-align: right;
min-width: 18%;
margin-right: 12px;
}
}
`
......@@ -45,10 +43,11 @@ export default function PositionList({ positions }: PositionListProps) {
return (
<>
<DesktopHeader>
<div>{t('Position')}</div>
<div>{t('Range')}</div>
<div>{t('Liquidity')}</div>
<div>{t('Fees Earned')}</div>
<div>
{t('Your positions')}
{positions && ' (' + positions.length + ')'}
</div>
<div>{t('Price range')}</div>
</DesktopHeader>
<MobileHeader>Your positions</MobileHeader>
{positions.map((p) => {
......
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'
import { Position } from '@uniswap/v3-sdk'
import Badge, { BadgeVariant } from 'components/Badge'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { usePool } from 'hooks/usePools'
import { useToken } from 'hooks/Tokens'
import { AlertTriangle } from 'react-feather'
import { AlertCircle } from 'react-feather'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { MEDIA_WIDTHS } from 'theme'
import { PositionDetails } from 'types/position'
import { TokenAmount, WETH9, Price, Token, Percent } from '@uniswap/sdk-core'
import { formatPrice, formatTokenAmount } from 'utils/formatTokenAmount'
import { WETH9, Price, Token, Percent } from '@uniswap/sdk-core'
import { formatPrice } from 'utils/formatTokenAmount'
import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/wrappedCurrency'
import { useV3PositionFees } from 'hooks/useV3PositionFees'
import { DAI, USDC, USDT, WBTC } from '../../constants'
import { MouseoverTooltip } from '../Tooltip'
import { RowFixed } from 'components/Row'
const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success};
......@@ -28,29 +29,29 @@ const Row = styled(Link)`
align-items: center;
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
color: ${({ theme }) => theme.text1};
margin: 8px 0;
padding: 8px;
padding: 16px;
text-decoration: none;
font-weight: 500;
background-color: ${({ theme }) => theme.bg1};
&:first-of-type {
margin: 0 0 8px 0;
}
&:last-of-type {
margin: 8px 0 0 0;
}
& > div:not(:first-child) {
text-align: right;
min-width: 18%;
}
:hover {
background-color: ${({ theme }) => theme.bg2};
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex-direction: row;
}
:hover {
background-color: ${({ theme }) => theme.bg1};
}
`
const BadgeText = styled.div`
font-weight: 500;
......@@ -60,63 +61,44 @@ const BadgeWrapper = styled.div`
font-size: 14px;
`
const DataLineItem = styled.div`
text-align: right;
font-size: 14px;
`
const DoubleArrow = styled.span`
color: ${({ theme }) => theme.text3};
`
const RangeData = styled.div`
const RangeLineItem = styled(DataLineItem)`
display: flex;
flex-direction: column;
width: 100%;
& > div {
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
& > div {
display: block;
}
}
cursor: pointer;
justify-self: flex-end;
`
const AmountData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
const DoubleArrow = styled.span`
color: ${({ theme }) => theme.text3};
`
const FeeData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
const RangeText = styled.span`
background-color: ${({ theme }) => theme.bg2};
padding: 0.25rem 0.5rem;
border-radius: 8px;
`
const LabelData = styled.div`
align-items: center;
display: flex;
flex: 1 1 auto;
justify-content: space-between;
width: 100%;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
const ExtentsText = styled.span`
color: ${({ theme }) => theme.text3};
font-size: 14px;
margin-right: 4px;
`
const PrimaryPositionIdData = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 0 12px 0;
> * {
margin-right: 8px;
}
`
const DataText = styled.div`
font-weight: 500;
font-weight: 600;
font-size: 18px;
`
export interface PositionListItemProps {
......@@ -207,31 +189,33 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
return undefined
}, [liquidity, pool, tickLower, tickUpper])
// liquidity amounts in tokens
const amount0: TokenAmount | undefined = position?.amount0
const amount1: TokenAmount | undefined = position?.amount1
const formattedAmount0 = formatTokenAmount(amount0, 4)
const formattedAmount1 = formatTokenAmount(amount1, 4)
// prices
const { priceLower, priceUpper, base } = getPriceOrderingFromPositionForUI(position)
let { priceLower, priceUpper, base, quote } = getPriceOrderingFromPositionForUI(position)
const inverted = token1 ? base?.equals(token1) : undefined
const currencyQuote = inverted ? currency0 : currency1
const currencyBase = inverted ? currency1 : currency0
// fees
const [feeValue0, feeValue1] = useV3PositionFees(pool ?? undefined, positionDetails)
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
const positionSummaryLink = '/pool/' + positionDetails.tokenId
const [manuallyInverted, setManuallyInverted] = useState(true)
if (manuallyInverted) {
;[priceLower, priceUpper, base, quote] = [priceUpper?.invert(), priceLower?.invert(), quote, base]
}
const quotePrice = useMemo(() => {
return manuallyInverted
? position?.pool.priceOf(position?.pool.token0)
: position?.pool.priceOf(position?.pool.token1)
}, [manuallyInverted, position?.pool])
return (
<Row to={positionSummaryLink}>
<LabelData>
<RowFixed>
<PrimaryPositionIdData>
<DoubleCurrencyLogo currency0={currencyBase} currency1={currencyQuote} size={16} margin />
<DoubleCurrencyLogo currency0={currencyBase} currency1={currencyQuote} size={18} margin />
<DataText>
&nbsp;{currencyQuote?.symbol}&nbsp;/&nbsp;{currencyBase?.symbol}
</DataText>
......@@ -242,61 +226,63 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
</PrimaryPositionIdData>
<BadgeWrapper>
{outOfRange ? (
<MouseoverTooltip
text={`The price of this pair is outside of your selected range. Your positions is not earning fees. Current price: ${quotePrice?.toSignificant(
6
)} ${manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} / ${
manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol
}`}
>
<Badge variant={BadgeVariant.WARNING}>
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
<AlertCircle width={14} height={14} style={{ marginRight: '' }} />
&nbsp;
<BadgeText>{t('Out of range')}</BadgeText>
</Badge>
</MouseoverTooltip>
) : (
<MouseoverTooltip
text={`The price of this pair is within your selected range. Your positions is earning fees. Current price: ${quotePrice?.toSignificant(
6
)} ${manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} / ${
manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol
}`}
>
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<BadgeText>{t('Active')}</BadgeText>
<BadgeText>{t('In range')}</BadgeText>
</Badge>
</MouseoverTooltip>
)}
</BadgeWrapper>
</LabelData>
<RangeData>
</RowFixed>
{priceLower && priceUpper ? (
<>
<DataLineItem>
{formatPrice(priceLower, 4)} <DoubleArrow></DoubleArrow> {formatPrice(priceUpper, 4)}{' '}
{currencyQuote?.symbol}
&nbsp;/&nbsp;
{currencyBase?.symbol}
</DataLineItem>
</>
) : (
<Loader />
)}
</RangeData>
<AmountData>
{formattedAmount0 && formattedAmount1 ? (
<>
<DataLineItem>
{inverted ? formattedAmount0 : formattedAmount1}&nbsp;{currencyQuote?.symbol}
</DataLineItem>
<DataLineItem>
{inverted ? formattedAmount1 : formattedAmount0}&nbsp;{currencyBase?.symbol}
</DataLineItem>
</>
) : (
<Loader />
)}
</AmountData>
<FeeData>
{feeValue0 && feeValue1 ? (
<>
<DataLineItem>
{formatTokenAmount(inverted ? feeValue0 : feeValue1, 4)}&nbsp;{currencyQuote?.symbol}
</DataLineItem>
<DataLineItem>
{formatTokenAmount(inverted ? feeValue1 : feeValue0, 4)}&nbsp;{currencyBase?.symbol}
</DataLineItem>
{' '}
<RangeLineItem
onClick={(e) => {
e.stopPropagation()
setManuallyInverted(!manuallyInverted)
}}
>
<span>
<RangeText>
<ExtentsText>Min: </ExtentsText>
{formatPrice(priceLower, 4)} {manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} {' / '}{' '}
{manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol}
</RangeText>{' '}
<DoubleArrow></DoubleArrow>{' '}
<RangeText>
<ExtentsText>Max:</ExtentsText>
{formatPrice(priceUpper, 4)} {manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} {' / '}{' '}
{manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol}
</RangeText>{' '}
</span>
</RangeLineItem>
</>
) : (
<Loader />
)}
</FeeData>
</Row>
)
}
......@@ -3,6 +3,15 @@ import { Currency } from '@uniswap/sdk-core'
import { ToggleElement, ToggleWrapper } from 'components/Toggle/MultiToggle'
import { useActiveWeb3React } from 'hooks'
import { wrappedCurrency } from 'utils/wrappedCurrency'
import Switch from '../../assets/svg/switch.svg'
import { useDarkModeManager } from '../../state/user/hooks'
import styled from 'styled-components'
const StyledSwitchIcon = styled.img<{ darkMode: boolean }>`
margin: 0 4px;
opacity: 0.4;
filter: ${({ darkMode }) => (darkMode ? 'invert(0)' : 'invert(1)')};
`
// the order of displayed base currencies from left to right is always in sort order
// currencyA is treated as the preferred base currency
......@@ -22,14 +31,21 @@ export default function RateToggle({
const isSorted = tokenA && tokenB && tokenA.sortsBefore(tokenB)
const [darkMode] = useDarkModeManager()
return tokenA && tokenB ? (
<div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }}>
<ToggleWrapper width="fit-content">
<ToggleElement isActive={isSorted} fontSize="12px" onClick={handleRateToggle}>
{isSorted ? currencyA.symbol : currencyB.symbol}
{isSorted ? currencyA.symbol : currencyB.symbol} {' price'}
</ToggleElement>
<StyledSwitchIcon onClick={handleRateToggle} width={'16px'} src={Switch} alt="logo" darkMode={darkMode} />
<ToggleElement isActive={!isSorted} fontSize="12px" onClick={handleRateToggle}>
{isSorted ? currencyB.symbol : currencyA.symbol}
{' price'}
</ToggleElement>
</ToggleWrapper>
</div>
) : null
}
......@@ -4,11 +4,11 @@ import styled from 'styled-components'
export const ToggleWrapper = styled.button<{ width?: string }>`
display: flex;
align-items: center;
width: ${({ width }) => width ?? '100%'}
padding: 1px;
background: ${({ theme }) => theme.bg0};
width: ${({ width }) => width ?? '100%'};
padding: 2px;
background: ${({ theme }) => theme.bg1};
border-radius: 8px;
border: ${({ theme }) => '2px solid ' + theme.bg2};
border: ${({ theme }) => '1px solid ' + theme.bg1};
cursor: pointer;
outline: none;
`
......@@ -21,7 +21,7 @@ export const ToggleElement = styled.span<{ isActive?: boolean; fontSize?: string
border-radius: 6px;
justify-content: center;
height: 100%;
background: ${({ theme, isActive }) => (isActive ? theme.bg2 : 'none')};
background: ${({ theme, isActive }) => (isActive ? theme.bg0 : 'none')};
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
font-size: ${({ fontSize }) => fontSize ?? '1rem'};
font-weight: 500;
......@@ -32,6 +32,15 @@ export const ToggleElement = styled.span<{ isActive?: boolean; fontSize?: string
}
`
export const ToggleText = styled.div`
color: ${({ theme }) => theme.text3};
font-size: 12px;
margin-right: 0.5rem;
width: 100%;
white-space: nowrap;
padding: 0 0 0 4px;
`
export interface ToggleProps {
options: string[]
activeIndex: number
......
import React from 'react'
import styled from 'styled-components'
import { TYPE } from 'theme'
import { useTranslation } from 'react-i18next'
import { ExternalLink } from '../../theme'
const CTASection = styled.section`
display: grid;
grid-template-columns: 2fr 1fr;
gap: 8px;
`
const CTA1 = styled(ExternalLink)`
background-color: ${({ theme }) => theme.bg1};
padding: 32px;
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 220px;
border: 1px solid ${({ theme }) => theme.bg4};
* {
color: ${({ theme }) => theme.text1};
text-decoration: none !important;
}
:hover {
border: 1px solid ${({ theme }) => theme.bg5};
background-color: ${({ theme }) => theme.bg2};
text-decoration: none;
* {
text-decoration: none !important;
}
}
`
export default function CTACards() {
const { t } = useTranslation()
return (
<CTASection>
<CTA1 href={''}>
<span>
<TYPE.largeHeader fontWeight={400} style={{ alignItems: 'center', display: 'flex', marginBottom: '24px' }}>
{t('What’s new in V3 Liquidity Pools?')}
</TYPE.largeHeader>
<TYPE.body fontWeight={300} style={{ alignItems: 'center', display: 'flex' }}>
{t(
'Learn all about concentrated liquidity and get informed about how to choose ranges that make sense for you.'
)}
</TYPE.body>
</span>
<TYPE.largeHeader fontWeight={400} style={{ alignItems: 'center', display: 'flex' }}>
{t('')}
</TYPE.largeHeader>
</CTA1>
<CTA1 href={''}>
<span>
<TYPE.largeHeader fontWeight={400} style={{ alignItems: 'center', display: 'flex', marginBottom: '24px' }}>
{t('Top pools')}
</TYPE.largeHeader>
<TYPE.body fontWeight={300} style={{ alignItems: 'center', display: 'flex' }}>
{t('Explore the top pools with Uniswap Analytics.')}
</TYPE.body>
</span>
<TYPE.largeHeader fontWeight={400} style={{ alignItems: 'center', display: 'flex' }}>
{t('')}
</TYPE.largeHeader>
</CTA1>
</CTASection>
)
}
This diff is collapsed.
......@@ -15,6 +15,8 @@ import styled, { ThemeContext } from 'styled-components'
import { HideSmall, TYPE } from 'theme'
import { LoadingRows } from './styleds'
import CTACards from './CTACards'
const PageWrapper = styled(AutoColumn)`
max-width: 870px;
width: 100%;
......@@ -76,7 +78,7 @@ const ResponsiveButtonPrimary = styled(ButtonPrimary)`
const MainContentWrapper = styled.main`
background-color: ${({ theme }) => theme.bg0};
padding: 16px;
padding: 8px;
border-radius: 20px;
display: flex;
flex-direction: column;
......@@ -131,7 +133,7 @@ export default function Pool() {
<AutoColumn gap="lg" style={{ width: '100%' }}>
<TitleRow style={{ marginTop: '1rem' }} padding={'0'}>
<HideSmall>
<TYPE.mediumHeader>Your Positions</TYPE.mediumHeader>
<TYPE.mediumHeader>{t('Pools Overview')}</TYPE.mediumHeader>
</HideSmall>
<ButtonRow>
<Menu
......@@ -152,6 +154,8 @@ export default function Pool() {
</ButtonRow>
</TitleRow>
<CTACards />
<MainContentWrapper>
{positionsLoading ? (
<LoadingRows>
......@@ -194,6 +198,16 @@ export default function Pool() {
</NoLiquidity>
)}
</MainContentWrapper>
<RowFixed justify="center" style={{ width: '100%' }}>
<ButtonGray
as={Link}
to="/migrate/v2"
id="import-pool-link"
style={{ padding: '8px 16px', borderRadius: '12px', width: 'fit-content' }}
>
<TYPE.subHeader>{t('Looking for your V2 Liquidity')}?</TYPE.subHeader>
</ButtonGray>
</RowFixed>
</AutoColumn>
</AutoColumn>
</PageWrapper>
......
......@@ -68,6 +68,7 @@ const loadingAnimation = keyframes`
export const LoadingRows = styled.div`
display: grid;
min-width: 75%;
max-width: 960px;
grid-column-gap: 0.5em;
grid-row-gap: 0.8em;
grid-template-columns: repeat(3, 1fr);
......
......@@ -143,7 +143,7 @@ export const TYPE = {
return <TextWrapper fontWeight={500} color={'primary1'} {...props} />
},
label(props: TextProps) {
return <TextWrapper fontWeight={600} color={'text1'} {...props} />
return <TextWrapper fontWeight={500} color={'text1'} {...props} />
},
black(props: TextProps) {
return <TextWrapper fontWeight={500} color={'text1'} {...props} />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment