Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
751ba3c62d | ||
|
|
83bc6db74e | ||
|
|
058aa52faf | ||
|
|
2e3950018a | ||
|
|
d86120a257 | ||
|
|
e2fea4a5fb | ||
|
|
1f810f84be | ||
|
|
f6ffc68ef7 | ||
|
|
b3c44f20d7 | ||
|
|
8a5045f635 | ||
|
|
c1607bbd52 | ||
|
|
dfbed6b89d | ||
|
|
c871e55d82 | ||
|
|
f15ac091b4 | ||
|
|
6037d74cfb | ||
|
|
d0e4659d32 | ||
|
|
2d87e692e6 | ||
|
|
f65fb5bc2b | ||
|
|
83597c0efe | ||
|
|
21ee680d3a | ||
|
|
627af50841 | ||
|
|
a955b3730e | ||
|
|
2f7c5b1df4 | ||
|
|
8fca286099 | ||
|
|
8be9701700 | ||
|
|
d66002dc75 | ||
|
|
b12e5270fa | ||
|
|
a717818920 | ||
|
|
d9434a1a9c | ||
|
|
a920a93b3d | ||
|
|
0987a311cf | ||
|
|
a97a6b7fa8 | ||
|
|
414b221727 | ||
|
|
bab2f47ac9 | ||
|
|
0323725543 | ||
|
|
f6a7c8568e | ||
|
|
a21bbfd5a7 |
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -103,8 +103,10 @@ export const suggestionImage = sprinkles({
|
||||
export const suggestionPrimaryContainer = style([
|
||||
sprinkles({
|
||||
alignItems: 'flex-start',
|
||||
width: 'full',
|
||||
}),
|
||||
{
|
||||
width: '90%',
|
||||
},
|
||||
])
|
||||
|
||||
export const suggestionSecondaryContainer = sprinkles({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
{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} />
|
||||
{formattedBalance} {token.symbol}
|
||||
</BalanceItem>
|
||||
<BalanceItem>{formattedUsdValue}</BalanceItem>
|
||||
</BalanceRow>
|
||||
</BalanceLink>
|
||||
</BalanceSection>
|
||||
</BalancesCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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't have chart data because it hasn'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't have chart data because it hasn'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}`}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
225
src/components/Tokens/TokenDetails/Skeleton.tsx
Normal file
225
src/components/Tokens/TokenDetails/Skeleton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ const Menu = styled.div`
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
max-height: 450px;
|
||||
|
||||
// Firefox scrollbar styling
|
||||
scrollbar-width: thin;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum FeatureFlag {
|
||||
nft = 'nfts',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
multiNetworkBalances = 'multiNetworkBalances',
|
||||
nftGraphQl = 'nftGraphQl',
|
||||
}
|
||||
|
||||
7
src/featureFlags/flags/nftGraphQl.ts
Normal file
7
src/featureFlags/flags/nftGraphQl.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useNftGraphQlFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.nftGraphQl)
|
||||
}
|
||||
|
||||
export { BaseVariant as NftGraphQlVariant }
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useRedesignFlag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as RedesignVariant }
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
84
src/graphql/data/TokenPrice.ts
Normal file
84
src/graphql/data/TokenPrice.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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())} 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
ETH
|
||||
</Box>
|
||||
<Box className={styles.collectionName}>{assetUSDPriceFormatted}</Box>
|
||||
</Column>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
36
src/nft/components/bag/EmptyContent.tsx
Normal file
36
src/nft/components/bag/EmptyContent.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
3
src/nft/components/bag/__snapshots__/bag.test.tsx.snap
Normal file
3
src/nft/components/bag/__snapshots__/bag.test.tsx.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Bag.tsx matches base snapshot 1`] = `<DocumentFragment />`;
|
||||
21
src/nft/components/bag/bag.test.tsx
Normal file
21
src/nft/components/bag/bag.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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' },
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
14
src/nft/components/collection/PriceRange.css.ts
Normal file
14
src/nft/components/collection/PriceRange.css.ts
Normal 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',
|
||||
}),
|
||||
])
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
475
src/nft/components/collection/Sweep.tsx
Normal file
475
src/nft/components/collection/Sweep.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
70
src/nft/components/collection/TraitsHeader.tsx
Normal file
70
src/nft/components/collection/TraitsHeader.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
37
src/nft/hooks/usePriceRange.ts
Normal file
37
src/nft/hooks/usePriceRange.ts
Normal 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' }
|
||||
)
|
||||
)
|
||||
29
src/nft/hooks/useTraitsOpen.ts
Normal file
29
src/nft/hooks/useTraitsOpen.ts
Normal 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
Reference in New Issue
Block a user