Compare commits

...

20 Commits

Author SHA1 Message Date
lynn
1361f99639 fix: remove ALL in time periods (temporary until v2 backend data available) (#4780)
* remove ALL

* fix to zach comments

* fix comment
2022-10-03 18:19:46 -04:00
lynn
d70a87a89a fix: new swap confirmation modal scroll style (#4768)
* init

* fix in response to cmcewen comment

* top getting cut off fix

* persist change for mobile
2022-10-03 16:27:32 -04:00
Jack Short
2cb0d9527e style: filling background color for collection header (#4774)
* style: filling background color for collection header

* udpating color
2022-10-03 11:17:07 -04:00
cartcrom
1839e145ec style: updating explore language and css (#4772)
* finished updating explore language and css
* implemented feedback from fred
* refactored css for row height
* extended filter option
2022-10-03 10:49:17 -04:00
lynn
8c1e41a3a8 fix: glitchy lazy loading (big jump / unnecessary scroll) (#4776)
* fix glitchy loading

* fix initial no tokens state
2022-09-30 17:23:05 -04:00
Charles Bachmeier
9859c0b4dd feat: add empty wallet state (#4765)
add empty wallet state

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-30 14:19:33 -07:00
Charles Bachmeier
1138101dd0 feat: profile entry point in wallet dropdown (#4760)
* refactor sell and select page to profile page

* add renamed profile pages

* add profile entry button

* add profile details header

* small adjustments for small screens

* add new details component

* show tag on correct page

* fix wallet dropdown height

* update header spacing

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-30 13:54:32 -07:00
Connor McEwen
106ac7ea35 chore: bump widget version (#4775)
* chore: bump widget version

* .2
2022-09-30 16:39:57 -04:00
Zach Pomerantz
19b4ee463b fix: memoize tokens in TokenDetails (#4777) 2022-09-30 13:04:52 -07:00
Connor McEwen
2aea96c3ba feat: fade in text on details page (#4773) 2022-09-30 14:28:02 -04:00
Connor McEwen
b1fb499e29 feat: animate in token details line chart (#4745)
* WIP

* animated in chart

* add comment

* revert env change

* comments

* merge main

* fix hard reload
2022-09-30 14:27:52 -04:00
Greg Bugyis
64207f29b0 feat: Log events on NavBar Search (#4761) 2022-09-30 20:56:16 +03:00
lynn
7b6ac6cfaa fix: update network warning styling (#4767)
* init

* respond to cmcewen comments
2022-09-30 13:51:58 -04:00
vignesh mohankumar
8a9ade5f12 fix: shorten SearchBar height (#4766)
* fix: shorten SearchBar height

* fix positioning
2022-09-30 13:27:33 -04:00
Connor McEwen
a3e567bc8a chore: upgrade react-spring version (#4770)
* chore: upgrade react-spring version

* remove interpolate

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-30 13:09:06 -04:00
Jack Short
a887666bf5 feat: mobile hover bag (#4742)
* initial hover bag

* feat: mobile hover bag

* updating mobile bag

* addressing comments and adding stuff to usebag
2022-09-30 11:26:27 -04:00
Jack Short
ed8aa08255 chore: remove extra decimals on cards (#4757)
* chore: removing decimals

* only on cards

* slight fix

* updating across app
2022-09-30 11:04:16 -04:00
aballerr
53f4fb9ede chore: Merging Loading states 2 (#4708)
* adding in remaining loading styles


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-29 14:56:53 -04:00
lynn
bb1ccb7f1a fix: price chart crash when undefined price point (#4763)
fix price chart crash
2022-09-29 14:08:37 -04:00
Jack Short
03fe90ad53 chore: updating pool tooltip (#4756)
* chore: updating pool tooltip

* updating to bodySmall
2022-09-29 13:24:52 -04:00
92 changed files with 1335 additions and 537 deletions

View File

@@ -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",

View File

@@ -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.

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
) : (

View File

@@ -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}

View File

@@ -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} />

View File

@@ -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([

View File

@@ -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 && (

View File

@@ -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}
/>
)

View File

@@ -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 ? (

View File

@@ -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

View File

@@ -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>

View File

@@ -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'

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) => (

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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}
>

View File

@@ -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`

View File

@@ -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 */

View File

@@ -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={

View File

@@ -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 */

View File

@@ -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'))

View File

@@ -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 ? (

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -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))
}
},
})

View File

@@ -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,
}

View File

@@ -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'
}
}

View File

@@ -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]

View File

@@ -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>) => {

View File

@@ -195,3 +195,11 @@ export const toolTip = sprinkles({
display: 'flex',
flexShrink: '0',
})
export const removeAssetOverlay = style([
sprinkles({
position: 'absolute',
right: '4',
top: '4',
}),
])

View File

@@ -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(

View 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',
}),
])

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

View File

@@ -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,
},
])

View File

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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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()

View 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',
}),
])

View File

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

View File

@@ -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,
},
])

View File

@@ -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" />
</>
)}

View File

@@ -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',
}),
])

View File

@@ -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 ? (

View File

@@ -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,
},
])

View File

@@ -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"

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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"

View File

@@ -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

View 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>&nbsp;{shortenAddress(account ?? '')}
</EmptyWalletText>
<ExploreNFTsButton onClick={() => navigate('/nfts')}>Explore NFTs</ExploreNFTsButton>
</EmptyWalletContainer>
)
}

View File

@@ -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' }}

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

View File

@@ -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

View File

@@ -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'

View File

@@ -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 }

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

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

View File

@@ -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' }
)
)

View File

@@ -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') {

View File

@@ -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',

View File

@@ -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 />
</>
)
}

View File

@@ -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

View File

@@ -184,6 +184,6 @@ export interface DropDownOption {
export enum DetailsOrigin {
COLLECTION = 'collection',
SELL = 'sell',
PROFILE = 'profile',
EXPLORE = 'explore',
}

View File

@@ -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,
}

View File

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

View File

@@ -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'

View File

@@ -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 />} />

View File

@@ -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>

View File

@@ -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
View 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};
`

View File

@@ -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

View File

@@ -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
View File

@@ -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"