Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1361f99639 | ||
|
|
d70a87a89a | ||
|
|
2cb0d9527e | ||
|
|
1839e145ec | ||
|
|
8c1e41a3a8 | ||
|
|
9859c0b4dd | ||
|
|
1138101dd0 | ||
|
|
106ac7ea35 | ||
|
|
19b4ee463b | ||
|
|
2aea96c3ba | ||
|
|
b1fb499e29 | ||
|
|
64207f29b0 | ||
|
|
7b6ac6cfaa | ||
|
|
8a9ade5f12 | ||
|
|
a3e567bc8a | ||
|
|
a887666bf5 | ||
|
|
ed8aa08255 | ||
|
|
53f4fb9ede | ||
|
|
bb1ccb7f1a | ||
|
|
03fe90ad53 |
@@ -146,7 +146,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.8.1",
|
||||
"@uniswap/widgets": "^2.9.2",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -155,6 +155,7 @@
|
||||
"@visx/event": "^2.6.0",
|
||||
"@visx/glyph": "^2.10.0",
|
||||
"@visx/group": "^2.10.0",
|
||||
"@visx/react-spring": "^2.12.2",
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "1.7.1",
|
||||
@@ -207,7 +208,7 @@
|
||||
"react-redux": "^8.0.2",
|
||||
"react-relay": "^14.1.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-spring": "^9.5.5",
|
||||
"react-table": "^7.8.0",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
|
||||
@@ -11,6 +11,8 @@ export enum EventName {
|
||||
EXPLORE_SEARCH_SELECTED = 'Explore Search Selected',
|
||||
EXPLORE_TOKEN_ROW_CLICKED = 'Explore Token Row Clicked',
|
||||
PAGE_VIEWED = 'Page Viewed',
|
||||
NAVBAR_SEARCH_SELECTED = 'Navbar Search Selected',
|
||||
NAVBAR_SEARCH_EXITED = 'Navbar Search Exited',
|
||||
SWAP_AUTOROUTER_VISUALIZATION_EXPANDED = 'Swap Autorouter Visualization Expanded',
|
||||
SWAP_DETAILS_EXPANDED = 'Swap Details Expanded',
|
||||
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
|
||||
@@ -110,6 +112,7 @@ export enum ElementName {
|
||||
EXPLORE_SEARCH_INPUT = 'explore_search_input',
|
||||
IMPORT_TOKEN_BUTTON = 'import-token-button',
|
||||
MAX_TOKEN_AMOUNT_BUTTON = 'max-token-amount-button',
|
||||
NAVBAR_SEARCH_INPUT = 'navbar-search-input',
|
||||
PRICE_UPDATE_ACCEPT_BUTTON = 'price-update-accept-button',
|
||||
SWAP_BUTTON = 'swap-button',
|
||||
SWAP_DETAILS_DROPDOWN = 'swap-details-dropdown',
|
||||
@@ -126,6 +129,7 @@ export enum ElementName {
|
||||
*/
|
||||
export enum Event {
|
||||
onClick = 'onClick',
|
||||
onFocus = 'onFocus',
|
||||
onKeyPress = 'onKeyPress',
|
||||
onSelect = 'onSelect',
|
||||
// alphabetize additional events.
|
||||
|
||||
90
src/components/Charts/AnimatedInLineChart.tsx
Normal file
90
src/components/Charts/AnimatedInLineChart.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Group } from '@visx/group'
|
||||
import { LinePath } from '@visx/shape'
|
||||
import { easeCubicInOut } from 'd3'
|
||||
import React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { animated, useSpring } from 'react-spring'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LineChartProps } from './LineChart'
|
||||
|
||||
const config = {
|
||||
duration: 800,
|
||||
easing: easeCubicInOut,
|
||||
}
|
||||
|
||||
// code reference: https://airbnb.io/visx/lineradial
|
||||
|
||||
function AnimatedInLineChart<T>({
|
||||
data,
|
||||
getX,
|
||||
getY,
|
||||
marginTop,
|
||||
curve,
|
||||
color,
|
||||
strokeWidth,
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
}: LineChartProps<T>) {
|
||||
const lineRef = useRef<SVGPathElement>(null)
|
||||
const [lineLength, setLineLength] = useState(0)
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false)
|
||||
const [hasAnimatedIn, setHasAnimatedIn] = useState(false)
|
||||
|
||||
const spring = useSpring({
|
||||
frame: shouldAnimate ? 0 : 1,
|
||||
config,
|
||||
onRest: () => {
|
||||
setShouldAnimate(false)
|
||||
setHasAnimatedIn(true)
|
||||
},
|
||||
})
|
||||
|
||||
const effectDependency = lineRef.current
|
||||
useEffect(() => {
|
||||
if (lineRef.current) {
|
||||
setLineLength(lineRef.current.getTotalLength())
|
||||
setShouldAnimate(true)
|
||||
}
|
||||
}, [effectDependency])
|
||||
const theme = useTheme()
|
||||
const lineColor = color ?? theme.accentAction
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<Group top={marginTop}>
|
||||
<LinePath curve={curve} x={getX} y={getY}>
|
||||
{({ path }) => {
|
||||
const d = path(data) || ''
|
||||
return (
|
||||
<>
|
||||
<animated.path
|
||||
d={d}
|
||||
ref={lineRef}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeOpacity={hasAnimatedIn ? 1 : 0}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
/>
|
||||
{shouldAnimate && lineLength !== 0 && (
|
||||
<animated.path
|
||||
d={d}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
strokeDashoffset={spring.frame.to((v) => v * lineLength)}
|
||||
strokeDasharray={lineLength}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</LinePath>
|
||||
</Group>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedInLineChart
|
||||
@@ -6,7 +6,7 @@ import { ReactNode } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
interface LineChartProps<T> {
|
||||
export interface LineChartProps<T> {
|
||||
data: T[]
|
||||
getX: (t: T) => number
|
||||
getY: (t: T) => number
|
||||
|
||||
@@ -35,9 +35,7 @@ function SparklineChart({ width, height, tokenData, pricePercentChange, timePeri
|
||||
[0, 110]
|
||||
)
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([30, 0])
|
||||
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
|
||||
const curveTension = 0.9
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
|
||||
@@ -2,17 +2,25 @@ import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { AlertOctagon } from 'react-feather'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { AlertOctagon, AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
|
||||
const BodyRow = styled.div`
|
||||
color: ${({ theme }) => theme.deprecated_black};
|
||||
const BodyRow = styled.div<{ $redesignFlag?: boolean }>`
|
||||
color: ${({ theme, $redesignFlag }) => ($redesignFlag ? theme.textPrimary : theme.black)};
|
||||
font-size: 12px;
|
||||
font-weight: ${({ $redesignFlag }) => $redesignFlag && '400'};
|
||||
font-size: ${({ $redesignFlag }) => ($redesignFlag ? '14px' : '12px')};
|
||||
line-height: ${({ $redesignFlag }) => $redesignFlag && '20px'};
|
||||
`
|
||||
const CautionIcon = styled(AlertOctagon)`
|
||||
const CautionOctagon = styled(AlertOctagon)`
|
||||
color: ${({ theme }) => theme.deprecated_black};
|
||||
`
|
||||
|
||||
const CautionTriangle = styled(AlertTriangle)`
|
||||
color: ${({ theme }) => theme.accentWarning};
|
||||
`
|
||||
const Link = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.deprecated_black};
|
||||
text-decoration: underline;
|
||||
@@ -23,21 +31,22 @@ const TitleRow = styled.div`
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
const TitleText = styled.div`
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
const TitleText = styled.div<{ redesignFlag?: boolean }>`
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textPrimary : theme.black)};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '500' : '600')};
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
line-height: ${({ redesignFlag }) => (redesignFlag ? '24px' : '20px')};
|
||||
margin: 0px 12px;
|
||||
`
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.deprecated_yellow3};
|
||||
const Wrapper = styled.div<{ redesignFlag?: boolean }>`
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_yellow3)};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
bottom: 60px;
|
||||
display: none;
|
||||
max-width: 348px;
|
||||
padding: 16px 20px;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
|
||||
display: block;
|
||||
@@ -48,20 +57,21 @@ export function ChainConnectivityWarning() {
|
||||
const { chainId } = useWeb3React()
|
||||
const info = getChainInfoOrDefault(chainId)
|
||||
const label = info?.label
|
||||
const redesignFlag = useRedesignFlag() === RedesignVariant.Enabled
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Wrapper redesignFlag={redesignFlag}>
|
||||
<TitleRow>
|
||||
<CautionIcon />
|
||||
<TitleText>
|
||||
{redesignFlag ? <CautionTriangle /> : <CautionOctagon />}
|
||||
<TitleText redesignFlag={redesignFlag}>
|
||||
<Trans>Network Warning</Trans>
|
||||
</TitleText>
|
||||
</TitleRow>
|
||||
<BodyRow>
|
||||
<BodyRow $redesignFlag={redesignFlag}>
|
||||
{chainId === SupportedChainId.MAINNET ? (
|
||||
<Trans>You may have lost your network connection.</Trans>
|
||||
) : (
|
||||
<Trans>You may have lost your network connection, or {label} might be down right now.</Trans>
|
||||
<Trans>{label} might be down right now, or you may have lost your network connection.</Trans>
|
||||
)}{' '}
|
||||
{(info as L2ChainInfo).statusPage !== undefined && (
|
||||
<span>
|
||||
|
||||
@@ -5,9 +5,9 @@ import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const StyledIdenticon = styled.div<{ isNavbarEnabled: boolean }>`
|
||||
height: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
|
||||
width: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
|
||||
const StyledIdenticon = styled.div<{ iconSize: number }>`
|
||||
height: ${({ iconSize }) => `${iconSize}px`};
|
||||
width: ${({ iconSize }) => `${iconSize}px`};
|
||||
border-radius: 1.125rem;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg4};
|
||||
font-size: initial;
|
||||
@@ -19,12 +19,12 @@ const StyledAvatar = styled.img`
|
||||
border-radius: inherit;
|
||||
`
|
||||
|
||||
export default function Identicon() {
|
||||
export default function Identicon({ size }: { size?: number }) {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const [fetchable, setFetchable] = useState(true)
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const iconSize = isNavbarEnabled ? 24 : 16
|
||||
const iconSize = size ? size : isNavbarEnabled ? 24 : 16
|
||||
|
||||
const icon = useMemo(() => account && jazzicon(iconSize, parseInt(account.slice(2, 10), 16)), [account, iconSize])
|
||||
const iconRef = useRef<HTMLDivElement>(null)
|
||||
@@ -44,7 +44,7 @@ export default function Identicon() {
|
||||
}, [icon, iconRef])
|
||||
|
||||
return (
|
||||
<StyledIdenticon isNavbarEnabled={isNavbarEnabled}>
|
||||
<StyledIdenticon iconSize={iconSize}>
|
||||
{avatar && fetchable ? (
|
||||
<StyledAvatar alt="avatar" src={avatar} onError={() => setFetchable(false)}></StyledAvatar>
|
||||
) : (
|
||||
|
||||
@@ -10,7 +10,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 }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean; scrollOverlay?: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: ${Z_INDEX.modalBackdrop};
|
||||
background-color: transparent;
|
||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
|
||||
justify-content: center;
|
||||
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundScrim : theme.deprecated_modalBG)};
|
||||
@@ -27,7 +28,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
|
||||
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, ...rest }) => (
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, scrollOverlay, ...rest }) => (
|
||||
<AnimatedDialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog',
|
||||
@@ -35,7 +36,7 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
overflow-y: auto;
|
||||
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
margin: ${({ redesignFlag }) => (redesignFlag ? 'auto' : '0 0 2rem 0')};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg1};
|
||||
box-shadow: ${({ theme, redesignFlag }) =>
|
||||
@@ -45,7 +46,7 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
|
||||
align-self: ${({ mobile }) => mobile && 'flex-end'};
|
||||
|
||||
max-width: 420px;
|
||||
${({ maxHeight }) =>
|
||||
@@ -58,11 +59,11 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
css`
|
||||
min-height: ${minHeight}vh;
|
||||
`}
|
||||
display: flex;
|
||||
display: ${({ scrollOverlay }) => (scrollOverlay ? 'inline-table' : 'flex')};
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
${({ theme, redesignFlag }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
width: 65vw;
|
||||
margin: 0;
|
||||
margin: ${redesignFlag ? 'auto' : '0'};
|
||||
`}
|
||||
${({ theme, mobile }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
width: 85vw;
|
||||
@@ -87,6 +88,7 @@ interface ModalProps {
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
redesignFlag?: boolean
|
||||
scrollOverlay?: boolean
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
@@ -97,8 +99,9 @@ export default function Modal({
|
||||
initialFocusRef,
|
||||
children,
|
||||
redesignFlag,
|
||||
scrollOverlay,
|
||||
}: ModalProps) {
|
||||
const fadeTransition = useTransition(isOpen, null, {
|
||||
const fadeTransition = useTransition(isOpen, {
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
@@ -119,16 +122,17 @@ export default function Modal({
|
||||
|
||||
return (
|
||||
<>
|
||||
{fadeTransition.map(
|
||||
({ item, key, props }) =>
|
||||
{fadeTransition(
|
||||
({ opacity }, item) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
as={AnimatedDialogOverlay}
|
||||
style={{ opacity: opacity.to({ range: [0.0, 1.0], output: [0, 1] }) }}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
unstable_lockFocusAcrossFrames={false}
|
||||
redesignFlag={redesignFlag}
|
||||
scrollOverlay={scrollOverlay}
|
||||
>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
@@ -142,6 +146,7 @@ export default function Modal({
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
redesignFlag={redesignFlag}
|
||||
scrollOverlay={scrollOverlay}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
EllipsisIcon,
|
||||
GithubIconMenu,
|
||||
GovernanceIcon,
|
||||
ThinTagIcon,
|
||||
TwitterIconMenu,
|
||||
} from 'nft/components/icons'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
@@ -117,7 +115,6 @@ export const MenuDropdown = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
const nftFlag = useNftFlag()
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
@@ -133,16 +130,6 @@ export const MenuDropdown = () => {
|
||||
<NavDropdown top={{ sm: 'unset', lg: '56' }} bottom={{ sm: '56', lg: 'unset' }} right="0">
|
||||
<Column gap="16">
|
||||
<Column paddingX="8" gap="4">
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<PrimaryMenuRow to="/nfts/sell" close={toggleOpen}>
|
||||
<Icon>
|
||||
<ThinTagIcon width={24} height={24} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Sell NFTs</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
)}
|
||||
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
||||
<Icon>
|
||||
<GovernanceIcon width={24} height={24} />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const searchBarContainer = style([
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.lg}px)`]: {
|
||||
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
|
||||
top: '-5px',
|
||||
top: '-3px',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -57,10 +57,9 @@ export const searchBarInput = style([
|
||||
color: { default: 'textPrimary', placeholder: 'textTertiary' },
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
lineHeight: '24',
|
||||
height: 'full',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBarDropdown = style([
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import clsx from 'clsx'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
@@ -92,6 +95,10 @@ export const SearchBar = () => {
|
||||
const showCenteredSearchContent =
|
||||
!isOpen && phase1Flag !== NftVariant.Enabled && !isMobileOrTablet && searchValue.length === 0
|
||||
|
||||
const navbarSearchEventProperties = {
|
||||
navbar_search_input_text: debouncedSearchValue,
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Box
|
||||
@@ -122,20 +129,27 @@ export const SearchBar = () => {
|
||||
<ChevronLeftIcon />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
as="input"
|
||||
placeholder={placeholderText}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
className={`${styles.searchBarInput} ${
|
||||
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
|
||||
}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
|
||||
/>
|
||||
<TraceEvent
|
||||
events={[Event.onFocus]}
|
||||
name={EventName.NAVBAR_SEARCH_SELECTED}
|
||||
element={ElementName.NAVBAR_SEARCH_INPUT}
|
||||
>
|
||||
<Box
|
||||
as="input"
|
||||
placeholder={placeholderText}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
|
||||
className={`${styles.searchBarInput} ${
|
||||
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
|
||||
}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
|
||||
/>
|
||||
</TraceEvent>
|
||||
</Row>
|
||||
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
|
||||
{isOpen && (
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import { EventName } from 'analytics/constants'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -58,6 +60,15 @@ export const SearchBarDropdownSection = ({
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
traceEvent={() =>
|
||||
sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, {
|
||||
position: index,
|
||||
selected_type: 'collection',
|
||||
suggestion_count: suggestions.length,
|
||||
selected_name: suggestion.name,
|
||||
selected_address: suggestion.address,
|
||||
})
|
||||
}
|
||||
index={index + startingIndex}
|
||||
/>
|
||||
) : (
|
||||
@@ -67,6 +78,15 @@ export const SearchBarDropdownSection = ({
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
traceEvent={() =>
|
||||
sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, {
|
||||
position: index,
|
||||
selected_type: 'token',
|
||||
suggestion_count: suggestions.length,
|
||||
selected_name: suggestion.name,
|
||||
selected_address: suggestion.address,
|
||||
})
|
||||
}
|
||||
index={index + startingIndex}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -23,11 +23,11 @@ export const ShoppingBag = () => {
|
||||
setSellQuantity(sellAssets.length)
|
||||
}, [sellAssets])
|
||||
|
||||
const isSell = location.pathname === '/nfts/sell'
|
||||
const isProfilePage = location.pathname === '/profile'
|
||||
|
||||
return (
|
||||
<NavIcon onClick={toggleBag}>
|
||||
{isSell ? (
|
||||
{isProfilePage ? (
|
||||
<>
|
||||
<TagIcon width={20} height={20} />
|
||||
{sellQuantity ? (
|
||||
|
||||
@@ -19,10 +19,18 @@ interface CollectionRowProps {
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
traceEvent: () => void
|
||||
index: number
|
||||
}
|
||||
|
||||
export const CollectionRow = ({ collection, isHovered, setHoveredIndex, toggleOpen, index }: CollectionRowProps) => {
|
||||
export const CollectionRow = ({
|
||||
collection,
|
||||
isHovered,
|
||||
setHoveredIndex,
|
||||
toggleOpen,
|
||||
traceEvent,
|
||||
index,
|
||||
}: CollectionRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
@@ -33,7 +41,8 @@ export const CollectionRow = ({ collection, isHovered, setHoveredIndex, toggleOp
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(collection)
|
||||
toggleOpen()
|
||||
}, [addToSearchHistory, collection, toggleOpen])
|
||||
traceEvent()
|
||||
}, [addToSearchHistory, collection, toggleOpen, traceEvent])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -96,10 +105,11 @@ interface TokenRowProps {
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
traceEvent: () => void
|
||||
index: number
|
||||
}
|
||||
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index }: TokenRowProps) => {
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceEvent, index }: TokenRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
@@ -110,7 +120,8 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(token)
|
||||
toggleOpen()
|
||||
}, [addToSearchHistory, toggleOpen, token])
|
||||
traceEvent()
|
||||
}, [addToSearchHistory, toggleOpen, token, traceEvent])
|
||||
|
||||
const tokenDetailsPath = getTokenDetailsURL(token.address, undefined, token.chainId)
|
||||
// Close the modal on escape
|
||||
|
||||
@@ -68,7 +68,7 @@ const PageTabs = () => {
|
||||
|
||||
const Navbar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const isNftPage = pathname.startsWith('/nfts')
|
||||
const showShoppingBag = pathname.startsWith('/nfts') || pathname.startsWith('/profile')
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -96,7 +96,7 @@ const Navbar = () => {
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
{isNftPage && <ShoppingBag />}
|
||||
{showShoppingBag && <ShoppingBag />}
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<ChainSelector />
|
||||
</Box>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { animated } from 'react-spring'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import { useSpring } from 'react-spring'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
|
||||
import Resource from './Resource'
|
||||
|
||||
@@ -49,6 +50,7 @@ const TRUNCATE_CHARACTER_COUNT = 400
|
||||
export const AboutContainer = styled.div`
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
${textFadeIn}
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import styled from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import { useIsFavorited, useToggleFavorite } from '../state'
|
||||
@@ -42,6 +43,7 @@ export const TokenNameCell = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
${textFadeIn}
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -3,18 +3,9 @@ import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Line } from '@visx/shape'
|
||||
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
import {
|
||||
bisect,
|
||||
curveCardinal,
|
||||
NumberValue,
|
||||
scaleLinear,
|
||||
timeDay,
|
||||
timeHour,
|
||||
timeMinute,
|
||||
timeMonth,
|
||||
timeTicks,
|
||||
} from 'd3'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
|
||||
import { PricePoint } from 'graphql/data/Token'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
@@ -28,11 +19,9 @@ import {
|
||||
monthDayFormatter,
|
||||
monthTickFormatter,
|
||||
monthYearDayFormatter,
|
||||
monthYearFormatter,
|
||||
weekFormatter,
|
||||
} from 'utils/formatChartTimes'
|
||||
|
||||
import LineChart from '../../Charts/LineChart'
|
||||
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
|
||||
|
||||
@@ -221,12 +210,6 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
monthYearDayFormatter(locale),
|
||||
timeMonth.range(startDateWithOffset, endDateWithOffset, 2).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.ALL:
|
||||
return [
|
||||
monthYearFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeTicks(startDateWithOffset, endDateWithOffset, 6).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,8 +234,10 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
|
||||
}
|
||||
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
setDisplayPrice(pricePoint)
|
||||
if (pricePoint) {
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
setDisplayPrice(pricePoint)
|
||||
}
|
||||
},
|
||||
[timeScale, prices]
|
||||
)
|
||||
@@ -274,8 +259,12 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
const crosshairEdgeMax = width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
|
||||
|
||||
/* Default curve doesn't look good for the HOUR/ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
/*
|
||||
* Default curve doesn't look good for the HOUR chart.
|
||||
* Higher values make the curve more rigid, lower values smooth the curve but make it less "sticky" to real data points,
|
||||
* making it unacceptable for shorter durations / smaller variances.
|
||||
*/
|
||||
const curveTension = timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -286,7 +275,7 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<LineChart
|
||||
<AnimatedInLineChart
|
||||
data={prices}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
@@ -355,7 +344,7 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={resetDisplay}
|
||||
/>
|
||||
</LineChart>
|
||||
</AnimatedInLineChart>
|
||||
<TimeOptionsWrapper>
|
||||
<TimeOptionsContainer>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
export const StatWrapper = styled.div`
|
||||
@@ -16,6 +17,7 @@ export const StatWrapper = styled.div`
|
||||
export const TokenStatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
${textFadeIn}
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -64,7 +64,6 @@ const StyledMenuContent = styled.div`
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
`
|
||||
@@ -85,6 +84,9 @@ const CheckContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: flex-end;
|
||||
`
|
||||
const NetworkFilterOption = styled(FilterOption)`
|
||||
width: 156px;
|
||||
`
|
||||
|
||||
export default function NetworkFilter() {
|
||||
const theme = useTheme()
|
||||
@@ -101,7 +103,7 @@ export default function NetworkFilter() {
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<FilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
|
||||
<NetworkFilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<Logo src={circleLogoUrl ?? logoUrl} /> {label}
|
||||
@@ -114,7 +116,7 @@ export default function NetworkFilter() {
|
||||
)}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</FilterOption>
|
||||
</NetworkFilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{BACKEND_CHAIN_NAMES.map((network) => {
|
||||
|
||||
@@ -23,10 +23,10 @@ const SearchInput = styled.input`
|
||||
background-position: 12px center;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border: 1.5px solid ${({ theme }) => theme.backgroundOutline};
|
||||
height: 100%;
|
||||
width: min(200px, 100%);
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
padding-left: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
@@ -79,7 +79,7 @@ export default function SearchBar() {
|
||||
<Trans
|
||||
render={({ translation }) => (
|
||||
<TraceEvent
|
||||
events={[Event.onSelect]}
|
||||
events={[Event.onFocus]}
|
||||
name={EventName.EXPLORE_SEARCH_SELECTED}
|
||||
element={ElementName.EXPLORE_SEARCH_INPUT}
|
||||
>
|
||||
|
||||
@@ -17,16 +17,14 @@ export const DISPLAYS: Record<TimePeriod, string> = {
|
||||
[TimePeriod.WEEK]: '1W',
|
||||
[TimePeriod.MONTH]: '1M',
|
||||
[TimePeriod.YEAR]: '1Y',
|
||||
[TimePeriod.ALL]: 'All',
|
||||
}
|
||||
|
||||
export const ORDERED_TIMES = [
|
||||
export const ORDERED_TIMES: TimePeriod[] = [
|
||||
TimePeriod.HOUR,
|
||||
TimePeriod.DAY,
|
||||
TimePeriod.WEEK,
|
||||
TimePeriod.MONTH,
|
||||
TimePeriod.YEAR,
|
||||
TimePeriod.ALL,
|
||||
]
|
||||
|
||||
const InternalMenuItem = styled.div`
|
||||
|
||||
@@ -7,7 +7,7 @@ import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL, TimePeriod } from 'graphql/data/util'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { CSSProperties, ReactNode } from 'react'
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
} from '../state'
|
||||
import { useTokenLogoURI } from '../TokenDetails/ChartSection'
|
||||
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
|
||||
import { DISPLAYS } from './TimeSelector'
|
||||
|
||||
const Cell = styled.div`
|
||||
display: flex;
|
||||
@@ -50,15 +49,17 @@ const StyledTokenRow = styled.div<{
|
||||
}>`
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
grid-template-columns: ${({ favoriteTokensEnabled }) =>
|
||||
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
|
||||
height: 60px;
|
||||
line-height: 24px;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
min-width: 390px;
|
||||
padding-top: ${({ first }) => (first ? '4px' : '0px')};
|
||||
padding-bottom: ${({ last }) => (last ? '4px' : '0px')};
|
||||
${({ first, last }) => css`
|
||||
height: ${first || last ? '72px' : '64px'};
|
||||
padding-top: ${first ? '8px' : '0px'};
|
||||
padding-bottom: ${last ? '8px' : '0px'};
|
||||
`}
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
transition: ${({
|
||||
@@ -150,7 +151,7 @@ const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
border-color: ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 16px;
|
||||
padding: 0px 12px;
|
||||
@@ -169,6 +170,7 @@ const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
const ListNumberCell = styled(Cell)<{ header: boolean }>`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
min-width: 32px;
|
||||
font-size: 14px;
|
||||
height: ${({ header }) => (header ? '48px' : '60px')};
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
@@ -328,13 +330,6 @@ export const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
/* formatting for volume with timeframe header display */
|
||||
function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
|
||||
if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
|
||||
return `${DISPLAYS[timeframe]} ${method}`
|
||||
return method
|
||||
}
|
||||
|
||||
/* Get singular header cell for header row */
|
||||
function HeaderCell({
|
||||
category,
|
||||
@@ -347,19 +342,18 @@ function HeaderCell({
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
const handleSortCategory = useSetSortMethod(category)
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const timeframe = useAtomValue(filterTimeAtom)
|
||||
|
||||
if (sortMethod === category) {
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
<SortArrowCell>
|
||||
{sortAscending ? (
|
||||
<ArrowUp size={14} color={theme.accentActive} />
|
||||
<ArrowUp size={20} strokeWidth={1.8} color={theme.accentActive} />
|
||||
) : (
|
||||
<ArrowDown size={14} color={theme.accentActive} />
|
||||
<ArrowDown size={20} strokeWidth={1.8} color={theme.accentActive} />
|
||||
)}
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
{category}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
@@ -369,11 +363,11 @@ function HeaderCell({
|
||||
<SortArrowCell>
|
||||
<ArrowUp size={14} visibility="hidden" />
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
{category}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
return <HeaderCellWrapper>{getHeaderDisplay(category, timeframe)}</HeaderCellWrapper>
|
||||
return <HeaderCellWrapper>{category}</HeaderCellWrapper>
|
||||
}
|
||||
|
||||
/* Token Row: skeleton row component */
|
||||
|
||||
@@ -22,13 +22,16 @@ const GridContainer = styled.div`
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const TokenDataContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`
|
||||
@@ -72,8 +75,7 @@ export default function TokenTable() {
|
||||
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens, maxFetchable } =
|
||||
useTopTokens(chainName)
|
||||
const { error, loading, tokens, hasMore, loadMoreTokens, maxFetchable } = useTopTokens(chainName)
|
||||
const showMoreLoadingRows = Boolean(loading && hasMore)
|
||||
|
||||
const observer = useRef<IntersectionObserver>()
|
||||
@@ -93,9 +95,9 @@ export default function TokenTable() {
|
||||
|
||||
/* loading and error state */
|
||||
if (loading && (!tokens || tokens?.length === 0)) {
|
||||
return <LoadingTokenTable rowCount={Math.min(tokensWithoutPriceHistoryCount, PAGE_SIZE)} />
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else {
|
||||
if (!tokens) {
|
||||
if (error || !tokens) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
|
||||
@@ -8,7 +8,7 @@ export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
|
||||
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
|
||||
export const filterStringAtom = atomWithReset<string>('')
|
||||
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
|
||||
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
|
||||
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.VOLUME)
|
||||
export const sortAscendingAtom = atom<boolean>(false)
|
||||
|
||||
/* for favoriting tokens */
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
const Cart = lazy(() => import('nft/components/sell/modal/ListingTag'))
|
||||
const Cart = lazy(() => import('nft/components/profile/modal/ListingTag'))
|
||||
const Bag = lazy(() => import('nft/components/bag/Bag'))
|
||||
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
|
||||
|
||||
|
||||
@@ -454,7 +454,7 @@ export default function TransactionConfirmationModal({
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
|
||||
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
|
||||
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
|
||||
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : attemptingTxn ? (
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Copy, ExternalLink, Power } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
@@ -16,15 +18,14 @@ import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { shortenAddress } from '../../nft/utils/address'
|
||||
import { useToggleModal } from '../../state/application/hooks'
|
||||
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
|
||||
const UNIbutton = styled(ButtonPrimary)`
|
||||
background: linear-gradient(to right, #9139b0 0%, #4261d6 100%);
|
||||
const WalletButton = styled(ButtonPrimary)`
|
||||
border-radius: 12px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
@@ -33,6 +34,16 @@ const UNIbutton = styled(ButtonPrimary)`
|
||||
border: none;
|
||||
`
|
||||
|
||||
const ProfileButton = styled(WalletButton)`
|
||||
background: ${({ theme }) => theme.backgroundInteractive};
|
||||
transition: ${({ theme }) => theme.transition.duration.fast} ${({ theme }) => theme.transition.timing.ease}
|
||||
background-color;
|
||||
`
|
||||
|
||||
const UNIButton = styled(WalletButton)`
|
||||
background: linear-gradient(to right, #9139b0 0%, #4261d6 100%);
|
||||
`
|
||||
|
||||
const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -97,6 +108,9 @@ const AuthenticatedHeader = () => {
|
||||
nativeCurrency: { symbol: nativeCurrencySymbol },
|
||||
explorer,
|
||||
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
|
||||
const nftFlag = useNftFlag()
|
||||
const navigate = useNavigate()
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
|
||||
const isUnclaimed = useUserHasAvailableClaim(account)
|
||||
@@ -118,6 +132,11 @@ const AuthenticatedHeader = () => {
|
||||
return price * balance
|
||||
}, [balanceString, nativeCurrencyPrice])
|
||||
|
||||
const navigateToProfile = () => {
|
||||
navigate('/profile')
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticatedHeaderWrapper>
|
||||
<HeaderWrapper>
|
||||
@@ -147,10 +166,15 @@ const AuthenticatedHeader = () => {
|
||||
</Text>
|
||||
<USDText>${amountUSD.toFixed(2)} USD</USDText>
|
||||
</BalanceWrapper>
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<ProfileButton onClick={navigateToProfile}>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</ProfileButton>
|
||||
)}
|
||||
{isUnclaimed && (
|
||||
<UNIbutton onClick={openClaimModal}>
|
||||
<UNIButton onClick={openClaimModal}>
|
||||
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
|
||||
</UNIbutton>
|
||||
</UNIButton>
|
||||
)}
|
||||
</Column>
|
||||
</AuthenticatedHeaderWrapper>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { TransactionHistoryMenu } from './TransactionMenu'
|
||||
const WalletWrapper = styled.div`
|
||||
border-radius: 12px;
|
||||
width: 320px;
|
||||
max-height: 376px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useMemo } from 'react'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
@@ -34,7 +33,7 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
|
||||
<SwapWidget
|
||||
disableBranding
|
||||
hideConnectionUI
|
||||
jsonRpcUrlMap={RPC_PROVIDERS}
|
||||
// jsonRpcUrlMap is excluded - network providers are always passed directly
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
width={WIDGET_WIDTH}
|
||||
locale={locale}
|
||||
|
||||
@@ -92,7 +92,6 @@ const tokenPriceQuery = graphql`
|
||||
$skip1W: Boolean!
|
||||
$skip1M: Boolean!
|
||||
$skip1Y: Boolean!
|
||||
$skipMax: Boolean!
|
||||
) {
|
||||
tokens(contracts: [$contract]) {
|
||||
market(currency: USD) {
|
||||
@@ -116,10 +115,6 @@ const tokenPriceQuery = graphql`
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistoryMAX: priceHistory(duration: MAX) @skip(if: $skipMax) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +156,6 @@ export function useTokenPricesCached(token: SingleTokenData) {
|
||||
skip1W: timePeriod === TimePeriod.WEEK && !!fetchedTokenPrices,
|
||||
skip1M: timePeriod === TimePeriod.MONTH && !!fetchedTokenPrices,
|
||||
skip1Y: timePeriod === TimePeriod.YEAR && !!fetchedTokenPrices,
|
||||
skipMax: timePeriod === TimePeriod.ALL && !!fetchedTokenPrices,
|
||||
}).subscribe({
|
||||
next: (data) => {
|
||||
const market = data.tokens?.[0]?.market
|
||||
@@ -171,7 +165,6 @@ export function useTokenPricesCached(token: SingleTokenData) {
|
||||
market.priceHistory1W && updatePrices(TimePeriod.WEEK, filterPrices(market.priceHistory1W))
|
||||
market.priceHistory1M && updatePrices(TimePeriod.MONTH, filterPrices(market.priceHistory1M))
|
||||
market.priceHistory1Y && updatePrices(TimePeriod.YEAR, filterPrices(market.priceHistory1Y))
|
||||
market.priceHistoryMAX && updatePrices(TimePeriod.ALL, filterPrices(market.priceHistoryMAX))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
sortMethodAtom,
|
||||
} from 'components/Tokens/state'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useRelayEnvironment } from 'react-relay'
|
||||
|
||||
import {
|
||||
Chain,
|
||||
@@ -20,10 +20,6 @@ import {
|
||||
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
||||
import { toHistoryDuration } from './util'
|
||||
|
||||
export function usePrefetchTopTokens(duration: HistoryDuration, chain: Chain) {
|
||||
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
|
||||
}
|
||||
|
||||
const topTokens100Query = graphql`
|
||||
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
@@ -166,29 +162,48 @@ const checkIfAllTokensCached = (duration: HistoryDuration, tokens: PrefetchedTop
|
||||
|
||||
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
|
||||
interface UseTopTokensReturnValue {
|
||||
error: Error | undefined
|
||||
loading: boolean
|
||||
tokens: TopToken[] | undefined
|
||||
tokensWithoutPriceHistoryCount: number
|
||||
hasMore: boolean
|
||||
loadMoreTokens: () => void
|
||||
maxFetchable: number
|
||||
}
|
||||
export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingTokensWithoutPriceHistory, setLoadingTokensWithoutPriceHistory] = useState(true)
|
||||
const [loadingTokensWithPriceHistory, setLoadingTokensWithPriceHistory] = useState(true)
|
||||
const [tokens, setTokens] = useState<TopToken[]>()
|
||||
const [page, setPage] = useState(0)
|
||||
const prefetchedData = usePrefetchTopTokens(duration, chain)
|
||||
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
|
||||
const [error, setError] = useState<Error | undefined>()
|
||||
const [prefetchedData, setPrefetchedData] = useState<PrefetchedTopToken[]>([])
|
||||
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData))
|
||||
const maxFetchable = useMemo(
|
||||
() => prefetchedSelectedTokensWithoutPriceHistory.length,
|
||||
[prefetchedSelectedTokensWithoutPriceHistory]
|
||||
)
|
||||
|
||||
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
|
||||
|
||||
const environment = useRelayEnvironment()
|
||||
|
||||
const loadTokensWithoutPriceHistory = useCallback(
|
||||
({ duration, chain }: { duration: HistoryDuration; chain: Chain }) => {
|
||||
fetchQuery<TopTokens100Query>(
|
||||
environment,
|
||||
topTokens100Query,
|
||||
{ duration, chain },
|
||||
{ fetchPolicy: 'store-or-network' }
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
if (data?.topTokens) setPrefetchedData([...data?.topTokens])
|
||||
},
|
||||
error: setError,
|
||||
complete: () => setLoadingTokensWithoutPriceHistory(false),
|
||||
})
|
||||
},
|
||||
[environment]
|
||||
)
|
||||
|
||||
// TopTokens should ideally be fetched with usePaginationFragment. The backend does not current support graphql cursors;
|
||||
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
|
||||
const loadTokensWithPriceHistory = useCallback(
|
||||
@@ -208,25 +223,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
tokensQuery,
|
||||
{ contracts, duration },
|
||||
{ fetchPolicy: 'store-or-network' }
|
||||
)
|
||||
.toPromise()
|
||||
.then((data) => {
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
if (data?.tokens) {
|
||||
const priceHistoryCacheForCurrentDuration = tokensWithPriceHistoryCache[duration]
|
||||
data.tokens.map((token) =>
|
||||
!!token ? (priceHistoryCacheForCurrentDuration[`${token.chain}${token.address}`] = token) : null
|
||||
)
|
||||
appendingTokens ? setTokens([...(tokens ?? []), ...data.tokens]) : setTokens([...data.tokens])
|
||||
setLoading(false)
|
||||
setLoadingTokensWithPriceHistory(false)
|
||||
setPage(page + 1)
|
||||
}
|
||||
})
|
||||
},
|
||||
error: setError,
|
||||
complete: () => setLoadingTokensWithPriceHistory(false),
|
||||
})
|
||||
},
|
||||
[duration, environment]
|
||||
)
|
||||
|
||||
const loadMoreTokens = useCallback(() => {
|
||||
setLoading(true)
|
||||
setLoadingTokensWithPriceHistory(true)
|
||||
const contracts = prefetchedSelectedTokensWithoutPriceHistory
|
||||
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
|
||||
.map(toContractInput)
|
||||
@@ -241,21 +258,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
)
|
||||
if (everyTokenInCache) {
|
||||
setTokens(cachedTokens)
|
||||
setLoading(false)
|
||||
return
|
||||
setLoadingTokensWithPriceHistory(false)
|
||||
} else {
|
||||
setLoading(true)
|
||||
setLoadingTokensWithPriceHistory(true)
|
||||
setTokens([])
|
||||
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
|
||||
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
|
||||
}
|
||||
}, [loadTokensWithPriceHistory, prefetchedSelectedTokensWithoutPriceHistory, duration])
|
||||
|
||||
// Trigger fetching top 100 tokens without price history on first load, and on
|
||||
// each change of chain or duration.
|
||||
useEffect(() => {
|
||||
setLoadingTokensWithoutPriceHistory(true)
|
||||
loadTokensWithoutPriceHistory({ duration, chain })
|
||||
}, [chain, duration, loadTokensWithoutPriceHistory])
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
loading: loadingTokensWithPriceHistory || loadingTokensWithoutPriceHistory,
|
||||
tokens,
|
||||
hasMore,
|
||||
tokensWithoutPriceHistoryCount: prefetchedSelectedTokensWithoutPriceHistory.length,
|
||||
loadMoreTokens,
|
||||
maxFetchable,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export enum TimePeriod {
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
|
||||
@@ -24,8 +23,6 @@ export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
|
||||
return 'MONTH'
|
||||
case TimePeriod.YEAR:
|
||||
return 'YEAR'
|
||||
case TimePeriod.ALL:
|
||||
return 'MAX'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,10 @@ export const Box = React.forwardRef<HTMLElement, Props>(({ as = 'div', className
|
||||
})
|
||||
})
|
||||
|
||||
export const AnimatedBox = animated(Box)
|
||||
// We get this error around the codebase: https://github.com/microsoft/TypeScript/issues/34933
|
||||
// so you see ts-ignore almost everywhere this component is used
|
||||
// since we are going to deprecate vanilla-extract, this will be `any` for now
|
||||
export const AnimatedBox: any = animated(Box) as any
|
||||
|
||||
export type BoxProps = Parameters<typeof Box>[0]
|
||||
|
||||
|
||||
@@ -112,14 +112,16 @@ const Bag = () => {
|
||||
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
|
||||
const bagExpanded = useBag((s) => s.bagExpanded)
|
||||
const toggleBag = useBag((s) => s.toggleBag)
|
||||
const setTotalEthPrice = useBag((s) => s.setTotalEthPrice)
|
||||
const setTotalUsdPrice = useBag((s) => s.setTotalUsdPrice)
|
||||
|
||||
const { address, balance: balanceInEth, provider } = useWalletBalance()
|
||||
const isConnected = !!provider && !!address
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isNFTSellPage = pathname.startsWith('/nfts/sell')
|
||||
const isProfilePage = pathname.startsWith('/profile')
|
||||
const isNFTPage = pathname.startsWith('/nfts')
|
||||
const shouldShowBag = isNFTPage && !isNFTSellPage
|
||||
const shouldShowBag = isNFTPage && !isProfilePage
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
|
||||
@@ -300,6 +302,11 @@ const Bag = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [transactionStateRef.current])
|
||||
|
||||
useEffect(() => {
|
||||
setTotalEthPrice(totalEthPrice)
|
||||
setTotalUsdPrice(totalUsdPrice)
|
||||
}, [totalEthPrice, totalUsdPrice, setTotalEthPrice, setTotalUsdPrice])
|
||||
|
||||
const hasAssetsToShow = itemsInBag.length > 0 || unavailableAssets.length > 0
|
||||
|
||||
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -195,3 +195,11 @@ export const toolTip = sprinkles({
|
||||
display: 'flex',
|
||||
flexShrink: '0',
|
||||
})
|
||||
|
||||
export const removeAssetOverlay = style([
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
right: '4',
|
||||
top: '4',
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Column, Row } from 'nft/components/Flex'
|
||||
import {
|
||||
ChevronDownBagIcon,
|
||||
ChevronUpBagIcon,
|
||||
CircularCloseIcon,
|
||||
CloseTimerIcon,
|
||||
SquareArrowDownIcon,
|
||||
SquareArrowUpIcon,
|
||||
@@ -57,6 +58,7 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
|
||||
const [noImageAvailable, setNoImageAvailable] = useState(!asset.smallImageUrl)
|
||||
const handleCardHover = () => setCardHovered(!cardHovered)
|
||||
const assetCardRef = useRef<HTMLDivElement>(null)
|
||||
const showRemoveButton = showRemove && cardHovered
|
||||
|
||||
if (cardHovered && assetCardRef.current && assetCardRef.current.matches(':hover') === false) setCardHovered(false)
|
||||
|
||||
@@ -64,6 +66,19 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
|
||||
<Link to={getAssetHref(asset)} style={{ textDecoration: 'none' }}>
|
||||
<Row ref={assetCardRef} className={styles.bagRow} onMouseEnter={handleCardHover} onMouseLeave={handleCardHover}>
|
||||
<Box position="relative" display="flex">
|
||||
<Box
|
||||
display={showRemove && isMobile ? 'block' : 'none'}
|
||||
className={styles.removeAssetOverlay}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
removeAsset(asset)
|
||||
}}
|
||||
transition="250"
|
||||
zIndex="1"
|
||||
>
|
||||
<CircularCloseIcon />
|
||||
</Box>
|
||||
{!noImageAvailable && (
|
||||
<Box
|
||||
as="img"
|
||||
@@ -91,7 +106,7 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
|
||||
{asset.collectionIsVerified && <VerifiedIcon className={styles.icon} />}
|
||||
</Row>
|
||||
</Column>
|
||||
{cardHovered && showRemove && (
|
||||
{showRemoveButton && !isMobile && (
|
||||
<Box
|
||||
marginLeft="16"
|
||||
className={styles.removeBagRowButton}
|
||||
@@ -104,7 +119,7 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is
|
||||
Remove
|
||||
</Box>
|
||||
)}
|
||||
{(!cardHovered || !showRemove) && (
|
||||
{(!showRemoveButton || isMobile) && (
|
||||
<Column flexShrink="0">
|
||||
<Box className={styles.bagRowPrice}>
|
||||
{`${formatWeiToDecimal(
|
||||
|
||||
29
src/nft/components/bag/MobileHoverBag.css.ts
Normal file
29
src/nft/components/bag/MobileHoverBag.css.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { buttonTextSmall } from 'nft/css/common.css'
|
||||
import { sprinkles } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const bagContainer = style([
|
||||
sprinkles({
|
||||
position: 'fixed',
|
||||
bottom: '72',
|
||||
left: '16',
|
||||
right: '16',
|
||||
background: 'backgroundModule',
|
||||
padding: '8',
|
||||
zIndex: 'fixed',
|
||||
borderRadius: '8',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
])
|
||||
|
||||
export const viewBagButton = style([
|
||||
buttonTextSmall,
|
||||
sprinkles({
|
||||
color: 'explicitWhite',
|
||||
backgroundColor: 'accentAction',
|
||||
paddingY: '8',
|
||||
paddingX: '18',
|
||||
borderRadius: '12',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
])
|
||||
63
src/nft/components/bag/MobileHoverBag.tsx
Normal file
63
src/nft/components/bag/MobileHoverBag.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils'
|
||||
|
||||
import * as styles from './MobileHoverBag.css'
|
||||
export const MobileHoverBag = () => {
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
const toggleBag = useBag((state) => state.toggleBag)
|
||||
const totalEthPrice = useBag((state) => state.totalEthPrice)
|
||||
const totalUsdPrice = useBag((state) => state.totalUsdPrice)
|
||||
|
||||
const shouldShowBag = itemsInBag.length > 0
|
||||
|
||||
return (
|
||||
<Row display={{ sm: shouldShowBag ? 'flex' : 'none', md: 'none' }} className={styles.bagContainer}>
|
||||
<Row gap="8">
|
||||
<Box position="relative" style={{ width: '34px', height: '34px' }}>
|
||||
{itemsInBag.slice(0, 3).map((item, index) => {
|
||||
return (
|
||||
<Box
|
||||
as="img"
|
||||
key={index}
|
||||
position="absolute"
|
||||
src={item.asset.smallImageUrl}
|
||||
top="1/2"
|
||||
left="1/2"
|
||||
width="26"
|
||||
height="26"
|
||||
borderRadius="4"
|
||||
style={{
|
||||
transform:
|
||||
index === 0
|
||||
? 'translate(-50%, -50%) rotate(-4.42deg)'
|
||||
: index === 1
|
||||
? 'translate(-50%, -50%) rotate(-14.01deg)'
|
||||
: 'translate(-50%, -50%) rotate(10.24deg)',
|
||||
zIndex: index,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
<Column>
|
||||
<Box className={body} fontWeight="semibold">
|
||||
{roundAndPluralize(itemsInBag.length, 'item')}
|
||||
</Box>
|
||||
<Row gap="8">
|
||||
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box>
|
||||
<Box color="textSecondary" className={bodySmall}>{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</Box>
|
||||
</Row>
|
||||
</Column>
|
||||
</Row>
|
||||
<Box className={styles.viewBagButton} onClick={toggleBag}>
|
||||
View bag
|
||||
</Box>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { buttonTextMedium } from 'nft/css/common.css'
|
||||
import { loadingAsset } from 'nft/css/loading.css'
|
||||
import { sprinkles, vars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const baseActivitySwitcherToggle = style([
|
||||
@@ -40,3 +41,11 @@ export const selectedActivitySwitcherToggle = style([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const styledLoading = style([
|
||||
loadingAsset,
|
||||
{
|
||||
width: 58,
|
||||
height: 20,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { useIsCollectionLoading } from 'nft/hooks'
|
||||
|
||||
import * as styles from './ActivitySwitcher.css'
|
||||
|
||||
@@ -10,22 +11,31 @@ export const ActivitySwitcher = ({
|
||||
showActivity: boolean
|
||||
toggleActivity: () => void
|
||||
}) => {
|
||||
const isLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
const loadingVals = new Array(2).fill(<div className={styles.styledLoading} />)
|
||||
|
||||
return (
|
||||
<Row gap="24" marginBottom="28">
|
||||
<Box
|
||||
as="button"
|
||||
className={showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
|
||||
onClick={() => showActivity && toggleActivity()}
|
||||
>
|
||||
Items
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
className={!showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
|
||||
onClick={() => !showActivity && toggleActivity()}
|
||||
>
|
||||
Activity
|
||||
</Box>
|
||||
{isLoading ? (
|
||||
loadingVals
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
as="button"
|
||||
className={showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
|
||||
onClick={() => showActivity && toggleActivity()}
|
||||
>
|
||||
Items
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
className={!showActivity ? styles.activitySwitcherToggle : styles.selectedActivitySwitcherToggle}
|
||||
onClick={() => !showActivity && toggleActivity()}
|
||||
>
|
||||
Activity
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
RarityVerifiedIcon,
|
||||
SuspiciousIcon20,
|
||||
} from 'nft/components/icons'
|
||||
import { body, subheadSmall } from 'nft/css/common.css'
|
||||
import { body, bodySmall, subheadSmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset, Rarity, UniformHeight, UniformHeights } from 'nft/types'
|
||||
@@ -551,7 +551,7 @@ const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps)
|
||||
<Box display="flex" marginRight="4">
|
||||
<img src={rarityLogo} alt="cardLogo" width={16} />
|
||||
</Box>
|
||||
<Box width="full" fontSize="14">
|
||||
<Box width="full" className={bodySmall}>
|
||||
{rarityVerified
|
||||
? `Verified by ${asset.collectionName}`
|
||||
: `Ranking by ${rarity.primaryProvider === 'Genie' ? fallbackProvider : rarity.primaryProvider}`}
|
||||
@@ -577,7 +577,7 @@ const Suspicious = () => {
|
||||
return (
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Box fontSize="14">
|
||||
<Box className={bodySmall}>
|
||||
Reported for suspicious activity
|
||||
<br />
|
||||
on Opensea
|
||||
@@ -595,7 +595,11 @@ const Suspicious = () => {
|
||||
const Pool = () => {
|
||||
return (
|
||||
<MouseoverTooltip
|
||||
text={<Box fontSize="14">This item is part of an NFT liquidity pool. Price increases as supply decreases.</Box>}
|
||||
text={
|
||||
<Box className={bodySmall}>
|
||||
This NFT is part of a liquidity pool. Buying this will increase the price of the remaining pooled NFTs.
|
||||
</Box>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Box display="flex" flexShrink="0" marginLeft="4" color="textSecondary">
|
||||
|
||||
@@ -110,7 +110,7 @@ export const CollectionAsset = ({
|
||||
<Card.SecondaryRow>
|
||||
<Card.SecondaryDetails>
|
||||
<Card.SecondaryInfo>
|
||||
{notForSale ? '' : `${formatWeiToDecimal(asset.currentEthPrice)} ETH`}
|
||||
{notForSale ? '' : `${formatWeiToDecimal(asset.currentEthPrice, true)} ETH`}
|
||||
</Card.SecondaryInfo>
|
||||
{(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />}
|
||||
</Card.SecondaryDetails>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useFiltersExpanded,
|
||||
useIsMobile,
|
||||
} from 'nft/hooks'
|
||||
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
|
||||
import { AssetsFetcher } from 'nft/queries'
|
||||
import { DropDownOption, GenieCollection, UniformHeight, UniformHeights } from 'nft/types'
|
||||
import { getRarityStatus } from 'nft/utils/asset'
|
||||
@@ -46,6 +47,7 @@ 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 setIsCollectionNftsLoading = useIsCollectionLoading((state) => state.setIsCollectionNftsLoading)
|
||||
|
||||
const debouncedMinPrice = useDebounce(minPrice, 500)
|
||||
const debouncedMaxPrice = useDebounce(maxPrice, 500)
|
||||
@@ -115,6 +117,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollectionNftsLoading(isLoading)
|
||||
}, [isLoading, setIsCollectionNftsLoading])
|
||||
|
||||
const [uniformHeight, setUniformHeight] = useState<UniformHeight>(UniformHeights.unset)
|
||||
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
|
||||
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
|
||||
|
||||
10
src/nft/components/collection/CollectionSearch.css.ts
Normal file
10
src/nft/components/collection/CollectionSearch.css.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { loadingAsset } from 'nft/css/loading.css'
|
||||
import { sprinkles } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const filterButtonLoading = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
border: 'none',
|
||||
}),
|
||||
])
|
||||
@@ -1,10 +1,14 @@
|
||||
import clsx from 'clsx'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import * as styles from 'nft/components/collection/CollectionSearch.css'
|
||||
import { useIsCollectionLoading } from 'nft/hooks'
|
||||
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
|
||||
import { FormEvent } from 'react'
|
||||
|
||||
export const CollectionSearch = () => {
|
||||
const setSearchByNameText = useCollectionFilters((state) => state.setSearch)
|
||||
const searchByNameText = useCollectionFilters((state) => state.search)
|
||||
const iscollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -19,7 +23,8 @@ export const CollectionSearch = () => {
|
||||
height="44"
|
||||
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
|
||||
value={searchByNameText}
|
||||
placeholder={'Search by name'}
|
||||
placeholder={iscollectionStatsLoading ? '' : 'Search by name'}
|
||||
className={clsx(iscollectionStatsLoading && styles.filterButtonLoading)}
|
||||
onChange={(e: FormEvent<HTMLInputElement>) => {
|
||||
setSearchByNameText(e.currentTarget.value)
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { loadingAsset, loadingBlock } from 'nft/css/loading.css'
|
||||
|
||||
import { breakpoints, sprinkles } from '../../css/sprinkles.css'
|
||||
|
||||
@@ -113,3 +114,54 @@ export const statsValue = style([
|
||||
lineHeight: '24px',
|
||||
},
|
||||
])
|
||||
|
||||
export const statsValueLoading = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
width: '60',
|
||||
height: '20',
|
||||
marginTop: '8',
|
||||
}),
|
||||
])
|
||||
|
||||
export const statsLabelLoading = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
width: '60',
|
||||
height: '16',
|
||||
}),
|
||||
])
|
||||
|
||||
export const descriptionLoading = style([
|
||||
loadingAsset,
|
||||
{
|
||||
maxWidth: 'min(calc(100% - 112px), 600px)',
|
||||
},
|
||||
])
|
||||
|
||||
export const collectionImageIsLoadingBackground = style([
|
||||
collectionImage,
|
||||
sprinkles({
|
||||
backgroundColor: 'backgroundSurface',
|
||||
}),
|
||||
])
|
||||
|
||||
export const collectionImageIsLoading = style([
|
||||
loadingBlock,
|
||||
collectionImage,
|
||||
sprinkles({
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '4px',
|
||||
borderColor: 'backgroundSurface',
|
||||
}),
|
||||
])
|
||||
|
||||
export const nameTextLoading = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
height: '32',
|
||||
}),
|
||||
{
|
||||
width: 236,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Column, Row } from 'nft/components/Flex'
|
||||
import { Marquee } from 'nft/components/layout/Marquee'
|
||||
import { headlineMedium } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
@@ -124,14 +125,13 @@ const CollectionName = ({
|
||||
collectionSocialsIsOpen: boolean
|
||||
toggleCollectionSocials: () => void
|
||||
}) => {
|
||||
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
const nameClass = isCollectionStatsLoading ? styles.nameTextLoading : clsx(headlineMedium, styles.nameText)
|
||||
|
||||
return (
|
||||
<Row justifyContent="space-between">
|
||||
<Row minWidth="0">
|
||||
<Box
|
||||
marginRight={!isVerified ? '12' : '0'}
|
||||
className={clsx(isMobile ? headlineMedium : headlineMedium, styles.nameText)}
|
||||
style={{ lineHeight: '32px' }}
|
||||
>
|
||||
<Box marginRight={!isVerified ? '12' : '0'} className={nameClass}>
|
||||
{name}
|
||||
</Box>
|
||||
{isVerified && <VerifiedIcon style={{ width: '32px', height: '32px' }} />}
|
||||
@@ -144,7 +144,7 @@ const CollectionName = ({
|
||||
height="32"
|
||||
>
|
||||
{collectionStats.discordUrl ? (
|
||||
<SocialsIcon href={collectionStats.discordUrl}>
|
||||
<SocialsIcon href={collectionStats.discordUrl ?? ''}>
|
||||
<DiscordIcon
|
||||
fill={themeVars.colors.textSecondary}
|
||||
color={themeVars.colors.textSecondary}
|
||||
@@ -170,7 +170,7 @@ const CollectionName = ({
|
||||
</SocialsIcon>
|
||||
) : null}
|
||||
{collectionStats.externalUrl ? (
|
||||
<SocialsIcon href={collectionStats.externalUrl}>
|
||||
<SocialsIcon href={collectionStats.externalUrl ?? ''}>
|
||||
<ExternalIcon fill={themeVars.colors.textSecondary} width="26px" height="26px" />
|
||||
</SocialsIcon>
|
||||
) : null}
|
||||
@@ -196,6 +196,7 @@ const CollectionDescription = ({ description }: { description: string }) => {
|
||||
const [readMore, toggleReadMore] = useReducer((state) => !state, false)
|
||||
const baseRef = useRef<HTMLDivElement>(null)
|
||||
const descriptionRef = useRef<HTMLDivElement>(null)
|
||||
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -209,7 +210,9 @@ const CollectionDescription = ({ description }: { description: string }) => {
|
||||
setShowReadMore(true)
|
||||
}, [descriptionRef, baseRef])
|
||||
|
||||
return (
|
||||
return isCollectionStatsLoading ? (
|
||||
<Box marginTop={{ sm: '12', md: '16' }} className={styles.descriptionLoading}></Box>
|
||||
) : (
|
||||
<Box ref={baseRef} marginTop={{ sm: '12', md: '16' }} style={{ maxWidth: '680px' }}>
|
||||
<Box
|
||||
ref={descriptionRef}
|
||||
@@ -228,29 +231,44 @@ const CollectionDescription = ({ description }: { description: string }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label: string; isMobile: boolean }) => (
|
||||
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min">
|
||||
<Box as="span" className={styles.statsLabel}>
|
||||
{`${label}${isMobile ? ': ' : ''}`}
|
||||
const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label: string; isMobile: boolean }) => {
|
||||
return (
|
||||
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min">
|
||||
<Box as="span" className={styles.statsLabel}>
|
||||
{`${label}${isMobile ? ': ' : ''}`}
|
||||
</Box>
|
||||
<span className={styles.statsValue}>{children}</span>
|
||||
</Box>
|
||||
<span className={styles.statsValue}>{children}</span>
|
||||
</Box>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => {
|
||||
const numOwnersStr = stats.stats ? putCommas(stats.stats.num_owners) : 0
|
||||
const totalSupplyStr = stats.stats ? putCommas(stats.stats.total_supply) : 0
|
||||
const totalListingsStr = stats.stats ? putCommas(stats.stats.total_listings) : 0
|
||||
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
|
||||
// round daily volume & floorPrice to 3 decimals or less
|
||||
const totalVolumeStr = ethNumberStandardFormatter(stats.stats?.total_volume)
|
||||
const floorPriceStr = ethNumberStandardFormatter(stats.floorPrice)
|
||||
|
||||
const statsLoadingSkeleton = new Array(5).fill(
|
||||
<>
|
||||
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min">
|
||||
<div className={styles.statsLabelLoading} />
|
||||
<span className={styles.statsValueLoading} />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Row gap={{ sm: '20', md: '60' }} {...props}>
|
||||
<StatsItem label="Items" isMobile={isMobile ?? false}>
|
||||
{totalSupplyStr}
|
||||
</StatsItem>
|
||||
{isCollectionStatsLoading && statsLoadingSkeleton}
|
||||
{totalSupplyStr ? (
|
||||
<StatsItem label="Items" isMobile={isMobile ?? false}>
|
||||
{totalSupplyStr}
|
||||
</StatsItem>
|
||||
) : null}
|
||||
{numOwnersStr ? (
|
||||
<StatsItem label="Owners" isMobile={isMobile ?? false}>
|
||||
{numOwnersStr}
|
||||
@@ -277,10 +295,7 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
|
||||
|
||||
export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; isMobile: boolean }) => {
|
||||
const [collectionSocialsIsOpen, toggleCollectionSocials] = useReducer((state) => !state, false)
|
||||
|
||||
if (!stats) {
|
||||
return <div>Loading CollectionStats...</div>
|
||||
}
|
||||
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -291,11 +306,15 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i
|
||||
flexDirection="column"
|
||||
width="full"
|
||||
>
|
||||
{isCollectionStatsLoading && (
|
||||
<Box as="div" borderRadius="round" position="absolute" className={styles.collectionImageIsLoadingBackground} />
|
||||
)}
|
||||
<Box
|
||||
as="img"
|
||||
as={isCollectionStatsLoading ? 'div' : 'img'}
|
||||
background="explicitWhite"
|
||||
borderRadius="round"
|
||||
position="absolute"
|
||||
className={styles.collectionImage}
|
||||
className={isCollectionStatsLoading ? styles.collectionImageIsLoading : styles.collectionImage}
|
||||
src={stats.isFoundation && !stats.imageUrl ? '/nft/svgs/marketplaces/foundation.svg' : stats.imageUrl}
|
||||
/>
|
||||
<Box className={styles.statsText}>
|
||||
@@ -309,7 +328,9 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i
|
||||
/>
|
||||
{!isMobile && (
|
||||
<>
|
||||
{stats.description && <CollectionDescription description={stats.description} />}
|
||||
{(stats.description || isCollectionStatsLoading) && (
|
||||
<CollectionDescription description={stats.description} />
|
||||
)}
|
||||
<StatsRow stats={stats} marginTop="20" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { loadingAsset } from 'nft/css/loading.css'
|
||||
import { sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const filterButton = sprinkles({
|
||||
@@ -21,3 +22,11 @@ export const filterBadge = style([
|
||||
top: '-3px',
|
||||
},
|
||||
])
|
||||
|
||||
export const filterButtonLoading = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
height: '44',
|
||||
width: '100',
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box } from 'nft/components/Box'
|
||||
import * as styles from 'nft/components/collection/FilterButton.css'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { FilterIcon } from 'nft/components/icons'
|
||||
import { useCollectionFilters, useWalletCollections } from 'nft/hooks'
|
||||
import { useCollectionFilters, useIsCollectionLoading, useWalletCollections } from 'nft/hooks'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -31,14 +31,19 @@ export const FilterButton = ({
|
||||
}))
|
||||
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
|
||||
const { pathname } = useLocation()
|
||||
const isSellPage = pathname.startsWith('/nfts/sell')
|
||||
const isProfilePage = pathname.startsWith('/profile')
|
||||
const isCollectionNftsLoading = useIsCollectionLoading((state) => state.isCollectionNftsLoading)
|
||||
|
||||
const showFilterBadge = isSellPage
|
||||
const showFilterBadge = isProfilePage
|
||||
? collectionFilters.length > 0
|
||||
: minPrice || maxPrice || minRarity || maxRarity || traits.length || markets.length || buyNow
|
||||
return (
|
||||
<Box
|
||||
className={clsx(styles.filterButton, !isFiltersExpanded && styles.filterButtonExpanded)}
|
||||
className={
|
||||
isCollectionNftsLoading
|
||||
? styles.filterButtonLoading
|
||||
: clsx(styles.filterButton, !isFiltersExpanded && styles.filterButtonExpanded)
|
||||
}
|
||||
borderRadius="12"
|
||||
fontSize="16"
|
||||
cursor="pointer"
|
||||
@@ -52,14 +57,20 @@ export const FilterButton = ({
|
||||
height="44"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{showFilterBadge && (
|
||||
<Row className={styles.filterBadge} color={isFiltersExpanded ? 'grey700' : 'blue400'}>
|
||||
•
|
||||
</Row>
|
||||
{!isCollectionNftsLoading && (
|
||||
<>
|
||||
{showFilterBadge && (
|
||||
<Row className={styles.filterBadge} color={isFiltersExpanded ? 'grey700' : 'blue400'}>
|
||||
•
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<FilterIcon
|
||||
style={{ marginBottom: '-4px', paddingRight: `${!isFiltersExpanded || showFilterBadge ? '6px' : '0px'}` }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FilterIcon
|
||||
style={{ marginBottom: '-4px', paddingRight: `${!isFiltersExpanded || showFilterBadge ? '6px' : '0px'}` }}
|
||||
/>
|
||||
|
||||
{!isMobile && !isFiltersExpanded && 'Filter'}
|
||||
|
||||
{showFilterBadge && !isMobile ? (
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { loadingAsset } from 'nft/css/loading.css'
|
||||
import { sprinkles } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const activeDropdown = style({
|
||||
borderBottom: 'none',
|
||||
@@ -7,3 +9,13 @@ export const activeDropdown = style({
|
||||
export const activeDropDownItems = style({
|
||||
borderTop: 'none',
|
||||
})
|
||||
|
||||
export const isLoadingDropdown = style([
|
||||
loadingAsset,
|
||||
sprinkles({
|
||||
height: '44',
|
||||
}),
|
||||
{
|
||||
width: 220,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Row } from 'nft/components/Flex'
|
||||
import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/icons'
|
||||
import { buttonTextMedium } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsCollectionLoading } from 'nft/hooks'
|
||||
import { DropDownOption } from 'nft/types'
|
||||
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
|
||||
@@ -28,6 +29,7 @@ export const SortDropdown = ({
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const [isReversed, toggleReversed] = useReducer((s) => !s, false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
|
||||
|
||||
const [maxWidth, setMaxWidth] = useState(0)
|
||||
|
||||
@@ -41,6 +43,8 @@ export const SortDropdown = ({
|
||||
[selectedIndex, dropDownOptions]
|
||||
)
|
||||
|
||||
const width = isCollectionStatsLoading ? 220 : inFilters ? 'full' : mini ? 'min' : maxWidth ? maxWidth : '300px'
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
@@ -49,7 +53,7 @@ export const SortDropdown = ({
|
||||
borderBottomLeftRadius={isOpen ? '0' : undefined}
|
||||
borderBottomRightRadius={isOpen ? '0' : undefined}
|
||||
height="44"
|
||||
style={{ width: inFilters ? 'full' : mini ? 'min' : maxWidth ? maxWidth : '300px' }}
|
||||
style={{ width }}
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
@@ -70,51 +74,56 @@ export const SortDropdown = ({
|
||||
width={inFilters ? 'full' : 'inherit'}
|
||||
onClick={toggleOpen}
|
||||
cursor="pointer"
|
||||
className={clsx(isOpen && !mini && styles.activeDropdown)}
|
||||
className={isCollectionStatsLoading ? styles.isLoadingDropdown : clsx(isOpen && !mini && styles.activeDropdown)}
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
{!isOpen && reversable && (
|
||||
<Row
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
{!isCollectionStatsLoading && (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
{!isOpen && reversable && (
|
||||
<Row
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (dropDownOptions[selectedIndex].reverseOnClick) {
|
||||
dropDownOptions[selectedIndex].reverseOnClick?.()
|
||||
toggleReversed()
|
||||
} else {
|
||||
const dropdownIndex = dropDownOptions[selectedIndex].reverseIndex ?? 1
|
||||
dropDownOptions[dropdownIndex - 1].onClick()
|
||||
setSelectedIndex(dropdownIndex - 1)
|
||||
}
|
||||
if (dropDownOptions[selectedIndex].reverseOnClick) {
|
||||
dropDownOptions[selectedIndex].reverseOnClick?.()
|
||||
toggleReversed()
|
||||
} else {
|
||||
const dropdownIndex = dropDownOptions[selectedIndex].reverseIndex ?? 1
|
||||
dropDownOptions[dropdownIndex - 1].onClick()
|
||||
setSelectedIndex(dropdownIndex - 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dropDownOptions[selectedIndex].reverseOnClick &&
|
||||
(isReversed ? <ArrowsIcon /> : <ReversedArrowsIcon />)}
|
||||
{dropDownOptions[selectedIndex].reverseIndex &&
|
||||
(selectedIndex > (dropDownOptions[selectedIndex].reverseIndex ?? 1) - 1 ? (
|
||||
<ArrowsIcon />
|
||||
) : (
|
||||
<ReversedArrowsIcon />
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Box
|
||||
marginLeft={reversable ? '4' : '0'}
|
||||
marginRight={mini ? '2' : '0'}
|
||||
color="textPrimary"
|
||||
className={buttonTextMedium}
|
||||
>
|
||||
{mini ? miniPrompt : isOpen ? 'Sort by' : dropDownOptions[selectedIndex].displayText}
|
||||
</Box>
|
||||
</Box>
|
||||
<ChevronUpIcon
|
||||
secondaryColor={mini ? themeVars.colors.textPrimary : undefined}
|
||||
secondaryWidth={mini ? '20' : undefined}
|
||||
secondaryHeight={mini ? '20' : undefined}
|
||||
style={{
|
||||
transform: isOpen ? '' : 'rotate(180deg)',
|
||||
}}
|
||||
>
|
||||
{dropDownOptions[selectedIndex].reverseOnClick && (isReversed ? <ArrowsIcon /> : <ReversedArrowsIcon />)}
|
||||
{dropDownOptions[selectedIndex].reverseIndex &&
|
||||
(selectedIndex > (dropDownOptions[selectedIndex].reverseIndex ?? 1) - 1 ? (
|
||||
<ArrowsIcon />
|
||||
) : (
|
||||
<ReversedArrowsIcon />
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
<Box
|
||||
marginLeft={reversable ? '4' : '0'}
|
||||
marginRight={mini ? '2' : '0'}
|
||||
color="textPrimary"
|
||||
className={buttonTextMedium}
|
||||
>
|
||||
{mini ? miniPrompt : isOpen ? 'Sort by' : dropDownOptions[selectedIndex].displayText}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ChevronUpIcon
|
||||
secondaryColor={mini ? themeVars.colors.textPrimary : undefined}
|
||||
secondaryWidth={mini ? '20' : undefined}
|
||||
secondaryHeight={mini ? '20' : undefined}
|
||||
style={{
|
||||
transform: isOpen ? '' : 'rotate(180deg)',
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
|
||||
@@ -35,7 +35,7 @@ export const WithCommaCell = ({ value }: CellProps) => <span>{value.value ? putC
|
||||
|
||||
export const EthCell = ({ value }: { value: number }) => (
|
||||
<Row justifyContent="flex-end" color="textPrimary">
|
||||
{value ? <>{formatWeiToDecimal(value.toString())} ETH</> : '-'}
|
||||
{value ? <>{formatWeiToDecimal(value.toString(), true)} ETH</> : '-'}
|
||||
</Row>
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ export const EthWithDayChange = ({ value }: CellProps) => (
|
||||
export const WeiWithDayChange = ({ value }: CellProps) => (
|
||||
<Column gap="4">
|
||||
<Row justifyContent="flex-end" color="textPrimary">
|
||||
{value && value.value ? <>{formatWeiToDecimal(value.value.toString())} ETH</> : '-'}
|
||||
{value && value.value ? <>{formatWeiToDecimal(value.value.toString(), true)} ETH</> : '-'}
|
||||
</Row>
|
||||
{value.change ? (
|
||||
<Box
|
||||
|
||||
@@ -1471,3 +1471,30 @@ export const BagCloseIcon = (props: SVGProps) => (
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const EmptyNFTWalletIcon = (props: SVGProps) => (
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<rect
|
||||
x="4.46976"
|
||||
y="35.0579"
|
||||
width="43.9542"
|
||||
height="43.9542"
|
||||
rx="2.75"
|
||||
transform="rotate(-11.245 4.46976 35.0579)"
|
||||
stroke="#5E6887"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M32.0341 22.8646L34.3119 14.3637C34.8837 12.2298 37.077 10.9635 39.2109 11.5353L76.3548 21.488C78.4886 22.0597 79.755 24.2531 79.1832 26.3869L69.2305 63.5308C68.6588 65.6647 66.4654 66.931 64.3315 66.3593L60.3421 65.2903"
|
||||
stroke="#5E6887"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M81.5762 40.2598L90.5463 49.2299C92.1084 50.792 92.1084 53.3246 90.5463 54.8867L63.355 82.0779C61.7929 83.64 59.2603 83.64 57.6982 82.0779L52.3573 76.737"
|
||||
stroke="#5E6887"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -26,8 +26,15 @@ import {
|
||||
subheadSmall,
|
||||
} from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useBag, useNFTList, useSellAsset, useSellPageState } from 'nft/hooks'
|
||||
import { DropDownOption, ListingMarket, ListingStatus, ListingWarning, SellPageStateType, WalletAsset } from 'nft/types'
|
||||
import { useBag, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import {
|
||||
DropDownOption,
|
||||
ListingMarket,
|
||||
ListingStatus,
|
||||
ListingWarning,
|
||||
ProfilePageStateType,
|
||||
WalletAsset,
|
||||
} from 'nft/types'
|
||||
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { ListingMarkets } from 'nft/utils/listNfts'
|
||||
@@ -834,7 +841,7 @@ const NFTListRow = ({ asset, globalPriceMethod, globalPrice, setGlobalPrice, sel
|
||||
}
|
||||
|
||||
export const ListPage = () => {
|
||||
const { setSellPageState } = useSellPageState()
|
||||
const { setProfilePageState: setSellPageState } = useProfilePageState()
|
||||
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
|
||||
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[2]]) // default marketplace: x2y2
|
||||
const toggleBag = useBag((s) => s.toggleBag)
|
||||
@@ -869,7 +876,7 @@ export const ListPage = () => {
|
||||
aria-label="Back"
|
||||
as="button"
|
||||
border="none"
|
||||
onClick={() => setSellPageState(SellPageStateType.SELECTING)}
|
||||
onClick={() => setSellPageState(ProfilePageStateType.VIEWING)}
|
||||
type="button"
|
||||
backgroundColor="transparent"
|
||||
cursor="pointer"
|
||||
@@ -3,8 +3,8 @@ import { Box } from 'nft/components/Box'
|
||||
import { Column } from 'nft/components/Flex'
|
||||
import { CloseDropDownIcon } from 'nft/components/icons'
|
||||
import { bodySmall, buttonMedium, headlineSmall } from 'nft/css/common.css'
|
||||
import { useBag, useIsMobile, useSellAsset, useSellPageState } from 'nft/hooks'
|
||||
import { SellPageStateType } from 'nft/types'
|
||||
import { useBag, useIsMobile, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -15,10 +15,10 @@ const ListingModal = lazy(() => import('./ListingModal'))
|
||||
|
||||
const Cart = () => {
|
||||
const { pathname } = useLocation()
|
||||
const isNFTSellPage = pathname.startsWith('/nfts/sell')
|
||||
const isProfilePage = pathname.startsWith('/profile')
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const setSellPageState = useSellPageState((state) => state.setSellPageState)
|
||||
const sellPageState = useSellPageState((state) => state.state)
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const sellPageState = useProfilePageState((state) => state.state)
|
||||
const toggleCart = useBag((state) => state.toggleBag)
|
||||
const isMobile = useIsMobile()
|
||||
const bagExpanded = useBag((s) => s.bagExpanded)
|
||||
@@ -32,7 +32,7 @@ const Cart = () => {
|
||||
left={{ sm: '0', md: 'unset' }}
|
||||
right={{ sm: 'unset', md: '0' }}
|
||||
top={{ sm: '0', md: 'unset' }}
|
||||
display={bagExpanded && isNFTSellPage ? 'flex' : 'none'}
|
||||
display={bagExpanded && isProfilePage ? 'flex' : 'none'}
|
||||
>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Column
|
||||
@@ -45,7 +45,7 @@ const Cart = () => {
|
||||
marginLeft="0"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{sellPageState === SellPageStateType.LISTING ? (
|
||||
{sellPageState === ProfilePageStateType.LISTING ? (
|
||||
<ListingModal />
|
||||
) : (
|
||||
<>
|
||||
@@ -70,7 +70,7 @@ const Cart = () => {
|
||||
disabled={sellAssets.length === 0}
|
||||
onClick={() => {
|
||||
isMobile && toggleCart()
|
||||
setSellPageState(SellPageStateType.LISTING)
|
||||
setSellPageState(ProfilePageStateType.LISTING)
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
48
src/nft/components/profile/view/EmptyWalletContent.tsx
Normal file
48
src/nft/components/profile/view/EmptyWalletContent.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { EmptyNFTWalletIcon } from 'nft/components/icons'
|
||||
import { headlineMedium } from 'nft/css/common.css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { shortenAddress } from 'utils'
|
||||
|
||||
const EmptyWalletContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 190px;
|
||||
flex: none;
|
||||
`
|
||||
|
||||
const EmptyWalletText = styled.div`
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
margin-top: 12px;
|
||||
`
|
||||
|
||||
const ExploreNFTsButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
padding: 10px 24px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
width: min-content;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
`
|
||||
|
||||
export const EmptyWalletContent = () => {
|
||||
const { account } = useWeb3React()
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<EmptyWalletContainer>
|
||||
<EmptyNFTWalletIcon />
|
||||
<EmptyWalletText className={headlineMedium}>
|
||||
<Trans>No NFTs in</Trans> {shortenAddress(account ?? '')}
|
||||
</EmptyWalletText>
|
||||
<ExploreNFTsButton onClick={() => navigate('/nfts')}>Explore NFTs</ExploreNFTsButton>
|
||||
</EmptyWalletContainer>
|
||||
)
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useFiltersExpanded, useIsMobile, useWalletCollections } from 'nft/hooks'
|
||||
import { WalletCollection } from 'nft/types'
|
||||
import { Dispatch, FormEvent, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import { useSpring } from 'react-spring'
|
||||
|
||||
import * as styles from './SelectPage.css'
|
||||
import * as styles from './ProfilePage.css'
|
||||
|
||||
export const FilterSidebar = ({ SortDropdown }: { SortDropdown: () => JSX.Element }) => {
|
||||
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
|
||||
@@ -35,7 +35,7 @@ export const FilterSidebar = ({ SortDropdown }: { SortDropdown: () => JSX.Elemen
|
||||
height={{ sm: 'full', md: 'auto' }}
|
||||
zIndex={{ sm: '3', md: 'auto' }}
|
||||
display={isFiltersExpanded ? 'flex' : 'none'}
|
||||
style={{ transform: sidebarX.interpolate((x) => `translateX(${x}px)`) }}
|
||||
style={{ transform: sidebarX.to((x) => `translateX(${x}px)`) }}
|
||||
>
|
||||
<Box
|
||||
paddingTop={{ sm: '24', md: '0' }}
|
||||
40
src/nft/components/profile/view/ProfileAccountDetails.tsx
Normal file
40
src/nft/components/profile/view/ProfileAccountDetails.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Identicon from 'components/Identicon'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { caption, headlineLarge, lightGrayOverlayOnHover } from 'nft/css/common.css'
|
||||
import { useCallback } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import { shortenAddress } from 'utils'
|
||||
|
||||
export const ProfileAccountDetails = () => {
|
||||
const { account, ENSName } = useWeb3React()
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
const copy = useCallback(() => {
|
||||
setCopied(account ?? '')
|
||||
}, [account, setCopied])
|
||||
|
||||
return account ? (
|
||||
<Row className={headlineLarge} marginBottom="48" gap="4">
|
||||
<Identicon size={44} />
|
||||
<Box textOverflow="ellipsis" overflow="hidden" marginLeft="8">
|
||||
{ENSName ?? shortenAddress(account)}
|
||||
</Box>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Box className={caption} color="textPrimary">
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
</Box>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Box paddingX="12" borderRadius="12" cursor="pointer" className={lightGrayOverlayOnHover} onClick={copy}>
|
||||
<Copy strokeWidth={1.5} size={20} />{' '}
|
||||
</Box>
|
||||
</MouseoverTooltip>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
@@ -14,27 +14,29 @@ import {
|
||||
TagFillIcon,
|
||||
VerifiedIcon,
|
||||
} from 'nft/components/icons'
|
||||
import { FilterSidebar } from 'nft/components/sell/select/FilterSidebar'
|
||||
import { FilterSidebar } from 'nft/components/profile/view/FilterSidebar'
|
||||
import { subhead, subheadSmall } from 'nft/css/common.css'
|
||||
import { vars } from 'nft/css/sprinkles.css'
|
||||
import {
|
||||
useBag,
|
||||
useFiltersExpanded,
|
||||
useIsMobile,
|
||||
useProfilePageState,
|
||||
useSellAsset,
|
||||
useSellPageState,
|
||||
useWalletBalance,
|
||||
useWalletCollections,
|
||||
} from 'nft/hooks'
|
||||
import { fetchMultipleCollectionStats, fetchWalletAssets, OSCollectionsFetcher } from 'nft/queries'
|
||||
import { DropDownOption, SellPageStateType, WalletAsset, WalletCollection } from 'nft/types'
|
||||
import { DropDownOption, ProfilePageStateType, WalletAsset, WalletCollection } from 'nft/types'
|
||||
import { Dispatch, FormEvent, SetStateAction, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useInfiniteQuery, useQuery } from 'react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import { useSpring } from 'react-spring'
|
||||
|
||||
import * as styles from './SelectPage.css'
|
||||
import { EmptyWalletContent } from './EmptyWalletContent'
|
||||
import { ProfileAccountDetails } from './ProfileAccountDetails'
|
||||
import * as styles from './ProfilePage.css'
|
||||
|
||||
enum SortBy {
|
||||
FloorPrice,
|
||||
@@ -58,7 +60,7 @@ function roundFloorPrice(price?: number, n?: number) {
|
||||
return price ? Math.round(price * Math.pow(10, n ?? 3) + Number.EPSILON) / Math.pow(10, n ?? 3) : 0
|
||||
}
|
||||
|
||||
export const SelectPage = () => {
|
||||
export const ProfilePage = () => {
|
||||
const { address } = useWalletBalance()
|
||||
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
|
||||
const setCollectionFilters = useWalletCollections((state) => state.setCollectionFilters)
|
||||
@@ -115,7 +117,7 @@ export const SelectPage = () => {
|
||||
const listFilter = useWalletCollections((state) => state.listFilter)
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const reset = useSellAsset((state) => state.reset)
|
||||
const setSellPageState = useSellPageState((state) => state.setSellPageState)
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const [sortBy, setSortBy] = useState(SortBy.DateAcquired)
|
||||
const [orderByASC, setOrderBy] = useState(true)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
@@ -245,69 +247,72 @@ export const SelectPage = () => {
|
||||
const SortWalletAssetsDropdown = () => <SortDropdown dropDownOptions={sortDropDownOptions} />
|
||||
|
||||
return (
|
||||
<Column width="full">
|
||||
<Row
|
||||
alignItems="flex-start"
|
||||
position="relative"
|
||||
paddingLeft={{ sm: '16', md: '52' }}
|
||||
paddingRight={{ sm: '0', md: '72' }}
|
||||
paddingTop={{ sm: '16', md: '40' }}
|
||||
>
|
||||
<FilterSidebar SortDropdown={SortWalletAssetsDropdown} />
|
||||
<Column
|
||||
width="full"
|
||||
paddingLeft={{ sm: '16', md: '52' }}
|
||||
paddingRight={{ sm: '0', md: '72' }}
|
||||
paddingTop={{ sm: '16', md: '40' }}
|
||||
>
|
||||
{walletAssets.length === 0 ? (
|
||||
<EmptyWalletContent />
|
||||
) : (
|
||||
<Row alignItems="flex-start" position="relative">
|
||||
<FilterSidebar SortDropdown={SortWalletAssetsDropdown} />
|
||||
|
||||
{(!isMobile || !isFiltersExpanded) && (
|
||||
// @ts-ignore
|
||||
<AnimatedBox
|
||||
paddingLeft={isFiltersExpanded ? '24' : '16'}
|
||||
flexShrink="0"
|
||||
style={{
|
||||
transform: gridX.interpolate(
|
||||
(x) => `translate(${Number(x) - (!isMobile && isFiltersExpanded ? 300 : 0)}px)`
|
||||
),
|
||||
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x}px)`),
|
||||
}}
|
||||
>
|
||||
<Row gap="8" flexWrap="nowrap">
|
||||
<FilterButton
|
||||
isMobile={isMobile}
|
||||
isFiltersExpanded={isFiltersExpanded}
|
||||
results={displayAssets.length}
|
||||
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
|
||||
/>
|
||||
{!isMobile && <SortDropdown dropDownOptions={sortDropDownOptions} />}
|
||||
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
|
||||
<SelectAllButton />
|
||||
</Row>
|
||||
<Row>
|
||||
<CollectionFiltersRow
|
||||
collections={walletCollections}
|
||||
collectionFilters={collectionFilters}
|
||||
setCollectionFilters={setCollectionFilters}
|
||||
clearCollectionFilters={clearCollectionFilters}
|
||||
/>
|
||||
</Row>
|
||||
<InfiniteScroll
|
||||
next={fetchNextPage}
|
||||
hasMore={hasNextPage ?? false}
|
||||
loader={
|
||||
hasNextPage ? (
|
||||
<Center>
|
||||
<LoadingSparkle />
|
||||
</Center>
|
||||
) : null
|
||||
}
|
||||
dataLength={displayAssets.length}
|
||||
style={{ overflow: 'unset' }}
|
||||
>
|
||||
<div className={assetList}>
|
||||
{displayAssets && displayAssets.length
|
||||
? displayAssets.map((asset, index) => <WalletAssetDisplay asset={asset} key={index} />)
|
||||
: null}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
</AnimatedBox>
|
||||
)}
|
||||
</Row>
|
||||
{(!isMobile || !isFiltersExpanded) && (
|
||||
<Column width="full">
|
||||
<ProfileAccountDetails />
|
||||
<AnimatedBox
|
||||
paddingLeft={isFiltersExpanded ? '24' : '16'}
|
||||
flexShrink="0"
|
||||
style={{
|
||||
transform: gridX.to((x) => `translate(${Number(x) - (!isMobile && isFiltersExpanded ? 300 : 0)}px)`),
|
||||
width: gridWidthOffset.to((x) => `calc(100% - ${x}px)`),
|
||||
}}
|
||||
>
|
||||
<Row gap="8" flexWrap="nowrap">
|
||||
<FilterButton
|
||||
isMobile={isMobile}
|
||||
isFiltersExpanded={isFiltersExpanded}
|
||||
results={displayAssets.length}
|
||||
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
|
||||
/>
|
||||
{!isMobile && <SortDropdown dropDownOptions={sortDropDownOptions} />}
|
||||
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
|
||||
<SelectAllButton />
|
||||
</Row>
|
||||
<Row>
|
||||
<CollectionFiltersRow
|
||||
collections={walletCollections}
|
||||
collectionFilters={collectionFilters}
|
||||
setCollectionFilters={setCollectionFilters}
|
||||
clearCollectionFilters={clearCollectionFilters}
|
||||
/>
|
||||
</Row>
|
||||
<InfiniteScroll
|
||||
next={fetchNextPage}
|
||||
hasMore={hasNextPage ?? false}
|
||||
loader={
|
||||
hasNextPage ? (
|
||||
<Center>
|
||||
<LoadingSparkle />
|
||||
</Center>
|
||||
) : null
|
||||
}
|
||||
dataLength={displayAssets.length}
|
||||
style={{ overflow: 'unset' }}
|
||||
>
|
||||
<div className={assetList}>
|
||||
{displayAssets && displayAssets.length
|
||||
? displayAssets.map((asset, index) => <WalletAssetDisplay asset={asset} key={index} />)
|
||||
: null}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
</AnimatedBox>
|
||||
</Column>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
{sellAssets.length > 0 && (
|
||||
<Row
|
||||
display={{ sm: 'flex', md: 'none' }}
|
||||
@@ -340,7 +345,7 @@ export const SelectPage = () => {
|
||||
fontSize="14"
|
||||
cursor="pointer"
|
||||
backgroundColor="genieBlue"
|
||||
onClick={() => setSellPageState(SellPageStateType.LISTING)}
|
||||
onClick={() => setSellPageState(ProfilePageStateType.LISTING)}
|
||||
lineHeight="16"
|
||||
borderRadius="12"
|
||||
padding="8"
|
||||
@@ -386,7 +391,7 @@ export const WalletAssetDisplay = ({ asset }: { asset: WalletAsset }) => {
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}?origin=sell`}
|
||||
to={`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}?origin=profile`}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<Column
|
||||
@@ -1,15 +1,16 @@
|
||||
export * from './useBag'
|
||||
export * from './useCollectionFilters'
|
||||
export * from './useFiltersExpanded'
|
||||
export * from './useIsCollectionLoading'
|
||||
export * from './useIsMobile'
|
||||
export * from './useIsTablet'
|
||||
export * from './useMarketplaceSelect'
|
||||
export * from './useNFTList'
|
||||
export * from './useNFTSelect'
|
||||
export * from './useProfilePageState'
|
||||
export * from './useSearchHistory'
|
||||
export * from './useSelectAsset'
|
||||
export * from './useSellAsset'
|
||||
export * from './useSellPageState'
|
||||
export * from './useSendTransaction'
|
||||
export * from './useSweep'
|
||||
export * from './useTransactionResponse'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import create from 'zustand'
|
||||
@@ -8,6 +9,10 @@ interface BagState {
|
||||
setBagStatus: (state: BagStatus) => void
|
||||
itemsInBag: BagItem[]
|
||||
setItemsInBag: (items: BagItem[]) => void
|
||||
totalEthPrice: BigNumber
|
||||
setTotalEthPrice: (totalEthPrice: BigNumber) => void
|
||||
totalUsdPrice: number | undefined
|
||||
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
|
||||
addAssetToBag: (asset: UpdatedGenieAsset) => void
|
||||
removeAssetFromBag: (asset: UpdatedGenieAsset) => void
|
||||
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
|
||||
@@ -61,6 +66,16 @@ export const useBag = create<BagState>()(
|
||||
set(() => ({
|
||||
itemsInBag: items,
|
||||
})),
|
||||
totalEthPrice: BigNumber.from(0),
|
||||
setTotalEthPrice: (totalEthPrice) =>
|
||||
set(() => ({
|
||||
totalEthPrice,
|
||||
})),
|
||||
totalUsdPrice: undefined,
|
||||
setTotalUsdPrice: (totalUsdPrice) =>
|
||||
set(() => ({
|
||||
totalUsdPrice,
|
||||
})),
|
||||
addAssetToBag: (asset) =>
|
||||
set(({ itemsInBag }) => {
|
||||
if (get().isLocked) return { itemsInBag: get().itemsInBag }
|
||||
|
||||
27
src/nft/hooks/useIsCollectionLoading.ts
Normal file
27
src/nft/hooks/useIsCollectionLoading.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import create from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface State {
|
||||
isCollectionNftsLoading: boolean
|
||||
setIsCollectionNftsLoading: (isCollectionNftsLoading: boolean) => void
|
||||
isCollectionStatsLoading: boolean
|
||||
setIsCollectionStatsLoading: (isCollectionStatsLoading: boolean) => void
|
||||
}
|
||||
|
||||
export const useIsCollectionLoading = create<State>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
isCollectionNftsLoading: false,
|
||||
setIsCollectionNftsLoading: (isCollectionNftsLoading) =>
|
||||
set(() => {
|
||||
return { isCollectionNftsLoading }
|
||||
}),
|
||||
isCollectionStatsLoading: false,
|
||||
setIsCollectionStatsLoading: (isCollectionStatsLoading) =>
|
||||
set(() => {
|
||||
return { isCollectionStatsLoading }
|
||||
}),
|
||||
}),
|
||||
{ name: 'useIsCollectionLoading' }
|
||||
)
|
||||
)
|
||||
25
src/nft/hooks/useProfilePageState.ts
Normal file
25
src/nft/hooks/useProfilePageState.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import create from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { ProfilePageStateType } from '../types'
|
||||
|
||||
interface profilePageState {
|
||||
/**
|
||||
* State of user settings
|
||||
*/
|
||||
state: ProfilePageStateType
|
||||
setProfilePageState: (state: ProfilePageStateType) => void
|
||||
}
|
||||
|
||||
export const useProfilePageState = create<profilePageState>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
state: ProfilePageStateType.VIEWING,
|
||||
setProfilePageState: (newState) =>
|
||||
set(() => ({
|
||||
state: newState,
|
||||
})),
|
||||
}),
|
||||
{ name: 'useProfilePageState' }
|
||||
)
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
import create from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { SellPageStateType } from '../types'
|
||||
|
||||
interface sellPageState {
|
||||
/**
|
||||
* State of user settings
|
||||
*/
|
||||
state: SellPageStateType
|
||||
setSellPageState: (state: SellPageStateType) => void
|
||||
}
|
||||
|
||||
export const useSellPageState = create<sellPageState>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
state: SellPageStateType.SELECTING,
|
||||
setSellPageState: (newState) =>
|
||||
set(() => ({
|
||||
state: newState,
|
||||
})),
|
||||
}),
|
||||
{ name: 'useSellPageState' }
|
||||
)
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useQuery } from 'react-query'
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import { useSpring } from 'react-spring'
|
||||
|
||||
import { MouseoverTooltip } from '../../../components/Tooltip/index'
|
||||
import { AnimatedBox, Box } from '../../components/Box'
|
||||
@@ -182,7 +182,7 @@ const Asset = () => {
|
||||
<AnimatedBox
|
||||
style={{
|
||||
// @ts-ignore
|
||||
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x}px)`),
|
||||
width: gridWidthOffset.to((x) => `calc(100% - ${x}px)`),
|
||||
}}
|
||||
className={styles.container}
|
||||
>
|
||||
@@ -254,8 +254,8 @@ const Asset = () => {
|
||||
onClick={() => {
|
||||
if (!parsed.origin || parsed.origin === 'collection') {
|
||||
navigate(`/nfts/collection/${asset.address}`)
|
||||
} else if (parsed.origin === 'sell') {
|
||||
navigate('/nfts/sell', undefined)
|
||||
} else if (parsed.origin === 'profile') {
|
||||
navigate('/profile', undefined)
|
||||
} else if (parsed.origin === 'explore') {
|
||||
navigate(`/nfts`, undefined)
|
||||
} else if (parsed.origin === 'activity') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { buttonTextMedium } from 'nft/css/common.css'
|
||||
import { loadingBlock } from 'nft/css/loading.css'
|
||||
|
||||
import { sprinkles, vars } from '../../css/sprinkles.css'
|
||||
|
||||
@@ -46,6 +47,14 @@ export const selectedActivitySwitcherToggle = style([
|
||||
},
|
||||
])
|
||||
|
||||
export const loadingBanner = style([
|
||||
loadingBlock,
|
||||
sprinkles({
|
||||
width: 'full',
|
||||
height: '100',
|
||||
}),
|
||||
])
|
||||
|
||||
export const noCollectionAssets = sprinkles({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag'
|
||||
import { AnimatedBox, Box } from 'nft/components/Box'
|
||||
import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters } from 'nft/components/collection'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
|
||||
import { useBag, useCollectionFilters, useFiltersExpanded, useIsCollectionLoading, useIsMobile } from 'nft/hooks'
|
||||
import * as styles from 'nft/pages/collection/index.css'
|
||||
import { CollectionStatsFetcher } from 'nft/queries'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { useEffect } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
|
||||
import * as styles from './index.css'
|
||||
import { useSpring } from 'react-spring'
|
||||
|
||||
const FILTER_WIDTH = 332
|
||||
const BAG_WIDTH = 324
|
||||
|
||||
const Collection = () => {
|
||||
const { contractAddress } = useParams()
|
||||
const setIsCollectionStatsLoading = useIsCollectionLoading((state) => state.setIsCollectionStatsLoading)
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
|
||||
@@ -28,6 +30,10 @@ const Collection = () => {
|
||||
CollectionStatsFetcher(contractAddress as string)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollectionStatsLoading(isLoading)
|
||||
}, [isLoading, setIsCollectionStatsLoading])
|
||||
|
||||
const { gridX, gridWidthOffset } = useSpring({
|
||||
gridX: isFiltersExpanded ? FILTER_WIDTH : 0,
|
||||
gridWidthOffset: isFiltersExpanded
|
||||
@@ -54,69 +60,83 @@ const Collection = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Column width="full">
|
||||
{collectionStats && contractAddress ? (
|
||||
<>
|
||||
{' '}
|
||||
<Box width="full" height="160">
|
||||
<Box
|
||||
as="img"
|
||||
maxHeight="full"
|
||||
width="full"
|
||||
src={collectionStats?.bannerImageUrl}
|
||||
className={`${styles.bannerImage}`}
|
||||
/>
|
||||
</Box>
|
||||
<Column paddingX="32">
|
||||
{collectionStats && <CollectionStats stats={collectionStats} isMobile={isMobile} />}
|
||||
<ActivitySwitcher
|
||||
showActivity={isActivityToggled}
|
||||
toggleActivity={() => {
|
||||
isFiltersExpanded && setFiltersExpanded(false)
|
||||
toggleActivity()
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<Row alignItems="flex-start" position="relative" paddingX="48">
|
||||
<Box position="sticky" top="72" width="0">
|
||||
{isFiltersExpanded && (
|
||||
<Filters
|
||||
traitsByAmount={collectionStats?.numTraitsByAmount ?? []}
|
||||
traits={collectionStats?.traits ?? []}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<Column width="full">
|
||||
{contractAddress ? (
|
||||
<>
|
||||
{' '}
|
||||
<Box width="full" height="160">
|
||||
<Box width="full" height="160">
|
||||
{isLoading ? (
|
||||
<Box height="full" width="full" className={styles.loadingBanner} />
|
||||
) : (
|
||||
<Box
|
||||
as="img"
|
||||
height="full"
|
||||
width="full"
|
||||
src={collectionStats?.bannerImageUrl}
|
||||
className={isLoading ? styles.loadingBanner : styles.bannerImage}
|
||||
background="none"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Column paddingX="32">
|
||||
{(isLoading || collectionStats !== undefined) && (
|
||||
<CollectionStats stats={collectionStats || ({} as GenieCollection)} isMobile={isMobile} />
|
||||
)}
|
||||
|
||||
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
|
||||
<AnimatedBox
|
||||
style={{
|
||||
transform: gridX.interpolate((x) => `translate(${x as number}px)`),
|
||||
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x as number}px)`),
|
||||
}}
|
||||
>
|
||||
{isActivityToggled
|
||||
? contractAddress && (
|
||||
<Activity
|
||||
contractAddress={contractAddress}
|
||||
rarityVerified={collectionStats?.rarityVerified ?? false}
|
||||
collectionName={collectionStats?.name ?? ''}
|
||||
/>
|
||||
)
|
||||
: contractAddress && (
|
||||
<CollectionNfts
|
||||
contractAddress={contractAddress}
|
||||
collectionStats={collectionStats}
|
||||
rarityVerified={collectionStats?.rarityVerified}
|
||||
/>
|
||||
)}
|
||||
</AnimatedBox>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
// TODO: Put no collection asset page here
|
||||
!isLoading && <div className={styles.noCollectionAssets}>No collection assets exist at this address</div>
|
||||
)}
|
||||
</Column>
|
||||
<ActivitySwitcher
|
||||
showActivity={isActivityToggled}
|
||||
toggleActivity={() => {
|
||||
isFiltersExpanded && setFiltersExpanded(false)
|
||||
toggleActivity()
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<Row alignItems="flex-start" position="relative" paddingX="48">
|
||||
<Box position="sticky" top="72" width="0">
|
||||
{isFiltersExpanded && (
|
||||
<Filters
|
||||
traitsByAmount={collectionStats?.numTraitsByAmount ?? []}
|
||||
traits={collectionStats?.traits ?? []}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
|
||||
<AnimatedBox
|
||||
style={{
|
||||
transform: gridX.to((x) => `translate(${x as number}px)`),
|
||||
width: gridWidthOffset.to((x) => `calc(100% - ${x as number}px)`),
|
||||
}}
|
||||
>
|
||||
{isActivityToggled
|
||||
? contractAddress && (
|
||||
<Activity
|
||||
contractAddress={contractAddress}
|
||||
rarityVerified={collectionStats?.rarityVerified ?? false}
|
||||
collectionName={collectionStats?.name ?? ''}
|
||||
/>
|
||||
)
|
||||
: contractAddress &&
|
||||
(isLoading || collectionStats !== undefined) && (
|
||||
<CollectionNfts
|
||||
collectionStats={collectionStats || ({} as GenieCollection)}
|
||||
contractAddress={contractAddress}
|
||||
rarityVerified={collectionStats?.rarityVerified}
|
||||
/>
|
||||
)}
|
||||
</AnimatedBox>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
// TODO: Put no collection asset page here
|
||||
!isLoading && <div className={styles.noCollectionAssets}>No collection assets exist at this address</div>
|
||||
)}
|
||||
</Column>
|
||||
<MobileHoverBag />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Center, Column, Row } from 'nft/components/Flex'
|
||||
import { ChevronLeftIcon, XMarkIcon } from 'nft/components/icons'
|
||||
import { ListPage } from 'nft/components/sell/list/ListPage'
|
||||
import { SelectPage } from 'nft/components/sell/select/SelectPage'
|
||||
import { ListPage } from 'nft/components/profile/list/ListPage'
|
||||
import { ProfilePage } from 'nft/components/profile/view/ProfilePage'
|
||||
import { buttonMedium, headlineMedium, headlineSmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useBag, useNFTList, useSellAsset, useSellPageState, useWalletCollections } from 'nft/hooks'
|
||||
import { ListingStatus, SellPageStateType } from 'nft/types'
|
||||
import { useBag, useNFTList, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { ListingStatus, ProfilePageStateType } from 'nft/types'
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
@@ -16,9 +16,9 @@ import * as styles from './sell.css'
|
||||
|
||||
const SHOPPING_BAG_WIDTH = 324
|
||||
|
||||
const Sell = () => {
|
||||
const sellPageState = useSellPageState((state) => state.state)
|
||||
const setSellPageState = useSellPageState((state) => state.setSellPageState)
|
||||
const Profile = () => {
|
||||
const sellPageState = useProfilePageState((state) => state.state)
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
|
||||
const resetSellAssets = useSellAsset((state) => state.reset)
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
@@ -35,7 +35,7 @@ const Sell = () => {
|
||||
|
||||
useEffect(() => {
|
||||
resetSellAssets()
|
||||
setSellPageState(SellPageStateType.SELECTING)
|
||||
setSellPageState(ProfilePageStateType.VIEWING)
|
||||
clearCollectionFilters()
|
||||
}, [account, resetSellAssets, setSellPageState, clearCollectionFilters])
|
||||
const cartExpanded = useBag((state) => state.bagExpanded)
|
||||
@@ -50,13 +50,13 @@ const Sell = () => {
|
||||
<title>Genie | Sell</title>
|
||||
</Head> */}
|
||||
<Row className={styles.mobileSellHeader}>
|
||||
{sellPageState === SellPageStateType.LISTING && (
|
||||
<Box marginRight="4" onClick={() => setSellPageState(SellPageStateType.SELECTING)}>
|
||||
{sellPageState === ProfilePageStateType.LISTING && (
|
||||
<Box marginRight="4" onClick={() => setSellPageState(ProfilePageStateType.VIEWING)}>
|
||||
<ChevronLeftIcon height={28} width={28} />
|
||||
</Box>
|
||||
)}
|
||||
<Box className={headlineSmall} paddingBottom="4" style={{ lineHeight: '28px' }}>
|
||||
{sellPageState === SellPageStateType.SELECTING ? 'Select NFTs' : 'Create Listing'}
|
||||
{sellPageState === ProfilePageStateType.VIEWING ? 'Select NFTs' : 'Create Listing'}
|
||||
</Box>
|
||||
<Box cursor="pointer" marginLeft="auto" marginRight="0" onClick={exitSellFlow}>
|
||||
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
|
||||
@@ -64,7 +64,7 @@ const Sell = () => {
|
||||
</Row>
|
||||
{account != null ? (
|
||||
<Box style={{ width: `calc(100% - ${cartExpanded ? SHOPPING_BAG_WIDTH : 0}px)` }}>
|
||||
{sellPageState === SellPageStateType.SELECTING ? <SelectPage /> : <ListPage />}
|
||||
{sellPageState === ProfilePageStateType.VIEWING ? <ProfilePage /> : <ListPage />}
|
||||
</Box>
|
||||
) : (
|
||||
<Column as="section" gap="60" className={styles.section}>
|
||||
@@ -84,4 +84,4 @@ const Sell = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Sell
|
||||
export default Profile
|
||||
@@ -184,6 +184,6 @@ export interface DropDownOption {
|
||||
|
||||
export enum DetailsOrigin {
|
||||
COLLECTION = 'collection',
|
||||
SELL = 'sell',
|
||||
PROFILE = 'profile',
|
||||
EXPLORE = 'explore',
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ export interface CollectionRow extends AssetRow {
|
||||
}
|
||||
|
||||
// Creating this as an enum and not boolean as we will likely have a success screen state to show
|
||||
export enum SellPageStateType {
|
||||
SELECTING,
|
||||
export enum ProfilePageStateType {
|
||||
VIEWING,
|
||||
LISTING,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,11 @@ export const numberToWei = (amount: number) => {
|
||||
return parseEther(amount.toString())
|
||||
}
|
||||
|
||||
export const ethNumberStandardFormatter = (amount: string | number | undefined, includeDollarSign = false): string => {
|
||||
export const ethNumberStandardFormatter = (
|
||||
amount: string | number | undefined,
|
||||
includeDollarSign = false,
|
||||
removeZeroes = false
|
||||
): string => {
|
||||
if (!amount) return '-'
|
||||
|
||||
const amountInDecimals = parseFloat(amount.toString())
|
||||
@@ -49,16 +53,13 @@ export const ethNumberStandardFormatter = (amount: string | number | undefined,
|
||||
|
||||
if (amountInDecimals < 0.0001) return `< ${conditionalDollarSign}0.00001`
|
||||
if (amountInDecimals < 1) return `${conditionalDollarSign}${amountInDecimals.toFixed(3)}`
|
||||
return (
|
||||
conditionalDollarSign +
|
||||
amountInDecimals
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
)
|
||||
const formattedPrice = (removeZeroes ? parseFloat(amountInDecimals.toFixed(2)) : amountInDecimals.toFixed(2))
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return conditionalDollarSign + formattedPrice
|
||||
}
|
||||
|
||||
export const formatWeiToDecimal = (amount: string) => {
|
||||
export const formatWeiToDecimal = (amount: string, removeZeroes = false) => {
|
||||
if (!amount) return '-'
|
||||
return ethNumberStandardFormatter(formatEther(amount))
|
||||
return ethNumberStandardFormatter(formatEther(amount), false, removeZeroes)
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ export * from './isVideo'
|
||||
export * from './listNfts'
|
||||
export * from './putCommas'
|
||||
export * from './rarity'
|
||||
export * from './roundAndPluralize'
|
||||
export * from './transactionResponse'
|
||||
export * from './updatedAssets'
|
||||
|
||||
@@ -48,7 +48,7 @@ const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
const NftExplore = lazy(() => import('nft/pages/explore'))
|
||||
const Collection = lazy(() => import('nft/pages/collection'))
|
||||
const Sell = lazy(() => import('nft/pages/sell/sell'))
|
||||
const Profile = lazy(() => import('nft/pages/profile/profile'))
|
||||
const Asset = lazy(() => import('nft/pages/asset/Asset'))
|
||||
|
||||
const AppWrapper = styled.div<{ redesignFlagEnabled: boolean }>`
|
||||
@@ -236,8 +236,8 @@ export default function App() {
|
||||
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<>
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/nfts" element={<NftExplore />} />
|
||||
<Route path="/nfts/sell" element={<Sell />} />
|
||||
<Route path="/nfts/asset/:contractAddress/:tokenId" element={<Asset />} />
|
||||
<Route path="/nfts/collection/:contractAddress" element={<Collection />} />
|
||||
<Route path="/nfts/collection/:contractAddress/activity" element={<Collection />} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { NativeCurrency, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { formatToDecimal } from 'analytics/utils'
|
||||
import {
|
||||
@@ -21,7 +21,7 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget, { WIDGET_WIDTH } from 'components/Widget'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
|
||||
import { isCelo, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { isCelo, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
|
||||
import { useTokenQuery } from 'graphql/data/Token'
|
||||
@@ -99,10 +99,10 @@ export default function TokenDetails() {
|
||||
const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[validateUrlChainParam(chainName)]
|
||||
let tokenAddress = tokenAddressParam
|
||||
let nativeCurrency
|
||||
let nativeCurrency: NativeCurrency | Token | undefined
|
||||
if (tokenAddressParam === 'NATIVE') {
|
||||
nativeCurrency = nativeOnChain(chainId)
|
||||
tokenAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address?.toLowerCase()
|
||||
tokenAddress = nativeCurrency.wrapped.address
|
||||
}
|
||||
|
||||
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
|
||||
@@ -152,7 +152,10 @@ export default function TokenDetails() {
|
||||
const { chainId: connectedChainId, account } = useWeb3React()
|
||||
|
||||
// TODO: consider updating useTokenBalance to work with just address/chain to avoid using Token data structure here
|
||||
const balanceValue = useTokenBalance(account, new Token(chainId, tokenAddress ?? '', 18))
|
||||
const balanceValue = useTokenBalance(
|
||||
account,
|
||||
useMemo(() => new Token(chainId, tokenAddress ?? '', 18), [chainId, tokenAddress])
|
||||
)
|
||||
const balance = balanceValue ? formatToDecimal(balanceValue, Math.min(balanceValue.currency.decimals, 6)) : undefined
|
||||
const balanceUsdValue = useStablecoinValue(balanceValue)?.toFixed(2)
|
||||
const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined
|
||||
@@ -188,11 +191,18 @@ export default function TokenDetails() {
|
||||
})
|
||||
: null
|
||||
|
||||
const defaultWidgetToken =
|
||||
nativeCurrency ??
|
||||
(token?.address && token.symbol && token.name
|
||||
? new Token(CHAIN_NAME_TO_CHAIN_ID[currentChainName], token.address, 18, token.symbol, token.name)
|
||||
: undefined)
|
||||
const widgetToken = useMemo(() => {
|
||||
const currentChainId = CHAIN_NAME_TO_CHAIN_ID[currentChainName]
|
||||
// The widget is not yet configured to use Celo.
|
||||
if (isCelo(chainId) || isCelo(currentChainId)) return undefined
|
||||
|
||||
return (
|
||||
nativeCurrency ??
|
||||
(token?.address && token.symbol && token.name
|
||||
? new Token(currentChainId, token.address, 18, token.symbol, token.name)
|
||||
: undefined)
|
||||
)
|
||||
}, [chainId, currentChainName, nativeCurrency, token?.address, token?.name, token?.symbol])
|
||||
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
@@ -219,7 +229,7 @@ export default function TokenDetails() {
|
||||
<AddressSection address={token.address ?? ''} />
|
||||
</LeftPanel>
|
||||
<RightPanel>
|
||||
<Widget defaultToken={!isCelo(chainId) ? defaultWidgetToken : undefined} onReviewSwapClick={onReviewSwap} />
|
||||
<Widget defaultToken={widgetToken} onReviewSwapClick={onReviewSwap} />
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address ?? ''} warning={tokenWarning} />}
|
||||
<BalanceSummary address={token.address ?? ''} balance={balance} balanceUsd={balanceUsd} />
|
||||
</RightPanel>
|
||||
|
||||
@@ -30,11 +30,8 @@ const ExploreContainer = styled.div`
|
||||
padding-top: 20px;
|
||||
}
|
||||
`
|
||||
const TokenTableContainer = styled.div`
|
||||
padding: 16px 0px;
|
||||
`
|
||||
export const TitleContainer = styled.div`
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 32px;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -62,6 +59,7 @@ const FiltersWrapper = styled.div`
|
||||
display: flex;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
flex-direction: column;
|
||||
@@ -86,7 +84,7 @@ const Tokens = () => {
|
||||
<ExploreContainer>
|
||||
<TitleContainer>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Explore Tokens</Trans>
|
||||
<Trans>Top tokens on Uniswap</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</TitleContainer>
|
||||
<FiltersWrapper>
|
||||
@@ -99,9 +97,7 @@ const Tokens = () => {
|
||||
<SearchBar />
|
||||
</SearchContainer>
|
||||
</FiltersWrapper>
|
||||
<TokenTableContainer>
|
||||
<TokenTable />
|
||||
</TokenTableContainer>
|
||||
<TokenTable />
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
)
|
||||
@@ -112,7 +108,7 @@ export const LoadingTokens = () => {
|
||||
<ExploreContainer>
|
||||
<TitleContainer>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Explore Tokens</Trans>
|
||||
<Trans>Top tokens on Uniswap</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</TitleContainer>
|
||||
<FiltersWrapper>
|
||||
@@ -125,9 +121,7 @@ export const LoadingTokens = () => {
|
||||
<SearchBar />
|
||||
</SearchContainer>
|
||||
</FiltersWrapper>
|
||||
<TokenTableContainer>
|
||||
<LoadingTokenTable />
|
||||
</TokenTableContainer>
|
||||
<LoadingTokenTable />
|
||||
</ExploreContainer>
|
||||
)
|
||||
}
|
||||
|
||||
28
src/theme/animations.ts
Normal file
28
src/theme/animations.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { css, keyframes } from 'styled-components/macro'
|
||||
|
||||
const transitions = {
|
||||
duration: {
|
||||
slow: '500ms',
|
||||
medium: '250ms',
|
||||
fast: '125ms',
|
||||
},
|
||||
timing: {
|
||||
ease: 'ease',
|
||||
in: 'ease-in',
|
||||
out: 'ease-out',
|
||||
inOut: 'ease-in-out',
|
||||
},
|
||||
}
|
||||
|
||||
export const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export const textFadeIn = css`
|
||||
animation: ${fadeIn} ${transitions.duration.fast} ${transitions.timing.in};
|
||||
`
|
||||
@@ -36,6 +36,7 @@ const BREAKPOINTS = {
|
||||
xxxl: 1920,
|
||||
}
|
||||
|
||||
// deprecated - please use the animations.ts file
|
||||
const transitions = {
|
||||
duration: {
|
||||
slow: '500ms',
|
||||
@@ -272,11 +273,10 @@ function getTheme(darkMode: boolean, isNewColorsEnabled: boolean): DefaultTheme
|
||||
// media queries
|
||||
deprecated_mediaWidth: deprecated_mediaWidthTemplates,
|
||||
|
||||
//breakpoints
|
||||
// deprecated - please use hardcoded exported values instead of
|
||||
// adding to the theme object
|
||||
breakpoint: BREAKPOINTS,
|
||||
|
||||
transition: transitions,
|
||||
|
||||
opacity: opacities,
|
||||
|
||||
// css snippets
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es5",
|
||||
"types": ["react-spring", "jest"],
|
||||
"types": ["jest"],
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"exclude": ["node_modules", "cypress"],
|
||||
|
||||
143
yarn.lock
143
yarn.lock
@@ -1074,7 +1074,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@>=7.17.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@>=7.17.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.18.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
||||
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
|
||||
@@ -2840,6 +2840,92 @@
|
||||
"@react-hook/event" "^1.2.1"
|
||||
"@react-hook/throttle" "^2.2.0"
|
||||
|
||||
"@react-spring/animated@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.5.5.tgz#d3bfd0f62ed13a337463a55d2c93bb23c15bbf3e"
|
||||
integrity sha512-glzViz7syQ3CE6BQOwAyr75cgh0qsihm5lkaf24I0DfU63cMm/3+br299UEYkuaHNmfDfM414uktiPlZCNJbQA==
|
||||
dependencies:
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/core@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.5.5.tgz#1d8a4c64630ee26b2295361e1eedfd716a85b4ae"
|
||||
integrity sha512-shaJYb3iX18Au6gkk8ahaF0qx0LpS0Yd+ajb4asBaAQf6WPGuEdJsbsNSgei1/O13JyEATsJl20lkjeslJPMYA==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/rafz" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/konva@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.5.5.tgz#ddbb30cfa268219d69552aa71188832ca8ab4905"
|
||||
integrity sha512-0CNh+1vCIjNUklTFwMvxg+H83Jo2OWykBrdEA28ccmnpZgkQ8Kq5xyvaPFLzcDKV67OXHnaWiCYKpRbhLy2wng==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/native@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.5.5.tgz#4ecc420c7b4c3fefeebd55d852640d36c29ec9c8"
|
||||
integrity sha512-kauqmyJ8u7aVy2bBs22vl1SdB2i5uYIL4rP53k1KDWrFSqJh4j3efWkbTt9uzR5cMXuNVbkNo9OYVFUcQBz50A==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/rafz@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.5.5.tgz#62a49c5e294104b79db2a8afdf4f3a274c7f44ca"
|
||||
integrity sha512-F/CLwB0d10jL6My5vgzRQxCNY2RNyDJZedRBK7FsngdCmzoq3V4OqqNc/9voJb9qRC2wd55oGXUeXv2eIaFmsw==
|
||||
|
||||
"@react-spring/shared@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.5.5.tgz#9be0b391d546e3e184a24ecbaf40acbaeab7fc73"
|
||||
integrity sha512-YwW70Pa/YXPOwTutExHZmMQSHcNC90kJOnNR4G4mCDNV99hE98jWkIPDOsgqbYx3amIglcFPiYKMaQuGdr8dyQ==
|
||||
dependencies:
|
||||
"@react-spring/rafz" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/three@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.5.5.tgz#c6fbee977007d1980406db20a28ac3f5dc2ce153"
|
||||
integrity sha512-9kTIaSceqFIl5EIrdwM7Z53o5I+9BGNVzbp4oZZYMao+GMAWOosnlQdDG5GeqNsIqfW9fZCEquGqagfKAxftcA==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/types@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.5.5.tgz#c8e94f1b9232ca7cb9d860ea67762ec401b1de14"
|
||||
integrity sha512-7I/qY8H7Enwasxr4jU6WmtNK+RZ4Z/XvSlDvjXFVe7ii1x0MoSlkw6pD7xuac8qrHQRm9BTcbZNyeeKApYsvCg==
|
||||
|
||||
"@react-spring/web@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.5.5.tgz#d416abc591aaed930401f0c98a991a8c5b90c382"
|
||||
integrity sha512-+moT8aDX/ho/XAhU+HRY9m0LVV9y9CK6NjSRaI+30Re150pB3iEip6QfnF4qnhSCQ5drpMF0XRXHgOTY/xbtFw==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@react-spring/zdog@~9.5.5":
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.5.5.tgz#916dba337637d1151c3c2bc829b5105d15adacb5"
|
||||
integrity sha512-LZgjo2kLlGmUqfE2fdVnvLXz+4eYyQARRvB9KQ4PTEynaETTG89Xgn9YxLrh1p57DzH7gEmTGDZ5hEw3pWqu8g==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.5.5"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/shared" "~9.5.5"
|
||||
"@react-spring/types" "~9.5.5"
|
||||
|
||||
"@reduxjs/toolkit@^1.6.1":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.0.tgz#8ae875e481ed97e4a691aafa034f876bfd0413c4"
|
||||
@@ -4219,10 +4305,10 @@
|
||||
"@uniswap/v3-core" "1.0.0"
|
||||
"@uniswap/v3-periphery" "^1.0.1"
|
||||
|
||||
"@uniswap/widgets@^2.8.1":
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.8.1.tgz#6b92d9d7026b06e67e576ec76f8424ba80ca303e"
|
||||
integrity sha512-57WDm1NvGEKz2buk8eNRgdI/RTvA5WkXZNapz/rFqtO5RU30NmPduiEqYSK3xrXnaJ3qIU3Qq8MNSfv1p480Lw==
|
||||
"@uniswap/widgets@^2.9.2":
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.9.2.tgz#3ab5e84fc61fb62c3635b52526a67febfcdbb081"
|
||||
integrity sha512-4NNckJJ3jaOubIwU0hzfcPbdjNzlO6n8WIIcAScyc+WGpb1S9iBUjJ0EFMCEUhP/zsNK0+I5qv1ytjPF4XZTog==
|
||||
dependencies:
|
||||
"@babel/runtime" ">=7.17.0"
|
||||
"@fontsource/ibm-plex-mono" "^4.5.1"
|
||||
@@ -4436,7 +4522,7 @@
|
||||
dependencies:
|
||||
"@vibrant/types" "^3.2.1-alpha.1"
|
||||
|
||||
"@visx/axis@^2.12.2":
|
||||
"@visx/axis@2.12.2", "@visx/axis@^2.12.2":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-2.12.2.tgz#0aa50ae35d0cd6d8a11c59ad0d874cfeea9e3b89"
|
||||
integrity sha512-nE+DGNwRzXOmp6ZwMQ1yUhbF7uR2wd3j6Xja/kVgGA7wSbqUeCZzqKZvhRsCqyay6PtHVlRRAhHP31Ob39+jtw==
|
||||
@@ -4478,6 +4564,20 @@
|
||||
d3-shape "^1.2.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/grid@2.12.2":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/grid/-/grid-2.12.2.tgz#32956dbb2ca88b24a057a7d559a46ba5e617df08"
|
||||
integrity sha512-lyMQvq5afjOh0nRqF0OBjgsLfsgUeLcFc95oj0FJ/NJ/MvtI6Gd5BxxbmYzuVfZ4f0Dm1pvtBu1swoB3451tkg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@visx/curve" "2.1.0"
|
||||
"@visx/group" "2.10.0"
|
||||
"@visx/point" "2.6.0"
|
||||
"@visx/scale" "2.2.2"
|
||||
"@visx/shape" "2.12.2"
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/group@2.10.0", "@visx/group@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-2.10.0.tgz#95839851832545621eb0d091866a61dafe552ae1"
|
||||
@@ -4492,6 +4592,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@visx/point/-/point-2.6.0.tgz#c4316ca409b5b829c5455f07118d8c14a92cc633"
|
||||
integrity sha512-amBi7yMz4S2VSchlPdliznN41TuES64506ySI22DeKQ+mc1s1+BudlpnY90sM1EIw4xnqbKmrghTTGfy6SVqvQ==
|
||||
|
||||
"@visx/react-spring@^2.12.2":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/react-spring/-/react-spring-2.12.2.tgz#f753ead7fc62ff2541e56ea128c109601dd7a016"
|
||||
integrity sha512-+Oo9S75lSbpF6VV3Ym8kB/I4H7O3qYk5Nltv2CNUoVuWvzd4tRnxiVLBxYuGjtj6lIAhDJ+W8QYHXhZhe0zNHw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@visx/axis" "2.12.2"
|
||||
"@visx/grid" "2.12.2"
|
||||
"@visx/scale" "2.2.2"
|
||||
"@visx/text" "2.12.2"
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/responsive@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/responsive/-/responsive-2.10.0.tgz#3e5c5853c7b2b33481e99a64678063cef717de0b"
|
||||
@@ -14360,7 +14473,7 @@ prompts@2.4.0, prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -14888,13 +15001,17 @@ react-scripts@^4.0.3:
|
||||
optionalDependencies:
|
||||
fsevents "^2.1.3"
|
||||
|
||||
react-spring@^8.0.27:
|
||||
version "8.0.27"
|
||||
resolved "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz"
|
||||
integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==
|
||||
react-spring@^9.5.5:
|
||||
version "9.5.5"
|
||||
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.5.5.tgz#314009a65efc04d0ef157d3d60590dbb9de65f3c"
|
||||
integrity sha512-vMGVd2yjgxWcRCzoLn9AD1d24+WpunHBRg5DoehcRdiBocaOH6qgle0xN9C5LPplXfv4yIpS5QWGN5MKrWxSZg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
prop-types "^15.5.8"
|
||||
"@react-spring/core" "~9.5.5"
|
||||
"@react-spring/konva" "~9.5.5"
|
||||
"@react-spring/native" "~9.5.5"
|
||||
"@react-spring/three" "~9.5.5"
|
||||
"@react-spring/web" "~9.5.5"
|
||||
"@react-spring/zdog" "~9.5.5"
|
||||
|
||||
react-style-singleton@^2.1.0:
|
||||
version "2.1.1"
|
||||
|
||||
Reference in New Issue
Block a user