feat: add tooltips when adding and removing from nft bag (#5222)

* init

* make sure tooltips dont appear during sweeps

* fixes comments from charlie

* style: example tooltip (#5238)

* refactor: remove unused CollectionProfile (#5229)

* ex

* centering

* fix: updating bag to not remove nfts on click (#5224)

* updating bag to not remove nfts on click

* chore: bump redux-multicall (#5211)

* chore: bump redux-multicall

* fix: updgrade multicall

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* refactor: remove unused nft utils (#5239)

* fix: remove Select All and Sell buttons in Profile (#5228)

* refactor: remove isSellMode in ProfilePage

* yes

* fix(token-details): remove balance summary links to current page (#5223)

* refactor: rm remaining unused nft code (#5243)

* chore: enable jsx-curly-brace-presence and autofix (#5242)

* fix: limit max volume change value to 9999% (#5227)

* fix web-2246

* chore: change to >

* chore: use single component

* fix: approve button font size (#5187)

* fix: approve button font size


Co-authored-by: 0xsaranonearth <saran.s@pillow.fund>

* fix: don't include accentActiveSoft background on navicon active state (#5240)

don't include accentActiveSoft background on navicon active state

* feat: render blurred collection cover photo in the header (#5233)

* initial commit

* feat: blurred header

* chore: replace with helper

* chore: cleanup

* chore: different extension

* chore: layout tweaks

* chore: tweaks

* chore: prevent weird text selection on double click

* chore: wip for linear gradient/plain color light mode

* feat: linear-gradient when image missing

* chore: clean up post merge

* feat: different opacity for dark/light mode

* chore: fix paddings

* refactor: remove unused nft css (#5241)

* refactor: remove unused nft css

* unused

* unused

* refactor: remove unused isSellMode, setIsSellMode in useSellAsset (#5236)

* refactor: remove unused isSellMode, setIsSellMode in useSellAsset

* rm

* fix: reverting navbar change (#5237)

* reverting mobile navbar changes

Co-authored-by: vignesh mohankumar <vignesh@vigneshmohankumar.com>
Co-authored-by: aballerr <alex.ball@uniswap.org>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: S A R A N <44068102+saranonearth@users.noreply.github.com>
Co-authored-by: 0xsaranonearth <saran.s@pillow.fund>
Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>

Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: vignesh mohankumar <vignesh@vigneshmohankumar.com>
Co-authored-by: aballerr <alex.ball@uniswap.org>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: S A R A N <44068102+saranonearth@users.noreply.github.com>
Co-authored-by: 0xsaranonearth <saran.s@pillow.fund>
This commit is contained in:
lynn 2022-11-16 14:18:37 -05:00 committed by GitHub
parent d38854749b
commit f391f1c719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 54 deletions

@ -74,10 +74,12 @@ const Arrow = styled.div`
export interface PopoverProps { export interface PopoverProps {
content: React.ReactNode content: React.ReactNode
show: boolean show: boolean
children: React.ReactNode children?: React.ReactNode
placement?: Placement placement?: Placement
offsetX?: number offsetX?: number
offsetY?: number offsetY?: number
hideArrow?: boolean
showInline?: boolean
style?: CSSProperties style?: CSSProperties
} }
@ -88,6 +90,8 @@ export default function Popover({
placement = 'auto', placement = 'auto',
offsetX = 8, offsetX = 8,
offsetY = 8, offsetY = 8,
hideArrow = false,
showInline = false,
style, style,
}: PopoverProps) { }: PopoverProps) {
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null) const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
@ -114,7 +118,9 @@ export default function Popover({
}, [update]) }, [update])
useInterval(updateCallback, show ? 100 : null) useInterval(updateCallback, show ? 100 : null)
return ( return showInline ? (
<PopoverContainer show={show}>{content}</PopoverContainer>
) : (
<> <>
<ReferenceElement style={style} ref={setReferenceElement as any}> <ReferenceElement style={style} ref={setReferenceElement as any}>
{children} {children}
@ -122,12 +128,14 @@ export default function Popover({
<Portal> <Portal>
<PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}> <PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
{content} {content}
<Arrow {!hideArrow && (
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`} <Arrow
ref={setArrowElement as any} className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
style={styles.arrow} ref={setArrowElement as any}
{...attributes.arrow} style={styles.arrow}
/> {...attributes.arrow}
/>
)}
</PopoverContainer> </PopoverContainer>
</Portal> </Portal>
</> </>

@ -1,10 +1,15 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics' import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, PageName } from '@uniswap/analytics-events' import { EventName, PageName } from '@uniswap/analytics-events'
import Tooltip from 'components/Tooltip'
import { Box } from 'nft/components/Box'
import { bodySmall } from 'nft/css/common.css'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { GenieAsset, Markets } from 'nft/types' import { GenieAsset, Markets, TokenType } from 'nft/types'
import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils' import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils'
import { useCallback, useMemo } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components/macro'
import { useAssetMediaType, useNotForSale } from './Card' import { useAssetMediaType, useNotForSale } from './Card'
import { AssetMediaType } from './Card' import { AssetMediaType } from './Card'
@ -18,6 +23,18 @@ interface CollectionAssetProps {
rarityVerified?: boolean rarityVerified?: boolean
} }
const TOOLTIP_TIMEOUT = 2000
const StyledContainer = styled.div`
position: absolute;
bottom: 12px;
left: 0px;
display: flex;
justify-content: center;
width: 100%;
z-index: 2;
`
export const CollectionAsset = ({ export const CollectionAsset = ({
asset, asset,
isMobile, isMobile,
@ -28,22 +45,27 @@ export const CollectionAsset = ({
const bagManuallyClosed = useBag((state) => state.bagManuallyClosed) const bagManuallyClosed = useBag((state) => state.bagManuallyClosed)
const addAssetsToBag = useBag((state) => state.addAssetsToBag) const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag) const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const usedSweep = useBag((state) => state.usedSweep)
const itemsInBag = useBag((state) => state.itemsInBag) const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
const setBagExpanded = useBag((state) => state.setBagExpanded) const setBagExpanded = useBag((state) => state.setBagExpanded)
const trace = useTrace({ page: PageName.NFT_COLLECTION_PAGE }) const trace = useTrace({ page: PageName.NFT_COLLECTION_PAGE })
const { quantity, isSelected } = useMemo(() => { const { erc1155TokenQuantity, isSelected } = useMemo(() => {
const matchingItems = itemsInBag.filter(
(item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
)
const erc1155TokenQuantity = matchingItems.filter((x) => x.asset.tokenType === TokenType.ERC1155).length
const isSelected = matchingItems.length > 0
return { return {
quantity: itemsInBag.filter( erc1155TokenQuantity,
(x) => x.asset.tokenType === 'ERC1155' && x.asset.tokenId === asset.tokenId && x.asset.address === asset.address isSelected,
).length,
isSelected: itemsInBag.some(
(item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
),
} }
}, [asset, itemsInBag]) }, [asset, itemsInBag])
const [showTooltip, setShowTooltip] = useState(false)
const isSelectedRef = useRef(isSelected)
const notForSale = useNotForSale(asset) const notForSale = useNotForSale(asset)
const assetMediaType = useAssetMediaType(asset) const assetMediaType = useAssetMediaType(asset)
@ -71,6 +93,22 @@ export const CollectionAsset = ({
} }
}, [addAssetsToBag, asset, bagExpanded, bagManuallyClosed, isMobile, setBagExpanded, trace]) }, [addAssetsToBag, asset, bagExpanded, bagManuallyClosed, isMobile, setBagExpanded, trace])
useEffect(() => {
if (isSelected !== isSelectedRef.current && !usedSweep) {
setShowTooltip(true)
isSelectedRef.current = isSelected
const tooltipTimer = setTimeout(() => {
setShowTooltip(false)
}, TOOLTIP_TIMEOUT)
return () => {
clearTimeout(tooltipTimer)
}
}
isSelectedRef.current = isSelected
return undefined
}, [isSelected, isSelectedRef, usedSweep])
const handleRemoveAssetFromBag = useCallback(() => { const handleRemoveAssetFromBag = useCallback(() => {
removeAssetsFromBag([asset]) removeAssetsFromBag([asset])
}, [asset, removeAssetsFromBag]) }, [asset, removeAssetsFromBag])
@ -83,7 +121,25 @@ export const CollectionAsset = ({
removeAssetFromBag={handleRemoveAssetFromBag} removeAssetFromBag={handleRemoveAssetFromBag}
> >
<Card.ImageContainer> <Card.ImageContainer>
{asset.tokenType === 'ERC1155' && quantity > 0 && <Card.Erc1155Controls quantity={quantity.toString()} />} <StyledContainer>
<Tooltip
text={
<Box as="span" className={bodySmall} color="textPrimary">
{isSelected ? <Trans>Added to bag</Trans> : <Trans>Removed from bag</Trans>}
</Box>
}
show={showTooltip}
style={{ display: 'block' }}
offsetX={0}
offsetY={0}
hideArrow={true}
placement="bottom"
showInline
/>
</StyledContainer>
{asset.tokenType === TokenType.ERC1155 && erc1155TokenQuantity > 0 && (
<Card.Erc1155Controls quantity={erc1155TokenQuantity.toString()} />
)}
{asset.rarity && provider && ( {asset.rarity && provider && (
<Card.Ranking <Card.Ranking
rarity={asset.rarity} rarity={asset.rarity}
@ -116,7 +172,7 @@ export const CollectionAsset = ({
</Card.SecondaryInfo> </Card.SecondaryInfo>
{(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />} {(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />}
</Card.SecondaryDetails> </Card.SecondaryDetails>
{asset.tokenType !== 'ERC1155' && asset.marketplace && ( {asset.tokenType !== TokenType.ERC1155 && asset.marketplace && (
<Card.MarketplaceIcon marketplace={asset.marketplace} /> <Card.MarketplaceIcon marketplace={asset.marketplace} />
)} )}
</Card.SecondaryRow> </Card.SecondaryRow>

@ -256,7 +256,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
if (sweepItemsInBag.length < value) { if (sweepItemsInBag.length < value) {
addAssetsToBag(sortedAssets.slice(sweepItemsInBag.length, value), true) addAssetsToBag(sortedAssets.slice(sweepItemsInBag.length, value), true)
} else { } else {
removeAssetsFromBag(sweepItemsInBag.slice(value, sweepItemsInBag.length)) removeAssetsFromBag(sweepItemsInBag.slice(value, sweepItemsInBag.length), true)
} }
setSweepAmount(value < 1 ? '' : value.toString()) setSweepAmount(value < 1 ? '' : value.toString())
} else { } else {
@ -290,7 +290,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
} }
if (wishAssets.length > 0) { if (wishAssets.length > 0) {
removeAssetsFromBag(wishAssets) removeAssetsFromBag(wishAssets, true)
} }
} }

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import * as Card from 'nft/components/collection/Card' import * as Card from 'nft/components/collection/Card'
import { AssetMediaType } from 'nft/components/collection/Card' import { AssetMediaType } from 'nft/components/collection/Card'
@ -7,7 +8,9 @@ import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks' import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
import { TokenType, WalletAsset } from 'nft/types' import { TokenType, WalletAsset } from 'nft/types'
import { useMemo } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
const TOOLTIP_TIMEOUT = 2000
interface ViewMyNftsAssetProps { interface ViewMyNftsAssetProps {
asset: WalletAsset asset: WalletAsset
@ -50,6 +53,9 @@ export const ViewMyNftsAsset = ({
) )
}, [asset, sellAssets]) }, [asset, sellAssets])
const [showTooltip, setShowTooltip] = useState(false)
const isSelectedRef = useRef(isSelected)
const onCardClick = () => handleSelect(isSelected) const onCardClick = () => handleSelect(isSelected)
const handleSelect = (removeAsset: boolean) => { const handleSelect = (removeAsset: boolean) => {
@ -64,40 +70,71 @@ export const ViewMyNftsAsset = ({
toggleCart() toggleCart()
} }
const assetMediaType = Card.useAssetMediaType(asset) useEffect(() => {
if (isSelected !== isSelectedRef.current) {
setShowTooltip(true)
isSelectedRef.current = isSelected
const tooltipTimer = setTimeout(() => {
setShowTooltip(false)
}, TOOLTIP_TIMEOUT)
return () => {
clearTimeout(tooltipTimer)
}
}
isSelectedRef.current = isSelected
return undefined
}, [isSelected, isSelectedRef])
const assetMediaType = Card.useAssetMediaType(asset)
const isDisabled = asset.asset_contract.tokenType === TokenType.ERC1155 || asset.susFlag const isDisabled = asset.asset_contract.tokenType === TokenType.ERC1155 || asset.susFlag
const disabledTooltipText =
asset.asset_contract.tokenType === TokenType.ERC1155 ? 'ERC-1155 support coming soon' : 'Blocked from trading'
return ( return (
<MouseoverTooltip <Card.Container
text={ asset={asset}
<Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}> selected={isSelected}
<Trans>{disabledTooltipText}</Trans>{' '} addAssetToBag={() => handleSelect(false)}
</Box> removeAssetFromBag={() => handleSelect(true)}
} onClick={onCardClick}
placement="bottom" isDisabled={isDisabled}
offsetX={0}
offsetY={-180}
style={{ display: 'block' }}
disableHover={!isDisabled}
> >
<Card.Container <MouseoverTooltip
asset={asset} text={
selected={isSelected} <Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}>
addAssetToBag={() => handleSelect(false)} {asset.asset_contract.tokenType === TokenType.ERC1155 ? (
removeAssetFromBag={() => handleSelect(true)} <Trans>ERC-1155 support coming soon</Trans>
onClick={onCardClick} ) : (
isDisabled={isDisabled} <Trans>Blocked from trading</Trans>
)}
</Box>
}
placement="bottom"
offsetX={0}
offsetY={-100}
style={{ display: 'block' }}
disableHover={!isDisabled}
> >
<Card.ImageContainer> <Tooltip
{getNftDisplayComponent(assetMediaType, mediaShouldBePlaying, setCurrentTokenPlayingMedia)} text={
</Card.ImageContainer> <Box as="span" className={bodySmall} color="textPrimary">
<Card.DetailsContainer> {isSelected ? <Trans>Selected</Trans> : <Trans>Deselected</Trans>}
<Card.ProfileNftDetails asset={asset} hideDetails={hideDetails} /> </Box>
</Card.DetailsContainer> }
</Card.Container> show={showTooltip}
</MouseoverTooltip> style={{ display: 'block' }}
offsetX={0}
offsetY={-52}
hideArrow={true}
placement="bottom"
>
<Card.ImageContainer>
{getNftDisplayComponent(assetMediaType, mediaShouldBePlaying, setCurrentTokenPlayingMedia)}
</Card.ImageContainer>
</Tooltip>
</MouseoverTooltip>
<Card.DetailsContainer>
<Card.ProfileNftDetails asset={asset} hideDetails={hideDetails} />
</Card.DetailsContainer>
</Card.Container>
) )
} }

@ -16,13 +16,14 @@ interface BagState {
totalUsdPrice: number | undefined totalUsdPrice: number | undefined
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void
removeAssetsFromBag: (assets: UpdatedGenieAsset[]) => void removeAssetsFromBag: (assets: UpdatedGenieAsset[], fromSweep?: boolean) => void
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
lockSweepItems: (contractAddress: string) => void lockSweepItems: (contractAddress: string) => void
didOpenUnavailableAssets: boolean didOpenUnavailableAssets: boolean
setDidOpenUnavailableAssets: (didOpen: boolean) => void setDidOpenUnavailableAssets: (didOpen: boolean) => void
bagExpanded: boolean bagExpanded: boolean
toggleBag: () => void toggleBag: () => void
usedSweep: boolean
isLocked: boolean isLocked: boolean
setLocked: (isLocked: boolean) => void setLocked: (isLocked: boolean) => void
reset: () => void reset: () => void
@ -59,6 +60,7 @@ export const useBag = create<BagState>()(
setBagExpanded: ({ bagExpanded, manualClose }) => setBagExpanded: ({ bagExpanded, manualClose }) =>
set(({ bagManuallyClosed }) => ({ bagExpanded, bagManuallyClosed: manualClose || bagManuallyClosed })), set(({ bagManuallyClosed }) => ({ bagExpanded, bagManuallyClosed: manualClose || bagManuallyClosed })),
toggleBag: () => set(({ bagExpanded }) => ({ bagExpanded: !bagExpanded })), toggleBag: () => set(({ bagExpanded }) => ({ bagExpanded: !bagExpanded })),
usedSweep: false,
isLocked: false, isLocked: false,
setLocked: (_isLocked) => setLocked: (_isLocked) =>
set(() => ({ set(() => ({
@ -107,14 +109,16 @@ export const useBag = create<BagState>()(
return { return {
itemsInBag: items, itemsInBag: items,
bagStatus: BagStatus.ADDING_TO_BAG, bagStatus: BagStatus.ADDING_TO_BAG,
usedSweep: fromSweep,
} }
else else
return { return {
itemsInBag: [...itemsInBagCopy, ...items], itemsInBag: [...itemsInBagCopy, ...items],
bagStatus: BagStatus.ADDING_TO_BAG, bagStatus: BagStatus.ADDING_TO_BAG,
usedSweep: fromSweep,
} }
}), }),
removeAssetsFromBag: (assets) => { removeAssetsFromBag: (assets, fromSweep = false) => {
set(({ bagManuallyClosed, itemsInBag }) => { set(({ bagManuallyClosed, itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag } if (get().isLocked) return { itemsInBag: get().itemsInBag }
if (itemsInBag.length === 0) return { itemsInBag: [] } if (itemsInBag.length === 0) return { itemsInBag: [] }
@ -126,7 +130,11 @@ export const useBag = create<BagState>()(
: asset.tokenId === item.asset.tokenId && asset.address === item.asset.address : asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
) )
) )
return { bagManuallyClosed: itemsCopy.length === 0 ? false : bagManuallyClosed, itemsInBag: itemsCopy } return {
bagManuallyClosed: itemsCopy.length === 0 ? false : bagManuallyClosed,
itemsInBag: itemsCopy,
usedSweep: fromSweep,
}
}) })
}, },
lockSweepItems: (contractAddress) => lockSweepItems: (contractAddress) =>