Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4529e3cc88 | ||
|
|
4d47470f33 | ||
|
|
aedc020646 | ||
|
|
fd8085722e | ||
|
|
a60a85db54 | ||
|
|
ad2472eac6 | ||
|
|
f4d4acacae | ||
|
|
a5d7af192c | ||
|
|
21a2863ae3 | ||
|
|
1f871d4e73 | ||
|
|
3690936aff | ||
|
|
e95e2321b4 | ||
|
|
8b1bf09ff1 | ||
|
|
6383e9e4bf |
@@ -3,8 +3,8 @@ describe('Redirect', () => {
|
||||
cy.visit('/create-proposal')
|
||||
cy.url().should('match', /\/vote\/create-proposal/)
|
||||
})
|
||||
it('should redirect to /swap when visiting nonexist url', () => {
|
||||
it('should redirect to /not-found when visiting nonexist url', () => {
|
||||
cy.visit('/none-exist-url')
|
||||
cy.url().should('match', /\/swap/)
|
||||
cy.url().should('match', /\/not-found/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe.skip('Wallet Dropdown', () => {
|
||||
beforeEach(() => {
|
||||
describe('Wallet Dropdown', () => {
|
||||
before(() => {
|
||||
cy.visit('/pool')
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ describe.skip('Wallet Dropdown', () => {
|
||||
})
|
||||
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
@@ -22,24 +21,20 @@ describe.skip('Wallet Dropdown', () => {
|
||||
})
|
||||
|
||||
it('should be able to view transactions', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-transactions')).click()
|
||||
cy.get(getTestSelector('wallet-empty-transaction-text')).should('exist')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
|
||||
it('should change the theme when not connected', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
|
||||
cy.get(getTestSelector('wallet-select-theme')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).contains('Dark theme').should('exist')
|
||||
cy.get(getTestSelector('wallet-select-theme')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
|
||||
})
|
||||
|
||||
it('should select a language when not connected', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
@@ -49,8 +44,6 @@ describe.skip('Wallet Dropdown', () => {
|
||||
})
|
||||
|
||||
it('should open the wallet connect modal from the drop down when not connected', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-connect-wallet')).click()
|
||||
cy.get(getTestSelector('wallet-modal')).should('exist')
|
||||
cy.get(getTestSelector('wallet-modal-close')).click()
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "2.22.10",
|
||||
"@uniswap/widgets": "2.22.11",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
|
||||
BIN
src/assets/images/404-page-dark.png
Normal file
BIN
src/assets/images/404-page-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
src/assets/images/404-page-light.png
Normal file
BIN
src/assets/images/404-page-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -79,6 +79,13 @@ export const ButtonPrimary = styled(BaseButton)`
|
||||
}
|
||||
`
|
||||
|
||||
export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
export const ButtonLight = styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Group } from '@visx/group'
|
||||
import { LinePath } from '@visx/shape'
|
||||
import { easeCubicInOut } from 'd3'
|
||||
import { easeSinOut } from 'd3'
|
||||
import ms from 'ms.macro'
|
||||
import React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { animated, useSpring } from 'react-spring'
|
||||
@@ -11,8 +12,8 @@ import { LineChartProps } from './LineChart'
|
||||
type AnimatedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'>
|
||||
|
||||
const config = {
|
||||
duration: 800,
|
||||
easing: easeCubicInOut,
|
||||
duration: ms`0.8s`,
|
||||
easing: easeSinOut,
|
||||
}
|
||||
|
||||
// code reference: https://airbnb.io/visx/lineradial
|
||||
@@ -91,4 +92,4 @@ function AnimatedInLineChart<T>({
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedInLineChart
|
||||
export default React.memo(AnimatedInLineChart) as typeof AnimatedInLineChart
|
||||
|
||||
86
src/components/Charts/FadeInLineChart.tsx
Normal file
86
src/components/Charts/FadeInLineChart.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Group } from '@visx/group'
|
||||
import { LinePath } from '@visx/shape'
|
||||
import { easeCubicInOut } from 'd3'
|
||||
import React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { animated, useSpring } from 'react-spring'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LineChartProps } from './LineChart'
|
||||
|
||||
type FadedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'> & { dashed?: boolean }
|
||||
|
||||
const config = {
|
||||
duration: 3000,
|
||||
easing: easeCubicInOut,
|
||||
}
|
||||
|
||||
// code reference: https://airbnb.io/visx/lineradial
|
||||
|
||||
function FadedInLineChart<T>({
|
||||
data,
|
||||
getX,
|
||||
getY,
|
||||
marginTop,
|
||||
curve,
|
||||
color,
|
||||
strokeWidth,
|
||||
dashed,
|
||||
}: FadedInLineChartProps<T>) {
|
||||
const lineRef = useRef<SVGPathElement>(null)
|
||||
const [lineLength, setLineLength] = useState(0)
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false)
|
||||
const [hasAnimatedIn, setHasAnimatedIn] = useState(false)
|
||||
|
||||
const spring = useSpring({
|
||||
frame: shouldAnimate ? 0 : 1,
|
||||
config,
|
||||
onRest: () => {
|
||||
setShouldAnimate(false)
|
||||
setHasAnimatedIn(true)
|
||||
},
|
||||
})
|
||||
|
||||
// We need to check to see after the "invisble" line has been drawn
|
||||
// what the length is to be able to animate in the line for the first time
|
||||
// This will run on each render to see if there is a new line length
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
if (lineRef.current) {
|
||||
const length = lineRef.current.getTotalLength()
|
||||
if (length !== lineLength) {
|
||||
setLineLength(length)
|
||||
}
|
||||
if (length > 0 && !shouldAnimate && !hasAnimatedIn) {
|
||||
setShouldAnimate(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
const theme = useTheme()
|
||||
const lineColor = color ?? theme.accentAction
|
||||
|
||||
return (
|
||||
<Group top={marginTop}>
|
||||
<LinePath curve={curve} x={getX} y={getY}>
|
||||
{({ path }) => {
|
||||
const d = path(data) || ''
|
||||
return (
|
||||
<>
|
||||
<animated.path
|
||||
d={d}
|
||||
ref={lineRef}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeOpacity={hasAnimatedIn ? 1 : spring.frame.to((v) => 1 - v)}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
strokeDasharray={dashed ? '4,4' : undefined}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</LinePath>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FadedInLineChart) as typeof FadedInLineChart
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { ButtonLight, ButtonPrimary } from 'components/Button'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
@@ -23,13 +23,6 @@ const BodyWrapper = styled.div<{ margin?: string }>`
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const SmallButtonLight = styled(ButtonLight)`
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
@@ -203,12 +202,6 @@ export default function FeatureFlagModal() {
|
||||
<X size={24} />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FeatureFlagOption
|
||||
variant={LandingPageVariant}
|
||||
value={useLandingPageFlag()}
|
||||
featureFlag={FeatureFlag.landingPage}
|
||||
label="Landing page"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={Permit2Variant}
|
||||
value={usePermit2Flag()}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
|
||||
import clsx from 'clsx'
|
||||
@@ -156,9 +156,9 @@ export const SearchBar = () => {
|
||||
>
|
||||
<Row
|
||||
className={clsx(
|
||||
` ${styles.nftSearchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${
|
||||
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
|
||||
} `
|
||||
styles.nftSearchBar,
|
||||
!isOpen && !isMobile && magicalGradientOnHover,
|
||||
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
|
||||
)}
|
||||
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
|
||||
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
|
||||
@@ -182,18 +182,23 @@ export const SearchBar = () => {
|
||||
element={ElementName.NAVBAR_SEARCH_INPUT}
|
||||
properties={{ ...trace }}
|
||||
>
|
||||
<Box
|
||||
as="input"
|
||||
placeholder={placeholderText}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
|
||||
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width="full"
|
||||
<Trans
|
||||
id={placeholderText}
|
||||
render={({ translation }) => (
|
||||
<Box
|
||||
as="input"
|
||||
placeholder={translation as string}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
|
||||
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width="full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TraceEvent>
|
||||
{!isOpen && <KeyShortCut>/</KeyShortCut>}
|
||||
|
||||
@@ -9,14 +9,12 @@ import { AutoColumn } from '../Column'
|
||||
import ClaimPopup from './ClaimPopup'
|
||||
import PopupItem from './PopupItem'
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
const MobilePopupWrapper = styled.div`
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
|
||||
|
||||
margin: 0 auto;
|
||||
display: none;
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: block;
|
||||
padding-top: 20px;
|
||||
@@ -74,16 +72,18 @@ export default function Popups() {
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
<MobilePopupInner>
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((item) => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
{activePopups?.length > 0 && (
|
||||
<MobilePopupWrapper>
|
||||
<MobilePopupInner>
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((item) => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const MobileWrapper = styled(AutoColumn)`
|
||||
`
|
||||
|
||||
const BaseWrapper = styled.div<{ disable?: boolean }>`
|
||||
border: 1px solid ${({ theme, disable }) => (disable ? theme.accentAction : theme.backgroundOutline)};
|
||||
border: 1px solid ${({ theme, disable }) => (disable ? theme.accentActive : theme.backgroundOutline)};
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
@@ -30,8 +30,8 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
|
||||
background-color: ${({ theme }) => theme.hoverDefault};
|
||||
}
|
||||
|
||||
color: ${({ theme, disable }) => disable && theme.accentAction};
|
||||
background-color: ${({ theme, disable }) => disable && theme.accentActionSoft};
|
||||
color: ${({ theme, disable }) => disable && theme.accentActive};
|
||||
background-color: ${({ theme, disable }) => disable && theme.accentActiveSoft};
|
||||
`
|
||||
|
||||
const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({
|
||||
|
||||
@@ -11,6 +11,7 @@ export const BreadcrumbNavLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
|
||||
@@ -4,11 +4,11 @@ import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
|
||||
import { isPricePoint, PricePoint } from 'graphql/data/util'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { startTransition, Suspense, useMemo, useState } from 'react'
|
||||
import { pageTimePeriodAtom } from 'pages/TokenDetails'
|
||||
import { startTransition, Suspense, useMemo } from 'react'
|
||||
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
|
||||
|
||||
import { filterTimeAtom } from '../state'
|
||||
import PriceChart from './PriceChart'
|
||||
import { PriceChart } from './PriceChart'
|
||||
import TimePeriodSelector from './TimeSelector'
|
||||
|
||||
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined {
|
||||
@@ -58,7 +58,7 @@ function Chart({
|
||||
}) {
|
||||
const prices = usePreloadedTokenPriceQuery(priceQueryReference)
|
||||
// Initializes time period to global & maintain separate time period for subsequent changes
|
||||
const [timePeriod, setTimePeriod] = useState(useAtomValue(filterTimeAtom))
|
||||
const timePeriod = useAtomValue(pageTimePeriodAtom)
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
@@ -69,7 +69,6 @@ function Chart({
|
||||
currentTimePeriod={timePeriod}
|
||||
onTimeChange={(t: TimePeriod) => {
|
||||
startTransition(() => refetchTokenPrices(t))
|
||||
setTimePeriod(t)
|
||||
}}
|
||||
/>
|
||||
</ChartContainer>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Line } from '@visx/shape'
|
||||
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
|
||||
import FadedInLineChart from 'components/Charts/FadeInLineChart'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
|
||||
import { PricePoint } from 'graphql/data/util'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
@@ -13,6 +13,8 @@ import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { textFadeIn } from 'theme/styles'
|
||||
import {
|
||||
dayHourFormatter,
|
||||
hourFormatter,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
monthYearDayFormatter,
|
||||
weekFormatter,
|
||||
} from 'utils/formatChartTimes'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
@@ -68,11 +71,19 @@ export const DeltaText = styled.span<{ delta: number | undefined }>`
|
||||
|
||||
const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
${textFadeIn};
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
export const TokenPrice = styled.span`
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
`
|
||||
const MissingPrice = styled(TokenPrice)`
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
const DeltaContainer = styled.div`
|
||||
height: 16px;
|
||||
display: flex;
|
||||
@@ -84,6 +95,29 @@ export const ArrowCell = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
function fixChart(prices: PricePoint[] | undefined | null) {
|
||||
if (!prices) return { prices: null, blanks: [] }
|
||||
|
||||
const fixedChart: PricePoint[] = []
|
||||
const blanks: PricePoint[][] = []
|
||||
let lastValue: PricePoint | undefined = undefined
|
||||
for (let i = 0; i < prices.length; i++) {
|
||||
if (prices[i].value !== 0) {
|
||||
if (fixedChart.length === 0 && i !== 0) {
|
||||
blanks.push([{ ...prices[0], value: prices[i].value }, prices[i]])
|
||||
}
|
||||
lastValue = prices[i]
|
||||
fixedChart.push(prices[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (lastValue && lastValue !== prices[prices.length - 1]) {
|
||||
blanks.push([lastValue, { ...prices[prices.length - 1], value: lastValue.value }])
|
||||
}
|
||||
|
||||
return { prices: fixedChart, blanks }
|
||||
}
|
||||
|
||||
const margin = { top: 100, bottom: 48, crosshair: 72 }
|
||||
const timeOptionsHeight = 44
|
||||
|
||||
@@ -94,24 +128,30 @@ interface PriceChartProps {
|
||||
timePeriod: TimePeriod
|
||||
}
|
||||
|
||||
function formatDisplayPrice(value: number) {
|
||||
const str = value.toFixed(9)
|
||||
const [digits, decimals] = str.split('.')
|
||||
// Displays longer string for numbers < $2 to show changes in both stablecoins & small values
|
||||
if (digits === '0' || digits === '1')
|
||||
return `$${digits + '.' + decimals.substring(0, 2) + decimals.substring(2).replace(/0+$/, '')}`
|
||||
|
||||
return formatUSDPrice(value)
|
||||
}
|
||||
|
||||
function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
export function PriceChart({ width, height, prices: originalPrices, timePeriod }: PriceChartProps) {
|
||||
const locale = useActiveLocale()
|
||||
const theme = useTheme()
|
||||
|
||||
const { prices, blanks } = useMemo(
|
||||
() => (originalPrices && originalPrices.length > 0 ? fixChart(originalPrices) : { prices: null, blanks: [] }),
|
||||
[originalPrices]
|
||||
)
|
||||
|
||||
const chartAvailable = !!prices && prices.length > 0
|
||||
const missingPricesMessage = !chartAvailable ? (
|
||||
prices?.length === 0 ? (
|
||||
<>
|
||||
<Trans>Missing price data due to recently low trading volume on Uniswap v3</Trans>
|
||||
</>
|
||||
) : (
|
||||
<Trans>Missing chart data</Trans>
|
||||
)
|
||||
) : null
|
||||
|
||||
// first price point on the x-axis of the current time period's chart
|
||||
const startingPrice = prices?.[0] ?? DATA_EMPTY
|
||||
const startingPrice = originalPrices?.[0] ?? DATA_EMPTY
|
||||
// last price point on the x-axis of the current time period's chart
|
||||
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
|
||||
const endingPrice = originalPrices?.[originalPrices.length - 1] ?? DATA_EMPTY
|
||||
const [displayPrice, setDisplayPrice] = useState(startingPrice)
|
||||
|
||||
// set display price to ending price when prices have changed.
|
||||
@@ -133,9 +173,9 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
const rdScale = useMemo(
|
||||
() =>
|
||||
scaleLinear()
|
||||
.domain(getPriceBounds(prices ?? []))
|
||||
.domain(getPriceBounds(originalPrices ?? []))
|
||||
.range([graphInnerHeight, 0]),
|
||||
[prices, graphInnerHeight]
|
||||
[originalPrices, graphInnerHeight]
|
||||
)
|
||||
|
||||
function tickFormat(
|
||||
@@ -221,7 +261,6 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const crosshairEdgeMax = width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
|
||||
const hasData = prices && prices.length > 0
|
||||
|
||||
/*
|
||||
* Default curve doesn't look good for the HOUR chart.
|
||||
@@ -233,27 +272,27 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
const getX = useMemo(() => (p: PricePoint) => timeScale(p.timestamp), [timeScale])
|
||||
const getY = useMemo(() => (p: PricePoint) => rdScale(p.value), [rdScale])
|
||||
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartHeader>
|
||||
<TokenPrice>{formatDisplayPrice(displayPrice.value)}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
<DeltaText delta={delta}>{formattedDelta}</DeltaText>
|
||||
</DeltaContainer>
|
||||
{displayPrice.value ? (
|
||||
<>
|
||||
<TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{formattedDelta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MissingPrice>Price Unavailable</MissingPrice>
|
||||
<ThemedText.Caption style={{ color: theme.textTertiary }}>{missingPricesMessage}</ThemedText.Caption>
|
||||
</>
|
||||
)}
|
||||
</ChartHeader>
|
||||
{!hasData ? (
|
||||
<MissingPriceChart
|
||||
width={width}
|
||||
height={graphHeight}
|
||||
message={
|
||||
prices?.length === 0 ? (
|
||||
<Trans>This token doesn't have chart data because it hasn't been traded on Uniswap v3</Trans>
|
||||
) : (
|
||||
<Trans>Missing chart data</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{!chartAvailable ? (
|
||||
<MissingPriceChart width={width} height={graphHeight} message={!!displayPrice.value && missingPricesMessage} />
|
||||
) : (
|
||||
<svg width={width} height={graphHeight} style={{ minWidth: '100%' }}>
|
||||
<AnimatedInLineChart
|
||||
@@ -264,6 +303,19 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
curve={curve}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{blanks.map((blank, index) => (
|
||||
<FadedInLineChart
|
||||
key={index}
|
||||
data={blank}
|
||||
getX={getX}
|
||||
getY={getY}
|
||||
marginTop={margin.top}
|
||||
curve={curve}
|
||||
strokeWidth={2}
|
||||
color={theme.textTertiary}
|
||||
dashed
|
||||
/>
|
||||
))}
|
||||
{crosshair !== null ? (
|
||||
<g>
|
||||
<AxisBottom
|
||||
@@ -354,9 +406,7 @@ const StyledMissingChart = styled.svg`
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
|
||||
const chartBottomPadding = 15
|
||||
|
||||
function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) {
|
||||
const theme = useTheme()
|
||||
const midPoint = height / 2 + 45
|
||||
@@ -369,18 +419,10 @@ function MissingPriceChart({ width, height, message }: { width: number; height:
|
||||
fill="transparent"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<TrendingUp stroke={theme.textTertiary} x={0} size={12} y={height - chartBottomPadding - 10} />
|
||||
{message && <TrendingUp stroke={theme.textTertiary} x={0} size={12} y={height - chartBottomPadding - 10} />}
|
||||
<text y={height - chartBottomPadding} x="20" fill={theme.textTertiary}>
|
||||
{message || <Trans>Missing chart data</Trans>}
|
||||
{message}
|
||||
</text>
|
||||
<path
|
||||
d={`M 0 ${height - 1}, ${width} ${height - 1}`}
|
||||
stroke={theme.backgroundOutline}
|
||||
fill="transparent"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</StyledMissingChart>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceChart
|
||||
|
||||
@@ -72,6 +72,8 @@ export const TokenInfoContainer = styled.div`
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
${textFadeIn};
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
export const TokenNameCell = styled.div`
|
||||
display: flex;
|
||||
@@ -79,7 +81,6 @@ export const TokenNameCell = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
${textFadeIn}
|
||||
`
|
||||
/* Loading state bubbles */
|
||||
const DetailBubble = styled(LoadingBubble)`
|
||||
|
||||
@@ -12,7 +12,6 @@ export const PageWrapper = styled.div`
|
||||
padding: 68px 8px 0px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
padding-top: 48px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum FeatureFlag {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
landingPage = 'landingPage',
|
||||
permit2 = 'permit2',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useLandingPageFlag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as LandingPageVariant }
|
||||
@@ -55,12 +55,13 @@ export enum BaseVariant {
|
||||
Enabled = 'enabled',
|
||||
}
|
||||
|
||||
export function useBaseFlag(flag: string): BaseVariant {
|
||||
export function useBaseFlag(flag: string, defaultValue = BaseVariant.Control): BaseVariant {
|
||||
switch (useFeatureFlagsContext().flags[flag]) {
|
||||
case 'enabled':
|
||||
return BaseVariant.Enabled
|
||||
case 'control':
|
||||
default:
|
||||
return BaseVariant.Control
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,30 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useHasPendingApproval } from 'state/transactions/hooks'
|
||||
import { ApproveTransactionInfo } from 'state/transactions/types'
|
||||
|
||||
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
|
||||
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'
|
||||
|
||||
enum SyncState {
|
||||
PENDING,
|
||||
SYNCING,
|
||||
SYNCED,
|
||||
}
|
||||
|
||||
export enum PermitState {
|
||||
INVALID,
|
||||
LOADING,
|
||||
PERMIT_NEEDED,
|
||||
PERMITTED,
|
||||
APPROVAL_OR_PERMIT_NEEDED,
|
||||
APPROVAL_LOADING,
|
||||
APPROVED_AND_PERMITTED,
|
||||
}
|
||||
|
||||
export interface Permit {
|
||||
state: PermitState
|
||||
signature?: PermitSignature
|
||||
callback?: (sPendingApproval: boolean) => Promise<{
|
||||
callback?: () => Promise<{
|
||||
response: ContractTransaction
|
||||
info: ApproveTransactionInfo
|
||||
} | void>
|
||||
@@ -28,7 +36,7 @@ export interface Permit {
|
||||
|
||||
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
|
||||
const { account } = useWeb3React()
|
||||
const tokenAllowance = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
|
||||
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
|
||||
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
|
||||
const isAllowed = useMemo(
|
||||
() => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)),
|
||||
@@ -71,32 +79,62 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
|
||||
true
|
||||
)
|
||||
|
||||
const callback = useCallback(
|
||||
async (isPendingApproval: boolean) => {
|
||||
let info
|
||||
if (!isAllowed && !isPendingApproval) {
|
||||
info = await updateTokenAllowance()
|
||||
}
|
||||
if (!isPermitted && !isSigned) {
|
||||
await updatePermitAllowance()
|
||||
}
|
||||
return info
|
||||
},
|
||||
[isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance]
|
||||
)
|
||||
// Permit2 should be marked syncing from the time approval is submitted (pending) until it is
|
||||
// synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval.
|
||||
const [syncState, setSyncState] = useState(SyncState.SYNCED)
|
||||
const isApprovalLoading = syncState !== SyncState.SYNCED
|
||||
const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS)
|
||||
useEffect(() => {
|
||||
if (hasPendingApproval) {
|
||||
setSyncState(SyncState.PENDING)
|
||||
} else {
|
||||
setSyncState((state) => {
|
||||
if (state === SyncState.PENDING && isApprovalSyncing) {
|
||||
return SyncState.SYNCING
|
||||
} else if (state === SyncState.SYNCING && !isApprovalSyncing) {
|
||||
return SyncState.SYNCED
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [hasPendingApproval, isApprovalSyncing])
|
||||
|
||||
const callback = useCallback(async () => {
|
||||
let info
|
||||
if (!isAllowed && !hasPendingApproval) {
|
||||
info = await updateTokenAllowance()
|
||||
}
|
||||
if (!isPermitted && !isSigned) {
|
||||
await updatePermitAllowance()
|
||||
}
|
||||
return info
|
||||
}, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance])
|
||||
|
||||
return useMemo(() => {
|
||||
if (!amount) {
|
||||
return { state: PermitState.INVALID }
|
||||
} else if (!tokenAllowance || !permitAllowance) {
|
||||
return { state: PermitState.LOADING }
|
||||
} else if (isAllowed) {
|
||||
if (isPermitted) {
|
||||
return { state: PermitState.PERMITTED }
|
||||
} else if (isSigned) {
|
||||
return { state: PermitState.PERMITTED, signature }
|
||||
} else if (!(isPermitted || isSigned)) {
|
||||
return { state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback }
|
||||
} else if (!isAllowed) {
|
||||
return {
|
||||
state: isApprovalLoading ? PermitState.APPROVAL_LOADING : PermitState.APPROVAL_OR_PERMIT_NEEDED,
|
||||
callback,
|
||||
}
|
||||
} else {
|
||||
return { state: PermitState.APPROVED_AND_PERMITTED, signature: isPermitted ? undefined : signature }
|
||||
}
|
||||
return { state: PermitState.PERMIT_NEEDED, callback }
|
||||
}, [amount, callback, isAllowed, isPermitted, isSigned, permitAllowance, signature, tokenAllowance])
|
||||
}, [
|
||||
amount,
|
||||
callback,
|
||||
isAllowed,
|
||||
isApprovalLoading,
|
||||
isPermitted,
|
||||
isSigned,
|
||||
permitAllowance,
|
||||
signature,
|
||||
tokenAllowance,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -8,16 +8,23 @@ import { calculateGasMargin } from 'utils/calculateGasMargin'
|
||||
|
||||
import { useTokenContract } from './useContract'
|
||||
|
||||
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): CurrencyAmount<Token> | undefined {
|
||||
export function useTokenAllowance(
|
||||
token?: Token,
|
||||
owner?: string,
|
||||
spender?: string
|
||||
): {
|
||||
tokenAllowance: CurrencyAmount<Token> | undefined
|
||||
isSyncing: boolean
|
||||
} {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const inputs = useMemo(() => [owner, spender], [owner, spender])
|
||||
const allowance = useSingleCallResult(contract, 'allowance', inputs).result
|
||||
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
|
||||
|
||||
return useMemo(
|
||||
() => (token && allowance ? CurrencyAmount.fromRawAmount(token, allowance.toString()) : undefined),
|
||||
[token, allowance]
|
||||
)
|
||||
return useMemo(() => {
|
||||
const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
|
||||
return { tokenAllowance, isSyncing }
|
||||
}, [isSyncing, result, token])
|
||||
}
|
||||
|
||||
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {
|
||||
|
||||
@@ -25,22 +25,22 @@ function useApprovalStateForSpender(
|
||||
const { account } = useWeb3React()
|
||||
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
|
||||
|
||||
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
|
||||
const { tokenAllowance } = useTokenAllowance(token, account ?? undefined, spender)
|
||||
const pendingApproval = useIsPendingApproval(token, spender)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
|
||||
// we might not have enough data to know whether or not we need to approve
|
||||
if (!currentAllowance) return ApprovalState.UNKNOWN
|
||||
if (!tokenAllowance) return ApprovalState.UNKNOWN
|
||||
|
||||
// amountToApprove will be defined if currentAllowance is
|
||||
return currentAllowance.lessThan(amountToApprove)
|
||||
// amountToApprove will be defined if tokenAllowance is
|
||||
return tokenAllowance.lessThan(amountToApprove)
|
||||
? pendingApproval
|
||||
? ApprovalState.PENDING
|
||||
: ApprovalState.NOT_APPROVED
|
||||
: ApprovalState.APPROVED
|
||||
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||
}, [amountToApprove, pendingApproval, spender, tokenAllowance])
|
||||
}
|
||||
|
||||
export function useApproval(
|
||||
|
||||
@@ -4,7 +4,6 @@ import Loader from 'components/Loader'
|
||||
import { MenuDropdown } from 'components/NavBar/MenuDropdown'
|
||||
import TopLevelModals from 'components/TopLevelModals'
|
||||
import { useFeatureFlagsIsLoaded } from 'featureFlags'
|
||||
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
|
||||
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
|
||||
@@ -37,6 +36,7 @@ import Manage from './Earn/Manage'
|
||||
import Landing from './Landing'
|
||||
import MigrateV2 from './MigrateV2'
|
||||
import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
|
||||
import NotFound from './NotFound'
|
||||
import Pool from './Pool'
|
||||
import { PositionPage } from './Pool/PositionPage'
|
||||
import PoolV2 from './Pool/v2'
|
||||
@@ -65,29 +65,19 @@ initializeAnalytics(ANALYTICS_DUMMY_KEY, OriginApplication.INTERFACE, {
|
||||
isProductionEnv: isProductionEnv(),
|
||||
})
|
||||
|
||||
const AppWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const BodyWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 72px 0px 0px 0px;
|
||||
min-height: 100vh;
|
||||
padding: ${({ theme }) => theme.navHeight}px 0px 5rem 0px;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
padding: 52px 0px 16px 0px;
|
||||
`};
|
||||
`
|
||||
|
||||
const MobileBottomBar = styled.div`
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
@@ -115,10 +105,6 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
|
||||
function getCurrentPageFromLocation(locationPathname: string): PageName | undefined {
|
||||
switch (true) {
|
||||
case locationPathname.startsWith('/swap'):
|
||||
@@ -202,134 +188,131 @@ export default function App() {
|
||||
|
||||
const isHeaderTransparent = !scrolledState
|
||||
|
||||
const landingPageFlag = useLandingPageFlag()
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DarkModeQueryParamReader />
|
||||
<ApeModeQueryParamReader />
|
||||
<AppWrapper>
|
||||
<Trace page={currentPage}>
|
||||
<HeaderWrapper transparent={isHeaderTransparent}>
|
||||
<NavBar />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<Popups />
|
||||
<Polling />
|
||||
<TopLevelModals />
|
||||
<Suspense fallback={<Loader />}>
|
||||
{isLoaded ? (
|
||||
<Routes>
|
||||
{landingPageFlag === LandingPageVariant.Enabled && <Route path="/" element={<Landing />} />}
|
||||
<Route path="tokens" element={<Tokens />}>
|
||||
<Route path=":chainName" />
|
||||
</Route>
|
||||
<Route path="tokens/:chainName/:tokenAddress" element={<TokenDetails />} />
|
||||
<Route
|
||||
path="vote/*"
|
||||
element={
|
||||
<Suspense fallback={<LazyLoadSpinner />}>
|
||||
<Vote />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
|
||||
<Route path="claim" element={<OpenClaimAddressModalAndRedirectToSwap />} />
|
||||
<Route path="uni" element={<Earn />} />
|
||||
<Route path="uni/:currencyIdA/:currencyIdB" element={<Manage />} />
|
||||
<Trace page={currentPage}>
|
||||
<HeaderWrapper transparent={isHeaderTransparent}>
|
||||
<NavBar />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<Popups />
|
||||
<Polling />
|
||||
<TopLevelModals />
|
||||
<Suspense fallback={<Loader />}>
|
||||
{isLoaded ? (
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
|
||||
<Route path="send" element={<RedirectPathToSwapOnly />} />
|
||||
<Route path="swap" element={<Swap />} />
|
||||
<Route path="tokens" element={<Tokens />}>
|
||||
<Route path=":chainName" />
|
||||
</Route>
|
||||
<Route path="tokens/:chainName/:tokenAddress" element={<TokenDetails />} />
|
||||
<Route
|
||||
path="vote/*"
|
||||
element={
|
||||
<Suspense fallback={<LazyLoadSpinner />}>
|
||||
<Vote />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
|
||||
<Route path="claim" element={<OpenClaimAddressModalAndRedirectToSwap />} />
|
||||
<Route path="uni" element={<Earn />} />
|
||||
<Route path="uni/:currencyIdA/:currencyIdB" element={<Manage />} />
|
||||
|
||||
<Route path="pool/v2/find" element={<PoolFinder />} />
|
||||
<Route path="pool/v2" element={<PoolV2 />} />
|
||||
<Route path="pool" element={<Pool />} />
|
||||
<Route path="pool/:tokenId" element={<PositionPage />} />
|
||||
<Route path="send" element={<RedirectPathToSwapOnly />} />
|
||||
<Route path="swap" element={<Swap />} />
|
||||
|
||||
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
</Route>
|
||||
<Route path="add" element={<RedirectDuplicateTokenIds />}>
|
||||
{/* this is workaround since react-router-dom v6 doesn't support optional parameters any more */}
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
|
||||
</Route>
|
||||
<Route path="pool/v2/find" element={<PoolFinder />} />
|
||||
<Route path="pool/v2" element={<PoolV2 />} />
|
||||
<Route path="pool" element={<Pool />} />
|
||||
<Route path="pool/:tokenId" element={<PositionPage />} />
|
||||
|
||||
<Route path="increase" element={<AddLiquidity />}>
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount/:tokenId" />
|
||||
</Route>
|
||||
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
</Route>
|
||||
<Route path="add" element={<RedirectDuplicateTokenIds />}>
|
||||
{/* this is workaround since react-router-dom v6 doesn't support optional parameters any more */}
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
|
||||
</Route>
|
||||
|
||||
<Route path="remove/v2/:currencyIdA/:currencyIdB" element={<RemoveLiquidity />} />
|
||||
<Route path="remove/:tokenId" element={<RemoveLiquidityV3 />} />
|
||||
<Route path="increase" element={<AddLiquidity />}>
|
||||
<Route path=":currencyIdA" />
|
||||
<Route path=":currencyIdA/:currencyIdB" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
|
||||
<Route path=":currencyIdA/:currencyIdB/:feeAmount/:tokenId" />
|
||||
</Route>
|
||||
|
||||
<Route path="migrate/v2" element={<MigrateV2 />} />
|
||||
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} />
|
||||
<Route path="remove/v2/:currencyIdA/:currencyIdB" element={<RemoveLiquidity />} />
|
||||
<Route path="remove/:tokenId" element={<RemoveLiquidityV3 />} />
|
||||
|
||||
<Route path="about" element={<About />} />
|
||||
<Route path="migrate/v2" element={<MigrateV2 />} />
|
||||
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} />
|
||||
|
||||
<Route path="*" element={<RedirectPathToSwapOnly />} />
|
||||
<Route path="about" element={<About />} />
|
||||
|
||||
<Route
|
||||
path="/nfts"
|
||||
element={
|
||||
// TODO: replace loading state during Apollo migration
|
||||
<Suspense fallback={null}>
|
||||
<NftExplore />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/asset/:contractAddress/:tokenId"
|
||||
element={
|
||||
<Suspense fallback={<AssetDetailsLoading />}>
|
||||
<Asset />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/profile"
|
||||
element={
|
||||
<Suspense fallback={<ProfilePageLoadingSkeleton />}>
|
||||
<Profile />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/collection/:contractAddress"
|
||||
element={
|
||||
<Suspense fallback={<CollectionPageSkeleton />}>
|
||||
<Collection />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/collection/:contractAddress/activity"
|
||||
element={
|
||||
<Suspense fallback={<CollectionPageSkeleton />}>
|
||||
<Collection />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</Suspense>
|
||||
<Marginer />
|
||||
</BodyWrapper>
|
||||
<MobileBottomBar>
|
||||
<PageTabs />
|
||||
<Box marginY="4">
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
</MobileBottomBar>
|
||||
</Trace>
|
||||
</AppWrapper>
|
||||
<Route
|
||||
path="/nfts"
|
||||
element={
|
||||
// TODO: replace loading state during Apollo migration
|
||||
<Suspense fallback={null}>
|
||||
<NftExplore />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/asset/:contractAddress/:tokenId"
|
||||
element={
|
||||
<Suspense fallback={<AssetDetailsLoading />}>
|
||||
<Asset />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/profile"
|
||||
element={
|
||||
<Suspense fallback={<ProfilePageLoadingSkeleton />}>
|
||||
<Profile />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/collection/:contractAddress"
|
||||
element={
|
||||
<Suspense fallback={<CollectionPageSkeleton />}>
|
||||
<Collection />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/nfts/collection/:contractAddress/activity"
|
||||
element={
|
||||
<Suspense fallback={<CollectionPageSkeleton />}>
|
||||
<Collection />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<Navigate to="/not-found" replace />} />
|
||||
<Route path="/not-found" element={<NotFound />} />
|
||||
</Routes>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</Suspense>
|
||||
</BodyWrapper>
|
||||
<MobileBottomBar>
|
||||
<PageTabs />
|
||||
<Box marginY="4">
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
</MobileBottomBar>
|
||||
</Trace>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
|
||||
import Swap from 'pages/Swap'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@@ -54,17 +53,17 @@ const ContentContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: min(720px, 90%);
|
||||
position: absolute;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
padding: 32px 0 64px;
|
||||
padding: 32px 0 80px;
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
padding: 64px 0;
|
||||
}
|
||||
`
|
||||
@@ -170,21 +169,14 @@ export default function Landing() {
|
||||
const location = useLocation()
|
||||
const isOpen = location.pathname === '/'
|
||||
|
||||
const landingPageFlag = useLandingPageFlag()
|
||||
|
||||
useEffect(() => {
|
||||
if (landingPageFlag) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
}
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
// need to have a return so the hook doesn't throw.
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
}, [landingPageFlag])
|
||||
}, [])
|
||||
|
||||
if (landingPageFlag === LandingPageVariant.Control || !isOpen) return null
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<Trace page={PageName.LANDING_PAGE} shouldLogImpression>
|
||||
|
||||
65
src/pages/NotFound/index.tsx
Normal file
65
src/pages/NotFound/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { SmallButtonPrimary } from 'components/Button'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import darkImage from '../../assets/images/404-page-dark.png'
|
||||
import lightImage from '../../assets/images/404-page-light.png'
|
||||
|
||||
const Image = styled.img`
|
||||
max-width: 510px;
|
||||
width: 100%;
|
||||
padding: 0 75px;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Header = styled(Container)`
|
||||
gap: 30px;
|
||||
`
|
||||
|
||||
const PageWrapper = styled(Container)`
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
gap: 50px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
justify-content: space-between;
|
||||
padding-top: 64px;
|
||||
}
|
||||
`
|
||||
|
||||
export default function NotFound() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const Title = isMobile ? ThemedText.LargeHeader : ThemedText.Hero
|
||||
const Paragraph = isMobile ? ThemedText.HeadlineMedium : ThemedText.HeadlineLarge
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Trace page="404-page" shouldLogImpression>
|
||||
<Header>
|
||||
<Container>
|
||||
<Title>404</Title>
|
||||
<Paragraph color="textSecondary">
|
||||
<Trans>Page not found!</Trans>
|
||||
</Paragraph>
|
||||
</Container>
|
||||
<Image src={isDarkMode ? darkImage : lightImage} alt="Liluni" />
|
||||
</Header>
|
||||
<SmallButtonPrimary as={Link} to="/">
|
||||
<Trans>Oops, take me back to Swap</Trans>
|
||||
</SmallButtonPrimary>
|
||||
</Trace>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ function WrongNetworkCard() {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh' }}>
|
||||
<>
|
||||
<PageWrapper>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<AutoColumn gap="lg" style={{ width: '100%' }}>
|
||||
@@ -189,7 +189,7 @@ function WrongNetworkCard() {
|
||||
</AutoColumn>
|
||||
</PageWrapper>
|
||||
<SwitchLocaleLink />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,86 +263,84 @@ export default function Pool() {
|
||||
|
||||
return (
|
||||
<Trace page={PageName.POOL_PAGE} shouldLogImpression>
|
||||
<div style={{ height: '100vh' }}>
|
||||
<PageWrapper>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<AutoColumn gap="lg" style={{ width: '100%' }}>
|
||||
<TitleRow padding="0">
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Pools</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
<ButtonRow>
|
||||
{showV2Features && (
|
||||
<PoolMenu
|
||||
menuItems={menuItems}
|
||||
flyoutAlignment={FlyoutAlignment.LEFT}
|
||||
ToggleUI={(props: any) => (
|
||||
<MoreOptionsButton {...props}>
|
||||
<MoreOptionsText>
|
||||
<Trans>More</Trans>
|
||||
<ChevronDown size={15} />
|
||||
</MoreOptionsText>
|
||||
</MoreOptionsButton>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveButtonPrimary data-cy="join-pool-button" id="join-pool-button" as={Link} to="/add/ETH">
|
||||
+ <Trans>New Position</Trans>
|
||||
</ResponsiveButtonPrimary>
|
||||
</ButtonRow>
|
||||
</TitleRow>
|
||||
|
||||
<MainContentWrapper>
|
||||
{positionsLoading ? (
|
||||
<PositionsLoadingPlaceholder />
|
||||
) : filteredPositions && closedPositions && filteredPositions.length > 0 ? (
|
||||
<PositionList
|
||||
positions={filteredPositions}
|
||||
setUserHideClosedPositions={setUserHideClosedPositions}
|
||||
userHideClosedPositions={userHideClosedPositions}
|
||||
<PageWrapper>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<AutoColumn gap="lg" style={{ width: '100%' }}>
|
||||
<TitleRow padding="0">
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Pools</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
<ButtonRow>
|
||||
{showV2Features && (
|
||||
<PoolMenu
|
||||
menuItems={menuItems}
|
||||
flyoutAlignment={FlyoutAlignment.LEFT}
|
||||
ToggleUI={(props: any) => (
|
||||
<MoreOptionsButton {...props}>
|
||||
<MoreOptionsText>
|
||||
<Trans>More</Trans>
|
||||
<ChevronDown size={15} />
|
||||
</MoreOptionsText>
|
||||
</MoreOptionsButton>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<ErrorContainer>
|
||||
<ThemedText.DeprecatedBody color={theme.textTertiary} textAlign="center">
|
||||
<InboxIcon strokeWidth={1} style={{ marginTop: '2em' }} />
|
||||
<div>
|
||||
<Trans>Your active V3 liquidity positions will appear here.</Trans>
|
||||
</div>
|
||||
</ThemedText.DeprecatedBody>
|
||||
{!showConnectAWallet && closedPositions.length > 0 && (
|
||||
<ButtonText
|
||||
style={{ marginTop: '.5rem' }}
|
||||
onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}
|
||||
>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ButtonText>
|
||||
)}
|
||||
{showConnectAWallet && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: false }}
|
||||
element={ElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<ButtonPrimary
|
||||
style={{ marginTop: '2em', marginBottom: '2em', padding: '8px 16px' }}
|
||||
onClick={toggleWalletModal}
|
||||
>
|
||||
<Trans>Connect a wallet</Trans>
|
||||
</ButtonPrimary>
|
||||
</TraceEvent>
|
||||
)}
|
||||
</ErrorContainer>
|
||||
)}
|
||||
</MainContentWrapper>
|
||||
<HideSmall>
|
||||
<CTACards />
|
||||
</HideSmall>
|
||||
</AutoColumn>
|
||||
<ResponsiveButtonPrimary data-cy="join-pool-button" id="join-pool-button" as={Link} to="/add/ETH">
|
||||
+ <Trans>New Position</Trans>
|
||||
</ResponsiveButtonPrimary>
|
||||
</ButtonRow>
|
||||
</TitleRow>
|
||||
|
||||
<MainContentWrapper>
|
||||
{positionsLoading ? (
|
||||
<PositionsLoadingPlaceholder />
|
||||
) : filteredPositions && closedPositions && filteredPositions.length > 0 ? (
|
||||
<PositionList
|
||||
positions={filteredPositions}
|
||||
setUserHideClosedPositions={setUserHideClosedPositions}
|
||||
userHideClosedPositions={userHideClosedPositions}
|
||||
/>
|
||||
) : (
|
||||
<ErrorContainer>
|
||||
<ThemedText.DeprecatedBody color={theme.textTertiary} textAlign="center">
|
||||
<InboxIcon strokeWidth={1} style={{ marginTop: '2em' }} />
|
||||
<div>
|
||||
<Trans>Your active V3 liquidity positions will appear here.</Trans>
|
||||
</div>
|
||||
</ThemedText.DeprecatedBody>
|
||||
{!showConnectAWallet && closedPositions.length > 0 && (
|
||||
<ButtonText
|
||||
style={{ marginTop: '.5rem' }}
|
||||
onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}
|
||||
>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ButtonText>
|
||||
)}
|
||||
{showConnectAWallet && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: false }}
|
||||
element={ElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<ButtonPrimary
|
||||
style={{ marginTop: '2em', marginBottom: '2em', padding: '8px 16px' }}
|
||||
onClick={toggleWalletModal}
|
||||
>
|
||||
<Trans>Connect a wallet</Trans>
|
||||
</ButtonPrimary>
|
||||
</TraceEvent>
|
||||
)}
|
||||
</ErrorContainer>
|
||||
)}
|
||||
</MainContentWrapper>
|
||||
<HideSmall>
|
||||
<CTACards />
|
||||
</HideSmall>
|
||||
</AutoColumn>
|
||||
</PageWrapper>
|
||||
<SwitchLocaleLink />
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</PageWrapper>
|
||||
<SwitchLocaleLink />
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events'
|
||||
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
@@ -28,7 +27,7 @@ import { Text } from 'rebass'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
||||
|
||||
@@ -300,14 +299,14 @@ export default function Swap({ className }: { className?: string }) {
|
||||
permit2Enabled ? maximumAmountIn : undefined,
|
||||
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||
)
|
||||
const isApprovalLoading = permit.state === PermitState.APPROVAL_LOADING
|
||||
const [isPermitPending, setIsPermitPending] = useState(false)
|
||||
const [isPermitFailed, setIsPermitFailed] = useState(false)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const isApprovalPending = useHasPendingApproval(maximumAmountIn?.currency, PERMIT2_ADDRESS)
|
||||
const updatePermit = useCallback(async () => {
|
||||
setIsPermitPending(true)
|
||||
try {
|
||||
const approval = await permit.callback?.(isApprovalPending)
|
||||
const approval = await permit.callback?.()
|
||||
if (approval) {
|
||||
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||
chain_id: chainId,
|
||||
@@ -325,14 +324,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
} finally {
|
||||
setIsPermitPending(false)
|
||||
}
|
||||
}, [
|
||||
addTransaction,
|
||||
chainId,
|
||||
isApprovalPending,
|
||||
maximumAmountIn?.currency.address,
|
||||
maximumAmountIn?.currency.symbol,
|
||||
permit,
|
||||
])
|
||||
}, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit])
|
||||
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approvalState, approveCallback] = useApproveCallbackFromTrade(
|
||||
@@ -794,10 +786,12 @@ export default function Swap({ className }: { className?: string }) {
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
) : isValid && permit.state === PermitState.PERMIT_NEEDED ? (
|
||||
) : isValid &&
|
||||
(permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED ||
|
||||
permit.state === PermitState.APPROVAL_LOADING) ? (
|
||||
<ButtonYellow
|
||||
onClick={updatePermit}
|
||||
disabled={isPermitPending || isApprovalPending}
|
||||
disabled={isPermitPending || isApprovalLoading}
|
||||
style={{ gap: 14 }}
|
||||
>
|
||||
{isPermitPending ? (
|
||||
@@ -814,7 +808,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
<Trans>Approval failed. Try again.</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</>
|
||||
) : isApprovalPending ? (
|
||||
) : isApprovalLoading ? (
|
||||
<>
|
||||
<Loader size="20px" stroke={theme.accentWarning} />
|
||||
<ThemedText.SubHeader color="accentWarning">
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
import TokenDetails from 'components/Tokens/TokenDetails'
|
||||
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { TokenQuery, tokenQuery } from 'graphql/data/Token'
|
||||
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useQueryLoader } from 'react-relay'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
|
||||
|
||||
export default function TokenDetailsPage() {
|
||||
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
||||
const chain = validateUrlChainParam(chainName)
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const isNative = tokenAddress === NATIVE_CHAIN_ID
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
|
||||
const [contract, duration] = useMemo(
|
||||
() => [
|
||||
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain },
|
||||
@@ -35,8 +37,9 @@ export default function TokenDetailsPage() {
|
||||
const refetchTokenPrices = useCallback(
|
||||
(t: TimePeriod) => {
|
||||
loadPriceQuery({ contract, duration: toHistoryDuration(t) })
|
||||
setTimePeriod(t)
|
||||
},
|
||||
[contract, loadPriceQuery]
|
||||
[contract, loadPriceQuery, setTimePeriod]
|
||||
)
|
||||
|
||||
if (!tokenQueryReference) {
|
||||
|
||||
@@ -31,12 +31,18 @@ export const ThemedText = {
|
||||
HeadlineSmall(props: TextProps) {
|
||||
return <TextWrapper fontWeight={600} fontSize={20} lineHeight="28px" color="textPrimary" {...props} />
|
||||
},
|
||||
HeadlineMedium(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} fontSize={28} color="textPrimary" {...props} />
|
||||
},
|
||||
HeadlineLarge(props: TextProps) {
|
||||
return <TextWrapper fontWeight={600} fontSize={36} lineHeight="44px" color="textPrimary" {...props} />
|
||||
return <TextWrapper fontWeight={600} fontSize={36} lineHeight="36px" color="textPrimary" {...props} />
|
||||
},
|
||||
LargeHeader(props: TextProps) {
|
||||
return <TextWrapper fontWeight={400} fontSize={36} color="textPrimary" {...props} />
|
||||
},
|
||||
Hero(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} fontSize={48} color="textPrimary" {...props} />
|
||||
},
|
||||
Link(props: TextProps) {
|
||||
return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} />
|
||||
},
|
||||
|
||||
@@ -91,7 +91,8 @@ function getSettings(darkMode: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTheme(darkMode: boolean) {
|
||||
// eslint-disable-next-line import/no-unused-modules -- used in styled.d.ts
|
||||
export function getTheme(darkMode: boolean) {
|
||||
return {
|
||||
darkMode,
|
||||
...(darkMode ? darkTheme : lightTheme),
|
||||
|
||||
@@ -7,6 +7,7 @@ export function isTestEnv(): boolean {
|
||||
}
|
||||
|
||||
export function isStagingEnv(): boolean {
|
||||
// NB: This is set in vercel builds.
|
||||
return Boolean(process.env.REACT_APP_STAGING)
|
||||
}
|
||||
|
||||
|
||||
@@ -4438,10 +4438,10 @@
|
||||
"@uniswap/v3-core" "1.0.0"
|
||||
"@uniswap/v3-periphery" "^1.0.1"
|
||||
|
||||
"@uniswap/widgets@2.22.10":
|
||||
version "2.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.10.tgz#3b4fbe3ca607c8b096aae58bd6e6d4188af7ddd5"
|
||||
integrity sha512-wFw68p9fiVt06rernsWWqlB3lccgfyCuHTGNFT2NPBQ1lnpxudpa6MKlsMih5gaWiZM1pT8jd3LYbgCX9OeOTw==
|
||||
"@uniswap/widgets@2.22.11":
|
||||
version "2.22.11"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.11.tgz#d64179e58d3923af1b80f63bd8fb44804fc047bd"
|
||||
integrity sha512-eBG7L/inLLHY2+cLKFsqqCK8rhoIzHWsUDm0glpLUyqs6NiZssoKHgIPWzjT2cNwupCYol08ruKM+JR1wBVdTQ==
|
||||
dependencies:
|
||||
"@babel/runtime" ">=7.17.0"
|
||||
"@fontsource/ibm-plex-mono" "^4.5.1"
|
||||
|
||||
Reference in New Issue
Block a user