Compare commits

...

11 Commits

Author SHA1 Message Date
aballerr
4529e3cc88 fix: failing cypress test (#5715)
* fix failing cypress test for 404 page
2022-12-16 16:49:23 -05:00
Mike Grabowski
4d47470f33 feat: not found page (#5708)
* chore: save

* save

* chore: finish

* chore: Fix link

* chore: remove div

* chore: tweaaks

* chore: tweaks
2022-12-16 11:21:29 -05:00
eddie
aedc020646 fix: use render function for SearchBar placeholder translation (#5710)
* fix: use render function for SearchBar placeholder translation

* fix: use render function for SearchBar placeholder translation

* fix: correct clsx usage
2022-12-16 11:07:42 -05:00
Zach Pomerantz
fd8085722e fix: mark permit not syncing if not permitted (#5706)
* fix: mark permit not syncing if not permitted

* fix: clarify naming

* fix: show approval when loading
2022-12-15 13:09:39 -08:00
Mike Grabowski
a60a85db54 fix: layout padding/margin & overflow (#5707)
* chore: fix

* chore: tweaks
2022-12-15 15:34:39 -05:00
eddie
ad2472eac6 fix: correct color for selected token in CurrencySearchModal (#5705)
Co-authored-by: Eddie Dugan <eddie.dugan@UniswapdieDugan.localdomain>
2022-12-15 15:14:16 -05:00
eddie
f4d4acacae fix: constrain width on token details back button (#5703)
Co-authored-by: Eddie Dugan <eddie.dugan@UniswapdieDugan.localdomain>
2022-12-15 14:46:04 -05:00
lynn
a5d7af192c fix: Web 1610 token details another funky state chart re prices (#5685)
* initial commit

* fixes

* move msg loc depending on display price avail

* fred copywriting + jordan comments changes

* fix build errors

Co-authored-by: cartcrom <cartergcromer@gmail.com>
2022-12-15 14:23:03 -05:00
Zach Pomerantz
21a2863ae3 build: default flags but maintain togglability (#5702)
fix: default flags but maintain togglability
2022-12-15 10:56:06 -08:00
Zach Pomerantz
1f871d4e73 build: upgrade widget to 2.22.11 (#5701) 2022-12-15 10:49:31 -08:00
Vignesh Mohankumar
3690936aff chore: remove landing page flag (#5673) 2022-12-15 13:48:50 -05:00
32 changed files with 551 additions and 369 deletions

View File

@@ -3,8 +3,8 @@ describe('Redirect', () => {
cy.visit('/create-proposal') cy.visit('/create-proposal')
cy.url().should('match', /\/vote\/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.visit('/none-exist-url')
cy.url().should('match', /\/swap/) cy.url().should('match', /\/not-found/)
}) })
}) })

View File

@@ -156,7 +156,7 @@
"@uniswap/v3-core": "1.0.0", "@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1", "@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0", "@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": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2", "@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2", "@vanilla-extract/dynamic": "^2.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -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)` export const ButtonLight = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentActionSoft}; background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.accentAction}; color: ${({ theme }) => theme.accentAction};

View File

@@ -1,6 +1,7 @@
import { Group } from '@visx/group' import { Group } from '@visx/group'
import { LinePath } from '@visx/shape' import { LinePath } from '@visx/shape'
import { easeCubicInOut } from 'd3' import { easeSinOut } from 'd3'
import ms from 'ms.macro'
import React from 'react' import React from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { animated, useSpring } from 'react-spring' import { animated, useSpring } from 'react-spring'
@@ -11,8 +12,8 @@ import { LineChartProps } from './LineChart'
type AnimatedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'> type AnimatedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'>
const config = { const config = {
duration: 800, duration: ms`0.8s`,
easing: easeCubicInOut, easing: easeSinOut,
} }
// code reference: https://airbnb.io/visx/lineradial // 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

View 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

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react' 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 { ChevronUpIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks' import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react' import React, { PropsWithChildren, useState } from 'react'
@@ -23,13 +23,6 @@ const BodyWrapper = styled.div<{ margin?: string }>`
padding: 1rem; padding: 1rem;
` `
const SmallButtonPrimary = styled(ButtonPrimary)`
width: auto;
font-size: 16px;
padding: 10px 16px;
border-radius: 12px;
`
const SmallButtonLight = styled(ButtonLight)` const SmallButtonLight = styled(ButtonLight)`
font-size: 16px; font-size: 16px;
padding: 10px 16px; padding: 10px 16px;

View File

@@ -1,5 +1,4 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2' import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@@ -203,12 +202,6 @@ export default function FeatureFlagModal() {
<X size={24} /> <X size={24} />
</CloseButton> </CloseButton>
</Header> </Header>
<FeatureFlagOption
variant={LandingPageVariant}
value={useLandingPageFlag()}
featureFlag={FeatureFlag.landingPage}
label="Landing page"
/>
<FeatureFlagOption <FeatureFlagOption
variant={Permit2Variant} variant={Permit2Variant}
value={usePermit2Flag()} value={usePermit2Flag()}

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line no-restricted-imports // 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 { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events' import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
import clsx from 'clsx' import clsx from 'clsx'
@@ -156,9 +156,9 @@ export const SearchBar = () => {
> >
<Row <Row
className={clsx( className={clsx(
` ${styles.nftSearchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${ styles.nftSearchBar,
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden) !isOpen && !isMobile && magicalGradientOnHover,
} ` isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
)} )}
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'} borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined} borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
@@ -182,18 +182,23 @@ export const SearchBar = () => {
element={ElementName.NAVBAR_SEARCH_INPUT} element={ElementName.NAVBAR_SEARCH_INPUT}
properties={{ ...trace }} properties={{ ...trace }}
> >
<Box <Trans
as="input" id={placeholderText}
placeholder={placeholderText} render={({ translation }) => (
onChange={(event: ChangeEvent<HTMLInputElement>) => { <Box
!isOpen && toggleOpen() as="input"
setSearchValue(event.target.value) placeholder={translation as string}
}} onChange={(event: ChangeEvent<HTMLInputElement>) => {
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)} !isOpen && toggleOpen()
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`} setSearchValue(event.target.value)
value={searchValue} }}
ref={inputRef} onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
width="full" className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
value={searchValue}
ref={inputRef}
width="full"
/>
)}
/> />
</TraceEvent> </TraceEvent>
{!isOpen && <KeyShortCut>/</KeyShortCut>} {!isOpen && <KeyShortCut>/</KeyShortCut>}

View File

@@ -9,14 +9,12 @@ import { AutoColumn } from '../Column'
import ClaimPopup from './ClaimPopup' import ClaimPopup from './ClaimPopup'
import PopupItem from './PopupItem' import PopupItem from './PopupItem'
const MobilePopupWrapper = styled.div<{ height: string | number }>` const MobilePopupWrapper = styled.div`
position: relative; position: relative;
max-width: 100%; max-width: 100%;
height: ${({ height }) => height}; margin: 0 auto;
margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
display: none; display: none;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: block; display: block;
padding-top: 20px; padding-top: 20px;
@@ -74,16 +72,18 @@ export default function Popups() {
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} /> <PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))} ))}
</FixedPopupColumn> </FixedPopupColumn>
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}> {activePopups?.length > 0 && (
<MobilePopupInner> <MobilePopupWrapper>
{activePopups // reverse so new items up front <MobilePopupInner>
.slice(0) {activePopups // reverse so new items up front
.reverse() .slice(0)
.map((item) => ( .reverse()
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} /> .map((item) => (
))} <PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
</MobilePopupInner> ))}
</MobilePopupWrapper> </MobilePopupInner>
</MobilePopupWrapper>
)}
</> </>
) )
} }

View File

@@ -18,7 +18,7 @@ const MobileWrapper = styled(AutoColumn)`
` `
const BaseWrapper = styled.div<{ disable?: boolean }>` 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; border-radius: 16px;
display: flex; display: flex;
padding: 6px; padding: 6px;
@@ -30,8 +30,8 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
background-color: ${({ theme }) => theme.hoverDefault}; background-color: ${({ theme }) => theme.hoverDefault};
} }
color: ${({ theme, disable }) => disable && theme.accentAction}; color: ${({ theme, disable }) => disable && theme.accentActive};
background-color: ${({ theme, disable }) => disable && theme.accentActionSoft}; background-color: ${({ theme, disable }) => disable && theme.accentActiveSoft};
` `
const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({ const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({

View File

@@ -11,6 +11,7 @@ export const BreadcrumbNavLink = styled(Link)`
text-decoration: none; text-decoration: none;
margin-bottom: 16px; margin-bottom: 16px;
transition-duration: ${({ theme }) => theme.transition.duration.fast}; transition-duration: ${({ theme }) => theme.transition.duration.fast};
width: fit-content;
&:hover { &:hover {
color: ${({ theme }) => theme.textTertiary}; color: ${({ theme }) => theme.textTertiary};

View File

@@ -4,11 +4,11 @@ import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { isPricePoint, PricePoint } from 'graphql/data/util' import { isPricePoint, PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' 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 { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { filterTimeAtom } from '../state' import { PriceChart } from './PriceChart'
import PriceChart from './PriceChart'
import TimePeriodSelector from './TimeSelector' import TimePeriodSelector from './TimeSelector'
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined { function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined {
@@ -58,7 +58,7 @@ function Chart({
}) { }) {
const prices = usePreloadedTokenPriceQuery(priceQueryReference) const prices = usePreloadedTokenPriceQuery(priceQueryReference)
// Initializes time period to global & maintain separate time period for subsequent changes // Initializes time period to global & maintain separate time period for subsequent changes
const [timePeriod, setTimePeriod] = useState(useAtomValue(filterTimeAtom)) const timePeriod = useAtomValue(pageTimePeriodAtom)
return ( return (
<ChartContainer> <ChartContainer>
@@ -69,7 +69,6 @@ function Chart({
currentTimePeriod={timePeriod} currentTimePeriod={timePeriod}
onTimeChange={(t: TimePeriod) => { onTimeChange={(t: TimePeriod) => {
startTransition(() => refetchTokenPrices(t)) startTransition(() => refetchTokenPrices(t))
setTimePeriod(t)
}} }}
/> />
</ChartContainer> </ChartContainer>

View File

@@ -1,11 +1,11 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { AxisBottom, TickFormatter } from '@visx/axis' import { AxisBottom, TickFormatter } from '@visx/axis'
import { localPoint } from '@visx/event' import { localPoint } from '@visx/event'
import { EventType } from '@visx/event/lib/types' import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph' import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape' import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart' import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import FadedInLineChart from 'components/Charts/FadeInLineChart'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3' import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { PricePoint } from 'graphql/data/util' import { PricePoint } from 'graphql/data/util'
import { TimePeriod } 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 { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather' import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/styles'
import { import {
dayHourFormatter, dayHourFormatter,
hourFormatter, hourFormatter,
@@ -21,6 +23,7 @@ import {
monthYearDayFormatter, monthYearDayFormatter,
weekFormatter, weekFormatter,
} from 'utils/formatChartTimes' } from 'utils/formatChartTimes'
import { formatDollar } from 'utils/formatNumbers'
const DATA_EMPTY = { value: 0, timestamp: 0 } const DATA_EMPTY = { value: 0, timestamp: 0 }
@@ -68,11 +71,19 @@ export const DeltaText = styled.span<{ delta: number | undefined }>`
const ChartHeader = styled.div` const ChartHeader = styled.div`
position: absolute; position: absolute;
${textFadeIn};
animation-duration: ${({ theme }) => theme.transition.duration.medium};
` `
export const TokenPrice = styled.span` export const TokenPrice = styled.span`
font-size: 36px; font-size: 36px;
line-height: 44px; line-height: 44px;
` `
const MissingPrice = styled(TokenPrice)`
font-size: 24px;
line-height: 44px;
color: ${({ theme }) => theme.textTertiary};
`
const DeltaContainer = styled.div` const DeltaContainer = styled.div`
height: 16px; height: 16px;
display: flex; display: flex;
@@ -84,6 +95,29 @@ export const ArrowCell = styled.div`
display: flex; 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 margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44 const timeOptionsHeight = 44
@@ -94,24 +128,30 @@ interface PriceChartProps {
timePeriod: TimePeriod timePeriod: TimePeriod
} }
function formatDisplayPrice(value: number) { export function PriceChart({ width, height, prices: originalPrices, timePeriod }: PriceChartProps) {
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) {
const locale = useActiveLocale() const locale = useActiveLocale()
const theme = useTheme() 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 // 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 // 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) const [displayPrice, setDisplayPrice] = useState(startingPrice)
// set display price to ending price when prices have changed. // set display price to ending price when prices have changed.
@@ -133,9 +173,9 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const rdScale = useMemo( const rdScale = useMemo(
() => () =>
scaleLinear() scaleLinear()
.domain(getPriceBounds(prices ?? [])) .domain(getPriceBounds(originalPrices ?? []))
.range([graphInnerHeight, 0]), .range([graphInnerHeight, 0]),
[prices, graphInnerHeight] [originalPrices, graphInnerHeight]
) )
function tickFormat( function tickFormat(
@@ -221,7 +261,6 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const arrow = getDeltaArrow(delta) const arrow = getDeltaArrow(delta)
const crosshairEdgeMax = width * 0.85 const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
const hasData = prices && prices.length > 0
/* /*
* Default curve doesn't look good for the HOUR chart. * 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 getX = useMemo(() => (p: PricePoint) => timeScale(p.timestamp), [timeScale])
const getY = useMemo(() => (p: PricePoint) => rdScale(p.value), [rdScale]) const getY = useMemo(() => (p: PricePoint) => rdScale(p.value), [rdScale])
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension]) const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
return ( return (
<> <>
<ChartHeader> <ChartHeader>
<TokenPrice>{formatDisplayPrice(displayPrice.value)}</TokenPrice> {displayPrice.value ? (
<DeltaContainer> <>
<ArrowCell>{arrow}</ArrowCell> <TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
<DeltaText delta={delta}>{formattedDelta}</DeltaText> <DeltaContainer>
</DeltaContainer> {formattedDelta}
<ArrowCell>{arrow}</ArrowCell>
</DeltaContainer>
</>
) : (
<>
<MissingPrice>Price Unavailable</MissingPrice>
<ThemedText.Caption style={{ color: theme.textTertiary }}>{missingPricesMessage}</ThemedText.Caption>
</>
)}
</ChartHeader> </ChartHeader>
{!hasData ? ( {!chartAvailable ? (
<MissingPriceChart <MissingPriceChart width={width} height={graphHeight} message={!!displayPrice.value && missingPricesMessage} />
width={width}
height={graphHeight}
message={
prices?.length === 0 ? (
<Trans>This token doesn&apos;t have chart data because it hasn&apos;t been traded on Uniswap v3</Trans>
) : (
<Trans>Missing chart data</Trans>
)
}
/>
) : ( ) : (
<svg width={width} height={graphHeight} style={{ minWidth: '100%' }}> <svg width={width} height={graphHeight} style={{ minWidth: '100%' }}>
<AnimatedInLineChart <AnimatedInLineChart
@@ -264,6 +303,19 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
curve={curve} curve={curve}
strokeWidth={2} 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 ? ( {crosshair !== null ? (
<g> <g>
<AxisBottom <AxisBottom
@@ -354,9 +406,7 @@ const StyledMissingChart = styled.svg`
font-weight: 400; font-weight: 400;
} }
` `
const chartBottomPadding = 15 const chartBottomPadding = 15
function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) { function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) {
const theme = useTheme() const theme = useTheme()
const midPoint = height / 2 + 45 const midPoint = height / 2 + 45
@@ -369,18 +419,10 @@ function MissingPriceChart({ width, height, message }: { width: number; height:
fill="transparent" fill="transparent"
strokeWidth="2" 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}> <text y={height - chartBottomPadding} x="20" fill={theme.textTertiary}>
{message || <Trans>Missing chart data</Trans>} {message}
</text> </text>
<path
d={`M 0 ${height - 1}, ${width} ${height - 1}`}
stroke={theme.backgroundOutline}
fill="transparent"
strokeWidth="1"
/>
</StyledMissingChart> </StyledMissingChart>
) )
} }
export default PriceChart

View File

@@ -72,6 +72,8 @@ export const TokenInfoContainer = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 4px; margin-bottom: 4px;
${textFadeIn};
animation-duration: ${({ theme }) => theme.transition.duration.medium};
` `
export const TokenNameCell = styled.div` export const TokenNameCell = styled.div`
display: flex; display: flex;
@@ -79,7 +81,6 @@ export const TokenNameCell = styled.div`
font-size: 20px; font-size: 20px;
line-height: 28px; line-height: 28px;
align-items: center; align-items: center;
${textFadeIn}
` `
/* Loading state bubbles */ /* Loading state bubbles */
const DetailBubble = styled(LoadingBubble)` const DetailBubble = styled(LoadingBubble)`

View File

@@ -12,7 +12,6 @@ export const PageWrapper = styled.div`
padding: 68px 8px 0px; padding: 68px 8px 0px;
max-width: 480px; max-width: 480px;
width: 100%; width: 100%;
height: 100vh;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
padding-top: 48px; padding-top: 48px;

View File

@@ -1,5 +1,4 @@
export enum FeatureFlag { export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc', traceJsonRpc = 'traceJsonRpc',
landingPage = 'landingPage',
permit2 = 'permit2', permit2 = 'permit2',
} }

View File

@@ -1,7 +0,0 @@
import { BaseVariant } from '../index'
export function useLandingPageFlag(): BaseVariant {
return BaseVariant.Enabled
}
export { BaseVariant as LandingPageVariant }

View File

@@ -55,12 +55,13 @@ export enum BaseVariant {
Enabled = 'enabled', Enabled = 'enabled',
} }
export function useBaseFlag(flag: string): BaseVariant { export function useBaseFlag(flag: string, defaultValue = BaseVariant.Control): BaseVariant {
switch (useFeatureFlagsContext().flags[flag]) { switch (useFeatureFlagsContext().flags[flag]) {
case 'enabled': case 'enabled':
return BaseVariant.Enabled return BaseVariant.Enabled
case 'control': case 'control':
default:
return BaseVariant.Control return BaseVariant.Control
default:
return defaultValue
} }
} }

View File

@@ -20,13 +20,13 @@ enum SyncState {
export enum PermitState { export enum PermitState {
INVALID, INVALID,
LOADING, LOADING,
PERMIT_NEEDED, APPROVAL_OR_PERMIT_NEEDED,
PERMITTED, APPROVAL_LOADING,
APPROVED_AND_PERMITTED,
} }
export interface Permit { export interface Permit {
state: PermitState state: PermitState
isSyncing?: boolean
signature?: PermitSignature signature?: PermitSignature
callback?: () => Promise<{ callback?: () => Promise<{
response: ContractTransaction response: ContractTransaction
@@ -81,9 +81,8 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
// Permit2 should be marked syncing from the time approval is submitted (pending) until it is // 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. // synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval.
// It should *not* be marked syncing if not permitted, because the user must still take action.
const [syncState, setSyncState] = useState(SyncState.SYNCED) const [syncState, setSyncState] = useState(SyncState.SYNCED)
const isSyncing = isPermitted || isSigned ? false : syncState !== SyncState.SYNCED const isApprovalLoading = syncState !== SyncState.SYNCED
const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS) const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS)
useEffect(() => { useEffect(() => {
if (hasPendingApproval) { if (hasPendingApproval) {
@@ -117,13 +116,25 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
return { state: PermitState.INVALID } return { state: PermitState.INVALID }
} else if (!tokenAllowance || !permitAllowance) { } else if (!tokenAllowance || !permitAllowance) {
return { state: PermitState.LOADING } return { state: PermitState.LOADING }
} else if (isAllowed) { } else if (!(isPermitted || isSigned)) {
if (isPermitted) { return { state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback }
return { state: PermitState.PERMITTED } } else if (!isAllowed) {
} else if (isSigned) { return {
return { state: PermitState.PERMITTED, signature } 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, isSyncing, callback } }, [
}, [amount, callback, isAllowed, isPermitted, isSigned, isSyncing, permitAllowance, signature, tokenAllowance]) amount,
callback,
isAllowed,
isApprovalLoading,
isPermitted,
isSigned,
permitAllowance,
signature,
tokenAllowance,
])
} }

View File

@@ -4,7 +4,6 @@ import Loader from 'components/Loader'
import { MenuDropdown } from 'components/NavBar/MenuDropdown' import { MenuDropdown } from 'components/NavBar/MenuDropdown'
import TopLevelModals from 'components/TopLevelModals' import TopLevelModals from 'components/TopLevelModals'
import { useFeatureFlagsIsLoaded } from 'featureFlags' import { useFeatureFlagsIsLoaded } from 'featureFlags'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader' import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton' import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
@@ -37,6 +36,7 @@ import Manage from './Earn/Manage'
import Landing from './Landing' import Landing from './Landing'
import MigrateV2 from './MigrateV2' import MigrateV2 from './MigrateV2'
import MigrateV2Pair from './MigrateV2/MigrateV2Pair' import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
import NotFound from './NotFound'
import Pool from './Pool' import Pool from './Pool'
import { PositionPage } from './Pool/PositionPage' import { PositionPage } from './Pool/PositionPage'
import PoolV2 from './Pool/v2' import PoolV2 from './Pool/v2'
@@ -65,29 +65,19 @@ initializeAnalytics(ANALYTICS_DUMMY_KEY, OriginApplication.INTERFACE, {
isProductionEnv: isProductionEnv(), isProductionEnv: isProductionEnv(),
}) })
const AppWrapper = styled.div`
display: flex;
flex-flow: column;
align-items: flex-start;
height: 100%;
`
const BodyWrapper = styled.div` const BodyWrapper = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; min-height: 100vh;
padding: 72px 0px 0px 0px; padding: ${({ theme }) => theme.navHeight}px 0px 5rem 0px;
align-items: center; align-items: center;
flex: 1; flex: 1;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
padding: 52px 0px 16px 0px;
`};
` `
const MobileBottomBar = styled.div` const MobileBottomBar = styled.div`
z-index: ${Z_INDEX.sticky}; z-index: ${Z_INDEX.sticky};
position: sticky; position: fixed;
display: flex; display: flex;
bottom: 0; bottom: 0;
right: 0; right: 0;
@@ -115,10 +105,6 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
z-index: ${Z_INDEX.sticky}; z-index: ${Z_INDEX.sticky};
` `
const Marginer = styled.div`
margin-top: 5rem;
`
function getCurrentPageFromLocation(locationPathname: string): PageName | undefined { function getCurrentPageFromLocation(locationPathname: string): PageName | undefined {
switch (true) { switch (true) {
case locationPathname.startsWith('/swap'): case locationPathname.startsWith('/swap'):
@@ -202,134 +188,131 @@ export default function App() {
const isHeaderTransparent = !scrolledState const isHeaderTransparent = !scrolledState
const landingPageFlag = useLandingPageFlag()
return ( return (
<ErrorBoundary> <ErrorBoundary>
<DarkModeQueryParamReader /> <DarkModeQueryParamReader />
<ApeModeQueryParamReader /> <ApeModeQueryParamReader />
<AppWrapper> <Trace page={currentPage}>
<Trace page={currentPage}> <HeaderWrapper transparent={isHeaderTransparent}>
<HeaderWrapper transparent={isHeaderTransparent}> <NavBar />
<NavBar /> </HeaderWrapper>
</HeaderWrapper> <BodyWrapper>
<BodyWrapper> <Popups />
<Popups /> <Polling />
<Polling /> <TopLevelModals />
<TopLevelModals /> <Suspense fallback={<Loader />}>
<Suspense fallback={<Loader />}> {isLoaded ? (
{isLoaded ? ( <Routes>
<Routes> <Route path="/" element={<Landing />} />
{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 />} />
<Route path="send" element={<RedirectPathToSwapOnly />} /> <Route path="tokens" element={<Tokens />}>
<Route path="swap" element={<Swap />} /> <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="send" element={<RedirectPathToSwapOnly />} />
<Route path="pool/v2" element={<PoolV2 />} /> <Route path="swap" element={<Swap />} />
<Route path="pool" element={<Pool />} />
<Route path="pool/:tokenId" element={<PositionPage />} />
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}> <Route path="pool/v2/find" element={<PoolFinder />} />
<Route path=":currencyIdA" /> <Route path="pool/v2" element={<PoolV2 />} />
<Route path=":currencyIdA/:currencyIdB" /> <Route path="pool" element={<Pool />} />
</Route> <Route path="pool/:tokenId" element={<PositionPage />} />
<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="increase" element={<AddLiquidity />}> <Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
<Route path=":currencyIdA" /> <Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" /> <Route path=":currencyIdA/:currencyIdB" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount" /> </Route>
<Route path=":currencyIdA/:currencyIdB/:feeAmount/:tokenId" /> <Route path="add" element={<RedirectDuplicateTokenIds />}>
</Route> {/* 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="increase" element={<AddLiquidity />}>
<Route path="remove/:tokenId" element={<RemoveLiquidityV3 />} /> <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="remove/v2/:currencyIdA/:currencyIdB" element={<RemoveLiquidity />} />
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} /> <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 <Route
path="/nfts" path="/nfts"
element={ element={
// TODO: replace loading state during Apollo migration // TODO: replace loading state during Apollo migration
<Suspense fallback={null}> <Suspense fallback={null}>
<NftExplore /> <NftExplore />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/nfts/asset/:contractAddress/:tokenId" path="/nfts/asset/:contractAddress/:tokenId"
element={ element={
<Suspense fallback={<AssetDetailsLoading />}> <Suspense fallback={<AssetDetailsLoading />}>
<Asset /> <Asset />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/nfts/profile" path="/nfts/profile"
element={ element={
<Suspense fallback={<ProfilePageLoadingSkeleton />}> <Suspense fallback={<ProfilePageLoadingSkeleton />}>
<Profile /> <Profile />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/nfts/collection/:contractAddress" path="/nfts/collection/:contractAddress"
element={ element={
<Suspense fallback={<CollectionPageSkeleton />}> <Suspense fallback={<CollectionPageSkeleton />}>
<Collection /> <Collection />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/nfts/collection/:contractAddress/activity" path="/nfts/collection/:contractAddress/activity"
element={ element={
<Suspense fallback={<CollectionPageSkeleton />}> <Suspense fallback={<CollectionPageSkeleton />}>
<Collection /> <Collection />
</Suspense> </Suspense>
} }
/> />
</Routes>
) : ( <Route path="*" element={<Navigate to="/not-found" replace />} />
<Loader /> <Route path="/not-found" element={<NotFound />} />
)} </Routes>
</Suspense> ) : (
<Marginer /> <Loader />
</BodyWrapper> )}
<MobileBottomBar> </Suspense>
<PageTabs /> </BodyWrapper>
<Box marginY="4"> <MobileBottomBar>
<MenuDropdown /> <PageTabs />
</Box> <Box marginY="4">
</MobileBottomBar> <MenuDropdown />
</Trace> </Box>
</AppWrapper> </MobileBottomBar>
</Trace>
</ErrorBoundary> </ErrorBoundary>
) )
} }

View File

@@ -1,7 +1,6 @@
import { Trace, TraceEvent } from '@uniswap/analytics' import { Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events' import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events'
import { BaseButton } from 'components/Button' import { BaseButton } from 'components/Button'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import Swap from 'pages/Swap' import Swap from 'pages/Swap'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@@ -170,21 +169,14 @@ export default function Landing() {
const location = useLocation() const location = useLocation()
const isOpen = location.pathname === '/' const isOpen = location.pathname === '/'
const landingPageFlag = useLandingPageFlag()
useEffect(() => { useEffect(() => {
if (landingPageFlag) { document.body.style.overflow = 'hidden'
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'auto'
}
}
return () => { 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 ( return (
<Trace page={PageName.LANDING_PAGE} shouldLogImpression> <Trace page={PageName.LANDING_PAGE} shouldLogImpression>

View 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>
)
}

View File

@@ -165,7 +165,7 @@ function WrongNetworkCard() {
const theme = useTheme() const theme = useTheme()
return ( return (
<div style={{ height: '100vh' }}> <>
<PageWrapper> <PageWrapper>
<AutoColumn gap="lg" justify="center"> <AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" style={{ width: '100%' }}> <AutoColumn gap="lg" style={{ width: '100%' }}>
@@ -189,7 +189,7 @@ function WrongNetworkCard() {
</AutoColumn> </AutoColumn>
</PageWrapper> </PageWrapper>
<SwitchLocaleLink /> <SwitchLocaleLink />
</div> </>
) )
} }
@@ -263,86 +263,84 @@ export default function Pool() {
return ( return (
<Trace page={PageName.POOL_PAGE} shouldLogImpression> <Trace page={PageName.POOL_PAGE} shouldLogImpression>
<div style={{ height: '100vh' }}> <PageWrapper>
<PageWrapper> <AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" justify="center"> <AutoColumn gap="lg" style={{ width: '100%' }}>
<AutoColumn gap="lg" style={{ width: '100%' }}> <TitleRow padding="0">
<TitleRow padding="0"> <ThemedText.LargeHeader>
<ThemedText.LargeHeader> <Trans>Pools</Trans>
<Trans>Pools</Trans> </ThemedText.LargeHeader>
</ThemedText.LargeHeader> <ButtonRow>
<ButtonRow> {showV2Features && (
{showV2Features && ( <PoolMenu
<PoolMenu menuItems={menuItems}
menuItems={menuItems} flyoutAlignment={FlyoutAlignment.LEFT}
flyoutAlignment={FlyoutAlignment.LEFT} ToggleUI={(props: any) => (
ToggleUI={(props: any) => ( <MoreOptionsButton {...props}>
<MoreOptionsButton {...props}> <MoreOptionsText>
<MoreOptionsText> <Trans>More</Trans>
<Trans>More</Trans> <ChevronDown size={15} />
<ChevronDown size={15} /> </MoreOptionsText>
</MoreOptionsText> </MoreOptionsButton>
</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}
/> />
) : (
<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> <ResponsiveButtonPrimary data-cy="join-pool-button" id="join-pool-button" as={Link} to="/add/ETH">
<HideSmall> + <Trans>New Position</Trans>
<CTACards /> </ResponsiveButtonPrimary>
</HideSmall> </ButtonRow>
</AutoColumn> </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> </AutoColumn>
</PageWrapper> </AutoColumn>
<SwitchLocaleLink /> </PageWrapper>
</div> <SwitchLocaleLink />
</Trace> </Trace>
) )
} }

View File

@@ -299,7 +299,7 @@ export default function Swap({ className }: { className?: string }) {
permit2Enabled ? maximumAmountIn : undefined, permit2Enabled ? maximumAmountIn : undefined,
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
) )
const isApprovalPending = permit.isSyncing const isApprovalLoading = permit.state === PermitState.APPROVAL_LOADING
const [isPermitPending, setIsPermitPending] = useState(false) const [isPermitPending, setIsPermitPending] = useState(false)
const [isPermitFailed, setIsPermitFailed] = useState(false) const [isPermitFailed, setIsPermitFailed] = useState(false)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
@@ -786,10 +786,12 @@ export default function Swap({ className }: { className?: string }) {
</ButtonError> </ButtonError>
</AutoColumn> </AutoColumn>
</AutoRow> </AutoRow>
) : isValid && permit.state === PermitState.PERMIT_NEEDED ? ( ) : isValid &&
(permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED ||
permit.state === PermitState.APPROVAL_LOADING) ? (
<ButtonYellow <ButtonYellow
onClick={updatePermit} onClick={updatePermit}
disabled={isPermitPending || isApprovalPending} disabled={isPermitPending || isApprovalLoading}
style={{ gap: 14 }} style={{ gap: 14 }}
> >
{isPermitPending ? ( {isPermitPending ? (
@@ -806,7 +808,7 @@ export default function Swap({ className }: { className?: string }) {
<Trans>Approval failed. Try again.</Trans> <Trans>Approval failed. Try again.</Trans>
</ThemedText.SubHeader> </ThemedText.SubHeader>
</> </>
) : isApprovalPending ? ( ) : isApprovalLoading ? (
<> <>
<Loader size="20px" stroke={theme.accentWarning} /> <Loader size="20px" stroke={theme.accentWarning} />
<ThemedText.SubHeader color="accentWarning"> <ThemedText.SubHeader color="accentWarning">

View File

@@ -1,21 +1,23 @@
import { filterTimeAtom } from 'components/Tokens/state'
import TokenDetails from 'components/Tokens/TokenDetails' import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton' import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { TokenQuery, tokenQuery } from 'graphql/data/Token' import { TokenQuery, tokenQuery } from 'graphql/data/Token'
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice' import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util' 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 { Suspense, useCallback, useEffect, useMemo } from 'react'
import { useQueryLoader } from 'react-relay' import { useQueryLoader } from 'react-relay'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
export default function TokenDetailsPage() { export default function TokenDetailsPage() {
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>() const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const chain = validateUrlChainParam(chainName) const chain = validateUrlChainParam(chainName)
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain] const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const isNative = tokenAddress === NATIVE_CHAIN_ID const isNative = tokenAddress === NATIVE_CHAIN_ID
const timePeriod = useAtomValue(filterTimeAtom) const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
const [contract, duration] = useMemo( const [contract, duration] = useMemo(
() => [ () => [
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain }, { address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain },
@@ -35,8 +37,9 @@ export default function TokenDetailsPage() {
const refetchTokenPrices = useCallback( const refetchTokenPrices = useCallback(
(t: TimePeriod) => { (t: TimePeriod) => {
loadPriceQuery({ contract, duration: toHistoryDuration(t) }) loadPriceQuery({ contract, duration: toHistoryDuration(t) })
setTimePeriod(t)
}, },
[contract, loadPriceQuery] [contract, loadPriceQuery, setTimePeriod]
) )
if (!tokenQueryReference) { if (!tokenQueryReference) {

View File

@@ -31,12 +31,18 @@ export const ThemedText = {
HeadlineSmall(props: TextProps) { HeadlineSmall(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={20} lineHeight="28px" color="textPrimary" {...props} /> 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) { 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) { LargeHeader(props: TextProps) {
return <TextWrapper fontWeight={400} fontSize={36} color="textPrimary" {...props} /> return <TextWrapper fontWeight={400} fontSize={36} color="textPrimary" {...props} />
}, },
Hero(props: TextProps) {
return <TextWrapper fontWeight={500} fontSize={48} color="textPrimary" {...props} />
},
Link(props: TextProps) { Link(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} /> return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} />
}, },

View File

@@ -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 { return {
darkMode, darkMode,
...(darkMode ? darkTheme : lightTheme), ...(darkMode ? darkTheme : lightTheme),

View File

@@ -7,6 +7,7 @@ export function isTestEnv(): boolean {
} }
export function isStagingEnv(): boolean { export function isStagingEnv(): boolean {
// NB: This is set in vercel builds.
return Boolean(process.env.REACT_APP_STAGING) return Boolean(process.env.REACT_APP_STAGING)
} }

View File

@@ -4438,10 +4438,10 @@
"@uniswap/v3-core" "1.0.0" "@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1" "@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@2.22.10": "@uniswap/widgets@2.22.11":
version "2.22.10" version "2.22.11"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.10.tgz#3b4fbe3ca607c8b096aae58bd6e6d4188af7ddd5" resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.11.tgz#d64179e58d3923af1b80f63bd8fb44804fc047bd"
integrity sha512-wFw68p9fiVt06rernsWWqlB3lccgfyCuHTGNFT2NPBQ1lnpxudpa6MKlsMih5gaWiZM1pT8jd3LYbgCX9OeOTw== integrity sha512-eBG7L/inLLHY2+cLKFsqqCK8rhoIzHWsUDm0glpLUyqs6NiZssoKHgIPWzjT2cNwupCYol08ruKM+JR1wBVdTQ==
dependencies: dependencies:
"@babel/runtime" ">=7.17.0" "@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1" "@fontsource/ibm-plex-mono" "^4.5.1"