Compare commits

...

37 Commits

Author SHA1 Message Date
Zach Pomerantz
751ba3c62d fix: token price (#5018)
* feat: add decimals, wrapper to token query

* fix: construct token in details page

* fix: clean widget default behavior

* fix: actual defaulting again

* fix: split token from price queries

* fix: reimplement TokenPrice query

* chore: rm old code

* fix: keep loading chart while loading

* fix: mv loader down

* fix: loading chart
2022-10-26 18:53:44 -07:00
Zach Pomerantz
83bc6db74e fix: clean up widget token lock (#5017)
* feat: add decimals, wrapper to token query

* fix: construct token in details page

* fix: clean widget default behavior

* fix: actual defaulting again
2022-10-26 18:16:04 -07:00
Zach Pomerantz
058aa52faf fix: fetch decimals from token query (#5016)
* feat: add decimals, wrapper to token query

* fix: construct token in details page
2022-10-26 18:00:53 -07:00
Jordan Frankfurt
2e3950018a fix: update "item" to "nft" in closed bag mobile copy (#5015)
https://uniswaplabs.atlassian.net/browse/WEB-1567
2022-10-26 18:53:24 -05:00
Greg Bugyis
d86120a257 feat: Collection page log events (#4971)
* Log collection page view

* Make property names/intention more clear

* Remove console log

* Add event for Activity tab

* Filter events (buy now, marketplaces, and price range)

* Handle trait items

* Bump collection stats mobile padding

* Use shouldLogImpression to conditionally fire event

* Adding back trace on Collection page, still necessary

* Add back address property

* Drop Buy Now log, not part of first set

* Update filter properties to match spreadsheet

* Only trigger price range log if inputs contain a value

* Fix ordering on Page Names

* Capitalize text

* Add constant for filter types

* Add chainId as property
2022-10-27 00:44:46 +03:00
Zach Pomerantz
e2fea4a5fb fix: do not flicker when navigating tokens (#5013) 2022-10-26 10:27:50 -07:00
lynn
1f810f84be fix: remove chain selector for nft pages (#5002)
remove chain selector for nft pages
2022-10-26 12:56:26 -04:00
Jordan Frankfurt
f6ffc68ef7 fix: update bag header layout (#5005)
* fix: update-bag-header

* pr feedback
2022-10-26 10:49:41 -05:00
Jordan Frankfurt
b3c44f20d7 fix: bag footer layout (#5003)
* fix: bag footer layout

* pr feedback

* remove balance from footer
2022-10-26 10:37:35 -05:00
Zach Pomerantz
8a5045f635 fix: do not test rpc urls before walletconnecting (#5008) 2022-10-26 08:15:06 -07:00
Zach Pomerantz
c1607bbd52 fix: token details layout gap (#5004) 2022-10-26 08:14:38 -07:00
Zach Pomerantz
dfbed6b89d fix: rm connect wallet border (#5009) 2022-10-25 17:44:50 -07:00
Zach Pomerantz
c871e55d82 fix: clean up balance summaries (#4984)
* fix: clean up balance summaries

* fix: nits

* fix: off-chain balances

* fix: only show token under details

* fix: consolidate formatting
2022-10-25 13:46:25 -07:00
Jordan Frankfurt
f15ac091b4 fix: hover flash (#4998) 2022-10-25 15:21:19 -05:00
Greg Bugyis
6037d74cfb feat: page view event on NFT Details (#4995)
* Add details page to page constants

* Add page view event to details page
2022-10-25 22:46:41 +03:00
Zach Pomerantz
d0e4659d32 feat: transition between tokens details (#4981)
* fix: navigate to widget-selected token

* fix: leave tokens if default is already set

* refactor: clean up widget skeleton

* fix: clean widget skeleton

* feat: transition between tokens

* fix: flicker on chart draw

* fix: nits

* fix: pixel-match loader

* fix: rm debug clause

* fix: hr color
2022-10-25 12:16:01 -07:00
lynn
2d87e692e6 fix: change suspicious activity warning txt (#5001)
change text
2022-10-25 15:12:00 -04:00
lynn
f65fb5bc2b fix: fixes show more / show less (#5000)
* fixes font

* oops merge again
2022-10-25 15:11:25 -04:00
lynn
83597c0efe fix: enlarge stat line font (#4999)
fix font
2022-10-25 14:56:43 -04:00
Jordan Frankfurt
21ee680d3a fix: bag row item usd price alignment (#4996) 2022-10-25 11:14:03 -05:00
Jordan Frankfurt
627af50841 fix: total and 'my bag' text style update (#4994) 2022-10-25 11:04:12 -05:00
Zach Pomerantz
a955b3730e feat: navigate to widget-selected token (#4975)
* fix: navigate to widget-selected token

* fix: leave tokens if default is already set

* refactor: clean up widget skeleton

* fix: clean widget skeleton

* fix: nits
2022-10-25 08:56:15 -07:00
vignesh mohankumar
2f7c5b1df4 chore: remove remaining redesign flag usage (#4958)
* chore: remove redesign flag in base shared components

* remove more flags

* rm pending view

* chore: remove liquidity redesign flags

* rm searchmodal

* rm searchmodal

* rm searchmodal

* numericalinput

* rm remaining

* rm flag

* fix

* fixes

* !isactive fix

* fix appbody

* hoverdefault fix
2022-10-25 09:08:14 -04:00
lynn
8fca286099 fix: resolve some tokens not searchable by contract address in token selector cx bug (#4988)
* init

* ask zzmp question

* fixes omg

* remove comment

* comments

* respond to zzmp

* fix
2022-10-24 23:53:42 -04:00
cartcrom
8be9701700 fix: token selector contract address search (#4987)
* fixed contract address search not working after importing token
* fixed unsupported asset bug
2022-10-24 16:22:26 -04:00
Jack Short
d66002dc75 feat: nft sweep (#4972)
* adding slider

* removing test css

* initial sweep pass

* cleaning up sweep functionality

* adding eth amount for sweep

* handling input

* sweep does not add duplicates

* updating fetcher to handle traits + price

* locking sweeped items on contract addr change/ filter change

* final touches to sweep for desktop

* desktop sweep finalizations

* keeping state after close

* added mobile sweep

* loading state

* dedup

* addressing comments
2022-10-24 14:55:55 -04:00
Zach Pomerantz
b12e5270fa fix: sync network connector to wallet (#4985)
* fix: sync network connector to wallet

* fix: clean todos
2022-10-24 11:51:19 -07:00
lynn
a717818920 fix: truncate search token name earlier (#4956)
* truncate search token name earlier

* fixes in response to comments
2022-10-24 12:57:01 -04:00
cartcrom
d9434a1a9c feat: update token safety / lists / verification (#4968)
* removed selected list logic and state
* updated copy
* updated warning color
* updated lists and fixed native currency bug
* removed no-longer-relevant active list tests
* removed leftover list code
* copy and color changes
2022-10-24 11:53:00 -04:00
Jack Short
a920a93b3d feat: erc1155 specific functionality to details page (#4867)
* adding erc1155 specific functionality to details page

* style update
2022-10-24 11:34:39 -04:00
Jordan Frankfurt
0987a311cf fix(nft): empty bag state render (#4947)
* fix empty bag state render

* add tests
2022-10-21 12:38:30 -05:00
Jack Short
a97a6b7fa8 chore: update price range slider to rc-slider (#4980)
* chore: update price range slider to rc-slider

* removing styles

* decreasing stepsize
2022-10-21 13:37:42 -04:00
vignesh mohankumar
414b221727 fix: no margin-top for LoadingRows (#4979) 2022-10-21 13:33:15 -04:00
aballerr
bab2f47ac9 style: filters update (#4749)
* styles filter update
2022-10-21 09:43:30 -05:00
vignesh mohankumar
0323725543 fix: add padding to LoadingRow (#4931)
* fix: add padding to LoadingRow

* remove sortable on header
2022-10-20 16:20:39 -04:00
aballerr
f6a7c8568e fix: overflow and empty popup (#4973)
* fixing a popup bug and wallet overflow
2022-10-20 11:28:03 -05:00
Charles Bachmeier
a21bbfd5a7 feat: NFT GraphQL Feature Flag (#4969)
* add nft graphQl feature flag

* connect flag to env provider

* attempting metadata relay env switch

* working config

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-10-20 08:51:00 -07:00
124 changed files with 2773 additions and 2821 deletions

View File

@@ -194,6 +194,7 @@
"polyfill-object.fromentries": "^1.0.1",
"popper-max-size-modifier": "^0.2.0",
"qs": "^6.9.4",
"rc-slider": "^10.0.1",
"react": "^18.2.0",
"react-confetti": "^6.0.0",
"react-dom": "^18.2.0",

View File

@@ -14,6 +14,9 @@ export enum EventName {
PAGE_VIEWED = 'Page Viewed',
NAVBAR_SEARCH_SELECTED = 'Navbar Search Selected',
NAVBAR_SEARCH_EXITED = 'Navbar Search Exited',
NFT_ACTIVITY_SELECTED = 'NFT Activity Selected',
NFT_FILTER_OPENED = 'NFT Collection Filter Opened',
NFT_FILTER_SELECTED = 'NFT Filter Selected',
SWAP_AUTOROUTER_VISUALIZATION_EXPANDED = 'Swap Autorouter Visualization Expanded',
SWAP_DETAILS_EXPANDED = 'Swap Details Expanded',
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
@@ -74,6 +77,8 @@ export enum SWAP_PRICE_UPDATE_USER_RESPONSE {
* Known pages in the app. Highest order context.
*/
export enum PageName {
NFT_COLLECTION_PAGE = 'nft-collection-page',
NFT_DETAILS_PAGE = 'nft-details-page',
TOKEN_DETAILS_PAGE = 'token-details',
TOKENS_PAGE = 'tokens-page',
POOL_PAGE = 'pool-page',
@@ -115,6 +120,9 @@ export enum ElementName {
IMPORT_TOKEN_BUTTON = 'import-token-button',
MAX_TOKEN_AMOUNT_BUTTON = 'max-token-amount-button',
NAVBAR_SEARCH_INPUT = 'navbar-search-input',
NFT_ACTIVITY_TAB = 'nft-activity-tab',
NFT_FILTER_BUTTON = 'nft-filter-button',
NFT_FILTER_OPTION = 'nft-filter-option',
PRICE_UPDATE_ACCEPT_BUTTON = 'price-update-accept-button',
SWAP_BUTTON = 'swap-button',
SWAP_DETAILS_DROPDOWN = 'swap-details-dropdown',
@@ -136,3 +144,12 @@ export enum Event {
onSelect = 'onSelect',
// alphabetize additional events.
}
/**
* Known Filter Types for NFTs
*/
export enum FilterTypes {
MARKETPLACE = 'Marketplace',
PRICE_RANGE = 'Price Range',
TRAIT = 'Trait',
}

View File

@@ -50,12 +50,12 @@ export const BaseButton = styled(RebassButton)<
}
`
export const ButtonPrimary = styled(BaseButton)<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
font-size: ${({ redesignFlag }) => redesignFlag && '20px'};
font-weight: ${({ redesignFlag }) => redesignFlag && '600'};
padding: ${({ redesignFlag }) => redesignFlag && '16px'};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentTextLightPrimary : 'white')};
export const ButtonPrimary = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentAction};
font-size: 20px;
font-weight: 600;
padding: 16px;
color: ${({ theme }) => theme.accentTextLightPrimary};
&:focus {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.deprecated_primary1)};
background-color: ${({ theme }) => darken(0.05, theme.deprecated_primary1)};
@@ -79,35 +79,28 @@ export const ButtonPrimary = styled(BaseButton)<{ redesignFlag?: boolean }>`
}
`
export const ButtonLight = styled(BaseButton)<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentActionSoft : theme.deprecated_primary5)};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primaryText1)};
font-size: ${({ redesignFlag }) => (redesignFlag ? '20px' : '16px')};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
export const ButtonLight = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.accentAction};
font-size: 20px;
font-weight: 600;
&:focus {
box-shadow: 0 0 0 1pt
${({ theme, disabled, redesignFlag }) =>
!disabled && (redesignFlag ? theme.accentActionSoft : darken(0.03, theme.deprecated_primary5))};
background-color: ${({ theme, disabled, redesignFlag }) =>
!disabled && (redesignFlag ? theme.accentActionSoft : darken(0.03, theme.deprecated_primary5))};
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
}
&:hover {
background-color: ${({ theme, disabled, redesignFlag }) =>
!disabled && (redesignFlag ? theme.accentActionSoft : darken(0.03, theme.deprecated_primary5))};
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
}
&:active {
box-shadow: 0 0 0 1pt
${({ theme, disabled, redesignFlag }) =>
!disabled && (redesignFlag ? theme.accentActionSoft : darken(0.05, theme.deprecated_primary5))};
background-color: ${({ theme, disabled, redesignFlag }) =>
!disabled && (redesignFlag ? theme.accentActionSoft : darken(0.05, theme.deprecated_primary5))};
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
}
:disabled {
opacity: 0.4;
:hover {
cursor: auto;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_primary5)};
background-color: transparent;
box-shadow: none;
border: 1px solid transparent;
outline: none;
@@ -176,28 +169,22 @@ export const ButtonOutlined = styled(BaseButton)`
}
`
export const ButtonYellow = styled(BaseButton)<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3)};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarning : 'white')};
export const ButtonYellow = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentWarningSoft};
color: ${({ theme }) => theme.accentWarning};
&:focus {
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${theme.deprecated_yellow3}`};
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : darken(0.05, theme.deprecated_yellow3)};
background-color: ${({ theme }) => theme.accentWarningSoft};
}
&:hover {
background: ${({ theme, redesignFlag }) => redesignFlag && theme.stateOverlayHover};
mix-blend-mode: ${({ redesignFlag }) => redesignFlag && 'normal'};
background-color: ${({ theme, redesignFlag }) => !redesignFlag && darken(0.05, theme.deprecated_yellow3)};
background: ${({ theme }) => theme.stateOverlayHover};
mix-blend-mode: normal;
}
&:active {
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${darken(0.1, theme.deprecated_yellow3)}`};
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : darken(0.1, theme.deprecated_yellow3)};
background-color: ${({ theme }) => theme.accentWarningSoft};
}
&:disabled {
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3};
opacity: ${({ redesignFlag }) => (redesignFlag ? '60%' : '50%')};
background-color: ${({ theme }) => theme.accentWarningSoft};
opacity: 60%;
cursor: auto;
}
`

View File

@@ -1,6 +1,6 @@
import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
import { curveCardinal, scaleLinear } from 'd3'
import { PricePoint } from 'graphql/data/Token'
import { PricePoint } from 'graphql/data/TokenPrice'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { memo } from 'react'

View File

@@ -1,5 +1,6 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
@@ -204,6 +205,12 @@ export default function FeatureFlagModal() {
</Header>
<FeatureFlagGroup name="Phase 1">
<FeatureFlagOption variant={NftVariant} value={useNftFlag()} featureFlag={FeatureFlag.nft} label="NFTs" />
<FeatureFlagOption
variant={NftGraphQlVariant}
value={useNftGraphQlFlag()}
featureFlag={FeatureFlag.nftGraphQl}
label="NFT GraphQL Endpoints"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption

View File

@@ -9,13 +9,12 @@ const rotate = keyframes`
}
`
const StyledSVG = styled.svg<{ size: string; stroke?: string; redesignFlag?: boolean }>`
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
animation: 2s ${rotate} linear infinite;
height: ${({ size }) => size};
width: ${({ size }) => size};
path {
stroke: ${({ stroke, redesignFlag, theme }) =>
redesignFlag ? theme.accentActive : stroke ?? theme.deprecated_primary1};
stroke: ${({ stroke, theme }) => theme.accentActive};
}
`
@@ -27,25 +26,15 @@ export default function Loader({
size = '16px',
stroke,
strokeWidth,
redesignFlag,
...rest
}: {
size?: string
stroke?: string
strokeWidth?: number
redesignFlag?: boolean
[k: string]: any
}) {
return (
<StyledSVG
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
size={size}
stroke={stroke}
redesignFlag={redesignFlag}
{...rest}
>
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
<path
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
strokeWidth={strokeWidth ?? '2.5'}

View File

@@ -1,5 +1,4 @@
import { DialogContent, DialogOverlay } from '@reach/dialog'
import { transparentize } from 'polished'
import React from 'react'
import { animated, useSpring, useTransition } from 'react-spring'
import { useGesture } from 'react-use-gesture'
@@ -10,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
const AnimatedDialogOverlay = animated(DialogOverlay)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean; scrollOverlay?: boolean }>`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
&[data-reach-dialog-overlay] {
z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent;
@@ -21,14 +20,14 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
justify-content: center;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundScrim : theme.deprecated_modalBG)};
background-color: ${({ theme }) => theme.backgroundScrim};
}
`
const AnimatedDialogContent = animated(DialogContent)
// destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, scrollOverlay, ...rest }) => (
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, scrollOverlay, ...rest }) => (
<AnimatedDialogContent {...rest} />
)).attrs({
'aria-label': 'dialog',
@@ -36,11 +35,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
overflow-y: auto;
&[data-reach-dialog-content] {
margin: ${({ redesignFlag }) => (redesignFlag ? 'auto' : '0 0 2rem 0')};
margin: auto;
background-color: ${({ theme }) => theme.deprecated_bg0};
border: 1px solid ${({ theme }) => theme.deprecated_bg1};
box-shadow: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.deepShadow : `0 4px 8px 0 ${transparentize(0.95, theme.shadow1)}`};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 0px;
width: 50vw;
overflow-y: auto;
@@ -61,9 +59,9 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
`}
display: ${({ scrollOverlay }) => (scrollOverlay ? 'inline-table' : 'flex')};
border-radius: 20px;
${({ theme, redesignFlag }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
width: 65vw;
margin: ${redesignFlag ? 'auto' : '0'};
margin: auto;
`}
${({ theme, mobile }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
width: 85vw;
@@ -87,7 +85,6 @@ interface ModalProps {
maxHeight?: number
initialFocusRef?: React.RefObject<any>
children?: React.ReactNode
redesignFlag?: boolean
scrollOverlay?: boolean
}
@@ -98,7 +95,6 @@ export default function Modal({
maxHeight = 90,
initialFocusRef,
children,
redesignFlag,
scrollOverlay,
}: ModalProps) {
const fadeTransition = useTransition(isOpen, {
@@ -131,7 +127,6 @@ export default function Modal({
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
unstable_lockFocusAcrossFrames={false}
redesignFlag={redesignFlag}
scrollOverlay={scrollOverlay}
>
<StyledDialogContent
@@ -145,7 +140,6 @@ export default function Modal({
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
redesignFlag={redesignFlag}
scrollOverlay={scrollOverlay}
>
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}

View File

@@ -103,8 +103,10 @@ export const suggestionImage = sprinkles({
export const suggestionPrimaryContainer = style([
sprinkles({
alignItems: 'flex-start',
width: 'full',
}),
{
width: '90%',
},
])
export const suggestionSecondaryContainer = sprinkles({

View File

@@ -1,8 +1,9 @@
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { getTokenDetailsURL } from 'graphql/data/util'
import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box'
@@ -87,7 +88,6 @@ export const CollectionRow = ({
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
</Column>
@@ -181,7 +181,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
<TokenSafetyIcon warning={checkWarning(token.address)} />
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>

View File

@@ -70,7 +70,7 @@ const PageTabs = () => {
const Navbar = () => {
const { pathname } = useLocation()
const showShoppingBag = pathname.startsWith('/nfts') || pathname.startsWith('/profile')
const isNftPage = pathname.startsWith('/nfts') || pathname.startsWith('/profile')
return (
<>
@@ -80,9 +80,11 @@ const Navbar = () => {
<Box as="a" href="#/swap" className={styles.logoContainer}>
<UniIcon width="48" height="48" className={styles.logo} />
</Box>
<Box display={{ sm: 'flex', lg: 'none' }}>
<ChainSelector leftAlign={true} />
</Box>
{!isNftPage && (
<Box display={{ sm: 'flex', lg: 'none' }}>
<ChainSelector leftAlign={true} />
</Box>
)}
<Row gap="8" display={{ sm: 'none', lg: 'flex' }}>
<PageTabs />
</Row>
@@ -98,10 +100,12 @@ const Navbar = () => {
<Box display={{ sm: 'none', lg: 'flex' }}>
<MenuDropdown />
</Box>
{showShoppingBag && <ShoppingBag />}
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSelector />
</Box>
{isNftPage && <ShoppingBag />}
{!isNftPage && (
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSelector />
</Box>
)}
<Web3Status />
</Row>

View File

@@ -1,18 +1,17 @@
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import React from 'react'
import styled from 'styled-components/macro'
import { escapeRegExp } from '../../utils'
const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string; redesignFlag: boolean }>`
const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>`
color: ${({ error, theme }) => (error ? theme.deprecated_red1 : theme.deprecated_text1)};
width: 0;
position: relative;
font-weight: ${({ redesignFlag }) => (redesignFlag ? 400 : 500)};
font-weight: 400;
outline: none;
border: none;
flex: 1 1 auto;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
background-color: transparent;
font-size: ${({ fontSize }) => fontSize ?? '28px'};
text-align: ${({ align }) => align && align};
white-space: nowrap;
@@ -36,7 +35,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
}
::placeholder {
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textTertiary : theme.deprecated_text4)};
color: ${({ theme }) => theme.textTertiary};
}
`
@@ -56,8 +55,6 @@ export const Input = React.memo(function InnerInput({
align?: 'right' | 'left'
prependSymbol?: string | undefined
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput)
@@ -68,7 +65,6 @@ export const Input = React.memo(function InnerInput({
<StyledInput
{...rest}
value={prependSymbol && value ? prependSymbol + value : value}
redesignFlag={redesignFlagEnabled}
onChange={(event) => {
if (prependSymbol) {
const value = event.target.value
@@ -91,7 +87,7 @@ export const Input = React.memo(function InnerInput({
// text-specific options
type="text"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder={placeholder || (redesignFlagEnabled ? '0' : '0.0')}
placeholder={placeholder || '0'}
minLength={1}
maxLength={79}
spellCheck="false"

View File

@@ -2,22 +2,17 @@ import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { AlertOctagon, AlertTriangle } from 'react-feather'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
const BodyRow = styled.div<{ $redesignFlag?: boolean }>`
color: ${({ theme, $redesignFlag }) => ($redesignFlag ? theme.textPrimary : theme.black)};
const BodyRow = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 12px;
font-weight: ${({ $redesignFlag }) => $redesignFlag && '400'};
font-size: ${({ $redesignFlag }) => ($redesignFlag ? '14px' : '12px')};
line-height: ${({ $redesignFlag }) => $redesignFlag && '20px'};
font-weight: 400;
font-size: 14px;
line-height: 20px;
`
const CautionOctagon = styled(AlertOctagon)`
color: ${({ theme }) => theme.deprecated_black};
`
const CautionTriangle = styled(AlertTriangle)`
color: ${({ theme }) => theme.accentWarning};
`
@@ -31,15 +26,15 @@ const TitleRow = styled.div`
justify-content: flex-start;
margin-bottom: 8px;
`
const TitleText = styled.div<{ redesignFlag?: boolean }>`
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textPrimary : theme.black)};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '500' : '600')};
const TitleText = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-weight: 500;
font-size: 16px;
line-height: ${({ redesignFlag }) => (redesignFlag ? '24px' : '20px')};
line-height: 24px;
margin: 0px 12px;
`
const Wrapper = styled.div<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_yellow3)};
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
bottom: 60px;
@@ -57,17 +52,16 @@ export function ChainConnectivityWarning() {
const { chainId } = useWeb3React()
const info = getChainInfoOrDefault(chainId)
const label = info?.label
const redesignFlag = useRedesignFlag() === RedesignVariant.Enabled
return (
<Wrapper redesignFlag={redesignFlag}>
<Wrapper>
<TitleRow>
{redesignFlag ? <CautionTriangle /> : <CautionOctagon />}
<TitleText redesignFlag={redesignFlag}>
<CautionTriangle />
<TitleText>
<Trans>Network Warning</Trans>
</TitleText>
</TitleRow>
<BodyRow $redesignFlag={redesignFlag}>
<BodyRow>
{chainId === SupportedChainId.MAINNET ? (
<Trans>You may have lost your network connection.</Trans>
) : (

View File

@@ -81,11 +81,11 @@ export default function PopupItem({
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
}
return (
return popupContent ? (
<Popup>
<StyledClose color={theme.deprecated_text2} onClick={removeThisPopup} />
{popupContent}
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
</Popup>
)
) : null
}

View File

@@ -6,7 +6,6 @@ import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { AutoRow } from 'components/Row'
import { COMMON_BASES } from 'constants/routing'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
@@ -18,17 +17,9 @@ const MobileWrapper = styled(AutoColumn)`
`};
`
const BaseWrapper = styled.div<{ disable?: boolean; redesignFlag?: boolean }>`
border: 1px solid
${({ theme, disable, redesignFlag }) =>
disable
? redesignFlag
? theme.accentAction
: 'transparent'
: redesignFlag
? theme.backgroundOutline
: theme.deprecated_bg3};
border-radius: ${({ redesignFlag }) => (redesignFlag ? '16px' : '10px')};
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? theme.accentAction : theme.backgroundOutline)};
border-radius: 16px;
display: flex;
padding: 6px;
padding-right: 12px;
@@ -36,15 +27,11 @@ const BaseWrapper = styled.div<{ disable?: boolean; redesignFlag?: boolean }>`
align-items: center;
:hover {
cursor: ${({ disable }) => !disable && 'pointer'};
background-color: ${({ theme, disable, redesignFlag }) =>
(redesignFlag && theme.hoverDefault) || (!disable && theme.deprecated_bg2)};
background-color: ${({ theme }) => theme.hoverDefault};
}
color: ${({ theme, disable, redesignFlag }) =>
disable && (redesignFlag ? theme.accentAction : theme.deprecated_text3)};
background-color: ${({ theme, disable, redesignFlag }) =>
disable && (redesignFlag ? theme.accentActionSoft : theme.deprecated_bg3)};
filter: ${({ disable, redesignFlag }) => disable && !redesignFlag && 'grayscale(1)'};
color: ${({ theme, disable }) => disable && theme.accentAction};
background-color: ${({ theme, disable }) => disable && theme.accentActionSoft};
`
const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({
@@ -73,8 +60,6 @@ export default function CommonBases({
isAddressSearch: string | false
}) {
const bases = typeof chainId !== 'undefined' ? COMMON_BASES[chainId] ?? [] : []
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
return bases.length > 0 ? (
<MobileWrapper gap="md">
@@ -95,7 +80,6 @@ export default function CommonBases({
onKeyPress={(e) => !isSelected && e.key === 'Enter' && onSelect(currency)}
onClick={() => !isSelected && onSelect(currency)}
disable={isSelected}
redesignFlag={redesignFlagEnabled}
key={currencyId(currency)}
>
<CurrencyLogoFromList currency={currency} />

View File

@@ -2,7 +2,25 @@
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
<DocumentFragment>
.c7 {
.c9 {
color: #99A1BD;
}
.c7 {
margin-left: 4px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c8 {
width: 1em;
height: 1em;
color: #99A1BD;
}
@@ -55,7 +73,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
justify-content: space-between;
}
.c8 {
.c10 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
@@ -111,9 +129,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
Dai Stablecoin
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
DAI
</div>
@@ -122,7 +172,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>
@@ -150,9 +200,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
USD//C
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
USDC
</div>
@@ -161,7 +243,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>
@@ -189,9 +271,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
Wrapped BTC
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
WBTC
</div>
@@ -200,7 +314,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>

View File

@@ -1,11 +1,9 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { checkWarning } from 'constants/tokenSafety'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
import { XOctagon } from 'react-feather'
import { Check } from 'react-feather'
@@ -15,10 +13,8 @@ import styled from 'styled-components/macro'
import { useIsUserAddedToken } from '../../../hooks/Tokens'
import { useCurrencyBalance } from '../../../state/connection/hooks'
import { useCombinedActiveList } from '../../../state/lists/hooks'
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
import { ThemedText } from '../../../theme'
import { isTokenOnList } from '../../../utils'
import Column, { AutoColumn } from '../../Column'
import CurrencyLogo from '../../CurrencyLogo'
import Loader from '../../Loader'
@@ -128,12 +124,9 @@ export function CurrencyRow({
}) {
const { account } = useWeb3React()
const key = currencyKey(currency)
const selectedTokenList = useCombinedActiveList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency.isToken ? currency : undefined)
const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency)
const warning = currency.isNative ? null : checkWarning(currency.address)
const redesignFlagEnabled = useRedesignFlag() === RedesignVariant.Enabled
const isBlockedToken = !!warning && !warning.canProceed
const blockedTokenOpacity = '0.6'
@@ -147,7 +140,6 @@ export function CurrencyRow({
>
<MenuItem
tabIndex={0}
redesignFlag={redesignFlagEnabled}
style={style}
className={`token-item-${key}`}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect(!!warning) : null)}
@@ -170,11 +162,7 @@ export function CurrencyRow({
{isBlockedToken && <BlockedTokenIcon />}
</Row>
<ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!currency.isNative && !isOnSelectedList && customAdded ? (
<Trans>{currency.symbol} Added by user</Trans>
) : (
currency.symbol
)}
{currency.symbol}
</ThemedText.DeprecatedDarkGray>
</AutoColumn>
<Column>
@@ -185,10 +173,9 @@ export function CurrencyRow({
{showCurrencyAmount ? (
<RowFixed style={{ justifySelf: 'flex-end' }}>
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
{redesignFlagEnabled && isSelected && <CheckIcon />}
{isSelected && <CheckIcon />}
</RowFixed>
) : (
redesignFlagEnabled &&
isSelected && (
<RowFixed style={{ justifySelf: 'flex-end' }}>
<CheckIcon />

View File

@@ -5,7 +5,6 @@ import { useWeb3React } from '@web3-react/core'
import { EventName, ModalName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { sendEvent } from 'components/analytics'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import useToggle from 'hooks/useToggle'
@@ -19,7 +18,7 @@ import { Text } from 'rebass'
import { useAllTokenBalances } from 'state/connection/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { useAllTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
import { useActiveTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
import { CloseIcon, ThemedText } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
@@ -29,8 +28,8 @@ import { CurrencyRow, formatAnalyticsEventProperties } from './CurrencyList'
import CurrencyList from './CurrencyList'
import { PaddedColumn, SearchInput, Separator } from './styleds'
const ContentWrapper = styled(Column)<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundSurface};
const ContentWrapper = styled(Column)`
background-color: ${({ theme }) => theme.backgroundSurface};
width: 100%;
flex: 1 1;
position: relative;
@@ -57,9 +56,6 @@ export function CurrencySearch({
onDismiss,
isOpen,
}: CurrencySearchProps) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const { chainId } = useWeb3React()
const theme = useTheme()
@@ -71,7 +67,8 @@ export function CurrencySearch({
const [searchQuery, setSearchQuery] = useState<string>('')
const debouncedQuery = useDebounce(searchQuery, 200)
const allTokens = useAllTokens()
// Only display 'imported' tokens when the search filter has input
const defaultTokens = useActiveTokens(debouncedQuery.length > 0)
// if they input an address, use it
const isAddressSearch = isAddress(debouncedQuery)
@@ -91,8 +88,8 @@ export function CurrencySearch({
}, [isAddressSearch])
const filteredTokens: Token[] = useMemo(() => {
return Object.values(allTokens).filter(getTokenFilter(debouncedQuery))
}, [allTokens, debouncedQuery])
return Object.values(defaultTokens).filter(getTokenFilter(debouncedQuery))
}, [defaultTokens, debouncedQuery])
const [balances, balancesAreLoading] = useAllTokenBalances()
const sortedTokens: Token[] = useMemo(
@@ -176,7 +173,7 @@ export function CurrencySearch({
}, [])
return (
<ContentWrapper redesignFlag={redesignFlagEnabled}>
<ContentWrapper>
<Trace name={EventName.TOKEN_SELECTOR_OPENED} modal={ModalName.TOKEN_SELECTOR} shouldLogImpression>
<PaddedColumn gap="16px">
<RowBetween>
@@ -191,7 +188,6 @@ export function CurrencySearch({
id="token-search-input"
placeholder={t`Search name or paste address`}
autoComplete="off"
redesignFlag={redesignFlagEnabled}
value={searchQuery}
ref={inputRef as RefObject<HTMLInputElement>}
onChange={handleInput}
@@ -208,7 +204,7 @@ export function CurrencySearch({
/>
)}
</PaddedColumn>
<Separator redesignFlag={redesignFlagEnabled} />
<Separator />
{searchToken && !searchTokenIsAdded ? (
<Column style={{ padding: '20px 0', height: '100%' }}>
<CurrencyRow

View File

@@ -1,18 +1,12 @@
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import TokenSafety from 'components/TokenSafety'
import usePrevious from 'hooks/usePrevious'
import { memo, useCallback, useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
import { ImportToken } from './ImportToken'
import Manage from './Manage'
interface CurrencySearchModalProps {
isOpen: boolean
@@ -27,9 +21,7 @@ interface CurrencySearchModalProps {
export enum CurrencyModalView {
search,
manage,
importToken,
importList,
tokenSafety,
}
@@ -69,25 +61,9 @@ export default memo(function CurrencySearchModal({
},
[onDismiss, onCurrencySelect, userAddedTokens]
)
// for token import view
const prevView = usePrevious(modalView)
// used for import token flow
const [importToken, setImportToken] = useState<Token | undefined>()
// used for import list
const [importList, setImportList] = useState<TokenList | undefined>()
const [listURL, setListUrl] = useState<string | undefined>()
// used for token safety
const [warningToken, setWarningToken] = useState<Token | undefined>()
const handleBackImport = useCallback(
() => setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search),
[setModalView, prevView]
)
const { height: windowHeight } = useWindowSize()
// change min height if not searching
let modalHeight: number | undefined = 80
@@ -124,38 +100,6 @@ export default memo(function CurrencySearchModal({
)
}
break
case CurrencyModalView.importToken:
if (importToken) {
modalHeight = undefined
showTokenSafetySpeedbump(importToken)
content = (
<ImportToken
tokens={[importToken]}
onDismiss={onDismiss}
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
onBack={handleBackImport}
handleCurrencySelect={handleCurrencySelect}
/>
)
}
break
case CurrencyModalView.importList:
modalHeight = 40
if (importList && listURL) {
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
}
break
case CurrencyModalView.manage:
content = (
<Manage
onDismiss={onDismiss}
setModalView={setModalView}
setImportToken={setImportToken}
setImportList={setImportList}
setListUrl={setListUrl}
/>
)
break
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>

View File

@@ -1,167 +0,0 @@
import { Trans } from '@lingui/macro'
import { TokenList } from '@uniswap/token-lists'
import { sendEvent } from 'components/analytics'
import { ButtonPrimary } from 'components/Button'
import Card from 'components/Card'
import { AutoColumn } from 'components/Column'
import ListLogo from 'components/ListLogo'
import { AutoRow, RowBetween, RowFixed } from 'components/Row'
import { SectionBreak } from 'components/swap/styleds'
import { useFetchListCallback } from 'hooks/useFetchListCallback'
import { transparentize } from 'polished'
import { useCallback, useState } from 'react'
import { AlertTriangle, ArrowLeft } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import { enableList, removeList } from 'state/lists/actions'
import { useAllLists } from 'state/lists/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import { ExternalLink } from '../../theme'
import { CurrencyModalView } from './CurrencySearchModal'
import { Checkbox, PaddedColumn, TextDot } from './styleds'
const Wrapper = styled.div`
position: relative;
width: 100%;
overflow: auto;
`
interface ImportProps {
listURL: string
list: TokenList
onDismiss: () => void
setModalView: (view: CurrencyModalView) => void
}
export function ImportList({ listURL, list, setModalView, onDismiss }: ImportProps) {
const theme = useTheme()
const dispatch = useAppDispatch()
// user must accept
const [confirmed, setConfirmed] = useState(false)
const lists = useAllLists()
const fetchList = useFetchListCallback()
// monitor is list is loading
const adding = Boolean(lists[listURL]?.loadingRequestId)
const [addError, setAddError] = useState<string | null>(null)
const handleAddList = useCallback(() => {
if (adding) return
setAddError(null)
fetchList(listURL)
.then(() => {
sendEvent({
category: 'Lists',
action: 'Add List',
label: listURL,
})
// turn list on
dispatch(enableList(listURL))
// go back to lists
setModalView(CurrencyModalView.manage)
})
.catch((error) => {
sendEvent({
category: 'Lists',
action: 'Add List Failed',
label: listURL,
})
setAddError(error.message)
dispatch(removeList(listURL))
})
}, [adding, dispatch, fetchList, listURL, setModalView])
return (
<Wrapper>
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
<RowBetween>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.manage)} />
<ThemedText.DeprecatedMediumHeader>
<Trans>Import List</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={onDismiss} />
</RowBetween>
</PaddedColumn>
<SectionBreak />
<PaddedColumn gap="md">
<AutoColumn gap="md">
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
<RowBetween>
<RowFixed>
{list.logoURI && <ListLogo logoURI={list.logoURI} size="40px" />}
<AutoColumn gap="sm" style={{ marginLeft: '20px' }}>
<RowFixed>
<ThemedText.DeprecatedBody fontWeight={600} mr="6px">
{list.name}
</ThemedText.DeprecatedBody>
<TextDot />
<ThemedText.DeprecatedMain fontSize={'16px'} ml="6px">
<Trans>{list.tokens.length} tokens</Trans>
</ThemedText.DeprecatedMain>
</RowFixed>
<ExternalLink href={`https://tokenlists.org/token-list?url=${listURL}`}>
<ThemedText.DeprecatedMain fontSize={'12px'} color={theme.deprecated_blue1}>
{listURL}
</ThemedText.DeprecatedMain>
</ExternalLink>
</AutoColumn>
</RowFixed>
</RowBetween>
</Card>
<Card style={{ backgroundColor: transparentize(0.8, theme.deprecated_red1) }}>
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<AlertTriangle stroke={theme.deprecated_red1} size={32} />
<ThemedText.DeprecatedBody fontWeight={500} fontSize={20} color={theme.deprecated_red1}>
<Trans>Import at your own risk</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<ThemedText.DeprecatedBody fontWeight={500} color={theme.deprecated_red1}>
<Trans>
By adding this list you are implicitly trusting that the data is correct. Anyone can create a list,
including creating fake versions of existing lists and lists that claim to represent projects that do
not have one.
</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedBody fontWeight={600} color={theme.deprecated_red1}>
<Trans>If you purchase a token from this list, you may not be able to sell it back.</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
<Checkbox
name="confirmed"
type="checkbox"
checked={confirmed}
onChange={() => setConfirmed(!confirmed)}
/>
<ThemedText.DeprecatedBody ml="10px" fontSize="16px" color={theme.deprecated_red1} fontWeight={500}>
<Trans>I understand</Trans>
</ThemedText.DeprecatedBody>
</AutoRow>
</Card>
<ButtonPrimary
disabled={!confirmed}
altDisabledStyle={true}
$borderRadius="20px"
padding="10px 1rem"
onClick={handleAddList}
>
<Trans>Import</Trans>
</ButtonPrimary>
{addError ? (
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
{addError}
</ThemedText.DeprecatedError>
) : null}
</AutoColumn>
{/* </Card> */}
</PaddedColumn>
</Wrapper>
)
}

View File

@@ -1,92 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { RowBetween } from 'components/Row'
import { useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import { CloseIcon } from 'theme'
import { CurrencyModalView } from './CurrencySearchModal'
import { ManageLists } from './ManageLists'
import ManageTokens from './ManageTokens'
import { PaddedColumn, Separator } from './styleds'
const Wrapper = styled.div`
width: 100%;
position: relative;
display: flex;
flex-flow: column;
`
const ToggleWrapper = styled(RowBetween)`
background-color: ${({ theme }) => theme.deprecated_bg3};
border-radius: 12px;
padding: 6px;
`
const ToggleOption = styled.div<{ active?: boolean }>`
width: 48%;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
font-weight: 600;
background-color: ${({ theme, active }) => (active ? theme.deprecated_bg1 : theme.deprecated_bg3)};
color: ${({ theme, active }) => (active ? theme.deprecated_text1 : theme.deprecated_text2)};
user-select: none;
:hover {
cursor: pointer;
opacity: 0.7;
}
`
export default function Manage({
onDismiss,
setModalView,
setImportList,
setImportToken,
setListUrl,
}: {
onDismiss: () => void
setModalView: (view: CurrencyModalView) => void
setImportToken: (token: Token) => void
setImportList: (list: TokenList) => void
setListUrl: (url: string) => void
}) {
// toggle between tokens and lists
const [showLists, setShowLists] = useState(true)
return (
<Wrapper>
<PaddedColumn>
<RowBetween>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.search)} />
<Text fontWeight={500} fontSize={20}>
<Trans>Manage</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
</PaddedColumn>
<Separator />
<PaddedColumn style={{ paddingBottom: 0 }}>
<ToggleWrapper>
<ToggleOption onClick={() => setShowLists(!showLists)} active={showLists}>
<Trans>Lists</Trans>
</ToggleOption>
<ToggleOption onClick={() => setShowLists(!showLists)} active={!showLists}>
<Trans>Tokens</Trans>
</ToggleOption>
</ToggleWrapper>
</PaddedColumn>
{showLists ? (
<ManageLists setModalView={setModalView} setImportList={setImportList} setListUrl={setListUrl} />
) : (
<ManageTokens setModalView={setModalView} setImportToken={setImportToken} />
)}
</Wrapper>
)
}

View File

@@ -1,415 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { TokenList } from '@uniswap/token-lists'
import { useWeb3React } from '@web3-react/core'
import { sendEvent } from 'components/analytics'
import Card from 'components/Card'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { useListColor } from 'hooks/useColor'
import parseENSAddress from 'lib/utils/parseENSAddress'
import uriToHttp from 'lib/utils/uriToHttp'
import { ChangeEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { CheckCircle, Settings } from 'react-feather'
import { usePopper } from 'react-popper'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import useToggle from '../../hooks/useToggle'
import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions'
import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks'
import { ExternalLink, IconWrapper, LinkStyledButton, ThemedText } from '../../theme'
import listVersionLabel from '../../utils/listVersionLabel'
import { ButtonEmpty, ButtonPrimary } from '../Button'
import Column, { AutoColumn } from '../Column'
import ListLogo from '../ListLogo'
import Row, { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle'
import { CurrencyModalView } from './CurrencySearchModal'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
const Wrapper = styled(Column)`
flex: 1;
overflow-y: hidden;
`
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
padding: 0;
font-size: 1rem;
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
`
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 100;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear;
background: ${({ theme }) => theme.deprecated_bg2};
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
color: ${({ theme }) => theme.deprecated_text2};
border-radius: 0.5rem;
padding: 1rem;
display: grid;
grid-template-rows: 1fr;
grid-gap: 8px;
font-size: 1rem;
text-align: left;
`
const StyledMenu = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: relative;
border: none;
`
const StyledTitleText = styled.div<{ active: boolean }>`
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
`
const StyledListUrlText = styled(ThemedText.DeprecatedMain)<{ active: boolean }>`
font-size: 12px;
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
`
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>`
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.deprecated_bg2)};
opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)};
transition: 200ms;
align-items: center;
padding: 1rem;
border-radius: 20px;
`
function listUrlRowHTMLId(listUrl: string) {
return `list-row-${listUrl.replace(/\./g, '-')}`
}
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
const { chainId } = useWeb3React()
const listsByUrl = useAppSelector((state) => state.lists.byUrl)
const dispatch = useAppDispatch()
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
const activeTokensOnThisChain = useMemo(() => {
if (!list || !chainId) {
return 0
}
return list.tokens.reduce((acc, cur) => (cur.chainId === chainId ? acc + 1 : acc), 0)
}, [chainId, list])
const theme = useTheme()
const listColor = useListColor(list?.logoURI)
const isActive = useIsListActive(listUrl)
const [open, toggle] = useToggle(false)
const node = useRef<HTMLDivElement>()
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'auto',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }],
})
useOnClickOutside(node, open ? toggle : undefined)
const handleAcceptListUpdate = useCallback(() => {
if (!pending) return
sendEvent({
category: 'Lists',
action: 'Update List from List Select',
label: listUrl,
})
dispatch(acceptListUpdate(listUrl))
}, [dispatch, listUrl, pending])
const handleRemoveList = useCallback(() => {
sendEvent({
category: 'Lists',
action: 'Start Remove List',
label: listUrl,
})
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
sendEvent({
category: 'Lists',
action: 'Confirm Remove List',
label: listUrl,
})
dispatch(removeList(listUrl))
}
}, [dispatch, listUrl])
const handleEnableList = useCallback(() => {
sendEvent({
category: 'Lists',
action: 'Enable List',
label: listUrl,
})
dispatch(enableList(listUrl))
}, [dispatch, listUrl])
const handleDisableList = useCallback(() => {
sendEvent({
category: 'Lists',
action: 'Disable List',
label: listUrl,
})
dispatch(disableList(listUrl))
}, [dispatch, listUrl])
if (!list) return null
return (
<RowWrapper
active={isActive}
hasActiveTokens={activeTokensOnThisChain > 0}
bgColor={listColor}
key={listUrl}
id={listUrlRowHTMLId(listUrl)}
>
{list.logoURI ? (
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
) : (
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
)}
<Column style={{ flex: '1' }}>
<Row>
<StyledTitleText active={isActive}>{list.name}</StyledTitleText>
</Row>
<RowFixed mt="4px">
<StyledListUrlText active={isActive} mr="6px">
<Trans>{activeTokensOnThisChain} tokens</Trans>
</StyledListUrlText>
<StyledMenu ref={node as any}>
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
<Settings stroke={isActive ? theme.deprecated_bg1 : theme.deprecated_text1} size={12} />
</ButtonEmpty>
{open && (
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
<div>{list && listVersionLabel(list.version)}</div>
<SeparatorDark />
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>
<Trans>View list</Trans>
</ExternalLink>
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
<Trans>Remove list</Trans>
</UnpaddedLinkStyledButton>
{pending && (
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>
<Trans>Update list</Trans>
</UnpaddedLinkStyledButton>
)}
</PopoverContainer>
)}
</StyledMenu>
</RowFixed>
</Column>
<Toggle
isActive={isActive}
bgColor={listColor}
toggle={() => {
isActive ? handleDisableList() : handleEnableList()
}}
/>
</RowWrapper>
)
})
const ListContainer = styled.div`
padding: 1rem;
height: 100%;
overflow: auto;
flex: 1;
`
export function ManageLists({
setModalView,
setImportList,
setListUrl,
}: {
setModalView: (view: CurrencyModalView) => void
setImportList: (list: TokenList) => void
setListUrl: (url: string) => void
}) {
const { chainId } = useWeb3React()
const theme = useTheme()
const [listUrlInput, setListUrlInput] = useState<string>('')
const lists = useAllLists()
const tokenCountByListName = useMemo<Record<string, number>>(
() =>
Object.values(lists).reduce((acc, { current: list }) => {
if (!list) {
return acc
}
return {
...acc,
[list.name]: list.tokens.reduce((count: number, token) => (token.chainId === chainId ? count + 1 : count), 0),
}
}, {}),
[chainId, lists]
)
// sort by active but only if not visible
const activeListUrls = useActiveListUrls()
const handleInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setListUrlInput(e.target.value)
}, [])
const fetchList = useFetchListCallback()
const validUrl: boolean = useMemo(() => {
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
}, [listUrlInput])
const sortedLists = useMemo(() => {
const listUrls = Object.keys(lists)
return listUrls
.filter((listUrl) => {
// only show loaded lists, hide unsupported lists
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
})
.sort((listUrlA, listUrlB) => {
const { current: listA } = lists[listUrlA]
const { current: listB } = lists[listUrlB]
// first filter on active lists
if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) {
return -1
}
if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) {
return 1
}
if (listA && listB) {
if (tokenCountByListName[listA.name] > tokenCountByListName[listB.name]) {
return -1
}
if (tokenCountByListName[listA.name] < tokenCountByListName[listB.name]) {
return 1
}
return listA.name.toLowerCase() < listB.name.toLowerCase()
? -1
: listA.name.toLowerCase() === listB.name.toLowerCase()
? 0
: 1
}
if (listA) return -1
if (listB) return 1
return 0
})
}, [lists, activeListUrls, tokenCountByListName])
// temporary fetched list for import flow
const [tempList, setTempList] = useState<TokenList>()
const [addError, setAddError] = useState<string | undefined>()
useEffect(() => {
async function fetchTempList() {
fetchList(listUrlInput, false)
.then((list) => setTempList(list))
.catch(() => setAddError(t`Error importing list`))
}
// if valid url, fetch details for card
if (validUrl) {
fetchTempList()
} else {
setTempList(undefined)
listUrlInput !== '' && setAddError(t`Enter valid list location`)
}
// reset error
if (listUrlInput === '') {
setAddError(undefined)
}
}, [fetchList, listUrlInput, validUrl])
// check if list is already imported
const isImported = Object.keys(lists).includes(listUrlInput)
// set list values and have parent modal switch to import list view
const handleImport = useCallback(() => {
if (!tempList) return
setImportList(tempList)
setModalView(CurrencyModalView.importList)
setListUrl(listUrlInput)
}, [listUrlInput, setImportList, setListUrl, setModalView, tempList])
return (
<Wrapper>
<PaddedColumn gap="14px">
<Row>
<SearchInput
type="text"
id="list-add-input"
placeholder={t`https:// or ipfs:// or ENS name`}
value={listUrlInput}
onChange={handleInput}
/>
</Row>
{addError ? (
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
{addError}
</ThemedText.DeprecatedError>
) : null}
</PaddedColumn>
{tempList && (
<PaddedColumn style={{ paddingTop: 0 }}>
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
<RowBetween>
<RowFixed>
{tempList.logoURI && <ListLogo logoURI={tempList.logoURI} size="40px" />}
<AutoColumn gap="4px" style={{ marginLeft: '20px' }}>
<ThemedText.DeprecatedBody fontWeight={600}>{tempList.name}</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedMain fontSize={'12px'}>
<Trans>{tempList.tokens.length} tokens</Trans>
</ThemedText.DeprecatedMain>
</AutoColumn>
</RowFixed>
{isImported ? (
<RowFixed>
<IconWrapper stroke={theme.deprecated_text2} size="16px" marginRight={'10px'}>
<CheckCircle />
</IconWrapper>
<ThemedText.DeprecatedBody color={theme.deprecated_text2}>
<Trans>Loaded</Trans>
</ThemedText.DeprecatedBody>
</RowFixed>
) : (
<ButtonPrimary
style={{ fontSize: '14px' }}
padding="6px 8px"
width="fit-content"
onClick={handleImport}
>
<Trans>Import</Trans>
</ButtonPrimary>
)}
</RowBetween>
</Card>
</PaddedColumn>
)}
<Separator />
<ListContainer>
<AutoColumn gap="md">
{sortedLists.map((listUrl) => (
<ListRow key={listUrl} listUrl={listUrl} />
))}
</AutoColumn>
</ListContainer>
</Wrapper>
)
}

View File

@@ -22,7 +22,7 @@ export const PaddedColumn = styled(AutoColumn)`
padding: 20px;
`
export const MenuItem = styled(RowBetween)<{ redesignFlag?: boolean; dim?: boolean }>`
export const MenuItem = styled(RowBetween)<{ dim?: boolean }>`
padding: 4px 20px;
height: 56px;
display: grid;
@@ -31,13 +31,12 @@ export const MenuItem = styled(RowBetween)<{ redesignFlag?: boolean; dim?: boole
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled, redesignFlag }) =>
(redesignFlag && theme.hoverDefault) || (!disabled && theme.deprecated_bg2)};
background-color: ${({ theme }) => theme.hoverDefault};
}
opacity: ${({ disabled, selected, dim }) => (dim || disabled || selected ? 0.4 : 1)};
`
export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
export const SearchInput = styled.input`
background: no-repeat scroll 7px 7px;
background-image: url(${searchIcon});
background-size: 20px 20px;
@@ -46,37 +45,36 @@ export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
display: flex;
padding: 16px;
padding-left: 40px;
height: ${({ redesignFlag }) => redesignFlag && '40px'};
height: 40px;
align-items: center;
width: 100%;
white-space: nowrap;
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundModule};
background-color: ${({ theme }) => theme.backgroundModule};
border: none;
outline: none;
border-radius: ${({ redesignFlag }) => (redesignFlag ? '12px' : '20px')};
border-radius: 12px;
color: ${({ theme }) => theme.deprecated_text1};
border-style: solid;
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : theme.deprecated_bg3)};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
-webkit-appearance: none;
font-size: ${({ redesignFlag }) => (redesignFlag ? '16px' : '18px')};
font-size: 16px;
::placeholder {
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textTertiary : theme.deprecated_text3)};
font-size: ${({ redesignFlag }) => redesignFlag && '16px'};
color: ${({ theme }) => theme.textTertiary};
font-size: 16px;
}
transition: border 100ms;
:focus {
border: 1px solid
${({ theme, redesignFlag }) => (redesignFlag ? theme.accentActiveSoft : theme.deprecated_primary1)};
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundSurface};
border: 1px solid ${({ theme }) => theme.accentActiveSoft};
background-color: ${({ theme }) => theme.backgroundSurface};
outline: none;
}
`
export const Separator = styled.div<{ redesignFlag?: boolean }>`
export const Separator = styled.div`
width: 100%;
height: 1px;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : theme.deprecated_bg2)};
background-color: ${({ theme }) => theme.backgroundOutline};
`
export const SeparatorDark = styled.div`

View File

@@ -3,7 +3,6 @@ import { t, Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendEvent } from 'components/analytics'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { isSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { useRef, useState } from 'react'
import { Settings, X } from 'react-feather'
@@ -23,16 +22,16 @@ import { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle'
import TransactionSettings from '../TransactionSettings'
const StyledMenuIcon = styled(Settings)<{ redesignFlag: boolean }>`
const StyledMenuIcon = styled(Settings)`
height: 20px;
width: 20px;
> * {
stroke: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textSecondary : theme.deprecated_text1)};
stroke: ${({ theme }) => theme.textSecondary};
}
`
const StyledCloseIcon = styled(X)<{ redesignFlag: boolean }>`
const StyledCloseIcon = styled(X)`
height: 20px;
width: 20px;
:hover {
@@ -40,7 +39,7 @@ const StyledCloseIcon = styled(X)<{ redesignFlag: boolean }>`
}
> * {
stroke: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textSecondary : theme.deprecated_text1)};
stroke: ${({ theme }) => theme.textSecondary};
}
`
@@ -83,10 +82,10 @@ const StyledMenu = styled.div`
text-align: left;
`
const MenuFlyout = styled.span<{ redesignFlag: boolean }>`
const MenuFlyout = styled.span`
min-width: 20.125rem;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_bg2)};
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : theme.deprecated_bg3)};
background-color: ${({ theme }) => theme.backgroundSurface};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 12px;
@@ -97,7 +96,7 @@ const MenuFlyout = styled.span<{ redesignFlag: boolean }>`
top: 2rem;
right: 0rem;
z-index: 100;
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textPrimary};
color: ${({ theme }) => theme.textPrimary};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
min-width: 18.125rem;
@@ -123,8 +122,6 @@ const ModalContentWrapper = styled.div`
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
const { chainId } = useWeb3React()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const node = useRef<HTMLDivElement>()
const open = useModalIsOpen(ApplicationModal.SETTINGS)
@@ -152,7 +149,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
<Text fontWeight={500} fontSize={20}>
<Trans>Are you sure?</Trans>
</Text>
<StyledCloseIcon onClick={() => setShowConfirmation(false)} redesignFlag={redesignFlagEnabled} />
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
</RowBetween>
<Break />
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
@@ -190,7 +187,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
id="open-settings-dialog-button"
aria-label={t`Transaction Settings`}
>
<StyledMenuIcon redesignFlag={redesignFlagEnabled} />
<StyledMenuIcon />
{expertMode ? (
<EmojiWrapper>
<span role="img" aria-label="wizard-icon">
@@ -200,10 +197,10 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
) : null}
</StyledMenuButton>
{open && (
<MenuFlyout redesignFlag={redesignFlagEnabled}>
<MenuFlyout>
<AutoColumn gap="md" style={{ padding: '1rem' }}>
<Text fontWeight={600} fontSize={14}>
<Trans>{redesignFlagEnabled ? 'Settings' : 'Transaction Settings'}</Trans>
<Trans>Settings</Trans>
</Text>
<TransactionSettings placeholderSlippage={placeholderSlippage} />
<Text fontWeight={600} fontSize={14}>

View File

@@ -1,23 +1,16 @@
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { darken } from 'polished'
import { useState } from 'react'
import styled, { keyframes } from 'styled-components/macro'
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean; redesignFlag: boolean }>`
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
align-items: center;
background: ${({ isActive, theme, redesignFlag }) =>
redesignFlag && isActive
? theme.accentActionSoft
: redesignFlag && !isActive
? 'transparent'
: theme.deprecated_bg1};
border: ${({ redesignFlag, theme, isActive }) =>
redesignFlag && !isActive ? `1px solid ${theme.backgroundOutline}` : 'none'};
background: ${({ isActive, theme }) => (isActive ? theme.accentActionSoft : 'transparent')};
border: ${({ theme, isActive }) => (isActive ? 'none' : `1px solid ${theme.backgroundOutline}`)};
border-radius: 20px;
cursor: pointer;
display: flex;
outline: none;
padding: ${({ redesignFlag }) => (redesignFlag ? '4px' : '0.4rem 0.4rem')};
padding: 4px;
width: fit-content;
`
@@ -64,8 +57,8 @@ const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInit
:hover {
${({ bgColor, theme, isActive }) => ToggleElementHoverStyle(!!bgColor, theme, isActive)}
}
margin-left: ${({ isActive }) => (isActive ? '2.2em' : '0em')};
margin-right: ${({ isActive }) => (!isActive ? '2.2em' : '0em')};
margin-left: ${({ isActive }) => isActive && '2.2em'};
margin-right: ${({ isActive }) => !isActive && '2.2em'};
width: 24px;
`
@@ -78,8 +71,6 @@ interface ToggleProps {
export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true)
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const switchToggle = () => {
toggle()
@@ -87,7 +78,7 @@ export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
}
return (
<Wrapper id={id} isActive={isActive} onClick={switchToggle} redesignFlag={redesignFlagEnabled}>
<Wrapper id={id} isActive={isActive} onClick={switchToggle}>
<ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
</Wrapper>
)

View File

@@ -1,5 +1,5 @@
import { ReactComponent as Verified } from 'assets/svg/verified.svg'
import { Warning } from 'constants/tokenSafety'
import { Warning, WARNING_LEVEL } from 'constants/tokenSafety'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
const VerifiedContainer = styled.div`
@@ -8,17 +8,17 @@ const VerifiedContainer = styled.div`
justify-content: center;
`
export const VerifiedIcon = styled(Verified)<{ size?: string }>`
export const WarningIcon = styled(AlertTriangle)<{ size?: string }>`
width: ${({ size }) => size ?? '1em'};
height: ${({ size }) => size ?? '1em'};
color: ${({ theme }) => theme.accentAction};
color: ${({ theme }) => theme.textTertiary};
`
export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) {
if (warning) return null
if (warning?.level !== WARNING_LEVEL.UNKNOWN) return null
return (
<VerifiedContainer>
<VerifiedIcon />
<WarningIcon />
</VerifiedContainer>
)
}

View File

@@ -9,7 +9,7 @@ import { Color } from 'theme/styled'
const Label = styled.div<{ color: Color }>`
width: 100%;
padding: 12px 20px;
padding: 12px 20px 16px;
background-color: ${({ color }) => color + '1F'};
border-radius: 16px;
color: ${({ color }) => color};
@@ -31,9 +31,15 @@ const Title = styled(Text)`
const DetailsRow = styled.div`
margin-top: 8px;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const StyledLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textSecondary};
font-weight: 700;
`
type TokenWarningMessageProps = {
warning: Warning
tokenAddress: string
@@ -56,9 +62,9 @@ export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarn
{description}
{Boolean(description) && ' '}
{tokenAddress && (
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
<StyledLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</ExternalLink>
</StyledLink>
)}
</DetailsRow>
</Label>

View File

@@ -244,6 +244,11 @@ export default function TokenSafety({
}
const { heading, description } = getWarningCopy(displayWarning, plural)
const learnMoreUrl = (
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
)
return (
displayWarning && (
@@ -255,13 +260,9 @@ export default function TokenSafety({
<ShortColumn>
<SafetyLabel warning={displayWarning} />
</ShortColumn>
<ShortColumn>{heading && <InfoText fontSize="20px">{heading}</InfoText>}</ShortColumn>
<ShortColumn>
<InfoText>
{description}{' '}
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
{heading} {description} {learnMoreUrl}
</InfoText>
</ShortColumn>
<LinkColumn>{urls}</LinkColumn>

View File

@@ -1,10 +1,13 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { formatToDecimal } from 'analytics/utils'
import CurrencyLogo from 'components/CurrencyLogo'
import { validateUrlChainParam } from 'graphql/data/util'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useParams } from 'react-router-dom'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useMemo } from 'react'
import styled from 'styled-components/macro'
import { StyledInternalLink } from 'theme'
import { currencyAmountToPreciseFloat, formatDollar } from 'utils/formatNumbers'
@@ -29,11 +32,11 @@ const BalancesCard = styled.div`
display: flex;
}
`
const TotalBalanceSection = styled.div`
const BalanceSection = styled.div`
height: fit-content;
width: 100%;
`
const TotalBalance = styled.div`
const BalanceRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
@@ -42,99 +45,52 @@ const TotalBalance = styled.div`
line-height: 28px;
margin-top: 12px;
`
const TotalBalanceItem = styled.div`
const BalanceItem = styled.div`
display: flex;
`
const BalanceRowLink = styled(StyledInternalLink)`
const BalanceLink = styled(StyledInternalLink)`
color: unset;
`
function BalanceRow({ currency, formattedBalance, usdValue, href }: BalanceRowData) {
const content = (
<TotalBalance key={currency.wrapped.address}>
<TotalBalanceItem>
<CurrencyLogo currency={currency} />
&nbsp;{formattedBalance} {currency?.symbol}
</TotalBalanceItem>
<TotalBalanceItem>{formatDollar({ num: usdValue === 0 ? undefined : usdValue, isPrice: true })}</TotalBalanceItem>
</TotalBalance>
export function useFormatBalance(balance: CurrencyAmount<Currency> | undefined) {
return useMemo(
() => (balance ? formatToDecimal(balance, Math.min(balance.currency.decimals, 2)) : undefined),
[balance]
)
if (href) {
return <BalanceRowLink to={href}>{content}</BalanceRowLink>
}
return content
}
interface BalanceRowData {
currency: Currency
formattedBalance: number
usdValue: number | undefined
href?: string
}
export interface BalanceSummaryProps {
tokenAmount: CurrencyAmount<Token> | undefined
nativeCurrencyAmount: CurrencyAmount<Currency> | undefined
isNative: boolean
export function useFormatUsdValue(usdValue: CurrencyAmount<Token> | null) {
return useMemo(() => {
const float = usdValue ? currencyAmountToPreciseFloat(usdValue) : undefined
if (!float) return undefined
return formatDollar({ num: float, isPrice: true })
}, [usdValue])
}
export default function BalanceSummary({ tokenAmount, nativeCurrencyAmount, isNative }: BalanceSummaryProps) {
const balanceUsdValue = useStablecoinValue(tokenAmount)
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)
const { chainName } = useParams<{ chainName?: string }>()
const pageChainName = validateUrlChainParam(chainName).toLowerCase()
const tokenIsWrappedNative =
tokenAmount &&
nativeCurrencyAmount &&
tokenAmount.currency.address.toLowerCase() === nativeCurrencyAmount.currency.wrapped.address.toLowerCase()
if (
(!tokenAmount && !nativeCurrencyAmount) ||
(!tokenAmount && !tokenIsWrappedNative && !isNative) ||
(!isNative && !tokenIsWrappedNative && tokenAmount?.equalTo(0)) ||
(isNative && tokenAmount?.equalTo(0) && nativeCurrencyAmount?.equalTo(0))
) {
return null
}
const showNative = tokenIsWrappedNative || isNative
const currencies = []
if (tokenAmount) {
const tokenData: BalanceRowData = {
currency: tokenAmount.currency,
formattedBalance: formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2)),
usdValue: balanceUsdValue ? currencyAmountToPreciseFloat(balanceUsdValue) : undefined,
}
if (isNative) {
tokenData.href = `/tokens/${pageChainName}/${tokenAmount.currency.address}`
}
currencies.push(tokenData)
}
if (showNative && nativeCurrencyAmount) {
const nativeData: BalanceRowData = {
currency: nativeCurrencyAmount.currency,
formattedBalance: formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2)),
usdValue: nativeBalanceUsdValue ? currencyAmountToPreciseFloat(nativeBalanceUsdValue) : undefined,
}
if (isNative) {
currencies.unshift(nativeData)
} else {
nativeData.href = `/tokens/${pageChainName}/NATIVE`
currencies.push(nativeData)
}
}
export default function BalanceSummary({ token }: { token: Currency }) {
const { account } = useWeb3React()
const balance = useCurrencyBalance(account, token)
const formattedBalance = useFormatBalance(balance)
const usdValue = useStablecoinValue(balance)
const formattedUsdValue = useFormatUsdValue(usdValue)
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
if (!account || !balance) return null
return (
<BalancesCard>
<TotalBalanceSection>
<BalanceSection>
<Trans>Your balance</Trans>
{currencies.map((props, i) => (
<BalanceRow {...props} key={props.currency.wrapped.address + i} />
))}
</TotalBalanceSection>
<BalanceLink to={`/tokens/${chain}/${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
<BalanceRow>
<BalanceItem>
<CurrencyLogo currency={token} />
&nbsp;{formattedBalance} {token.symbol}
</BalanceItem>
<BalanceItem>{formattedUsdValue}</BalanceItem>
</BalanceRow>
</BalanceLink>
</BalanceSection>
</BalancesCard>
)
}

View File

@@ -2,15 +2,13 @@ import { Trans } from '@lingui/macro'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive'
import CurrencyLogo from 'components/CurrencyLogo'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { PriceDurations, PricePoint, SingleTokenData } from 'graphql/data/Token'
import { TokenQueryData } from 'graphql/data/Token'
import { PriceDurations } from 'graphql/data/TokenPrice'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useMemo } from 'react'
import styled from 'styled-components/macro'
import { textFadeIn } from 'theme/animations'
@@ -56,7 +54,7 @@ const TokenActions = styled.div`
`
export function useTokenLogoURI(
token: NonNullable<SingleTokenData> | NonNullable<TopToken>,
token: NonNullable<TokenQueryData> | NonNullable<TopToken>,
nativeCurrency?: Token | NativeCurrency
) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
@@ -73,38 +71,17 @@ export default function ChartSection({
nativeCurrency,
prices,
}: {
token: NonNullable<SingleTokenData>
token: NonNullable<TokenQueryData>
currency?: Currency | null
nativeCurrency?: Token | NativeCurrency
prices: PriceDurations
prices?: PriceDurations
}) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const warning = checkWarning(token.address ?? '')
const timePeriod = useAtomValue(filterTimeAtom)
const logoSrc = useTokenLogoURI(token, nativeCurrency)
// Backend doesn't always return latest price point for every duration.
// Thus we need to manually determine latest price point available, and
// append it to the prices list for every duration.
useMemo(() => {
let latestPricePoint: PricePoint = { value: 0, timestamp: 0 }
let latestPricePointTimePeriod: TimePeriod
Object.keys(prices).forEach((key) => {
const latestPricePointForTimePeriod = prices[key as unknown as TimePeriod]?.slice(-1)[0]
if (latestPricePointForTimePeriod && latestPricePointForTimePeriod.timestamp > latestPricePoint.timestamp) {
latestPricePoint = latestPricePointForTimePeriod
latestPricePointTimePeriod = key as unknown as TimePeriod
}
})
Object.keys(prices).forEach((key) => {
if ((key as unknown as TimePeriod) !== latestPricePointTimePeriod) {
prices[key as unknown as TimePeriod]?.push(latestPricePoint)
}
})
}, [prices])
return (
<ChartHeader>
<TokenInfoContainer>
@@ -120,7 +97,6 @@ export default function ChartSection({
</LogoContainer>
{nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="16px" />}
</TokenNameCell>
<TokenActions>
{token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />}
@@ -128,7 +104,7 @@ export default function ChartSection({
</TokenInfoContainer>
<ChartContainer>
<ParentSize>
{({ width, height }) => prices && <PriceChart prices={prices[timePeriod]} width={width} height={height} />}
{({ width }) => <PriceChart prices={prices ? prices?.[timePeriod] : null} width={width} height={436} />}
</ParentSize>
</ChartContainer>
</ChartHeader>

View File

@@ -1,163 +0,0 @@
import { WidgetSkeleton } from 'components/Widget'
import { LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
import styled, { useTheme } from 'styled-components/macro'
import { LoadingBubble } from '../loading'
import { AboutContainer, AboutHeader, ResourcesContainer } from './About'
import { ContractAddressSection } from './AddressSection'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer, ChartHeader, TokenInfoContainer, TokenNameCell } from './ChartSection'
import { DeltaContainer, TokenPrice } from './PriceChart'
import { StatPair, StatWrapper, TokenStatsSection } from './StatsSection'
const LoadingChartContainer = styled(ChartContainer)`
height: 336px;
overflow: hidden;
`
/* Loading state bubbles */
const LoadingDetailBubble = styled(LoadingBubble)`
height: 16px;
width: 180px;
`
const TitleLoadingBubble = styled(LoadingDetailBubble)`
width: 140px;
`
const SquareLoadingBubble = styled(LoadingDetailBubble)`
height: 32px;
border-radius: 8px;
margin-bottom: 10px;
`
const PriceLoadingBubble = styled(SquareLoadingBubble)`
height: 40px;
`
const LongLoadingBubble = styled(LoadingDetailBubble)`
margin-top: 6px;
width: 100%;
`
const HalfLoadingBubble = styled(LoadingDetailBubble)`
margin-top: 6px;
width: 50%;
`
const IconLoadingBubble = styled(LoadingDetailBubble)`
width: 32px;
height: 32px;
border-radius: 50%;
`
const StatLoadingBubble = styled(SquareLoadingBubble)`
width: 116px;
`
const StatsLoadingContainer = styled.div`
display: flex;
gap: 24px;
flex-wrap: wrap;
`
const ChartAnimation = styled.div`
display: flex;
animation: wave 8s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
overflow: hidden;
@keyframes wave {
0% {
margin-left: 0;
}
100% {
margin-left: -800px;
}
}
`
const Space = styled.div<{ heightSize: number }>`
height: ${({ heightSize }) => `${heightSize}px`};
`
export function Wave() {
const theme = useTheme()
return (
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke={theme.backgroundOutline} fill="transparent" strokeWidth="2" />
</svg>
)
}
/* Loading State: row component with loading bubbles */
export default function LoadingTokenDetail() {
return (
<LeftPanel>
<BreadcrumbNavLink to="/explore">
<Space heightSize={20} />
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<IconLoadingBubble />
<TitleLoadingBubble />
</TokenNameCell>
</TokenInfoContainer>
<TokenPrice>
<PriceLoadingBubble />
</TokenPrice>
<DeltaContainer>
<Space heightSize={20} />
</DeltaContainer>
<LoadingChartContainer>
<div>
<ChartAnimation>
<Wave />
<Wave />
<Wave />
<Wave />
<Wave />
</ChartAnimation>
</div>
</LoadingChartContainer>
<Space heightSize={32} />
</ChartHeader>
<TokenStatsSection>
<StatsLoadingContainer>
<StatPair>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</StatWrapper>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</StatWrapper>
</StatPair>
<StatPair>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</StatWrapper>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</StatWrapper>
</StatPair>
</StatsLoadingContainer>
</TokenStatsSection>
<AboutContainer>
<AboutHeader>
<SquareLoadingBubble />
</AboutHeader>
<LongLoadingBubble />
<LongLoadingBubble />
<HalfLoadingBubble />
<ResourcesContainer>{null}</ResourcesContainer>
</AboutContainer>
<ContractAddressSection>{null}</ContractAddressSection>
</LeftPanel>
)
}
export function LoadingTokenDetails() {
return (
<TokenDetailsLayout>
<LoadingTokenDetail />
<RightPanel>
<WidgetSkeleton />
</RightPanel>
</TokenDetailsLayout>
)
}

View File

@@ -1,11 +1,14 @@
import { Trans } from '@lingui/macro'
import { formatToDecimal } from 'analytics/utils'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import styled from 'styled-components/macro'
import { StyledInternalLink } from 'theme'
import { currencyAmountToPreciseFloat, formatDollar } from 'utils/formatNumbers'
import { BalanceSummaryProps } from './BalanceSummary'
import { useFormatBalance, useFormatUsdValue } from './BalanceSummary'
const Wrapper = styled.div`
align-content: center;
@@ -41,7 +44,7 @@ const BalanceValue = styled.div`
display: flex;
gap: 8px;
`
const BalanceTotal = styled.div`
const Balance = styled.div`
align-items: center;
display: flex;
flex-direction: row;
@@ -78,51 +81,28 @@ const SwapButton = styled(StyledInternalLink)`
max-width: 100vw;
`
export default function MobileBalanceSummaryFooter({
tokenAmount,
nativeCurrencyAmount,
isNative,
tokenAddress,
}: BalanceSummaryProps & { tokenAddress: string }) {
const balanceUsdValue = useStablecoinValue(tokenAmount)
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)
const formattedBalance = tokenAmount
? formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2))
: undefined
const balanceUsd = balanceUsdValue ? currencyAmountToPreciseFloat(balanceUsdValue) : undefined
const formattedNativeBalance = nativeCurrencyAmount
? formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2))
: undefined
const nativeBalanceUsd = nativeBalanceUsdValue ? currencyAmountToPreciseFloat(nativeBalanceUsdValue) : undefined
export default function MobileBalanceSummaryFooter({ token }: { token: Currency }) {
const { account } = useWeb3React()
const balance = useCurrencyBalance(account, token)
const formattedBalance = useFormatBalance(balance)
const usdValue = useStablecoinValue(balance)
const formattedUsdValue = useFormatUsdValue(usdValue)
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
return (
<Wrapper>
{Boolean(formattedBalance !== undefined && !isNative && tokenAmount?.greaterThan(0)) && (
{Boolean(account && balance) && (
<BalanceInfo>
<Trans>Your {tokenAmount?.currency?.symbol} balance</Trans>
<BalanceTotal>
<Trans>Your {token.symbol} balance</Trans>
<Balance>
<BalanceValue>
{formattedBalance} {tokenAmount?.currency?.symbol}
{formattedBalance} {token.symbol}
</BalanceValue>
<FiatValue>{formatDollar({ num: balanceUsd, isPrice: true })}</FiatValue>
</BalanceTotal>
<FiatValue>{formattedUsdValue}</FiatValue>
</Balance>
</BalanceInfo>
)}
{Boolean(isNative && nativeCurrencyAmount?.greaterThan(0)) && (
<BalanceInfo>
<Trans>Your {nativeCurrencyAmount?.currency?.symbol} balance</Trans>
<BalanceTotal>
<BalanceValue>
{formattedNativeBalance} {nativeCurrencyAmount?.currency?.symbol}
</BalanceValue>
<FiatValue>{formatDollar({ num: nativeBalanceUsd, isPrice: true })}</FiatValue>
</BalanceTotal>
</BalanceInfo>
)}
<SwapButton to={`/swap?outputCurrency=${tokenAddress}`}>
<SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
<Trans>Swap</Trans>
</SwapButton>
</Wrapper>

View File

@@ -7,7 +7,7 @@ import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import { filterTimeAtom } from 'components/Tokens/state'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { PricePoint } from 'graphql/data/Token'
import { PricePoint } from 'graphql/data/TokenPrice'
import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useAtom } from 'jotai'
@@ -59,7 +59,7 @@ export function getDeltaArrow(delta: number | null | undefined) {
export function formatDelta(delta: number | null | undefined) {
// Null-check not including zero
if (delta === null || delta === undefined) {
if (delta === null || delta === undefined || delta === Infinity || isNaN(delta)) {
return '-'
}
let formattedDelta = delta.toFixed(2) + '%'
@@ -133,7 +133,7 @@ const timeOptionsHeight = 44
interface PriceChartProps {
width: number
height: number
prices: PricePoint[] | undefined
prices: PricePoint[] | undefined | null
}
export function PriceChart({ width, height, prices }: PriceChartProps) {
@@ -281,10 +281,18 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
<MissingPriceChart
width={width}
height={graphHeight}
message={prices && prices.length === 0 ? <NoV3DataMessage /> : <MissingDataMessage />}
message={
prices === null ? (
<Trans>Loading chart data</Trans>
) : 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}>
<svg width={width} height={graphHeight} style={{ minWidth: '100%' }}>
<AnimatedInLineChart
data={prices}
getX={getX}
@@ -341,6 +349,19 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
) : (
<AxisBottom scale={timeScale} stroke={theme.backgroundOutline} top={graphHeight - 1} hideTicks />
)}
{!width && (
// Ensures an axis is drawn even if the width is not yet initialized.
<line
x1={0}
y1={graphHeight - 1}
x2="100%"
y2={graphHeight - 1}
fill="transparent"
shapeRendering="crispEdges"
stroke={theme.backgroundOutline}
strokeWidth={1}
/>
)}
<rect
x={0}
y={0}
@@ -382,16 +403,11 @@ const StyledMissingChart = styled.svg`
const chartBottomPadding = 15
const NoV3DataMessage = () => (
<Trans>This token doesn&apos;t have chart data because it hasn&apos;t been traded on Uniswap v3</Trans>
)
const MissingDataMessage = () => <Trans>Missing chart data</Trans>
function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) {
const theme = useTheme()
const midPoint = height / 2 + 45
return (
<StyledMissingChart width={width} height={height}>
<StyledMissingChart width={width} height={height} style={{ minWidth: '100%' }}>
<path
d={`M 0 ${midPoint} Q 104 ${midPoint - 70}, 208 ${midPoint} T 416 ${midPoint}
M 416 ${midPoint} Q 520 ${midPoint - 70}, 624 ${midPoint} T 832 ${midPoint}`}

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { SingleTokenData } from 'graphql/data/Token'
import { TokenQueryData } from 'graphql/data/Token'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react'
import { Twitter } from 'react-feather'
@@ -64,7 +64,7 @@ const ShareAction = styled.div`
`
interface TokenInfo {
token: NonNullable<SingleTokenData>
token: NonNullable<TokenQueryData>
isNative: boolean
}

View File

@@ -0,0 +1,225 @@
import { WidgetSkeleton } from 'components/Widget'
import { WIDGET_WIDTH } from 'components/Widget'
import { ArrowLeft } from 'react-feather'
import { useParams } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { LoadingBubble } from '../loading'
import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer, ChartHeader, TokenInfoContainer, TokenNameCell } from './ChartSection'
import { DeltaContainer, TokenPrice } from './PriceChart'
import { StatPair, StatsWrapper, StatWrapper } from './StatsSection'
export const Hr = styled.hr`
background-color: ${({ theme }) => theme.backgroundOutline};
border: none;
height: 0.5px;
`
export const TokenDetailsLayout = styled.div`
display: flex;
padding: 0 8px 52px;
justify-content: center;
width: 100%;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
gap: 16px;
padding: 0 16px;
}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
gap: 40px;
padding: 48px 20px;
}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.xl}px) {
gap: 60px;
}
`
export const LeftPanel = styled.div`
flex: 1;
max-width: 780px;
overflow: hidden;
`
export const RightPanel = styled.div`
display: none;
flex-direction: column;
gap: 20px;
width: ${WIDGET_WIDTH}px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
display: flex;
}
`
const LoadingChartContainer = styled(ChartContainer)`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
height: 313px; // save 1px for the border-bottom (ie y-axis)
overflow: hidden;
`
/* Loading state bubbles */
const DetailBubble = styled(LoadingBubble)`
height: 16px;
width: 180px;
`
const SquaredBubble = styled(DetailBubble)`
height: 32px;
border-radius: 8px;
`
const TokenLogoBubble = styled(DetailBubble)`
width: 32px;
height: 32px;
border-radius: 50%;
`
const TitleBubble = styled(DetailBubble)`
width: 140px;
`
const PriceBubble = styled(SquaredBubble)`
height: 40px;
`
const DeltaBubble = styled(DetailBubble)`
width: 96px;
`
const SectionBubble = styled(SquaredBubble)`
width: 96px;
`
const StatTitleBubble = styled(DetailBubble)`
width: 25%;
margin-bottom: 4px;
`
const StatBubble = styled(SquaredBubble)`
width: 50%;
`
const WideBubble = styled(DetailBubble)`
margin-bottom: 6px;
width: 100%;
`
const HalfWideBubble = styled(WideBubble)`
width: 50%;
`
const StatsLoadingContainer = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
`
const ChartAnimation = styled.div`
animation: wave 8s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
display: flex;
overflow: hidden;
@keyframes wave {
0% {
margin-left: 0;
}
100% {
margin-left: -800px;
}
}
`
const Space = styled.div<{ heightSize: number }>`
height: ${({ heightSize }) => `${heightSize}px`};
`
function Wave() {
const theme = useTheme()
return (
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke={theme.backgroundOutline} fill="transparent" strokeWidth="2" />
</svg>
)
}
function LoadingChart() {
return (
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<TokenLogoBubble />
<TitleBubble />
</TokenNameCell>
</TokenInfoContainer>
<TokenPrice>
<PriceBubble />
</TokenPrice>
<DeltaContainer>
<DeltaBubble />
</DeltaContainer>
<Space heightSize={6} />
<LoadingChartContainer>
<div>
<ChartAnimation>
<Wave />
<Wave />
<Wave />
<Wave />
<Wave />
</ChartAnimation>
</div>
</LoadingChartContainer>
</ChartHeader>
)
}
function LoadingStats() {
return (
<StatsWrapper>
<SectionBubble />
<StatsLoadingContainer>
<StatPair>
<StatWrapper>
<StatTitleBubble />
<StatBubble />
</StatWrapper>
<StatWrapper>
<StatTitleBubble />
<StatBubble />
</StatWrapper>
</StatPair>
<StatPair>
<StatWrapper>
<StatTitleBubble />
<StatBubble />
</StatWrapper>
<StatWrapper>
<StatTitleBubble />
<StatBubble />
</StatWrapper>
</StatPair>
</StatsLoadingContainer>
</StatsWrapper>
)
}
/* Loading State: row component with loading bubbles */
export default function TokenDetailsSkeleton() {
const { chainName } = useParams<{ chainName?: string }>()
return (
<LeftPanel>
<BreadcrumbNavLink to={{ chainName } ? `/tokens/${chainName}` : `/explore`}>
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<LoadingChart />
<Space heightSize={45} />
<LoadingStats />
<Hr />
<AboutContainer>
<AboutHeader>
<SectionBubble />
</AboutHeader>
</AboutContainer>
<WideBubble />
<WideBubble />
<HalfWideBubble />
</LeftPanel>
)
}
export function TokenDetailsPageSkeleton() {
return (
<TokenDetailsLayout>
<TokenDetailsSkeleton />
<RightPanel>
<WidgetSkeleton />
</RightPanel>
</TokenDetailsLayout>
)
}

View File

@@ -44,7 +44,7 @@ const StatPrice = styled.span`
const NoData = styled.div`
color: ${({ theme }) => theme.textTertiary};
`
const Wrapper = styled.div`
export const StatsWrapper = styled.div`
gap: 16px;
${textFadeIn}
`
@@ -84,7 +84,7 @@ export default function StatsSection(props: StatsSectionProps) {
const { priceLow52W, priceHigh52W, TVL, volume24H } = props
if (TVL || volume24H || priceLow52W || priceHigh52W) {
return (
<Wrapper>
<StatsWrapper>
<Header>
<Trans>Stats</Trans>
</Header>
@@ -110,7 +110,7 @@ export default function StatsSection(props: StatsSectionProps) {
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} isPrice={true} />
</StatPair>
</TokenStatsSection>
</Wrapper>
</StatsWrapper>
)
} else {
return <NoData>No stats available</NoData>

View File

@@ -314,10 +314,8 @@ export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined>
/* Get singular header cell for header row */
function HeaderCell({
category,
sortable,
}: {
category: TokenSortMethod // TODO: change this to make it work for trans
sortable: boolean
}) {
const theme = useTheme()
const sortAscending = useAtomValue(sortAscendingAtom)
@@ -390,17 +388,17 @@ export function HeaderRow() {
header={true}
listNumber="#"
tokenInfo={<Trans>Token name</Trans>}
price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
tvl={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
price={<HeaderCell category={TokenSortMethod.PRICE} />}
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} />}
tvl={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} />}
volume={<HeaderCell category={TokenSortMethod.VOLUME} />}
sparkLine={null}
/>
)
}
/* Loading State: row component with loading bubbles */
export function LoadingRow() {
export function LoadingRow(props: { first?: boolean; last?: boolean }) {
return (
<TokenRow
header={false}
@@ -417,6 +415,7 @@ export function LoadingRow() {
tvl={<LoadingBubble />}
volume={<LoadingBubble />}
sparkLine={<SparkLineLoadingBubble />}
{...props}
/>
)
}

View File

@@ -54,25 +54,23 @@ function NoTokensState({ message }: { message: ReactNode }) {
)
}
const LoadingRowsWrapper = styled.div`
margin-top: 8px;
`
const LoadingRows = (rowCount?: number) => (
<LoadingRowsWrapper>
{Array(rowCount ?? PAGE_SIZE)
const LoadingRows = ({ rowCount }: { rowCount: number }) => (
<>
{Array(rowCount)
.fill(null)
.map((_, index) => {
return <LoadingRow key={index} />
return <LoadingRow key={index} first={index === 0} last={index === rowCount - 1} />
})}
</LoadingRowsWrapper>
</>
)
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
return (
<GridContainer>
<HeaderRow />
<TokenDataContainer>{LoadingRows(rowCount)}</TokenDataContainer>
<TokenDataContainer>
<LoadingRows rowCount={rowCount} />
</TokenDataContainer>
</GridContainer>
)
}

View File

@@ -4,7 +4,6 @@ import { useWeb3React } from '@web3-react/core'
import Badge from 'components/Badge'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedL2ChainId } from 'constants/chains'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { ReactNode, useCallback, useState } from 'react'
import { AlertCircle, AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
@@ -24,9 +23,9 @@ import Modal from '../Modal'
import { RowBetween, RowFixed } from '../Row'
import AnimatedConfirmation from './AnimatedConfirmation'
const Wrapper = styled.div<{ redesignFlag?: boolean }>`
background-color: ${({ redesignFlag, theme }) => redesignFlag && theme.backgroundSurface};
outline: ${({ redesignFlag, theme }) => redesignFlag && `1px solid ${theme.backgroundOutline}`};
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
outline: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
width: 100%;
padding: 1rem;
`
@@ -59,11 +58,9 @@ function ConfirmationPendingContent({
pendingText: ReactNode
inline?: boolean // not in modal
}) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const theme = useTheme()
return redesignFlagEnabled ? (
return (
<Wrapper>
<AutoColumn gap="md">
{!inline && (
@@ -88,31 +85,6 @@ function ConfirmationPendingContent({
</AutoColumn>
</AutoColumn>
</Wrapper>
) : (
<Wrapper>
<AutoColumn gap="md">
{!inline && (
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
)}
<ConfirmedIcon inline={inline}>
<CustomLightSpinner src={Circle} alt="loader" size={inline ? '40px' : '90px'} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20} textAlign="center">
<Trans>Waiting For Confirmation</Trans>
</Text>
<Text fontWeight={400} fontSize={16} textAlign="center">
{pendingText}
</Text>
<Text fontWeight={500} fontSize={14} color="#565A69" textAlign="center" marginBottom="12px">
<Trans>Confirm this transaction in your wallet</Trans>
</Text>
</AutoColumn>
</AutoColumn>
</Wrapper>
)
}
function TransactionSubmittedContent({
@@ -135,9 +107,6 @@ function TransactionSubmittedContent({
const token = currencyToAdd?.wrapped
const logoURL = useCurrencyLogoURIs(token)[0]
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const [success, setSuccess] = useState<boolean | undefined>()
const addToken = useCallback(() => {
@@ -153,7 +122,7 @@ function TransactionSubmittedContent({
.catch(() => setSuccess(false))
}, [connector, logoURL, token])
return redesignFlagEnabled ? (
return (
<Wrapper>
<Section inline={inline}>
{!inline && (
@@ -198,51 +167,6 @@ function TransactionSubmittedContent({
</AutoColumn>
</Section>
</Wrapper>
) : (
<Wrapper>
<Section inline={inline}>
{!inline && (
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
)}
<ConfirmedIcon inline={inline}>
<ArrowUpCircle strokeWidth={0.5} size={inline ? '40px' : '90px'} color={theme.deprecated_primary1} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20} textAlign="center">
<Trans>Transaction Submitted</Trans>
</Text>
{chainId && hash && (
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
<Text fontWeight={500} fontSize={14} color={theme.deprecated_primary1}>
<Trans>View on Explorer</Trans>
</Text>
</ExternalLink>
)}
{currencyToAdd && connector.watchAsset && (
<ButtonLight mt="12px" padding="6px 12px" width="fit-content" onClick={addToken}>
{!success ? (
<RowFixed>
<Trans>Add {currencyToAdd.symbol}</Trans>
</RowFixed>
) : (
<RowFixed>
<Trans>Added {currencyToAdd.symbol} </Trans>
<CheckCircle size={'16px'} stroke={theme.deprecated_green1} style={{ marginLeft: '6px' }} />
</RowFixed>
)}
</ButtonLight>
)}
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
</Text>
</ButtonPrimary>
</AutoColumn>
</Section>
</Wrapper>
)
}
@@ -274,50 +198,21 @@ export function ConfirmationModalContent({
}
export function TransactionErrorContent({ message, onDismiss }: { message: ReactNode; onDismiss: () => void }) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const theme = useTheme()
return redesignFlagEnabled ? (
<Wrapper redesignFlag={true}>
return (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={600} fontSize={16}>
<Trans>Error</Trans>
</Text>
<CloseIcon onClick={onDismiss} redesignFlag={true} />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.accentCritical} style={{ strokeWidth: 1 }} size={90} />
<ThemedText.MediumHeader textAlign="center">{message}</ThemedText.MediumHeader>
</AutoColumn>
</Section>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss} redesignFlag={true}>
<Trans>Dismiss</Trans>
</ButtonPrimary>
</BottomSection>
</Wrapper>
) : (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
<Trans>Error</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.deprecated_red1} style={{ strokeWidth: 1.5 }} size={64} />
<Text
fontWeight={500}
fontSize={16}
color={theme.deprecated_red1}
style={{ textAlign: 'center', width: '85%', wordBreak: 'break-word' }}
>
{message}
</Text>
</AutoColumn>
</Section>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss}>
<Trans>Dismiss</Trans>
@@ -447,14 +342,12 @@ export default function TransactionConfirmationModal({
currencyToAdd,
}: ConfirmationModalProps) {
const { chainId } = useWeb3React()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
if (!chainId) return null
// confirmation screen
return (
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (

View File

@@ -6,6 +6,7 @@ const Menu = styled.div`
height: 100%;
font-size: 16px;
overflow: auto;
max-height: 450px;
// Firefox scrollbar styling
scrollbar-width: thin;

View File

@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro'
import { Connector } from '@web3-react/types'
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
@@ -74,10 +73,7 @@ export default function PendingView({
tryActivation: (connector: Connector) => void
openOptions: () => void
}) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
return redesignFlagEnabled ? (
return (
<PendingSection>
<LoadingMessage>
<LoadingWrapper>
@@ -94,7 +90,6 @@ export default function PendingView({
</ThemedText.BodyPrimary>
<ButtonPrimary
$borderRadius="12px"
redesignFlag={true}
onClick={() => {
tryActivation(connector)
}}
@@ -111,7 +106,7 @@ export default function PendingView({
<>
<WaitingToConnectSection>
<LoaderContainer style={{ padding: '16px 0px' }}>
<Loader redesignFlag={true} strokeWidth={0.8} size="100px" />
<Loader strokeWidth={0.8} size="100px" />
</LoaderContainer>
<ThemedText.MediumHeader>
<Trans>Waiting to connect</Trans>
@@ -125,47 +120,5 @@ export default function PendingView({
</LoadingWrapper>
</LoadingMessage>
</PendingSection>
) : (
<PendingSection>
<LoadingMessage>
<LoadingWrapper>
{error ? (
<ErrorGroup>
<ThemedText.DeprecatedMediumHeader marginBottom={12}>
<Trans>Error connecting</Trans>
</ThemedText.DeprecatedMediumHeader>
<ThemedText.DeprecatedBody fontSize={14} marginBottom={36} textAlign="center">
<Trans>
The connection attempt failed. Please click try again and follow the steps to connect in your wallet.
</Trans>
</ThemedText.DeprecatedBody>
<ButtonPrimary
$borderRadius="12px"
padding="12px"
onClick={() => {
tryActivation(connector)
}}
>
<Trans>Try Again</Trans>
</ButtonPrimary>
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
<ThemedText.DeprecatedLink fontSize={12} onClick={openOptions}>
<Trans>Back to wallet selection</Trans>
</ThemedText.DeprecatedLink>
</ButtonEmpty>
</ErrorGroup>
) : (
<>
<ThemedText.DeprecatedBlack fontSize={20} marginY={16}>
<LoaderContainer>
<Loader stroke="currentColor" size="32px" />
</LoaderContainer>
<Trans>Connecting...</Trans>
</ThemedText.DeprecatedBlack>
</>
)}
</LoadingWrapper>
</LoadingMessage>
</PendingSection>
)
}

View File

@@ -6,6 +6,7 @@ import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'ana
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { networkConnection } from 'connection'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import usePrevious from 'hooks/usePrevious'
@@ -69,7 +70,6 @@ const HeaderRow = styled.div`
const ContentWrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
padding: 0 1rem 1rem 1rem;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
@@ -189,6 +189,13 @@ export default function WalletModal({
}
}, [pendingConnector, walletView])
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.
useEffect(() => {
if (chainId && connector !== networkConnection.connector) {
networkConnection.connector.activate(chainId)
}
}, [chainId, connector])
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {

View File

@@ -34,24 +34,32 @@ export const WIDGET_WIDTH = 360
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
function useWidgetTheme() {
return useIsDarkMode() ? DARK_THEME : LIGHT_THEME
}
export interface WidgetProps {
defaultToken?: Currency
token?: Currency
onTokenChange?: (token: Currency) => void
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps) {
const locale = useActiveLocale()
const theme = useIsDarkMode() ? DARK_THEME : LIGHT_THEME
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
const { connector, provider } = useWeb3React()
const { inputs, tokenSelector } = useSyncWidgetInputs(defaultToken)
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
const { settings } = useSyncWidgetSettings()
const { transactions } = useSyncWidgetTransactions()
const onSwitchChain = useCallback(
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
[connector]
)
const trace = useTrace({ section: SectionName.WIDGET })
const [initialQuoteDate, setInitialQuoteDate] = useState<Date>()
const onInitialSwapQuote = useCallback(
(trade: Trade<Currency, Currency, TradeType>) => {
setInitialQuoteDate(new Date())
@@ -68,7 +76,6 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
},
[trace]
)
const onApproveToken = useCallback(() => {
const input = inputs.value.INPUT
if (!input) return
@@ -80,11 +87,9 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
}
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, eventProperties)
}, [inputs.value.INPUT, trace])
const onExpandSwapDetails = useCallback(() => {
sendAnalyticsEvent(EventName.SWAP_DETAILS_EXPANDED, { ...trace })
}, [trace])
const onSwapPriceUpdateAck = useCallback(
(stale: Trade<Currency, Currency, TradeType>, update: Trade<Currency, Currency, TradeType>) => {
const eventProperties = {
@@ -99,7 +104,6 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
},
[trace]
)
const onSubmitSwapClick = useCallback(
(trade: Trade<Currency, Currency, TradeType>) => {
const eventProperties = {
@@ -127,13 +131,8 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
},
[initialQuoteDate, trace]
)
const onSwitchChain = useCallback(
// TODO: Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
[connector]
)
if (!inputs.value.INPUT && !inputs.value.OUTPUT) {
if (!(inputs.value.INPUT || inputs.value.OUTPUT)) {
return <WidgetSkeleton />
}
@@ -143,9 +142,9 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
disableBranding
hideConnectionUI
routerUrl={WIDGET_ROUTER_URL}
width={WIDGET_WIDTH}
locale={locale}
theme={theme}
width={WIDGET_WIDTH}
// defaultChainId is excluded - it is always inferred from the passed provider
provider={provider}
onSwitchChain={onSwitchChain}
@@ -166,5 +165,6 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
}
export function WidgetSkeleton() {
return <SwapWidgetSkeleton theme={useIsDarkMode() ? DARK_THEME : LIGHT_THEME} width={WIDGET_WIDTH} />
const theme = useWidgetTheme()
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
}

View File

@@ -7,15 +7,36 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
const EMPTY_AMOUNT = ''
type SwapValue = Required<SwapController>['value']
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT>
/**
* Integrates the Widget's inputs.
* Treats the Widget as a controlled component, using the app's own token selector for selection.
* Enforces that token is a part of the returned value.
*/
export function useSyncWidgetInputs(defaultToken?: Currency) {
export function useSyncWidgetInputs({
token: defaultToken,
onTokenChange,
}: {
token?: Currency
onTokenChange?: (token: Currency) => void
}) {
const trace = useTrace({ section: SectionName.WIDGET })
const [type, setType] = useState(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState(EMPTY_AMOUNT)
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: defaultToken })
const shouldDefault = useCallback(
(tokens: SwapTokens) => defaultToken && !Object.values(tokens).some((token) => token?.equals(defaultToken)),
[defaultToken]
)
useEffect(
() => setTokens((tokens) => (shouldDefault(tokens) ? { [Field.OUTPUT]: defaultToken } : tokens)),
[defaultToken, shouldDefault]
)
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {
if (origin === 'max') {
@@ -27,19 +48,6 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
[trace]
)
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
[Field.OUTPUT]: defaultToken,
})
useEffect(() => {
// Avoid overwriting tokens if none are specified, so that a loading token does not cause layout flashing.
if (!defaultToken) return
setTokens({
[Field.OUTPUT]: defaultToken,
})
setAmount(EMPTY_AMOUNT)
}, [defaultToken])
const onSwitchTokens = useCallback(() => {
sendAnalyticsEvent(EventName.SWAP_TOKENS_REVERSED, { ...trace })
setType((type) => invertTradeType(type))
@@ -50,47 +58,65 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
}, [trace])
const [selectingField, setSelectingField] = useState<Field>()
const otherField = useMemo(() => (selectingField === Field.INPUT ? Field.OUTPUT : Field.INPUT), [selectingField])
const [selectingToken, otherToken] = useMemo(() => {
if (selectingField === undefined) return [undefined, undefined]
return [tokens[selectingField], tokens[otherField]]
}, [otherField, selectingField, tokens])
const onTokenSelectorClick = useCallback((field: Field) => {
setSelectingField(field)
return false
}, [])
const onTokenSelect = useCallback(
(token: Currency) => {
if (selectingField === undefined) return
setType(TradeType.EXACT_INPUT)
setTokens(() => {
return {
[otherField]: otherToken?.equals(token) ? selectingToken : otherToken,
[selectingField]: token,
}
})
setType(toTradeType(selectingField))
const otherField = invertField(selectingField)
let otherToken = tokens[otherField]
otherToken = otherToken?.equals(token) ? tokens[selectingField] : otherToken
const update = {
[selectingField]: token,
[otherField]: otherToken,
}
if (shouldDefault(update)) {
onTokenChange?.(update[Field.OUTPUT] || update[Field.INPUT] || token)
}
setTokens(update)
},
[otherField, otherToken, selectingField, selectingToken]
[onTokenChange, selectingField, shouldDefault, tokens]
)
const tokenSelector = (
<CurrencySearchModal
isOpen={selectingField !== undefined}
onDismiss={() => setSelectingField(undefined)}
selectedCurrency={selectingToken}
otherSelectedCurrency={otherToken}
selectedCurrency={selectingField && tokens[selectingField]}
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
onCurrencySelect={onTokenSelect}
/>
)
const value: SwapController['value'] = useMemo(() => ({ type, amount, ...tokens }), [amount, tokens, type])
const value: SwapValue = useMemo(
() => ({
type,
amount,
...tokens,
}),
[amount, tokens, type]
)
const valueHandlers: SwapEventHandlers = useMemo(
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
[onAmountChange, onSwitchTokens, onTokenSelectorClick]
)
return { inputs: { value, ...valueHandlers }, tokenSelector }
}
// TODO(zzmp): Move to @uniswap/widgets.
function invertField(field: Field) {
switch (field) {
case Field.INPUT:
return Field.OUTPUT
case Field.OUTPUT:
return Field.INPUT
}
}
// TODO(zzmp): Move to @uniswap/widgets.
function toTradeType(modifiedField: Field) {
switch (modifiedField) {

View File

@@ -52,17 +52,25 @@ export const gnosisSafeConnection: Connection = {
type: ConnectionType.GNOSIS_SAFE,
}
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnect>(
(actions) =>
new WalletConnect({
actions,
options: {
rpc: RPC_URLS,
qrcode: true,
},
onError,
})
)
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnect>((actions) => {
// Avoid testing for the best URL by only passing a single URL per chain.
// Otherwise, WC will not initialize until all URLs have been tested (see getBestUrl in web3-react).
const RPC_URLS_WITHOUT_FALLBACKS = Object.entries(RPC_URLS).reduce(
(map, [chainId, urls]) => ({
...map,
[chainId]: urls[0],
}),
{}
)
return new WalletConnect({
actions,
options: {
rpc: RPC_URLS_WITHOUT_FALLBACKS,
qrcode: true,
},
onError,
})
})
export const walletConnectConnection: Connection = {
connector: web3WalletConnect,
hooks: web3WalletConnectHooks,

View File

@@ -1,6 +1,6 @@
export const UNI_LIST = 'https://tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LISTS = 'https://unsupportedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
@@ -16,12 +16,11 @@ export const OPTIMISM_LIST = 'https://static.optimism.io/optimism.tokenlist.json
export const ARBITRUM_LIST = 'https://bridge.arbitrum.io/token-list-42161.json'
export const CELO_LIST = 'https://celo-org.github.io/celo-token-list/celo.tokenlist.json'
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LISTS]
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LIST]
// this is the default list of lists that are exposed to users
// lower index == higher priority for token import
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
UNI_LIST,
// default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
UNI_EXTENDED_LIST,
COMPOUND_LIST,
AAVE_LIST,
@@ -37,10 +36,11 @@ const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
CELO_LIST,
]
// this is the default list of lists that are exposed to users
// lower index == higher priority for token import
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS]
export const DEFAULT_LIST_OF_LISTS: string[] = [
...DEFAULT_LIST_OF_LISTS_TO_DISPLAY,
...UNSUPPORTED_LIST_URLS, // need to load dynamic unsupported tokens as well
]
// default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST, GEMINI_LIST]

View File

@@ -1,5 +1,7 @@
import { Plural, Trans } from '@lingui/macro'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133'
@@ -14,17 +16,36 @@ export function getWarningCopy(warning: Warning | null, plural = false) {
let heading = null,
description = null
if (warning) {
if (warning.canProceed) {
heading = <Plural value={plural ? 2 : 1} _1="This token isn't verified." other="These tokens aren't verified." />
description = <Trans>Please do your own research before trading.</Trans>
} else {
description = (
<Plural
value={plural ? 2 : 1}
_1="You can't trade this token using the Uniswap App."
other="You can't trade these tokens using the Uniswap App."
/>
)
switch (warning.level) {
case WARNING_LEVEL.MEDIUM:
heading = (
<Plural
value={plural ? 2 : 1}
_1="This token isn't traded on leading U.S. centralized exchanges."
other="These tokens aren't traded on leading U.S. centralized exchanges."
/>
)
description = <Trans>Always conduct your own research before trading.</Trans>
break
case WARNING_LEVEL.UNKNOWN:
heading = (
<Plural
value={plural ? 2 : 1}
_1="This token isn't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
other="These tokens aren't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
/>
)
description = <Trans>Always conduct your own research before trading.</Trans>
break
case WARNING_LEVEL.BLOCKED:
description = (
<Plural
value={plural ? 2 : 1}
_1="You can't trade this token using the Uniswap App."
other="You can't trade these tokens using the Uniswap App."
/>
)
break
}
}
return { heading, description }
@@ -57,6 +78,9 @@ const BlockedWarning: Warning = {
}
export function checkWarning(tokenAddress: string) {
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
return null
}
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
case TOKEN_LIST_TYPES.UNI_DEFAULT:
return null

View File

@@ -6,6 +6,11 @@ import { SupportedChainId } from './chains'
export const NATIVE_CHAIN_ID = 'NATIVE'
// When decimals are not specified for an ERC20 token
// use default ERC20 token decimals as specified here:
// https://docs.openzeppelin.com/contracts/3.x/erc20
export const DEFAULT_ERC20_DECIMALS = 18
export const USDC_MAINNET = new Token(
SupportedChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',

View File

@@ -3,4 +3,5 @@ export enum FeatureFlag {
nft = 'nfts',
traceJsonRpc = 'traceJsonRpc',
multiNetworkBalances = 'multiNetworkBalances',
nftGraphQl = 'nftGraphQl',
}

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useNftGraphQlFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftGraphQl)
}
export { BaseVariant as NftGraphQlVariant }

View File

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

View File

@@ -1,41 +1,40 @@
import ms from 'ms.macro'
import { Variables } from 'react-relay'
import { Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime'
import { CacheConfig, Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime'
import RelayQueryResponseCache from 'relay-runtime/lib/network/RelayQueryResponseCache'
import fetchGraphQL from './fetchGraphQL'
// max number of request in cache, least-recently updated entries purged first
const size = 250
// number in milliseconds, how long records stay valid in cache
const ttl = ms`5m`
export const cache = new RelayQueryResponseCache({ size, ttl })
const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, variables: Variables) {
const fetchQuery = async function wrappedFetchQuery(
params: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig
) {
const queryID = params.name
const cachedData = cache.get(queryID, variables)
if (cachedData !== null) return cachedData
return fetchGraphQL(params, variables).then((data) => {
return fetchGraphQL(params, variables, cacheConfig).then((data) => {
if (params.operationKind !== 'mutation') {
cache.set(queryID, variables, data)
}
return data
})
}
// This property tells Relay to not immediately clear its cache when the user
// navigates around the app. Relay will hold onto the specified number of
// query results, allowing the user to return to recently visited pages
// and reusing cached data if its available/fresh.
const gcReleaseBufferSize = 10
const queryCacheExpirationTime = ms`1m`
const store = new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime })
const network = Network.create(fetchQuery)
// Export a singleton instance of Relay Environment configured with our network function:
export default new Environment({
network,

View File

@@ -1,11 +1,12 @@
import graphql from 'babel-plugin-relay/macro'
import { useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery } from 'react-relay'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import { useMemo } from 'react'
import { useLazyLoadQuery } from 'react-relay'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import environment from './RelayEnvironment'
import { TimePeriod, toHistoryDuration } from './util'
import { Chain } from './__generated__/TokenPriceQuery.graphql'
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { CHAIN_NAME_TO_CHAIN_ID } from './util'
/*
The difference between Token and TokenProject:
@@ -16,9 +17,10 @@ The difference between Token and TokenProject:
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/
const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!) {
query TokenQuery($contract: ContractInput!) {
tokens(contracts: [$contract]) {
id @required(action: LOG)
decimals
name
chain @required(action: LOG)
address @required(action: LOG)
@@ -28,10 +30,6 @@ const tokenQuery = graphql`
value
currency
}
priceHistory(duration: $duration) {
timestamp
value
}
price {
value
currency
@@ -61,113 +59,24 @@ const tokenQuery = graphql`
}
`
export type PricePoint = { value: number; timestamp: number }
export function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) {
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number]
export function useTokenQuery(address: string, chain: Chain): TokenQueryData | undefined {
const contract = useMemo(() => ({ address: address.toLowerCase(), chain }), [address, chain])
const token = useLazyLoadQuery<TokenQuery>(tokenQuery, { contract }).tokens?.[0]
return token
}
export type PriceDurations = Record<TimePeriod, PricePoint[] | undefined>
function fetchAllPriceDurations(contract: ContractInput, originalDuration: HistoryDuration) {
return fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, {
contract,
skip1H: originalDuration === 'HOUR',
skip1D: originalDuration === 'DAY',
skip1W: originalDuration === 'WEEK',
skip1M: originalDuration === 'MONTH',
skip1Y: originalDuration === 'YEAR',
})
}
export type SingleTokenData = NonNullable<TokenQuery$data['tokens']>[number]
export function useTokenQuery(
address: string,
chain: Chain,
timePeriod: TimePeriod
): [SingleTokenData | undefined, PriceDurations] {
const [prices, setPrices] = useState<PriceDurations>({
[TimePeriod.HOUR]: undefined,
[TimePeriod.DAY]: undefined,
[TimePeriod.WEEK]: undefined,
[TimePeriod.MONTH]: undefined,
[TimePeriod.YEAR]: undefined,
})
const contract = useMemo(() => {
return { address: address.toLowerCase(), chain }
}, [address, chain])
// eslint-disable-next-line react-hooks/exhaustive-deps
const originalTimePeriod = useMemo(() => timePeriod, [contract])
const updatePrices = (response: TokenPriceQuery['response']) => {
const priceData = response.tokens?.[0]?.market
if (priceData) {
setPrices((current) => {
return {
[TimePeriod.HOUR]: filterPrices(priceData.priceHistory1H) ?? current[TimePeriod.HOUR],
[TimePeriod.DAY]: filterPrices(priceData.priceHistory1D) ?? current[TimePeriod.DAY],
[TimePeriod.WEEK]: filterPrices(priceData.priceHistory1W) ?? current[TimePeriod.WEEK],
[TimePeriod.MONTH]: filterPrices(priceData.priceHistory1M) ?? current[TimePeriod.MONTH],
[TimePeriod.YEAR]: filterPrices(priceData.priceHistory1Y) ?? current[TimePeriod.YEAR],
}
})
}
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo {
constructor(data: NonNullable<TokenQueryData>) {
super({
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
address: data.address,
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
symbol: data.symbol ?? '',
name: data.name ?? '',
logoURI: data.project?.logoUrl ?? undefined,
})
}
// Fetch prices & token info in tandem so we can render faster
useMemo(
() => fetchAllPriceDurations(contract, toHistoryDuration(originalTimePeriod)).subscribe({ next: updatePrices }),
[contract, originalTimePeriod]
)
const token = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract,
duration: toHistoryDuration(originalTimePeriod),
}).tokens?.[0]
useMemo(
() =>
setPrices((current) => {
current[originalTimePeriod] = filterPrices(token?.market?.priceHistory)
return current
}),
[token, originalTimePeriod]
)
return [token, prices]
}
const tokenPriceQuery = graphql`
query TokenPriceQuery(
$contract: ContractInput!
$skip1H: Boolean!
$skip1D: Boolean!
$skip1W: Boolean!
$skip1M: Boolean!
$skip1Y: Boolean!
) {
tokens(contracts: [$contract]) {
market(currency: USD) {
priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) {
timestamp
value
}
priceHistory1D: priceHistory(duration: DAY) @skip(if: $skip1D) {
timestamp
value
}
priceHistory1W: priceHistory(duration: WEEK) @skip(if: $skip1W) {
timestamp
value
}
priceHistory1M: priceHistory(duration: MONTH) @skip(if: $skip1M) {
timestamp
value
}
priceHistory1Y: priceHistory(duration: YEAR) @skip(if: $skip1Y) {
timestamp
value
}
}
}
}
`

View File

@@ -0,0 +1,84 @@
import graphql from 'babel-plugin-relay/macro'
import { useEffect, useMemo, useState } from 'react'
import { fetchQuery } from 'react-relay'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import environment from './RelayEnvironment'
import { TimePeriod } from './util'
const tokenPriceQuery = graphql`
query TokenPriceQuery($contract: ContractInput!) {
tokens(contracts: [$contract]) {
market(currency: USD) {
priceHistory1H: priceHistory(duration: HOUR) {
timestamp
value
}
priceHistory1D: priceHistory(duration: DAY) {
timestamp
value
}
priceHistory1W: priceHistory(duration: WEEK) {
timestamp
value
}
priceHistory1M: priceHistory(duration: MONTH) {
timestamp
value
}
priceHistory1Y: priceHistory(duration: YEAR) {
timestamp
value
}
}
}
}
`
export type PricePoint = { timestamp: number; value: number }
export type PriceDurations = Partial<Record<TimePeriod, PricePoint[]>>
export function isPricePoint(p: { timestamp: number; value: number | null } | null): p is PricePoint {
return Boolean(p && p.value)
}
export function useTokenPriceQuery(address: string, chain: Chain): PriceDurations | undefined {
const contract = useMemo(() => ({ address: address.toLowerCase(), chain }), [address, chain])
const [prices, setPrices] = useState<PriceDurations>()
useEffect(() => {
const subscription = fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, { contract }).subscribe({
next: (response: TokenPriceQuery['response']) => {
const priceData = response.tokens?.[0]?.market
const prices = {
[TimePeriod.HOUR]: priceData?.priceHistory1H?.filter(isPricePoint),
[TimePeriod.DAY]: priceData?.priceHistory1D?.filter(isPricePoint),
[TimePeriod.WEEK]: priceData?.priceHistory1W?.filter(isPricePoint),
[TimePeriod.MONTH]: priceData?.priceHistory1M?.filter(isPricePoint),
[TimePeriod.YEAR]: priceData?.priceHistory1Y?.filter(isPricePoint),
}
// Ensure the latest price available is available for every TimePeriod.
const latests = Object.values(prices)
.map((prices) => prices?.slice(-1)?.[0] ?? null)
.filter(isPricePoint)
if (latests.length) {
const latest = latests.reduce((latest, pricePoint) =>
latest.timestamp > pricePoint.timestamp ? latest : pricePoint
)
Object.values(prices)
.filter((prices) => prices && prices.slice(-1)[0] !== latest)
.forEach((prices) => prices?.push(latest))
}
setPrices(prices)
},
})
return () => {
setPrices(undefined)
subscription.unsubscribe()
}
}, [contract])
return prices
}

View File

@@ -12,7 +12,7 @@ import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
import { filterPrices, PricePoint } from './Token'
import { isPricePoint, PricePoint } from './TokenPrice'
import { CHAIN_NAME_TO_CHAIN_ID, toHistoryDuration, unwrapToken } from './util'
const topTokens100Query = graphql`
@@ -137,7 +137,8 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
next(data) {
const map: SparklineMap = {}
data.topTokens?.forEach(
(current) => current?.address && (map[current.address] = filterPrices(current?.market?.priceHistory))
(current) =>
current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
)
setSparklines(map)
},

View File

@@ -1,22 +1,35 @@
import { Variables } from 'react-relay'
import { GraphQLResponse, RequestParameters } from 'relay-runtime'
import { CacheConfig, GraphQLResponse, RequestParameters } from 'relay-runtime'
const URL = process.env.REACT_APP_AWS_API_ENDPOINT
const NFT_URL = process.env.REACT_APP_NFT_AWS_API_ENDPOINT ?? ''
if (!URL) {
throw new Error('AWS URL MISSING FROM ENVIRONMENT')
}
const headers = {
const baseHeaders = {
'Content-Type': 'application/json',
}
const fetchQuery = (params: RequestParameters, variables: Variables): Promise<GraphQLResponse> => {
const nftHeaders = {
'Content-Type': 'application/json',
'x-api-key': process.env.REACT_APP_NFT_AWS_X_API_KEY ?? '',
}
const fetchQuery = (
params: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig
): Promise<GraphQLResponse> => {
const { metadata: { isNFT } = { isNFT: false } } = cacheConfig
const body = JSON.stringify({
query: params.text, // GraphQL text from input
variables,
})
const url = isNFT ? NFT_URL : URL
const headers = isNFT ? nftHeaders : baseHeaders
return fetch(URL, { method: 'POST', body, headers })
return fetch(url, { method: 'POST', body, headers })
.then((res) => res.json())
.catch((e) => {
console.error(e)

View File

@@ -2,7 +2,7 @@ import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { Chain, HistoryDuration } from './__generated__/TokenQuery.graphql'
import { Chain, HistoryDuration } from './__generated__/TopTokens100Query.graphql'
export enum TimePeriod {
HOUR,

View File

@@ -2,12 +2,13 @@ import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_INACTIVE_LIST_URLS } from 'constants/lists'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react'
import { isL2ChainId } from 'utils/chains'
import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks'
import { useAllLists, useCombinedActiveList } from '../state/lists/hooks'
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
@@ -54,6 +55,11 @@ export function useAllTokens(): { [address: string]: Token } {
return useTokensFromMap(allTokens, true)
}
export function useActiveTokens(includeUserAdded: boolean): { [address: string]: Token } {
const allTokens = useCombinedActiveList()
return useTokensFromMap(allTokens, includeUserAdded)
}
type BridgeInfo = Record<
SupportedChainId,
{
@@ -109,7 +115,7 @@ export function useUnsupportedTokens(): { [address: string]: Token } {
export function useSearchInactiveTokenLists(search: string | undefined, minResults = 10): WrappedTokenInfo[] {
const lists = useAllLists()
const inactiveUrls = useInactiveListUrls()
const inactiveUrls = DEFAULT_INACTIVE_LIST_URLS
const { chainId } = useWeb3React()
const activeTokens = useAllTokens()
return useMemo(() => {

View File

@@ -2,90 +2,17 @@ import { arrayify } from '@ethersproject/bytes'
import { parseBytes32String } from '@ethersproject/strings'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import ERC20_ABI from 'abis/erc20.json'
import { Erc20 } from 'abis/types'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { isSupportedChain } from 'constants/chains'
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useEffect, useMemo, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useMemo } from 'react'
import { DEFAULT_ERC20_DECIMALS } from '../../constants/tokens'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { getContract, isAddress } from '../../utils'
import { isAddress } from '../../utils'
import { supportedChainId } from '../../utils/supportedChainId'
/**
* Returns a Token from query data.
* Data should already include all fields except decimals, or it will be considered invalid.
* Returns null if the token is loading or null was passed.
* Returns undefined if invalid or the token does not exist.
*/
export function useTokenFromQuery({
address: tokenAddress,
chainId,
symbol,
name,
project,
}: {
address?: string
chainId?: SupportedChainId
symbol?: string | null
name?: string | null
project?: { logoUrl?: string | null } | null
} = {}): Token | null | undefined {
const { chainId: activeChainId } = useWeb3React()
const address = isAddress(tokenAddress)
const [decimals, setDecimals] = useState<number | null | undefined>(null)
const tokenContract = useTokenContract(chainId === activeChainId ? (address ? address : undefined) : undefined, false)
const { loading, result: [decimalsResult] = [] } = useSingleCallResult(
tokenContract,
'decimals',
undefined,
NEVER_RELOAD
)
useEffect(() => {
if (loading) {
setDecimals(null)
} else if (decimalsResult) {
setDecimals(decimalsResult)
} else if (!address || !chainId || chainId === activeChainId) {
setDecimals(undefined)
} else {
setDecimals(null)
// Load decimals from a cross-chain RPC provider.
const provider = RPC_PROVIDERS[chainId]
const contract = getContract(address, ERC20_ABI, provider) as Erc20
contract
.decimals()
.then((value) => {
if (!stale) setDecimals(value)
})
.catch(() => undefined)
}
let stale = false
return () => {
stale = true
}
}, [activeChainId, address, chainId, decimalsResult, loading])
return useMemo(() => {
if (!chainId || !address) return undefined
if (decimals === null || decimals === undefined) return decimals
if (!symbol || !name) {
return new Token(chainId, address, decimals, symbol ?? undefined, name ?? undefined)
} else {
const logoURI = project?.logoUrl ?? undefined
return new WrappedTokenInfo({ chainId, address, decimals, symbol, name, logoURI })
}
}, [address, chainId, decimals, name, project?.logoUrl, symbol])
}
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
@@ -121,7 +48,8 @@ export function useTokenFromActiveNetwork(tokenAddress: string | undefined): Tok
() => decimals.loading || symbol.loading || tokenName.loading,
[decimals.loading, symbol.loading, tokenName.loading]
)
const parsedDecimals = useMemo(() => decimals.result?.[0], [decimals.result])
const parsedDecimals = useMemo(() => decimals?.result?.[0] ?? DEFAULT_ERC20_DECIMALS, [decimals.result])
const parsedSymbol = useMemo(
() => parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
[symbol.result, symbolBytes32.result]
@@ -134,9 +62,7 @@ export function useTokenFromActiveNetwork(tokenAddress: string | undefined): Tok
return useMemo(() => {
// If the token is on another chain, we cannot fetch it on-chain, and it is invalid.
if (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined
if (isLoading || !chainId) return null
if (!parsedDecimals) return undefined
return new Token(chainId, formattedAddress, parsedDecimals, parsedSymbol, parsedName)
}, [chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
@@ -152,7 +78,6 @@ type TokenMap = { [address: string]: Token }
export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string | null): Token | null | undefined {
const address = isAddress(tokenAddress)
const token: Token | undefined = address ? tokens[address] : undefined
const tokenFromNetwork = useTokenFromActiveNetwork(token ? undefined : address ? address : undefined)
return tokenFromNetwork ?? token

View File

@@ -117,6 +117,7 @@ export function useCurrencyBalances(
[currencies]
)
const { chainId } = useWeb3React()
const tokenBalances = useTokenBalances(account, tokens)
const containsETH: boolean = useMemo(() => currencies?.some((currency) => currency?.isNative) ?? false, [currencies])
const ethBalance = useNativeCurrencyBalances(useMemo(() => (containsETH ? [account] : []), [containsETH, account]))
@@ -124,12 +125,12 @@ export function useCurrencyBalances(
return useMemo(
() =>
currencies?.map((currency) => {
if (!account || !currency) return undefined
if (!account || !currency || currency.chainId !== chainId) return undefined
if (currency.isToken) return tokenBalances[currency.address]
if (currency.isNative) return ethBalance[account]
return undefined
}) ?? [],
[account, currencies, ethBalance, tokenBalances]
[account, chainId, currencies, ethBalance, tokenBalances]
)
}

View File

@@ -16,7 +16,7 @@ function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Curr
type TokenBalances = { [tokenAddress: string]: CurrencyAmount<Token> | undefined }
/** Sorts tokens by currency amount (descending), then symbol (ascending). */
/** Sorts tokens by currency amount (descending), then safety, then symbol (ascending). */
export function tokenComparator(balances: TokenBalances, a: Token, b: Token) {
// Sorts by balances
const balanceComparison = balanceComparator(balances[a.address], balances[b.address])

View File

@@ -6,11 +6,9 @@ import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Center, Column } from 'nft/components/Flex'
import { LargeBagIcon, LargeTagIcon } from 'nft/components/icons'
import { Column } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import { buttonTextMedium, commonButtonStyles, subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { buttonTextMedium, commonButtonStyles } from 'nft/css/common.css'
import {
useBag,
useIsMobile,
@@ -34,39 +32,9 @@ import { useLocation } from 'react-router-dom'
import * as styles from './Bag.css'
import { BagContent } from './BagContent'
import { BagHeader } from './BagHeader'
import EmptyState from './EmptyContent'
import { ProfileBagContent } from './profile/ProfileBagContent'
const EmptyState = () => {
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/profile')
return (
<Center height="full">
<Column gap={isProfilePage ? '16' : '12'}>
<Center>
{isProfilePage ? (
<LargeTagIcon color={themeVars.colors.textTertiary} />
) : (
<LargeBagIcon color={themeVars.colors.textTertiary} />
)}
</Center>
{isProfilePage ? (
<span className={subhead}>No NFTs Selected</span>
) : (
<Column gap="16">
<Center className={subhead} style={{ lineHeight: '24px' }}>
Your bag is empty
</Center>
<Center fontSize="12" fontWeight="normal" color="textSecondary" style={{ lineHeight: '16px' }}>
Selected NFTs will appear here
</Center>
</Column>
)}
</Column>
</Center>
)
}
interface SeparatorProps {
top?: boolean
show?: boolean
@@ -155,11 +123,9 @@ const Bag = () => {
return { totalEthPrice, totalUsdPrice }
}, [itemsInBag, fetchedPriceData])
const { balance, sufficientBalance } = useMemo(() => {
const balance: BigNumber = parseEther(balanceInEth.toString())
const sufficientBalance = isConnected ? BigNumber.from(balance).gte(totalEthPrice) : true
return { balance, sufficientBalance }
const sufficientBalance = useMemo(() => {
const balance = parseEther(balanceInEth.toString())
return isConnected ? BigNumber.from(balance).gte(totalEthPrice) : true
}, [balanceInEth, totalEthPrice, isConnected])
const purchaseAssets = async (routingData: RouteResponse) => {
@@ -282,6 +248,13 @@ const Bag = () => {
setScrollProgress(scrollTop ? ((scrollTop + containerHeight) / scrollHeight) * 100 : 0)
}
const isBuyingAssets = itemsInBag.length > 0
const isSellingAssets = sellAssets.length > 0
const shouldRenderEmptyState = Boolean(
(!isProfilePage && !isBuyingAssets && bagStatus === BagStatus.ADDING_TO_BAG) || (isProfilePage && !isSellingAssets)
)
return (
<>
{bagExpanded && shouldShowBag ? (
@@ -295,8 +268,7 @@ const Bag = () => {
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{(!isProfilePage && itemsInBag.length === 0 && bagStatus === BagStatus.ADDING_TO_BAG) ||
(isProfilePage && sellAssets.length === 0 && <EmptyState />)}
{shouldRenderEmptyState && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
@@ -304,7 +276,6 @@ const Bag = () => {
<ScrollingIndicator show={userCanScroll && scrollProgress < 100} />
{hasAssetsToShow && !isProfilePage && (
<BagFooter
balance={balance}
sufficientBalance={sufficientBalance}
isConnected={isConnected}
totalEthPrice={totalEthPrice}
@@ -314,7 +285,7 @@ const Bag = () => {
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
/>
)}
{sellAssets.length !== 0 && isProfilePage && (
{isSellingAssets && isProfilePage && (
<Box
marginTop="32"
marginX="28"

View File

@@ -15,7 +15,7 @@ export const BagContent = () => {
const setDidOpenUnavailableAssets = useBag((s) => s.setDidOpenUnavailableAssets)
const uncheckedItemsInBag = useBag((s) => s.itemsInBag)
const setItemsInBag = useBag((s) => s.setItemsInBag)
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
const removeAssetsFromBag = useBag((s) => s.removeAssetsFromBag)
const isMobile = useIsMobile()
@@ -75,16 +75,19 @@ export const BagContent = () => {
))}
</Column>
<Column gap="8">
{unchangedAssets.map((asset) => (
<BagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
removeAsset={removeAssetFromBag}
showRemove={true}
isMobile={isMobile}
/>
))}
{unchangedAssets
.slice(0)
.reverse()
.map((asset) => (
<BagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
removeAsset={removeAssetsFromBag}
showRemove={true}
isMobile={isMobile}
/>
))}
</Column>
</>
)

View File

@@ -1,17 +1,46 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import Loader from 'components/Loader'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { bodySmall, headlineSmall } from 'nft/css/common.css'
import { bodySmall } from 'nft/css/common.css'
import { BagStatus } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
import { AlertTriangle } from 'react-feather'
import { useModalIsOpen, useToggleWalletModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import * as styles from './BagFooter.css'
const Footer = styled.div<{ $showWarning: boolean }>`
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
color: ${({ theme }) => theme.textPrimary};
display: flex;
flex-direction: column;
padding: 12px 16px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
border-top-left-radius: ${({ $showWarning }) => ($showWarning ? '0' : '12')}px;
border-top-right-radius: ${({ $showWarning }) => ($showWarning ? '0' : '12')}px;
`
const WarningIcon = styled(AlertTriangle)`
width: 14px;
margin-right: 4px;
color: ${({ theme }) => theme.accentWarning};
`
const WarningText = styled(ThemedText.BodyPrimary)`
align-items: center;
color: ${({ theme }) => theme.accentWarning};
display: flex;
justify-content: center;
margin: 12px 0 !important;
text-align: center;
`
interface BagFooterProps {
balance: BigNumber
isConnected: boolean
sufficientBalance: boolean
totalEthPrice: BigNumber
@@ -29,7 +58,6 @@ const PENDING_BAG_STATUSES = [
]
export const BagFooter = ({
balance,
isConnected,
sufficientBalance,
totalEthPrice,
@@ -48,31 +76,32 @@ export const BagFooter = ({
return (
<Column className={styles.footerContainer}>
{showWarning && (
<Row className={styles.warningContainer}>
{!sufficientBalance
? `Insufficient funds (${formatWeiToDecimal(balance.toString())} ETH)`
: `Something went wrong. Please try again.`}
</Row>
)}
<Column
borderTopLeftRadius={showWarning ? '0' : '12'}
borderTopRightRadius={showWarning ? '0' : '12'}
className={styles.footer}
>
<Footer $showWarning={showWarning}>
<Column gap="4" paddingTop="8" paddingBottom="20">
<Row justifyContent="space-between">
<Box fontWeight="semibold" className={headlineSmall}>
Total
<Box>
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
</Box>
<Box fontWeight="semibold" className={headlineSmall}>
{`${formatWeiToDecimal(totalEthPrice.toString())} ETH`}
<Box>
<ThemedText.HeadlineSmall>
{formatWeiToDecimal(totalEthPrice.toString())}&nbsp;ETH
</ThemedText.HeadlineSmall>
</Box>
</Row>
<Row justifyContent="flex-end" color="textSecondary" className={bodySmall}>
{`${ethNumberStandardFormatter(totalUsdPrice, true)}`}
</Row>
</Column>
{showWarning && (
<WarningText fontSize="14px" lineHeight="20px">
<WarningIcon />
{!sufficientBalance ? (
<Trans>Insufficient funds</Trans>
) : (
<Trans>Something went wrong. Please try again.</Trans>
)}
</WarningText>
)}
<Row
as="button"
color="explicitWhite"
@@ -95,7 +124,7 @@ export const BagFooter = ({
? 'Transaction pending'
: 'Pay'}
</Row>
</Column>
</Footer>
</Column>
)
}

View File

@@ -1,9 +1,9 @@
import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css'
import { sprinkles, vars } from 'nft/css/sprinkles.css'
import { headlineSmall } from 'nft/css/common.css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const header = style([
subhead,
headlineSmall,
sprinkles({
color: 'textPrimary',
justifyContent: 'space-between',
@@ -12,16 +12,3 @@ export const header = style([
lineHeight: '24px',
},
])
export const clearAll = style([
sprinkles({
color: 'textTertiary',
cursor: 'pointer',
fontWeight: 'semibold',
}),
{
':hover': {
color: vars.color.blue400,
},
},
])

View File

@@ -2,9 +2,29 @@ import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { BagCloseIcon } from 'nft/components/icons'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import styled from 'styled-components/macro'
import { ButtonText, ThemedText } from 'theme'
import * as styles from './BagHeader.css'
const ClearButton = styled(ButtonText)`
color: ${({ theme }) => theme.textSecondary};
cursor: pointer;
font-weight: 600;
font-size: 14px;
line-height: 16px;
transition: 150ms ease color;
:hover {
color: ${({ theme }) => theme.accentActive};
}
`
const ControlRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
`
interface BagHeaderProps {
numberOfAssets: number
toggleBag: () => void
@@ -16,24 +36,16 @@ export const BagHeader = ({ numberOfAssets, toggleBag, resetFlow, isProfilePage
return (
<Column gap="4" paddingX="32" marginBottom="20">
<Row className={styles.header}>
{isProfilePage ? 'Sell NFTs' : 'My bag'}
<Box display="flex" padding="2" color="textSecondary" cursor="pointer" onClick={toggleBag}>
<ThemedText.HeadlineSmall>{isProfilePage ? 'Sell NFTs' : 'My bag'}</ThemedText.HeadlineSmall>
<Box display="flex" padding="2" color="textPrimary" cursor="pointer" onClick={toggleBag}>
<BagCloseIcon />
</Box>
</Row>
{numberOfAssets > 0 && (
<Box fontSize="14" fontWeight="normal" style={{ lineHeight: '20px' }} color="textPrimary">
{roundAndPluralize(numberOfAssets, 'NFT')} ·{' '}
<Box
as="span"
className={styles.clearAll}
onClick={() => {
resetFlow()
}}
>
Clear all
</Box>
</Box>
<ControlRow>
<ThemedText.BodyPrimary>{roundAndPluralize(numberOfAssets, 'NFT')}</ThemedText.BodyPrimary>
<ClearButton onClick={resetFlow}>Clear all</ClearButton>
</ControlRow>
)}
</Column>
)

View File

@@ -17,7 +17,7 @@ import { bodySmall } from 'nft/css/common.css'
import { loadingBlock } from 'nft/css/loading.css'
import { GenieAsset, UpdatedGenieAsset } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal, getAssetHref } from 'nft/utils'
import { MouseEvent, useEffect, useReducer, useRef, useState } from 'react'
import { MouseEvent, useCallback, useEffect, useReducer, useState } from 'react'
import { Link } from 'react-router-dom'
import * as styles from './BagRow.css'
@@ -46,25 +46,31 @@ const NoContentContainer = () => (
interface BagRowProps {
asset: UpdatedGenieAsset
usdPrice: number | undefined
removeAsset: (asset: GenieAsset) => void
removeAsset: (assets: GenieAsset[]) => void
showRemove?: boolean
grayscale?: boolean
isMobile: boolean
}
export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, isMobile }: BagRowProps) => {
const [cardHovered, setCardHovered] = useState(false)
const [loadedImage, setImageLoaded] = useState(false)
const [noImageAvailable, setNoImageAvailable] = useState(!asset.smallImageUrl)
const handleCardHover = () => setCardHovered(!cardHovered)
const assetCardRef = useRef<HTMLDivElement>(null)
const [cardHovered, setCardHovered] = useState(false)
const handleMouseEnter = useCallback(() => setCardHovered(true), [])
const handleMouseLeave = useCallback(() => setCardHovered(false), [])
const showRemoveButton = showRemove && cardHovered
if (cardHovered && assetCardRef.current && assetCardRef.current.matches(':hover') === false) setCardHovered(false)
const assetEthPrice = asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice
const assetEthPriceFormatted = formatWeiToDecimal(assetEthPrice)
const assetUSDPriceFormatted = ethNumberStandardFormatter(
usdPrice ? parseFloat(formatEther(assetEthPrice)) * usdPrice : usdPrice,
true
)
return (
<Link to={getAssetHref(asset)} style={{ textDecoration: 'none' }}>
<Row ref={assetCardRef} className={styles.bagRow} onMouseEnter={handleCardHover} onMouseLeave={handleCardHover}>
<Row className={styles.bagRow} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<Box position="relative" display="flex">
<Box
display={showRemove && isMobile ? 'block' : 'none'}
@@ -72,7 +78,7 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
onClick={(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
removeAsset(asset)
removeAsset([asset])
}}
transition="250"
zIndex="1"
@@ -113,29 +119,19 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
removeAsset(asset)
removeAsset([asset])
}}
>
Remove
</Box>
)}
{(!showRemoveButton || isMobile) && (
<Column flexShrink="0">
<Column flexShrink="0" alignItems="flex-end">
<Box className={styles.bagRowPrice}>
{`${formatWeiToDecimal(
asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice
)} ETH`}
</Box>
<Box className={styles.collectionName}>
{`${ethNumberStandardFormatter(
usdPrice
? parseFloat(
formatEther(asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice)
) * usdPrice
: usdPrice,
true
)}`}
{assetEthPriceFormatted}
&nbsp;ETH
</Box>
<Box className={styles.collectionName}>{assetUSDPriceFormatted}</Box>
</Column>
)}
</Row>

View File

@@ -0,0 +1,36 @@
import { Center, Column } from 'nft/components/Flex'
import { LargeBagIcon, LargeTagIcon } from 'nft/components/icons'
import { subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useLocation } from 'react-router-dom'
const EmptyState = () => {
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/profile')
return (
<Column gap={isProfilePage ? '16' : '12'} marginTop="36">
<Center>
{isProfilePage ? (
<LargeTagIcon color={themeVars.colors.textTertiary} />
) : (
<LargeBagIcon color={themeVars.colors.textTertiary} />
)}
</Center>
{isProfilePage ? (
<span className={subhead}>No NFTs Selected</span>
) : (
<Column gap="16">
<Center className={subhead} style={{ lineHeight: '24px' }}>
Your bag is empty
</Center>
<Center fontSize="12" fontWeight="normal" color="textSecondary" style={{ lineHeight: '16px' }}>
Selected NFTs will appear here
</Center>
</Column>
)}
</Column>
)
}
export default EmptyState

View File

@@ -44,7 +44,7 @@ export const MobileHoverBag = () => {
</Box>
<Column>
<Box className={body} fontWeight="semibold">
{roundAndPluralize(itemsInBag.length, 'item')}
{roundAndPluralize(itemsInBag.length, 'NFT')}
</Box>
<Row gap="8">
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box>

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bag.tsx matches base snapshot 1`] = `<DocumentFragment />`;

View File

@@ -0,0 +1,21 @@
import { render } from 'test-utils'
import Bag from './Bag'
jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
useWeb3React: () => ({
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
isActive: true,
}),
...web3React,
}
})
describe('Bag.tsx', () => {
it('matches base snapshot', () => {
const { asFragment } = render(<Bag />)
expect(asFragment()).toMatchSnapshot()
})
})

View File

@@ -100,8 +100,8 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName }: Ac
)
const itemsInBag = useBag((state) => state.itemsInBag)
const addAssetToBag = useBag((state) => state.addAssetToBag)
const removeAssetFromBag = useBag((state) => state.removeAssetFromBag)
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const cartExpanded = useBag((state) => state.bagExpanded)
const toggleCart = useBag((state) => state.toggleBag)
const isMobile = useIsMobile()
@@ -167,8 +167,8 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName }: Ac
<BuyCell
event={event}
collectionName={collectionName}
selectAsset={addAssetToBag}
removeAsset={removeAssetFromBag}
selectAsset={addAssetsToBag}
removeAsset={removeAssetsFromBag}
itemsInBag={itemsInBag}
cartExpanded={cartExpanded}
toggleCart={toggleCart}

View File

@@ -45,8 +45,8 @@ const formatListingStatus = (status: OrderStatus): string => {
interface BuyCellProps {
event: ActivityEvent
collectionName: string
selectAsset: (asset: GenieAsset) => void
removeAsset: (asset: GenieAsset) => void
selectAsset: (assets: GenieAsset[]) => void
removeAsset: (assets: GenieAsset[]) => void
itemsInBag: BagItem[]
cartExpanded: boolean
toggleCart: () => void
@@ -81,7 +81,7 @@ export const BuyCell = ({
className={event.orderStatus === OrderStatus.VALID && isSelected ? styles.removeCell : styles.buyCell}
onClick={(e: MouseEvent) => {
e.preventDefault()
isSelected ? removeAsset(asset) : selectAsset(asset)
isSelected ? removeAsset([asset]) : selectAsset([asset])
!isSelected && !cartExpanded && !isMobile && toggleCart()
}}
disabled={event.orderStatus !== OrderStatus.VALID}

View File

@@ -1,3 +1,5 @@
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { useIsCollectionLoading } from 'nft/hooks'
@@ -27,13 +29,19 @@ export const ActivitySwitcher = ({
>
Items
</Box>
<Box
as="button"
className={!showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
onClick={() => !showActivity && toggleActivity()}
<TraceEvent
events={[Event.onClick]}
element={ElementName.NFT_ACTIVITY_TAB}
name={EventName.NFT_ACTIVITY_SELECTED}
>
Activity
</Box>
<Box
as="button"
className={!showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
onClick={() => !showActivity && toggleActivity()}
>
Activity
</Box>
</TraceEvent>
</>
)}
</Row>

View File

@@ -572,19 +572,11 @@ const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps)
</MouseoverTooltip>
)
}
const SUSPICIOUS_TEXT = 'Blocked on OpenSea'
const Suspicious = () => {
return (
<MouseoverTooltip
text={
<Box className={bodySmall}>
Reported for suspicious activity
<br />
on Opensea
</Box>
}
placement="top"
>
<MouseoverTooltip text={<Box className={bodySmall}>{SUSPICIOUS_TEXT}</Box>} placement="top">
<Box display="flex" flexShrink="0" marginLeft="2">
<SuspiciousIcon20 width="20" height="20" />
</Box>
@@ -679,6 +671,7 @@ export {
SecondaryInfo,
SecondaryRow,
Suspicious,
SUSPICIOUS_TEXT,
TertiaryInfo,
Video,
}

View File

@@ -31,8 +31,8 @@ export const CollectionAsset = ({
setCurrentTokenPlayingMedia,
rarityVerified,
}: CollectionAssetProps) => {
const addAssetToBag = useBag((state) => state.addAssetToBag)
const removeAssetFromBag = useBag((state) => state.removeAssetFromBag)
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded)
const toggleBag = useBag((state) => state.toggleBag)
@@ -124,12 +124,12 @@ export const CollectionAsset = ({
selectedChildren={'Remove'}
onClick={(e: MouseEvent) => {
e.preventDefault()
addAssetToBag(asset)
addAssetsToBag([asset])
!bagExpanded && !isMobile && toggleBag()
}}
onSelectedClick={(e: MouseEvent) => {
e.preventDefault()
removeAssetFromBag(asset)
removeAssetsFromBag([asset])
}}
>
{'Buy now'}

View File

@@ -5,7 +5,6 @@ import { sprinkles } from '../../css/sprinkles.css'
export const assetList = style([
sprinkles({
display: 'grid',
marginTop: '24',
gap: { sm: '8', md: '12', lg: '20' },
}),
{

View File

@@ -1,4 +1,8 @@
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import clsx from 'clsx'
import { loadingAnimation } from 'components/Loader/styled'
import useDebounce from 'hooks/useDebounce'
import { AnimatedBox, Box } from 'nft/components/Box'
import { CollectionSearch, FilterButton } from 'nft/components/collection'
@@ -6,20 +10,22 @@ import { CollectionAsset } from 'nft/components/collection/CollectionAsset'
import * as styles from 'nft/components/collection/CollectionNfts.css'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Center, Row } from 'nft/components/Flex'
import { NonRarityIcon, RarityIcon } from 'nft/components/icons'
import { NonRarityIcon, RarityIcon, SweepIcon } from 'nft/components/icons'
import { bodySmall, buttonTextMedium, headlineMedium } from 'nft/css/common.css'
import { vars } from 'nft/css/sprinkles.css'
import {
CollectionFilters,
initialCollectionFilterState,
SortBy,
useBag,
useCollectionFilters,
useFiltersExpanded,
useIsMobile,
} from 'nft/hooks'
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
import { usePriceRange } from 'nft/hooks/usePriceRange'
import { AssetsFetcher } from 'nft/queries'
import { DropDownOption, GenieCollection, UniformHeight, UniformHeights } from 'nft/types'
import { DropDownOption, GenieCollection, TokenType, UniformHeight, UniformHeights } from 'nft/types'
import { getRarityStatus } from 'nft/utils/asset'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { scrollToTop } from 'nft/utils/scrollToTop'
@@ -28,10 +34,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useInfiniteQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { CollectionAssetLoading } from './CollectionAssetLoading'
import { marketPlaceItems } from './MarketplaceSelect'
import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip'
interface CollectionNftsProps {
@@ -42,6 +50,12 @@ interface CollectionNftsProps {
const rarityStatusCache = new Map<string, boolean>()
const ActionsContainer = styled.div`
display: flex;
margin-top: 12px;
justify-content: space-between;
`
const ClearAllButton = styled.button`
color: ${({ theme }) => theme.textTertiary};
padding-left: 8px;
@@ -53,7 +67,47 @@ const ClearAllButton = styled.button`
background: none;
`
const SweepButton = styled.div<{ toggled: boolean; disabled?: boolean }>`
display: flex;
gap: 8px;
border: none;
border-radius: 12px;
padding: 10px 18px 10px 12px;
cursor: ${({ disabled }) => (disabled ? 'auto' : 'pointer')};
color: ${({ toggled, disabled, theme }) => (toggled && !disabled ? theme.white : theme.textPrimary)};
background: ${({ theme, toggled, disabled }) =>
!disabled && toggled
? 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)'
: theme.backgroundInteractive};
opacity: ${({ disabled }) => (disabled ? 0.4 : 1)};
:hover {
background-color: ${({ theme }) => theme.hoverState};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} background-color ${timing.in}`};
}
`
export const LoadingButton = styled.div`
border-radius: 12px;
height: 44px;
width: 114px;
animation: ${loadingAnimation} 1.5s infinite;
animation-fill-mode: both;
background: linear-gradient(
to left,
${({ theme }) => theme.backgroundInteractive} 25%,
${({ theme }) => theme.backgroundOutline} 50%,
${({ theme }) => theme.backgroundInteractive} 75%
);
will-change: background-position;
background-size: 400%;
`
export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => {
const { chainId } = useWeb3React()
const traits = useCollectionFilters((state) => state.traits)
const minPrice = useCollectionFilters((state) => state.minPrice)
const maxPrice = useCollectionFilters((state) => state.maxPrice)
@@ -63,6 +117,13 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const setMarketCount = useCollectionFilters((state) => state.setMarketCount)
const setSortBy = useCollectionFilters((state) => state.setSortBy)
const buyNow = useCollectionFilters((state) => state.buyNow)
const setPriceRangeLow = usePriceRange((state) => state.setPriceRangeLow)
const priceRangeLow = usePriceRange((state) => state.priceRangeLow)
const priceRangeHigh = usePriceRange((state) => state.priceRangeHigh)
const setPriceRangeHigh = usePriceRange((state) => state.setPriceRangeHigh)
const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax)
const setIsCollectionNftsLoading = useIsCollectionLoading((state) => state.setIsCollectionNftsLoading)
const removeTrait = useCollectionFilters((state) => state.removeTrait)
const removeMarket = useCollectionFilters((state) => state.removeMarket)
@@ -70,10 +131,17 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const setMin = useCollectionFilters((state) => state.setMinPrice)
const setMax = useCollectionFilters((state) => state.setMaxPrice)
const toggleBag = useBag((state) => state.toggleBag)
const bagExpanded = useBag((state) => state.bagExpanded)
const theme = useTheme()
const debouncedMinPrice = useDebounce(minPrice, 500)
const debouncedMaxPrice = useDebounce(maxPrice, 500)
const debouncedSearchByNameText = useDebounce(searchByNameText, 500)
const [sweepIsOpen, setSweepOpen] = useState(false)
const {
data: collectionAssets,
isSuccess: AssetsFetchSuccess,
@@ -205,6 +273,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
useEffect(() => {
setUniformHeight(UniformHeights.unset)
setSweepOpen(false)
return () => {
useCollectionFilters.setState(initialCollectionFilterState)
}
@@ -228,10 +297,11 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
)
const hasNfts = collectionNfts && collectionNfts.length > 0
const hasErc1155s = hasNfts && collectionNfts[0] && collectionNfts[0].tokenType === TokenType.ERC1155
const minMaxPriceChipText: string | undefined = useMemo(() => {
if (debouncedMinPrice && debouncedMaxPrice) {
return `Price: ${debouncedMinPrice}-${debouncedMaxPrice} ETH`
return `Price: ${debouncedMinPrice} - ${debouncedMaxPrice} ETH`
} else if (debouncedMinPrice) {
return `Min. Price: ${debouncedMinPrice} ETH`
} else if (debouncedMaxPrice) {
@@ -269,26 +339,86 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}
}, [collectionStats, location])
useEffect(() => {
if (collectionStats && collectionStats.floorPrice) {
const lowValue = collectionStats.floorPrice
const maxValue = 10 * collectionStats.floorPrice
if (priceRangeLow === '') {
setPriceRangeLow(lowValue?.toFixed(2))
}
if (priceRangeHigh === '') {
setPriceRangeHigh(maxValue.toFixed(2))
}
}
}, [collectionStats, priceRangeLow, priceRangeHigh, setPriceRangeHigh, setPriceRangeLow])
return (
<>
<AnimatedBox position="sticky" top="72" width="full" zIndex="3">
<Box
backgroundColor="backgroundFloating"
width="full"
paddingBottom="8"
style={{ backdropFilter: 'blur(24px)' }}
>
<Row marginTop="12" gap="12">
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
collectionCount={collectionNfts?.[0]?.totalCount ?? 0}
/>
<SortDropdown dropDownOptions={sortDropDownOptions} />
<CollectionSearch />
</Row>
<Row paddingTop="12" gap="8" flexWrap="wrap">
<AnimatedBox position="sticky" top="72" width="full" zIndex="3" marginBottom="20">
<Box backgroundColor="backgroundFloating" width="full" style={{ backdropFilter: 'blur(24px)' }}>
<ActionsContainer>
<Row gap="12">
<TraceEvent
events={[Event.onClick]}
element={ElementName.NFT_FILTER_BUTTON}
name={EventName.NFT_FILTER_OPENED}
shouldLogImpression={!isFiltersExpanded}
properties={{ collection_address: contractAddress, chain_id: chainId }}
>
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
collectionCount={collectionNfts?.[0]?.totalCount ?? 0}
/>
</TraceEvent>
<SortDropdown dropDownOptions={sortDropDownOptions} />
<CollectionSearch />
</Row>
{!hasErc1155s ? (
isLoading ? (
<LoadingButton />
) : (
<SweepButton
toggled={sweepIsOpen}
disabled={!buyNow}
onClick={() => {
if (!buyNow || hasErc1155s) return
if (!sweepIsOpen) {
scrollToTop()
if (!bagExpanded && !isMobile) toggleBag()
}
setSweepOpen(!sweepIsOpen)
}}
>
<SweepIcon width="24px" height="24px" />
<ThemedText.BodyPrimary
fontWeight={600}
color={sweepIsOpen && buyNow ? theme.white : theme.textPrimary}
lineHeight="20px"
marginTop="2px"
marginBottom="2px"
>
Sweep
</ThemedText.BodyPrimary>
</SweepButton>
)
) : null}
</ActionsContainer>
<Sweep
contractAddress={contractAddress}
collectionStats={collectionStats}
minPrice={debouncedMinPrice}
maxPrice={debouncedMaxPrice}
showSweep={sweepIsOpen && buyNow && !hasErc1155s}
/>
<Row
paddingTop={!!markets.length || !!traits.length || minMaxPriceChipText ? '12' : '0'}
gap="8"
flexWrap="wrap"
>
{markets.map((market) => (
<TraitChip
key={market}
@@ -320,13 +450,15 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
scrollToTop()
setMin('')
setMax('')
setPrevMinMax([0, 100])
}}
/>
)}
{traits.length || markets.length > 0 || minMaxPriceChipText ? (
{!!traits.length || !!markets.length || minMaxPriceChipText ? (
<ClearAllButton
onClick={() => {
reset()
setPrevMinMax([0, 100])
scrollToTop()
}}
>

View File

@@ -14,14 +14,15 @@ export const CollectionSearch = () => {
<Box
as="input"
borderColor={{ default: 'backgroundOutline', focus: 'genieBlue' }}
borderWidth="1px"
borderWidth="1.5px"
borderStyle="solid"
borderRadius="12"
padding="12"
backgroundColor="backgroundSurface"
width="332"
fontSize="16"
height="44"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
color={{ placeholder: 'textTertiary', default: 'textPrimary' }}
value={searchByNameText}
placeholder={iscollectionStatsLoading ? '' : 'Search by name'}
className={clsx(iscollectionStatsLoading && styles.filterButtonLoading)}

View File

@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css'
import { body, bodySmall } from 'nft/css/common.css'
import { bodySmall, buttonTextSmall, headlineSmall } from 'nft/css/common.css'
import { loadingAsset, loadingBlock } from 'nft/css/loading.css'
import { breakpoints, sprinkles } from '../../css/sprinkles.css'
@@ -85,11 +85,11 @@ export const readMore = style([
verticalAlign: 'top',
lineHeight: '20px',
},
buttonTextSmall,
sprinkles({
color: 'blue400',
color: 'textSecondary',
cursor: 'pointer',
marginLeft: '4',
fontSize: '14',
}),
])
@@ -106,7 +106,7 @@ export const statsLabel = style([
])
export const statsValue = style([
body,
headlineSmall,
sprinkles({
fontWeight: 'medium',
}),

View File

@@ -232,7 +232,7 @@ const CollectionDescription = ({ description }: { description: string }) => {
/>
</Box>
<Box as="span" display={showReadMore ? 'inline' : 'none'} className={styles.readMore} onClick={toggleReadMore}>
Show {readMore ? 'less' : 'more'}
show {readMore ? 'less' : 'more'}
</Box>
</Box>
)
@@ -277,7 +277,7 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
)
return (
<Row gap={{ sm: '20', md: '60' }} {...props}>
<Row gap={{ sm: '36', md: '60' }} {...props}>
{isCollectionStatsLoading && statsLoadingSkeleton}
{stats.floorPrice ? (
<StatsItem label="Global floor" isMobile={isMobile ?? false}>

View File

@@ -5,7 +5,7 @@ export const container = style([
sprinkles({
overflow: 'auto',
height: 'viewHeight',
paddingTop: '24',
paddingTop: '4',
}),
{
width: '300px',
@@ -26,57 +26,81 @@ export const container = style([
])
export const rowHover = style([
sprinkles({
borderRadius: '12',
}),
{
':hover': {
background: themeVars.colors.backgroundSurface,
background: themeVars.colors.backgroundInteractive,
borderRadius: 12,
},
},
])
export const rowHoverOpen = style([
export const row = style([
sprinkles({
borderTopLeftRadius: '12',
borderTopRightRadius: '12',
borderBottomLeftRadius: '0',
borderBottomRightRadius: '0',
display: 'flex',
paddingRight: '16',
cursor: 'pointer',
fontSize: '16',
lineHeight: '20',
justifyContent: 'space-between',
alignItems: 'center',
paddingLeft: '12',
paddingTop: '10',
paddingBottom: '10',
}),
{
':hover': {
background: themeVars.colors.backgroundOutline,
},
},
])
export const subRowHover = style({
':hover': {
background: themeVars.colors.backgroundOutline,
background: themeVars.colors.backgroundInteractive,
},
})
export const detailsOpen = sprinkles({
background: 'backgroundModule',
overflow: 'hidden',
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'backgroundOutline',
export const borderTop = sprinkles({
borderTopStyle: 'solid',
borderTopColor: 'backgroundOutline',
borderTopWidth: '1px',
})
export const summaryOpen = sprinkles({
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'backgroundOutline',
export const borderBottom = sprinkles({
borderBottomStyle: 'solid',
borderBottomColor: 'backgroundOutline',
borderBottomWidth: '1px',
})
export const detailsOpen = style([
borderTop,
sprinkles({
overflow: 'hidden',
marginTop: '8',
marginBottom: '8',
}),
])
export const filterDropDowns = style([
borderBottom,
sprinkles({
overflowY: 'scroll',
}),
{
maxHeight: '190px',
maxHeight: '302px',
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])
export const chevronIcon = style({
marginLeft: -1,
})
export const chevronContainer = style([
sprinkles({
color: 'textSecondary',
display: 'inline-block',
height: '28',
width: '28',
transition: '250',
}),
{
marginRight: -1,
},
])

View File

@@ -3,77 +3,41 @@ import * as styles from 'nft/components/collection/Filters.css'
import { MarketplaceSelect } from 'nft/components/collection/MarketplaceSelect'
import { PriceRange } from 'nft/components/collection/PriceRange'
import { Column, Row } from 'nft/components/Flex'
import { Radio } from 'nft/components/layout/Radio'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { subhead } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks'
import { Trait } from 'nft/hooks/useCollectionFilters'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { groupBy } from 'nft/utils/groupBy'
import { FocusEventHandler, FormEvent, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useReducer } from 'react'
import { Input } from '../layout/Input'
import { TraitSelect } from './TraitSelect'
export const Filters = ({
traits,
traitsByAmount,
}: {
traits: Trait[]
traitsByAmount: {
traitCount: number
numWithTrait: number
}[]
}) => {
export const Filters = ({ traits }: { traits: Trait[] }) => {
const { buyNow, setBuyNow } = useCollectionFilters((state) => ({
buyNow: state.buyNow,
setBuyNow: state.setBuyNow,
}))
const traitsByGroup: Record<string, Trait[]> = useMemo(() => {
if (traits) {
let groupedTraits = groupBy(traits, 'trait_type')
groupedTraits['Number of traits'] = []
for (let i = 0; i < traitsByAmount.length; i++) {
groupedTraits['Number of traits'].push({
trait_type: 'Number of traits',
trait_value: traitsByAmount[i].traitCount,
trait_count: traitsByAmount[i].numWithTrait,
})
}
groupedTraits = Object.assign({ 'Number of traits': null }, groupedTraits)
return groupedTraits
} else return {}
}, [traits, traitsByAmount])
const traitsByGroup: Record<string, Trait[]> = useMemo(() => (traits ? groupBy(traits, 'trait_type') : {}), [traits])
const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const [search, setSearch] = useState('')
const handleBuyNowToggle = () => {
setBuyNow(!buyNow)
}
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = ''
}
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = 'Search traits'
}
return (
<Box className={styles.container}>
<Row width="full" justifyContent="space-between">
<Row as="span" fontSize="20" color="textPrimary">
Filters
</Row>
</Row>
<Column paddingTop="8">
<Row width="full" justifyContent="space-between"></Row>
<Column marginTop="8">
<Row
justifyContent="space-between"
className={styles.rowHover}
className={`${styles.row} ${styles.rowHover}`}
gap="2"
borderRadius="12"
paddingTop="12"
paddingRight="16"
paddingBottom="12"
paddingLeft="12"
cursor="pointer"
onClick={(e) => {
e.preventDefault()
handleBuyNowToggle()
@@ -81,41 +45,30 @@ export const Filters = ({
onMouseEnter={toggleBuyNowHover}
onMouseLeave={toggleBuyNowHover}
>
<Box fontSize="14" fontWeight="medium" as="summary">
Buy now
</Box>
<Radio hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle} />
<Box className={subhead}>Buy now</Box>
<Checkbox hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle}>
<span />
</Checkbox>
</Row>
<MarketplaceSelect />
<Box marginTop="12" marginBottom="12">
<Box as="span" fontSize="20">
Price
</Box>
<PriceRange />
</Box>
<Box marginTop="12">
<Box as="span" fontSize="20">
Traits
</Box>
<PriceRange />
{Object.entries(traitsByGroup).length > 0 && (
<Box
as="span"
color="textSecondary"
paddingLeft="8"
marginTop="12"
marginBottom="12"
className={styles.borderTop}
></Box>
)}
<Column marginTop="12" marginBottom="60" gap={{ sm: '4' }}>
<Input
display={!traits?.length ? 'none' : undefined}
value={search}
onChange={(e: FormEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
width="full"
marginBottom="8"
placeholder="Search traits"
autoComplete="off"
onFocus={handleFocus}
onBlur={handleBlur}
style={{ border: '2px solid rgba(153, 161, 189, 0.24)', maxWidth: '300px' }}
/>
{Object.entries(traitsByGroup).map(([type, traits]) => (
<TraitSelect key={type} {...{ type, traits, search }} />
))}
</Column>
</Box>
<Column>
{Object.entries(traitsByGroup).map(([type, traits], index) => (
// the index is offset by two because price range and marketplace appear prior to it
<TraitSelect key={type} {...{ type, traits }} index={index + TraitPosition.TRAIT_START_INDEX} />
))}
</Column>
</Column>
</Box>
)

View File

@@ -1,3 +1,5 @@
import { sendAnalyticsEvent } from 'analytics'
import { EventName, FilterTypes } from 'analytics/constants'
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
@@ -5,6 +7,8 @@ import { Column, Row } from 'nft/components/Flex'
import { ChevronUpIcon } from 'nft/components/icons'
import { subheadSmall } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { FormEvent, useEffect, useReducer, useState } from 'react'
import { Checkbox } from '../layout/Checkbox'
@@ -46,6 +50,7 @@ const MarketplaceItem = ({
removeMarket(value)
setCheckboxSelected(false)
}
sendAnalyticsEvent(EventName.NFT_FILTER_SELECTED, { filter_type: FilterTypes.MARKETPLACE })
}
return (
@@ -58,9 +63,11 @@ const MarketplaceItem = ({
fontWeight="normal"
className={`${subheadSmall} ${styles.subRowHover}`}
paddingLeft="12"
paddingRight="12"
paddingRight="16"
borderRadius="12"
cursor="pointer"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
maxHeight="44"
style={{ paddingBottom: '22px', paddingTop: '22px' }}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
onClick={handleCheckbox}
@@ -91,55 +98,60 @@ export const MarketplaceSelect = () => {
}))
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
return (
<Box
as="details"
className={clsx(subheadSmall, !isOpen && styles.rowHover, isOpen && styles.detailsOpen)}
borderRadius="12"
open={isOpen}
>
<>
<Box className={styles.detailsOpen} opacity={isOpen ? '1' : '0'} />
<Box
as="summary"
className={clsx(isOpen && styles.summaryOpen, isOpen ? styles.rowHoverOpen : styles.rowHover)}
display="flex"
justifyContent="space-between"
cursor="pointer"
alignItems="center"
fontSize="14"
paddingTop="12"
paddingLeft="12"
paddingRight="12"
paddingBottom={isOpen ? '8' : '12'}
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
as="details"
className={clsx(subheadSmall, !isOpen && styles.rowHover)}
open={isOpen}
borderRadius={isOpen ? '0' : '12'}
>
Marketplaces
<Box
color="textSecondary"
transition="250"
height="28"
width="28"
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
as="summary"
className={`${styles.row} ${styles.rowHover}`}
display="flex"
justifyContent="space-between"
alignItems="center"
fontSize="16"
paddingTop="12"
paddingLeft="12"
paddingBottom="12"
lineHeight="20"
borderRadius="12"
maxHeight="48"
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
setTraitsOpen(TraitPosition.MARKPLACE_INDEX, !isOpen)
}}
>
<ChevronUpIcon />
Marketplaces
<Box display="flex" alignItems="center">
<Box
className={styles.chevronContainer}
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon className={styles.chevronIcon} />
</Box>
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingBottom="8" paddingLeft="0">
{Object.entries(marketPlaceItems).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
))}
</Column>
</Box>
<Column className={styles.filterDropDowns} paddingLeft="0">
{Object.entries(marketPlaceItems).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
))}
</Column>
</Box>
</>
)
}

View File

@@ -0,0 +1,14 @@
import { style } from '@vanilla-extract/css'
import { body } from 'nft/css/common.css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const priceInput = style([
body,
sprinkles({
backgroundColor: 'transparent',
padding: '12',
borderRadius: '12',
borderStyle: 'solid',
borderWidth: '1.5px',
}),
])

View File

@@ -1,11 +1,28 @@
import 'rc-slider/assets/index.css'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, FilterTypes } from 'analytics/constants'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { NumericInput } from 'nft/components/layout/Input'
import { body } from 'nft/css/common.css'
import { useIsMobile } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { usePriceRange } from 'nft/hooks/usePriceRange'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { scrollToTop } from 'nft/utils/scrollToTop'
import { useEffect, useState } from 'react'
import { FocusEventHandler, FormEvent } from 'react'
import { default as Slider } from 'rc-slider'
import { FormEvent, useEffect, useState } from 'react'
import { FocusEventHandler } from 'react'
import { useLocation } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import * as styles from './PriceRange.css'
import { TraitsHeader } from './TraitsHeader'
const StyledSlider = styled(Slider)`
cursor: pointer;
`
export const PriceRange = () => {
const [placeholderText, setPlaceholderText] = useState('')
@@ -13,14 +30,23 @@ export const PriceRange = () => {
const setMaxPrice = useCollectionFilters((state) => state.setMaxPrice)
const minPrice = useCollectionFilters((state) => state.minPrice)
const maxPrice = useCollectionFilters((state) => state.maxPrice)
const isMobile = useIsMobile()
const priceRangeLow = usePriceRange((state) => state.priceRangeLow)
const priceRangeHigh = usePriceRange((state) => state.priceRangeHigh)
const setPriceRangeLow = usePriceRange((statae) => statae.setPriceRangeLow)
const setPriceRangeHigh = usePriceRange((statae) => statae.setPriceRangeHigh)
const prevMinMax = usePriceRange((state) => state.prevMinMax)
const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax)
const theme = useTheme()
const isMobile = useIsMobile()
const location = useLocation()
useEffect(() => {
setMinPrice('')
setMaxPrice('')
}, [location.pathname, setMinPrice, setMaxPrice])
setPriceRangeLow('')
setPriceRangeHigh('')
}, [location.pathname, setMinPrice, setMaxPrice, setPriceRangeLow, setPriceRangeHigh])
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
setPlaceholderText(e.currentTarget.placeholder)
@@ -30,55 +56,152 @@ export const PriceRange = () => {
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = placeholderText
setPlaceholderText('')
if (minPrice || maxPrice)
sendAnalyticsEvent(EventName.NFT_FILTER_SELECTED, { filter_type: FilterTypes.PRICE_RANGE })
}
const updateMinPriceRange = (v: FormEvent<HTMLInputElement>) => {
const [, prevMax] = prevMinMax
// if there is actually a number, update the slider place
if (v.currentTarget.value) {
// we are calculating the new slider position here
const diff = parseInt(v.currentTarget.value) - parseInt(priceRangeLow)
const newLow = Math.floor(100 * (diff / (parseInt(priceRangeHigh) - parseInt(priceRangeLow))))
// if the slider min value is larger than or equal to the max, we don't want it to move past the max
// so we put the sliders on top of each other
// if it is less than, we can move it
if (parseInt(v.currentTarget.value) >= parseInt(maxPrice)) {
setPrevMinMax([prevMax, prevMax])
} else {
setPrevMinMax([newLow, prevMax])
}
} else {
// if there is no number, reset the slider position
setPrevMinMax([0, prevMax])
}
// set min price for price range querying
setMinPrice(v.currentTarget.value)
scrollToTop()
}
const updateMaxPriceRange = (v: FormEvent<HTMLInputElement>) => {
const [prevMin] = prevMinMax
if (v.currentTarget.value) {
const range = parseInt(priceRangeHigh) - parseInt(v.currentTarget.value)
const newMax = Math.floor(100 - 100 * (range / (parseInt(priceRangeHigh) - parseInt(priceRangeLow))))
if (parseInt(v.currentTarget.value) <= parseInt(minPrice)) {
setPrevMinMax([prevMin, prevMin])
} else {
setPrevMinMax([prevMin, newMax])
}
} else {
setPrevMinMax([prevMin, 100])
}
setMaxPrice(v.currentTarget.value)
scrollToTop()
}
const handleSliderLogic = (minMax: number | Array<number>) => {
if (typeof minMax === 'number') return
const [newMin, newMax] = minMax
// strip commas so parseFloat can parse properly
const priceRangeHighNumber = parseFloat(priceRangeHigh.replace(/,/g, ''))
const priceRangeLowNumber = parseFloat(priceRangeLow.replace(/,/g, ''))
const diff = priceRangeHighNumber - priceRangeLowNumber
// minprice
const minChange = newMin / 100
const newMinPrice = minChange * diff + priceRangeLowNumber
// max price
const maxChange = (100 - newMax) / 100
const newMaxPrice = priceRangeHighNumber - maxChange * diff
setMinPrice(newMinPrice.toFixed(2))
setMaxPrice(newMaxPrice.toFixed(2))
// set back to placeholder when they move back to end of range
if (newMin === 0) {
setMinPrice('')
}
if (newMax === 100) {
setMaxPrice('')
}
// update the previous minMax for future checks
setPrevMinMax(minMax)
}
return (
<Row gap="12" marginTop="12" color="textPrimary">
<Row position="relative" style={{ flex: 1 }}>
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
<TraitsHeader title="Price range" index={TraitPosition.PRICE_RANGE_INDEX}>
<Row gap="12" marginTop="12" color="textPrimary">
<Row position="relative">
<NumericInput
style={{
width: isMobile ? '100%' : '126px',
}}
className={styles.priceInput}
placeholder={priceRangeLow}
onChange={updateMinPriceRange}
onFocus={handleFocus}
value={minPrice}
onBlur={handleBlur}
/>
</Row>
<Box className={body}>to</Box>
<Row position="relative" flex="1">
<NumericInput
style={{
width: isMobile ? '100%' : '126px',
}}
className={styles.priceInput}
placeholder={priceRangeHigh}
value={maxPrice}
onChange={updateMaxPriceRange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Row>
</Row>
<Row marginTop="24" marginBottom="12" paddingLeft="8" paddingRight="8">
<StyledSlider
defaultValue={[0, 100]}
min={0}
max={100}
range
step={0.0001}
value={prevMinMax}
trackStyle={{
top: '3px',
height: '8px',
background: `${theme.accentAction}`,
}}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
backgroundColor="transparent"
placeholder="Min"
defaultValue={minPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMinPrice(v.currentTarget.value)
handleStyle={{
top: '3px',
width: '12px',
height: '20px',
backgroundColor: `${theme.textPrimary}`,
borderRadius: '4px',
border: 'none',
boxShadow: `${theme.shallowShadow.slice(0, -1)}`,
}}
onFocus={handleFocus}
value={minPrice}
onBlur={handleBlur}
railStyle={{
top: '3px',
height: '8px',
backgroundColor: `${theme.accentActionSoft}`,
}}
onChange={handleSliderLogic}
/>
</Row>
<Row position="relative" style={{ flex: 1 }}>
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
}}
borderColor={{ default: 'backgroundOutline', focus: 'textSecondary' }}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
backgroundColor="transparent"
placeholder="Max"
defaultValue={maxPrice}
value={maxPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMaxPrice(v.currentTarget.value)
}}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Row>
</Row>
</TraitsHeader>
)
}

View File

@@ -0,0 +1,475 @@
import 'rc-slider/assets/index.css'
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther, parseEther } from '@ethersproject/units'
import { useBag, useCollectionFilters } from 'nft/hooks'
import { fetchSweep } from 'nft/queries'
import { GenieAsset, GenieCollection, Markets } from 'nft/types'
import { calcPoolPrice, formatWeiToDecimal } from 'nft/utils'
import { default as Slider } from 'rc-slider'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { useQuery } from 'react-query'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
const SweepContainer = styled.div<{ showSweep: boolean }>`
display: ${({ showSweep }) => (showSweep ? 'flex' : 'none')};
gap: 60px;
margin-top: 20px;
padding: 16px;
border-radius: 12px;
background-color: ${({ theme }) => theme.backgroundModule};
justify-content: space-between;
`
const StyledSlider = styled(Slider)`
cursor: pointer;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
display: none;
}
`
const SweepLeftmostContainer = styled.div`
display: flex;
width: 100%;
gap: 24px;
`
const SweepRightmostContainer = styled.div`
display: flex;
flex-shrink: 0;
justify-content: flex-end;
align-items: center;
gap: 8px;
min-width: 160px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
display: none;
}
`
const SweepHeaderContainer = styled.div`
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
display: none;
}
`
const SweepSubContainer = styled.div`
display: flex;
width: 100%;
gap: 12px;
align-items: center;
`
const InputContainer = styled.input`
width: 96px;
color: ${({ theme }) => theme.textPrimary};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
background: none;
border-radius: 8px;
padding: 6px 8px;
font-size: 14px;
font-weight: 400px;
line-height: 20px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
width: 100%;
}
:hover,
:focus {
outline: none;
border: 1px solid ${({ theme }) => theme.accentAction};
}
`
const ToggleContainer = styled.div`
display: flex;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
background: none;
border-radius: 12px;
padding: 4px;
cursor: pointer;
`
const ToggleSwitch = styled.div<{ active: boolean }>`
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
padding: 4px 8px;
border-radius: 8px;
background-color: ${({ theme, active }) => (active ? theme.backgroundInteractive : `none`)};
font-size: 14px;
font-weight: 600;
line-height: 16px;
`
const NftDisplayContainer = styled.div`
position: relative;
width: 34px;
height: 34px;
`
const NftHolder = styled.div<{ index: number; src: string | undefined }>`
position: absolute;
top: 50%;
left: 50%;
width: 26px;
height: 26px;
border-radius: 4px;
background: ${({ theme, src }) => (src ? `url(${src})` : theme.textTertiary)};
background-size: 26px;
opacity: ${({ src, index }) => (src ? 1.0 : index === 0 ? 0.9 : index === 1 ? 0.6 : 0.3)};
transform: ${({ index }) =>
index === 0
? 'translate(-50%, -50%) rotate(-4.42deg)'
: index === 1
? 'translate(-50%, -50%) rotate(-14.01deg)'
: 'translate(-50%, -50%) rotate(10.24deg)'};
z-index: ${({ index }) => 3 - index};
`
const wholeNumberRegex = RegExp(`^(0|[1-9][0-9]*)$`)
const twoDecimalPlacesRegex = RegExp(`^\\d*\\.?\\d{0,2}$`)
interface NftDisplayProps {
nfts: GenieAsset[]
}
export const NftDisplay = ({ nfts }: NftDisplayProps) => {
return (
<NftDisplayContainer>
{[...Array(3)].map((_, index) => {
return (
<NftHolder
key={index}
index={index}
src={nfts.length - 1 >= index ? nfts[nfts.length - 1 - index].smallImageUrl : undefined}
/>
)
})}
</NftDisplayContainer>
)
}
interface SweepProps {
contractAddress: string
collectionStats: GenieCollection
minPrice: string
maxPrice: string
showSweep: boolean
}
export const Sweep = ({ contractAddress, collectionStats, minPrice, maxPrice, showSweep }: SweepProps) => {
const theme = useTheme()
const [isItemsToggled, toggleSweep] = useReducer((state) => !state, true)
const [sweepAmount, setSweepAmount] = useState<string>('')
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const itemsInBag = useBag((state) => state.itemsInBag)
const lockSweepItems = useBag((state) => state.lockSweepItems)
const traits = useCollectionFilters((state) => state.traits)
const markets = useCollectionFilters((state) => state.markets)
const getSweepFetcherParams = (market: Markets.NFTX | Markets.NFT20 | 'others') => {
const isMarketFiltered = !!markets.length
const allOtherMarkets = [Markets.Opensea, Markets.X2Y2, Markets.LooksRare]
if (isMarketFiltered) {
if (market === 'others') {
return { contractAddress, traits, markets }
}
if (!markets.includes(market)) return { contractAddress: '', traits: [], markets: [] }
}
switch (market) {
case Markets.NFTX:
case Markets.NFT20:
return {
contractAddress,
traits,
markets: [market],
price: {
low: minPrice,
high: maxPrice,
symbol: 'ETH',
},
}
case 'others':
return {
contractAddress,
traits,
markets: allOtherMarkets,
price: {
low: minPrice,
high: maxPrice,
symbol: 'ETH',
},
}
}
}
const { data: collectionAssets, isFetched: isCollectionAssetsFetched } = useQuery(
['sweepAssets', getSweepFetcherParams('others')],
() => fetchSweep(getSweepFetcherParams('others')),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const { data: nftxCollectionAssets, isFetched: isNftxCollectionAssetsFetched } = useQuery(
['nftxSweepAssets', collectionStats, getSweepFetcherParams(Markets.NFTX)],
() =>
collectionStats.marketplaceCount?.some(
(marketStat) => marketStat.marketplace === Markets.NFTX && marketStat.count > 0
)
? fetchSweep(getSweepFetcherParams(Markets.NFTX))
: [],
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const { data: nft20CollectionAssets, isFetched: isNft20CollectionAssetsFetched } = useQuery(
['nft20SweepAssets', getSweepFetcherParams(Markets.NFT20)],
() =>
collectionStats.marketplaceCount?.some(
(marketStat) => marketStat.marketplace === Markets.NFT20 && marketStat.count > 0
)
? fetchSweep(getSweepFetcherParams(Markets.NFT20))
: [],
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const allAssetsFetched = isCollectionAssetsFetched && isNftxCollectionAssetsFetched && isNft20CollectionAssetsFetched
const { sortedAssets, sortedAssetsTotalEth } = useMemo(() => {
if (!allAssetsFetched || !collectionAssets || !nftxCollectionAssets || !nft20CollectionAssets)
return { sortedAssets: undefined, sortedAssetsTotalEth: BigNumber.from(0) }
let counterNFTX = 0
let counterNFT20 = 0
let jointCollections = [...nftxCollectionAssets, ...nft20CollectionAssets]
jointCollections.forEach((asset) => {
if (!asset.openseaSusFlag) {
const isNFTX = asset.marketplace === Markets.NFTX
asset.currentEthPrice = calcPoolPrice(asset, isNFTX ? counterNFTX : counterNFT20)
BigNumber.from(asset.currentEthPrice).gte(0) && (isNFTX ? counterNFTX++ : counterNFT20++)
}
})
jointCollections = collectionAssets.concat(jointCollections)
jointCollections.sort((a, b) => {
return BigNumber.from(a.currentEthPrice).gt(BigNumber.from(b.currentEthPrice)) ? 1 : -1
})
let validAssets = jointCollections.filter(
(asset) => BigNumber.from(asset.currentEthPrice).gte(0) && !asset.openseaSusFlag
)
validAssets = validAssets.slice(
0,
Math.max(collectionAssets.length, nftxCollectionAssets.length, nft20CollectionAssets.length)
)
return {
sortedAssets: validAssets,
sortedAssetsTotalEth: validAssets.reduce(
(total, asset) => total.add(BigNumber.from(asset.priceInfo.ETHPrice)),
BigNumber.from(0)
),
}
}, [collectionAssets, nftxCollectionAssets, nft20CollectionAssets, allAssetsFetched])
const { sweepItemsInBag, sweepEthPrice } = useMemo(() => {
const sweepItemsInBag = itemsInBag
.filter((item) => item.inSweep && item.asset.address === contractAddress)
.map((item) => item.asset)
const sweepEthPrice = sweepItemsInBag.reduce(
(total, asset) => total.add(BigNumber.from(asset.priceInfo.ETHPrice)),
BigNumber.from(0)
)
return { sweepItemsInBag, sweepEthPrice }
}, [itemsInBag, contractAddress])
useEffect(() => {
if (sweepItemsInBag.length === 0) setSweepAmount('')
}, [sweepItemsInBag])
useEffect(() => {
lockSweepItems(contractAddress)
}, [contractAddress, traits, markets, minPrice, maxPrice, lockSweepItems])
const clearSweep = () => {
setSweepAmount('')
removeAssetsFromBag(sweepItemsInBag)
}
const handleSweep = (value: number) => {
if (sortedAssets) {
if (isItemsToggled) {
if (sweepItemsInBag.length < value) {
addAssetsToBag(sortedAssets.slice(sweepItemsInBag.length, value), true)
} else {
removeAssetsFromBag(sweepItemsInBag.slice(value, sweepItemsInBag.length))
}
setSweepAmount(value < 1 ? '' : value.toString())
} else {
const wishValueInWei = parseEther(value.toString())
if (sweepEthPrice.lte(wishValueInWei)) {
let curIndex = sweepItemsInBag.length
let curTotal = sweepEthPrice
const wishAssets: GenieAsset[] = []
while (
curIndex < sortedAssets.length &&
curTotal.add(BigNumber.from(sortedAssets[curIndex].priceInfo.ETHPrice)).lte(wishValueInWei)
) {
wishAssets.push(sortedAssets[curIndex])
curTotal = curTotal.add(BigNumber.from(sortedAssets[curIndex].priceInfo.ETHPrice))
curIndex++
}
if (wishAssets.length > 0) {
addAssetsToBag(wishAssets, true)
}
} else {
let curIndex = sweepItemsInBag.length - 1
let curTotal = sweepEthPrice
const wishAssets: GenieAsset[] = []
while (curIndex >= 0 && curTotal.gt(wishValueInWei)) {
wishAssets.push(sweepItemsInBag[curIndex])
curTotal = curTotal.sub(BigNumber.from(sweepItemsInBag[curIndex].priceInfo.ETHPrice))
curIndex--
}
if (wishAssets.length > 0) {
removeAssetsFromBag(wishAssets)
}
}
setSweepAmount(value === 0 ? '' : value.toFixed(2))
}
}
}
const handleSliderChange = (value: number | number[]) => {
if (typeof value === 'number') {
if (sortedAssets) {
if (isItemsToggled) {
if (Math.floor(value) !== Math.floor(sweepAmount !== '' ? parseFloat(sweepAmount) : 0))
handleSweep(Math.floor(value))
setSweepAmount(value < 1 ? '' : value.toString())
} else {
handleSweep(value)
setSweepAmount(value === 0 ? '' : value.toFixed(2))
}
}
}
}
const handleInput = (value: string) => {
if (value === '') {
handleSweep(0)
setSweepAmount('')
} else if (isItemsToggled && wholeNumberRegex.test(value)) {
handleSweep(parseFloat(value))
setSweepAmount(value)
} else if (!isItemsToggled && twoDecimalPlacesRegex.test(value)) {
handleSweep(parseFloat(value))
setSweepAmount(value)
}
}
const handleToggleSweep = () => {
clearSweep()
toggleSweep()
}
return (
<SweepContainer showSweep={showSweep}>
<SweepLeftmostContainer>
<SweepHeaderContainer>
<ThemedText.SubHeaderSmall color="textPrimary" lineHeight="20px" paddingTop="6px" paddingBottom="6px">
Sweep
</ThemedText.SubHeaderSmall>
</SweepHeaderContainer>
<SweepSubContainer>
<StyledSlider
defaultValue={0}
min={0}
max={isItemsToggled ? sortedAssets?.length ?? 0 : parseFloat(formatEther(sortedAssetsTotalEth).toString())}
value={isItemsToggled ? sweepItemsInBag.length : parseFloat(formatWeiToDecimal(sweepEthPrice.toString()))}
step={isItemsToggled ? 1 : 0.01}
trackStyle={{
top: '3px',
height: '8px',
background: `radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)`,
}}
handleStyle={{
top: '3px',
width: '12px',
height: '20px',
backgroundColor: `${theme.textPrimary}`,
borderRadius: '4px',
border: 'none',
boxShadow: `${theme.shallowShadow.slice(0, -1)}`,
}}
railStyle={{
top: '3px',
height: '8px',
backgroundColor: `${theme.accentActionSoft}`,
}}
onChange={handleSliderChange}
/>
<InputContainer
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// text-specific options
type="text"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
minLength={1}
maxLength={79}
spellCheck="false"
value={
isItemsToggled ? (sweepAmount !== '' ? Math.floor(parseFloat(sweepAmount)) : sweepAmount) : sweepAmount
}
onChange={(event) => {
handleInput(event.target.value.replace(/,/g, '.'))
}}
/>
<ToggleContainer onClick={handleToggleSweep}>
<ToggleSwitch active={isItemsToggled}>Items</ToggleSwitch>
<ToggleSwitch active={!isItemsToggled}>ETH</ToggleSwitch>
</ToggleContainer>
</SweepSubContainer>
</SweepLeftmostContainer>
<SweepRightmostContainer>
<ThemedText.SubHeader font-size="14px">{`${formatWeiToDecimal(
sweepEthPrice.toString()
)} ETH`}</ThemedText.SubHeader>
<NftDisplay nfts={sweepItemsInBag} />
</SweepRightmostContainer>
</SweepContainer>
)
}

View File

@@ -5,7 +5,7 @@ const TraitChipWrap = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 4px 8px 12px;
padding: 2px 4px 2px 12px;
font-weight: 600;
border-radius: 12px;
background-color: ${({ theme }) => theme.backgroundInteractive};
@@ -24,7 +24,6 @@ export const TraitChip = ({ onClick, value }: { value: string; onClick: () => vo
return (
<TraitChipWrap>
<span>{value}</span>
<CrossIconWrap onClick={onClick}>
<CrossIcon cursor="pointer" />
</CrossIconWrap>

View File

@@ -1,16 +1,18 @@
import clsx from 'clsx'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, FilterTypes } from 'analytics/constants'
import useDebounce from 'hooks/useDebounce'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ChevronUpIcon } from 'nft/components/icons'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { subheadSmall } from 'nft/css/common.css'
import { Trait, useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { scrollToTop } from 'nft/utils/scrollToTop'
import { FormEvent, MouseEvent, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { FormEvent, MouseEvent, useEffect, useMemo, useState } from 'react'
import { Input } from '../layout/Input'
import * as styles from './Filters.css'
import { TraitsHeader } from './TraitsHeader'
const TraitItem = ({
trait,
@@ -53,6 +55,7 @@ const TraitItem = ({
removeTrait(trait)
setCheckboxSelected(false)
}
sendAnalyticsEvent(EventName.NFT_FILTER_SELECTED, { filter_type: FilterTypes.TRAIT })
}
const showFullTraitName = shouldShow && trait_type === trait.trait_type && trait_value === trait.trait_value
@@ -68,8 +71,13 @@ const TraitItem = ({
justifyContent="space-between"
cursor="pointer"
paddingLeft="12"
paddingRight="12"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
paddingRight="16"
borderRadius="12"
style={{
paddingBottom: '22px',
paddingTop: '22px',
}}
maxHeight="44"
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={handleCheckbox}
@@ -79,6 +87,7 @@ const TraitItem = ({
whiteSpace="nowrap"
textOverflow="ellipsis"
overflow="hidden"
style={{ minHeight: 15 }}
maxWidth={!showFullTraitName ? '160' : 'full'}
onMouseOver={(e) => isEllipsisActive(e)}
onMouseLeave={() => toggleShowFullTraitName({ shouldShow: false, trait_type: '', trait_value: '' })}
@@ -88,7 +97,7 @@ const TraitItem = ({
: trait.trait_value}
</Box>
<Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}>
<Box as="span" color="textSecondary" minWidth={'8'} paddingTop={'2'} paddingRight={'12'} position={'relative'}>
<Box as="span" color="textTertiary" minWidth="8" paddingTop="2" paddingRight="12" position="relative">
{!showFullTraitName && trait.trait_count}
</Box>
</Checkbox>
@@ -96,83 +105,32 @@ const TraitItem = ({
)
}
export const TraitSelect = ({ traits, type, search }: { traits: Trait[]; type: string; search: string }) => {
const debouncedSearch = useDebounce(search, 300)
export const TraitSelect = ({ traits, type, index }: { traits: Trait[]; type: string; index: number }) => {
const addTrait = useCollectionFilters((state) => state.addTrait)
const removeTrait = useCollectionFilters((state) => state.removeTrait)
const selectedTraits = useCollectionFilters((state) => state.traits)
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
const [isOpen, setOpen] = useState(
traits.some(({ trait_type, trait_value }) => {
return selectedTraits.some((selectedTrait) => {
return selectedTrait.trait_type === trait_type && selectedTrait.trait_value === String(trait_value)
})
})
const searchedTraits = useMemo(
() => traits.filter((t) => t.trait_value.toString().toLowerCase().includes(debouncedSearch.toLowerCase())),
[debouncedSearch, traits]
)
const { isTypeIncluded, searchedTraits } = useMemo(() => {
const isTypeIncluded = type.includes(debouncedSearch)
const searchedTraits = traits.filter(
(t) => isTypeIncluded || t.trait_value.toString().toLowerCase().includes(debouncedSearch.toLowerCase())
)
return { searchedTraits, isTypeIncluded }
}, [debouncedSearch, traits, type])
useLayoutEffect(() => {
if (debouncedSearch && searchedTraits.length) {
setOpen(true)
return () => {
setOpen(false)
}
}
return
}, [searchedTraits, debouncedSearch, setOpen])
return searchedTraits.length || isTypeIncluded ? (
<Box
as="details"
className={clsx(subheadSmall, !isOpen && styles.rowHover, isOpen && styles.detailsOpen)}
borderRadius="12"
open={isOpen}
>
<Box
as="summary"
className={clsx(isOpen && styles.summaryOpen, isOpen ? styles.rowHoverOpen : styles.rowHover)}
display="flex"
paddingTop="8"
paddingRight="12"
paddingBottom="8"
paddingLeft="12"
justifyContent="space-between"
cursor="pointer"
alignItems="center"
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
>
{type}
<Box display="flex" alignItems="center">
<Box color="textSecondary" display="inline-block" marginRight="12">
{searchedTraits.length}
</Box>
<Box
color="textSecondary"
display="inline-block"
transition="250"
height="28"
width="28"
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon />
</Box>
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingLeft="0">
{searchedTraits.map((trait) => {
return traits.length ? (
<TraitsHeader index={index} numTraits={traits.length} title={type}>
<Input
value={search}
onChange={(e: FormEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
placeholder="Search"
marginTop="8"
marginBottom="8"
autoComplete="off"
position="static"
width="full"
/>
<Column className={styles.filterDropDowns} paddingLeft="0" paddingBottom="8">
{searchedTraits.map((trait, index) => {
const isTraitSelected = selectedTraits.find(
({ trait_type, trait_value }) =>
trait_type === trait.trait_type && String(trait_value) === String(trait.trait_value)
@@ -187,6 +145,6 @@ export const TraitSelect = ({ traits, type, search }: { traits: Trait[]; type: s
)
})}
</Column>
</Box>
</TraitsHeader>
) : null
}

View File

@@ -0,0 +1,70 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
import { ChevronUpIcon } from 'nft/components/icons'
import { subheadSmall } from 'nft/css/common.css'
import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { ReactNode, useEffect, useState } from 'react'
interface TraitsHeaderProps {
title: string
children: ReactNode
numTraits?: number
index?: number
}
export const TraitsHeader = (props: TraitsHeaderProps) => {
const { children, index, title } = props
const [isOpen, setOpen] = useState(false)
const traitsOpen = useTraitsOpen((state) => state.traitsOpen)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const prevTraitIsOpen = index !== undefined ? traitsOpen[index - 1] : false
const showBorderTop = index !== TraitPosition.TRAIT_START_INDEX
useEffect(() => {
if (index !== undefined) {
setTraitsOpen(index, isOpen)
}
}, [isOpen, index, setTraitsOpen])
return (
<>
{showBorderTop && (
<Box
className={clsx(subheadSmall, !isOpen && styles.rowHover, styles.detailsOpen)}
opacity={!prevTraitIsOpen && isOpen && index !== 0 ? '1' : '0'}
marginTop={prevTraitIsOpen ? '0' : '8'}
/>
)}
<Box as="details" className={clsx(subheadSmall, !isOpen && styles.rowHover)} open={isOpen}>
<Box
as="summary"
className={`${styles.row} ${styles.rowHover}`}
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
>
{title}
<Box display="flex" alignItems="center">
<Box color="textTertiary" display="inline-block" marginRight="12">
{props.numTraits}
</Box>
<Box
className={styles.chevronContainer}
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon className={styles.chevronIcon} />
</Box>
</Box>
</Box>
{children}
</Box>
</>
)
}

View File

@@ -6,6 +6,7 @@ import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/ic
import { buttonTextMedium } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useIsCollectionLoading } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks'
import { DropDownOption } from 'nft/types'
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
@@ -26,13 +27,18 @@ export const SortDropdown = ({
top?: number
left?: number
}) => {
const sortBy = useCollectionFilters((state) => state.sortBy)
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const [isReversed, toggleReversed] = useReducer((s) => !s, false)
const [selectedIndex, setSelectedIndex] = useState(0)
const [selectedIndex, setSelectedIndex] = useState(sortBy)
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
const [maxWidth, setMaxWidth] = useState(0)
useEffect(() => {
setSelectedIndex(sortBy)
}, [sortBy])
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, () => isOpen && toggleOpen())
@@ -127,7 +133,7 @@ export const SortDropdown = ({
</Box>
<Box
position="absolute"
zIndex="2"
zIndex="3"
width={inFilters ? 'auto' : 'inherit'}
right={inFilters ? '16' : 'auto'}
paddingBottom="8"

View File

@@ -27,6 +27,7 @@ import ReactMarkdown from 'react-markdown'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useSpring } from 'react-spring'
import { SUSPICIOUS_TEXT } from '../collection/Card'
import * as styles from './AssetDetails.css'
const AudioPlayer = ({
@@ -112,8 +113,8 @@ interface AssetDetailsProps {
export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
const { pathname, search } = useLocation()
const navigate = useNavigate()
const addAssetToBag = useBag((state) => state.addAssetToBag)
const removeAssetFromBag = useBag((state) => state.removeAssetFromBag)
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded)
const [creatorAddress, setCreatorAddress] = useState('')
@@ -271,7 +272,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
<Row as="h1" marginTop="0" marginBottom="12" gap="2" className={headlineMedium}>
{asset.openseaSusFlag && (
<Box marginTop="8">
<MouseoverTooltip text={<Box fontWeight="normal">Reported for suspicious activity on OpenSea</Box>}>
<MouseoverTooltip text={<Box fontWeight="normal">{SUSPICIOUS_TEXT}</Box>}>
<SuspiciousIcon height="30" width="30" viewBox="0 0 16 17" />
</MouseoverTooltip>
</Box>
@@ -387,8 +388,8 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
boxShadow={{ hover: 'elevation' }}
onClick={() => {
if (isSelected) {
removeAssetFromBag(asset)
} else addAssetToBag(asset)
removeAssetsFromBag([asset])
} else addAssetsToBag([asset])
setSelected((x) => !x)
}}
>

View File

@@ -1,7 +1,7 @@
import { useWeb3React } from '@web3-react/core'
import { CancelListingIcon } from 'nft/components/icons'
import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons'
import { useBag } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { CollectionInfoForAsset, GenieAsset, TokenType } from 'nft/types'
import { ethNumberStandardFormatter, formatEthPrice, getMarketplaceIcon, timeLeft } from 'nft/utils'
import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
@@ -58,6 +58,39 @@ const BuyNowButton = styled.div<{ assetInBag: boolean; margin: boolean; useAccen
cursor: pointer;
`
const Erc1155BuyNowButton = styled.div`
display: flex;
flex-direction: row;
width: 100%;
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
border-radius: 12px;
margin-top: 12px;
text-align: center;
cursor: pointer;
justify-content: space-between;
overflow-x: hidden;
`
const Erc1155BuyNowText = styled.div`
display: flex;
width: 100%;
padding: 10px 12px;
justify-content: center;
cursor: default;
`
const Erc1155ChangeButton = styled(Erc1155BuyNowText)<{ remove: boolean }>`
background-color: ${({ theme, remove }) => (remove ? theme.accentFailureSoft : theme.accentActionSoft)};
color: ${({ theme, remove }) => (remove ? theme.accentFailure : theme.accentAction)};
cursor: pointer;
:hover {
background-color: ${({ theme, remove }) => (remove ? theme.accentFailure : theme.accentAction)};
color: ${({ theme }) => theme.textPrimary};
}
`
const NotForSaleContainer = styled.div`
display: flex;
flex-direction: column;
@@ -154,12 +187,20 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined
const expirationDate = cheapestOrder ? new Date(cheapestOrder.orderClosingDate) : undefined
const itemsInBag = useBag((s) => s.itemsInBag)
const addAssetToBag = useBag((s) => s.addAssetToBag)
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
const addAssetsToBag = useBag((s) => s.addAssetsToBag)
const removeAssetsFromBag = useBag((s) => s.removeAssetsFromBag)
const isErc1555 = asset.tokenType === TokenType.ERC1155
const assetInBag = useMemo(() => {
return itemsInBag.some((item) => item.asset.tokenId === asset.tokenId && item.asset.address === asset.address)
}, [itemsInBag, asset])
const { quantity, assetInBag } = useMemo(() => {
return {
quantity: itemsInBag.filter(
(x) => x.asset.tokenType === 'ERC1155' && x.asset.tokenId === asset.tokenId && x.asset.address === asset.address
).length,
assetInBag: itemsInBag.some(
(item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
),
}
}, [asset, itemsInBag])
const isOwner =
asset.owner && typeof asset.owner === 'string' ? account?.toLowerCase() === asset.owner.toLowerCase() : false
@@ -189,14 +230,28 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
{expirationDate && (
<ThemedText.BodySecondary fontSize={'14px'}>Sale ends: {timeLeft(expirationDate)}</ThemedText.BodySecondary>
)}
<BuyNowButton
assetInBag={assetInBag}
margin={true}
useAccentColor={true}
onClick={() => (assetInBag ? removeAssetFromBag(asset) : addAssetToBag(asset))}
>
<ThemedText.SubHeader lineHeight={'20px'}>{assetInBag ? 'Remove' : 'Buy Now'}</ThemedText.SubHeader>
</BuyNowButton>
{!isErc1555 || !assetInBag ? (
<BuyNowButton
assetInBag={assetInBag}
margin={true}
useAccentColor={true}
onClick={() => (assetInBag ? removeAssetsFromBag([asset]) : addAssetsToBag([asset]))}
>
<ThemedText.SubHeader lineHeight={'20px'}>{assetInBag ? 'Remove' : 'Buy Now'}</ThemedText.SubHeader>
</BuyNowButton>
) : (
<Erc1155BuyNowButton>
<Erc1155ChangeButton remove={true} onClick={() => removeAssetsFromBag([asset])}>
<MinusIcon width="20px" height="20px" />
</Erc1155ChangeButton>
<Erc1155BuyNowText>
<ThemedText.SubHeader lineHeight={'20px'}>{quantity}</ThemedText.SubHeader>
</Erc1155BuyNowText>
<Erc1155ChangeButton remove={false} onClick={() => addAssetsToBag([asset])}>
<PlusIcon width="20px" height="20px" />
</Erc1155ChangeButton>
</Erc1155BuyNowButton>
)}
</BestPriceContainer>
) : (
<NotForSale collection={collection} />

View File

@@ -398,29 +398,16 @@ export const DownloadIcon = (props: SVGProps) => (
)
export const SweepIcon = (props: SVGProps) => (
<svg {...props} width="23" height="23" viewBox="0 0 23 23" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M22.5957 4.81261C22.8834 5.10528 23.0178 5.41067 22.9989 5.72877C22.98 6.04687 22.8004 6.354 22.46 6.65017L15.1344 13.1029L13.2046 11.1637L19.6611 3.84484C19.9593 3.5078 20.2663 3.32781 20.5824 3.30487C20.8984 3.28194 21.2018 3.41655 21.4925 3.70872L22.5957 4.81261Z"
fill="white"
d="M13.4177 11.9534C12.3508 11.6675 11.2541 12.3006 10.9682 13.3676C9.90129 13.0817 8.80461 13.7148 8.51873 14.7818L8.25991 15.7477M13.4177 11.9534C14.4846 12.2392 15.1178 13.3359 14.8319 14.4028C15.8989 14.6887 16.532 15.7855 16.2461 16.8524L15.9873 17.8183M13.4177 11.9534L16.0059 2.2941M8.25991 15.7477L15.9873 17.8183M8.25991 15.7477C8.25991 15.7477 7.74227 17.6796 7.48345 18.6455C7.22463 19.6114 5.99989 20.3185 5.99989 20.3185C9.86359 21.3538 12.3131 19.9396 12.3131 19.9396L11.7954 21.8714C13.4053 22.3028 14.9197 21.8027 15.2109 20.716L15.9873 17.8183"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.5252 14.1402C14.7538 14.3706 14.8683 14.6161 14.8688 14.8769C14.8693 15.1376 14.7582 15.3795 14.5356 15.6023L14.2375 15.8955C14.0118 16.1224 13.7723 16.2338 13.5189 16.2298C13.2655 16.2258 13.0227 16.1091 12.7907 15.8798L10.2564 13.3317C10.0238 13.0984 9.90678 12.8538 9.90529 12.5981C9.9038 12.3423 10.0156 12.1012 10.2407 11.8749L10.5277 11.5817C10.7498 11.3588 10.9901 11.2466 11.2485 11.2451C11.5069 11.2436 11.7523 11.3593 11.9849 11.5922L14.5252 14.1402Z"
fill="white"
/>
<path
d="M6.54973 15.645C6.5695 15.6425 6.58747 15.6323 6.5997 15.6165C6.61193 15.6007 6.61742 15.5807 6.61495 15.5609C6.61248 15.541 6.60225 15.523 6.58653 15.5107C6.5708 15.4985 6.55086 15.493 6.53109 15.4954C6.35891 15.4954 5.1402 15.4954 4.15406 15.3773C3.30371 15.2745 2.46362 15.0994 1.64286 14.8538C1.60087 14.8372 1.56568 14.8069 1.54298 14.7677C1.25974 14.0415 1.17029 13.5225 1.02122 12.8547C0.982456 12.7215 0.978729 12.6011 1.08159 12.4949C1.18446 12.3887 1.31713 12.3925 1.45056 12.4343C3.00916 12.9197 4.42613 13.2428 6.05106 13.4171C7.49338 13.5719 8.92899 13.5614 10.3467 13.2084C10.4137 13.1821 10.4835 13.1636 10.5547 13.1531L13.0487 15.3922C13.0487 15.5126 13.0487 15.5111 13.0487 15.6301C13.0398 16.6272 12.9814 17.6244 12.8736 18.6216C12.8009 19.326 12.6853 20.0254 12.5277 20.7157C12.3943 21.2878 12.2296 21.928 11.9448 22.4478C11.7823 22.7425 11.5334 22.8479 11.2061 22.7814C10.6657 22.7118 10.0299 22.6273 9.66767 22.4471C10.2275 21.6655 10.6866 20.4988 10.9684 19.5266C11.0618 19.1816 11.1379 18.8321 11.1965 18.4795C11.2023 18.4604 11.2003 18.4397 11.1909 18.422C11.1816 18.4043 11.1656 18.3911 11.1465 18.3853C11.1274 18.3794 11.1068 18.3814 11.0892 18.3908C11.0716 18.4002 11.0584 18.4162 11.0526 18.4354C10.8469 18.9051 10.5308 19.5841 10.1976 20.1727C9.8488 20.7845 8.56674 22.1075 8.33418 21.9811C7.17659 21.501 6.26499 20.967 5.36755 20.1929C5.35488 20.1824 6.56836 19.5198 7.50084 18.9215C7.75278 18.7592 9.05496 17.6636 9.28976 17.3293C9.29671 17.3222 9.30221 17.3139 9.30595 17.3047C9.3097 17.2956 9.3116 17.2858 9.31157 17.2759C9.31153 17.266 9.30956 17.2562 9.30575 17.2471C9.30194 17.2379 9.29638 17.2296 9.28939 17.2227C9.28239 17.2157 9.27409 17.2102 9.26496 17.2064C9.25583 17.2027 9.24606 17.2008 9.2362 17.2008C9.22633 17.2008 9.21658 17.2028 9.20748 17.2066C9.19838 17.2105 9.19012 17.216 9.18317 17.2231L9.10863 17.2777C8.55034 17.674 7.2802 18.2858 6.14648 18.656C5.59735 18.8275 5.03372 18.9482 4.46265 19.0165C4.15183 19.0344 4.14288 19.0255 3.90659 18.7846C3.1141 17.9679 2.48015 17.0103 2.03717 15.9606C2.02152 15.9374 3.37886 16.0294 4.58862 15.9606C5.25025 15.913 5.90645 15.8074 6.54973 15.645Z"
fill="currentColor"
/>
<path
d="M8.03215 3.20189C7.96945 2.92219 7.91547 2.70033 7.87341 2.53869C7.83915 2.39971 7.77253 2.27076 7.67895 2.16232C7.57558 2.06162 7.4479 1.98929 7.30829 1.95235C7.1432 1.90085 6.91144 1.84538 6.61856 1.78596C6.53919 1.76773 6.49633 1.72574 6.49633 1.65918C6.49528 1.64336 6.49773 1.62748 6.50349 1.6127C6.50925 1.59792 6.51818 1.58457 6.52966 1.5736C6.55489 1.55175 6.58564 1.53723 6.61856 1.53162C6.85148 1.48841 7.08176 1.43207 7.30829 1.36285C7.44771 1.32418 7.57517 1.25116 7.67895 1.15049C7.77253 1.04206 7.83915 0.913108 7.87341 0.774127C7.91944 0.611431 7.97236 0.391955 8.03215 0.11569C8.04723 0.0364546 8.08771 0 8.15438 0C8.22105 0 8.26312 0.0388317 8.28217 0.11569C8.3409 0.391955 8.39381 0.611431 8.44091 0.774127C8.47622 0.913188 8.54363 1.04208 8.63774 1.15049C8.74183 1.25117 8.86954 1.32418 9.0092 1.36285C9.23565 1.43236 9.46594 1.48871 9.69893 1.53162C9.73114 1.53346 9.76134 1.54787 9.78302 1.57173C9.80469 1.59559 9.81611 1.627 9.81481 1.65918C9.81481 1.72574 9.77592 1.76773 9.69893 1.78596C9.46636 1.8298 9.23616 1.88534 9.0092 1.95235C8.86936 1.9893 8.74142 2.06162 8.63774 2.16232C8.54363 2.27074 8.47622 2.39963 8.44091 2.53869C8.39249 2.69716 8.33931 2.92219 8.28217 3.20189C8.26312 3.28112 8.22105 3.31836 8.15438 3.31836C8.08771 3.31836 8.04564 3.27954 8.03215 3.20189Z"
fill="currentColor"
/>
<path
d="M12.3409 5.87055C12.2751 5.52982 12.2134 5.25823 12.1576 5.05662C12.1161 4.88332 12.0332 4.72267 11.9161 4.58843C11.7866 4.46353 11.6275 4.37354 11.4538 4.32685C11.1741 4.24532 10.8898 4.18022 10.6025 4.1319C10.5017 4.1169 10.4509 4.06442 10.4509 3.97528C10.4498 3.955 10.4529 3.93472 10.4599 3.91566C10.4669 3.89661 10.4777 3.8792 10.4917 3.86448C10.5223 3.83612 10.5611 3.81806 10.6025 3.81283C10.8896 3.76432 11.1738 3.70007 11.4538 3.62038C11.6283 3.57572 11.7879 3.48545 11.9161 3.3588C12.0329 3.22336 12.1157 3.06199 12.1576 2.8881C12.2134 2.6865 12.2751 2.41491 12.3409 2.07334C12.3412 2.05339 12.3455 2.03371 12.3536 2.01549C12.3618 1.99727 12.3735 1.98089 12.3881 1.96734C12.4028 1.95378 12.42 1.94335 12.4388 1.93665C12.4576 1.92996 12.4776 1.92714 12.4975 1.92839C12.5808 1.92839 12.6349 1.9767 12.6549 2.07334C12.721 2.41491 12.7821 2.6865 12.8381 2.8881C12.8793 3.0616 12.9609 3.22289 13.0764 3.3588C13.204 3.4861 13.3638 3.57648 13.5386 3.62038C13.8198 3.69992 14.1051 3.76416 14.3933 3.81283C14.4144 3.81297 14.4352 3.8175 14.4545 3.82613C14.4738 3.83476 14.4911 3.84732 14.5053 3.86298C14.5194 3.87865 14.5302 3.89709 14.5369 3.91714C14.5436 3.93719 14.546 3.95841 14.544 3.97945C14.544 4.06859 14.494 4.12107 14.3933 4.13607C14.1049 4.18455 13.8195 4.24963 13.5386 4.33101C13.3646 4.37691 13.2053 4.46702 13.0764 4.5926C12.9606 4.72734 12.8789 4.88791 12.8381 5.06079C12.7826 5.2624 12.7215 5.53371 12.6549 5.87472C12.6349 5.97219 12.5833 6.02051 12.4975 6.02051C12.4772 6.02165 12.4568 6.0186 12.4378 6.01156C12.4187 6.00452 12.4013 5.99363 12.3866 5.97957C12.3719 5.9655 12.3603 5.94856 12.3524 5.92981C12.3445 5.91105 12.3406 5.89088 12.3409 5.87055Z"
fill="currentColor"
/>
<path
d="M6.68457 11.7993C6.65283 11.8001 6.62123 11.7946 6.59165 11.783C6.56206 11.7715 6.53508 11.7541 6.51227 11.732C6.46495 11.6861 6.43286 11.6267 6.42032 11.5619C6.33379 11.0466 6.24828 10.6157 6.1638 10.2694C6.09886 9.97221 5.99165 9.68585 5.84546 9.41909C5.72369 9.205 5.5492 9.02565 5.3386 8.89806C5.08255 8.75332 4.80501 8.65049 4.51649 8.59348C4.17806 8.51773 3.75567 8.44583 3.24932 8.3778C3.18053 8.37018 3.11658 8.33874 3.06852 8.28891C3.02398 8.24144 2.99961 8.17853 3.00052 8.11343C2.99944 8.04831 3.02384 7.98533 3.06852 7.93795C3.11638 7.88786 3.18044 7.85636 3.24932 7.84905C3.75619 7.78412 4.17935 7.713 4.5188 7.63569C4.81087 7.57587 5.09204 7.47158 5.3525 7.32648C5.56664 7.20036 5.74549 7.02222 5.87251 6.80855C6.0212 6.54514 6.12856 6.26043 6.19084 5.9644C6.27171 5.61859 6.34898 5.18672 6.42264 4.66879C6.43207 4.60298 6.46337 4.54226 6.5115 4.49641C6.53457 4.47357 6.56197 4.45559 6.59209 4.4435C6.62221 4.43142 6.65444 4.42549 6.68689 4.42606C6.71949 4.4249 6.752 4.43026 6.78251 4.44181C6.81302 4.45335 6.84093 4.47086 6.8646 4.49331C6.9118 4.53934 6.94388 4.59867 6.95655 4.66338C7.04618 5.18132 7.13323 5.61318 7.21771 5.95899C7.28349 6.25595 7.39065 6.5422 7.53605 6.80933C7.65852 7.02267 7.83287 7.20162 8.04291 7.32958C8.29831 7.47496 8.57494 7.57931 8.86271 7.63879C9.19959 7.71609 9.6212 7.78721 10.1276 7.85215C10.1964 7.85945 10.2605 7.89095 10.3084 7.94105C10.3528 7.98851 10.3769 8.05149 10.3756 8.11652C10.3765 8.18217 10.3525 8.24572 10.3084 8.29432C10.2615 8.34542 10.1967 8.37643 10.1276 8.3809C9.7006 8.41009 9.27569 8.46403 8.85498 8.54246C8.56063 8.59403 8.27716 8.69518 8.01664 8.84162C7.79886 8.97348 7.61932 9.16002 7.49587 9.38275C7.34879 9.65617 7.24258 9.9497 7.18062 10.254C7.09924 10.6085 7.02454 11.0448 6.95655 11.5627C6.94854 11.6294 6.918 11.6913 6.87001 11.7382C6.8447 11.7602 6.81526 11.7769 6.78343 11.7874C6.75159 11.7978 6.71799 11.8019 6.68457 11.7993Z"
d="M5.18229 6.58808C5.25706 6.38601 5.54287 6.38601 5.61764 6.58808C5.99377 7.60457 6.79521 8.406 7.8117 8.78214C8.01377 8.85691 8.01377 9.14272 7.8117 9.21749C6.79521 9.59363 5.99377 10.3951 5.61764 11.4116C5.54286 11.6136 5.25706 11.6136 5.18229 11.4116C4.80615 10.3951 4.00471 9.59363 2.98822 9.21749C2.78615 9.14272 2.78615 8.85691 2.98822 8.78214C4.00471 8.406 4.80615 7.60457 5.18229 6.58808Z"
fill="currentColor"
/>
</svg>

View File

@@ -35,13 +35,13 @@ export const checkMark = sprinkles({
display: 'none',
height: '24',
width: '24',
color: 'blue400',
color: 'white',
})
export const checkMarkActive = style([
sprinkles({
display: 'inline-block',
color: 'blue400',
color: 'white',
position: 'absolute',
top: '0',
right: '1',

View File

@@ -24,8 +24,9 @@ export const Checkbox: React.FC<CheckboxProps> = ({ hovered, children, ...props
{children}
<Box
as="span"
borderColor={props.checked || hovered ? 'blue400' : 'grey400'}
borderColor={props.checked || hovered ? 'accentAction' : 'grey400'}
className={styles.checkbox}
background={props.checked ? 'accentAction' : undefined}
// This element is purely decorative so
// we hide it for screen readers
aria-hidden="true"

View File

@@ -28,6 +28,8 @@ export const NumericInput = forwardRef<HTMLInputElement, BoxProps>((props, ref)
as="input"
autoComplete="off"
type="text"
borderColor={{ default: 'backgroundOutline', focus: 'textSecondary' }}
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
onInput={(v: FormEvent<HTMLInputElement>) => {
if (v.currentTarget.value === '.') {
v.currentTarget.value = '0.'

View File

@@ -34,6 +34,7 @@ const themeContractValues = {
elevation: '',
tooltip: '',
deep: '',
shallow: '',
},
}
@@ -77,6 +78,7 @@ const dimensions = {
'276': '276px',
'288': '288px',
'292': '292px',
'332': '332px',
'386': '386px',
half: '50%',
full: '100%',
@@ -178,7 +180,7 @@ export const vars = createGlobalTheme(':root', {
grey300: '#99A1BD',
grey200: '#B7BED4',
grey100: '#DDE3F7',
grey50: '#EDEFF7',
grey50: '#F5F6FC',
accentTextLightTertiary: 'rgba(255, 255, 255, 0.12)',
outline: 'rgba(153, 161, 189, 0.24)',
lightGrayOverlay: '#99A1BD14',
@@ -383,6 +385,7 @@ const unresponsiveProperties = defineProperties({
cursor: ['default', 'pointer', 'auto'],
borderStyle,
borderBottomStyle: borderStyle,
borderTopStyle: borderStyle,
borderRadius: vars.radii,
borderTopLeftRadius: vars.radii,
borderTopRightRadius: vars.radii,
@@ -393,6 +396,7 @@ const unresponsiveProperties = defineProperties({
borderTop: vars.border,
borderWidth,
borderBottomWidth: borderWidth,
borderTopWidth: borderWidth,
fontFamily: vars.fonts,
overflow,
overflowX: overflow,

View File

@@ -1,5 +1,5 @@
import { BigNumber } from '@ethersproject/bignumber'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { BagItem, BagItemStatus, BagStatus, TokenType, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
@@ -13,9 +13,10 @@ interface BagState {
setTotalEthPrice: (totalEthPrice: BigNumber) => void
totalUsdPrice: number | undefined
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
addAssetToBag: (asset: UpdatedGenieAsset) => void
removeAssetFromBag: (asset: UpdatedGenieAsset) => void
addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void
removeAssetsFromBag: (assets: UpdatedGenieAsset[]) => void
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
lockSweepItems: (contractAddress: string) => void
didOpenUnavailableAssets: boolean
setDidOpenUnavailableAssets: (didOpen: boolean) => void
bagExpanded: boolean
@@ -76,34 +77,70 @@ export const useBag = create<BagState>()(
set(() => ({
totalUsdPrice,
})),
addAssetToBag: (asset) =>
addAssetsToBag: (assets, fromSweep = false) =>
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag }
const assetWithId = { asset: { id: uuidv4(), ...asset }, status: BagItemStatus.ADDED_TO_BAG }
const items: BagItem[] = []
const itemsInBagCopy = [...itemsInBag]
assets.forEach((asset) => {
let index = -1
if (asset.tokenType !== TokenType.ERC1155) {
index = itemsInBag.findIndex(
(n) => n.asset.tokenId === asset.tokenId && n.asset.address === asset.address
)
}
if (index !== -1) {
itemsInBagCopy[index].inSweep = fromSweep
} else {
const assetWithId = {
asset: { id: uuidv4(), ...asset },
status: BagItemStatus.ADDED_TO_BAG,
inSweep: fromSweep,
}
items.push(assetWithId)
}
})
if (itemsInBag.length === 0)
return {
itemsInBag: [assetWithId],
itemsInBag: items,
bagStatus: BagStatus.ADDING_TO_BAG,
}
else
return {
itemsInBag: [...itemsInBag, assetWithId],
itemsInBag: [...itemsInBagCopy, ...items],
bagStatus: BagStatus.ADDING_TO_BAG,
}
}),
removeAssetFromBag: (asset) => {
removeAssetsFromBag: (assets) => {
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag }
if (itemsInBag.length === 0) return { itemsInBag: [] }
const itemsCopy = [...itemsInBag]
const index = itemsCopy.findIndex((n) =>
asset.id ? n.asset.id === asset.id : n.asset.tokenId === asset.tokenId && n.asset.address === asset.address
const itemsCopy = itemsInBag.filter(
(item) =>
!assets.some((asset) =>
asset.id
? asset.id === item.asset.id
: asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
)
)
if (index === -1) return { itemsInBag: get().itemsInBag }
itemsCopy.splice(index, 1)
return { itemsInBag: itemsCopy }
})
},
lockSweepItems: (contractAddress) =>
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag }
const itemsInBagCopy = itemsInBag.map((item) =>
item.asset.address === contractAddress && item.inSweep ? { ...item, inSweep: false } : item
)
if (itemsInBag.length === 0)
return {
itemsInBag,
}
else
return {
itemsInBag: [...itemsInBagCopy],
}
}),
reset: () =>
set(() => {
if (!get().isLocked)

View File

@@ -0,0 +1,37 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface PriceRangeProps {
priceRangeLow: string
setPriceRangeLow: (priceRangeLow: string) => void
priceRangeHigh: string
setPriceRangeHigh: (priceRangeHigh: string) => void
prevMinMax: Array<number>
setPrevMinMax: (prevMinMax: Array<number>) => void
}
export const usePriceRange = create<PriceRangeProps>()(
devtools(
(set) => ({
priceRangeLow: '',
setPriceRangeLow: (priceRangeLow: string) => {
set(() => {
return { priceRangeLow }
})
},
priceRangeHigh: '',
setPriceRangeHigh: (priceRangeHigh: string) => {
set(() => {
return { priceRangeHigh }
})
},
prevMinMax: [0, 100],
setPrevMinMax: (prevMinMax: Array<number>) => {
set(() => {
return { prevMinMax }
})
},
}),
{ name: 'usePriceRange' }
)
)

View File

@@ -0,0 +1,29 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface traitOpen {
[key: number]: boolean
}
interface TraitsOpenState {
traitsOpen: traitOpen
setTraitsOpen: (index: number, isOpen: boolean) => void
}
export enum TraitPosition {
MARKPLACE_INDEX = 0,
PRICE_RANGE_INDEX = 1,
TRAIT_START_INDEX = 2,
}
export const useTraitsOpen = create<TraitsOpenState>()(
devtools(
(set) => ({
traitsOpen: {},
setTraitsOpen: (index, isOpen) => {
set(({ traitsOpen }) => ({ traitsOpen: { ...traitsOpen, [index]: isOpen } }))
},
}),
{ name: 'useTraitsOpen' }
)
)

Some files were not shown because too many files have changed in this diff Show More