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

@ -1,10 +1,15 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
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 { GenieAsset, Markets } from 'nft/types'
import { GenieAsset, Markets, TokenType } from 'nft/types'
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 { AssetMediaType } from './Card'
@ -18,6 +23,18 @@ interface CollectionAssetProps {
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 = ({
asset,
isMobile,
@ -28,22 +45,27 @@ export const CollectionAsset = ({
const bagManuallyClosed = useBag((state) => state.bagManuallyClosed)
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const usedSweep = useBag((state) => state.usedSweep)
const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded)
const setBagExpanded = useBag((state) => state.setBagExpanded)
const trace = useTrace({ page: PageName.NFT_COLLECTION_PAGE })
const { quantity, isSelected } = useMemo(() => {
return {
quantity: itemsInBag.filter(
(x) => x.asset.tokenType === 'ERC1155' && x.asset.tokenId === asset.tokenId && x.asset.address === asset.address
).length,
isSelected: itemsInBag.some(
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 {
erc1155TokenQuantity,
isSelected,
}
}, [asset, itemsInBag])
const [showTooltip, setShowTooltip] = useState(false)
const isSelectedRef = useRef(isSelected)
const notForSale = useNotForSale(asset)
const assetMediaType = useAssetMediaType(asset)
@ -71,6 +93,22 @@ export const CollectionAsset = ({
}
}, [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(() => {
removeAssetsFromBag([asset])
}, [asset, removeAssetsFromBag])
@ -83,7 +121,25 @@ export const CollectionAsset = ({
removeAssetFromBag={handleRemoveAssetFromBag}
>
<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 && (
<Card.Ranking
rarity={asset.rarity}
@ -116,7 +172,7 @@ export const CollectionAsset = ({
</Card.SecondaryInfo>
{(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />}
</Card.SecondaryDetails>
{asset.tokenType !== 'ERC1155' && asset.marketplace && (
{asset.tokenType !== TokenType.ERC1155 && asset.marketplace && (
<Card.MarketplaceIcon marketplace={asset.marketplace} />
)}
</Card.SecondaryRow>

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

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'
import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip'
import { Box } from 'nft/components/Box'
import * as Card 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 { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
import { TokenType, WalletAsset } from 'nft/types'
import { useMemo } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
const TOOLTIP_TIMEOUT = 2000
interface ViewMyNftsAssetProps {
asset: WalletAsset
@ -50,6 +53,9 @@ export const ViewMyNftsAsset = ({
)
}, [asset, sellAssets])
const [showTooltip, setShowTooltip] = useState(false)
const isSelectedRef = useRef(isSelected)
const onCardClick = () => handleSelect(isSelected)
const handleSelect = (removeAsset: boolean) => {
@ -64,25 +70,26 @@ export const ViewMyNftsAsset = ({
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 disabledTooltipText =
asset.asset_contract.tokenType === TokenType.ERC1155 ? 'ERC-1155 support coming soon' : 'Blocked from trading'
return (
<MouseoverTooltip
text={
<Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}>
<Trans>{disabledTooltipText}</Trans>{' '}
</Box>
}
placement="bottom"
offsetX={0}
offsetY={-180}
style={{ display: 'block' }}
disableHover={!isDisabled}
>
<Card.Container
asset={asset}
selected={isSelected}
@ -90,14 +97,44 @@ export const ViewMyNftsAsset = ({
removeAssetFromBag={() => handleSelect(true)}
onClick={onCardClick}
isDisabled={isDisabled}
>
<MouseoverTooltip
text={
<Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}>
{asset.asset_contract.tokenType === TokenType.ERC1155 ? (
<Trans>ERC-1155 support coming soon</Trans>
) : (
<Trans>Blocked from trading</Trans>
)}
</Box>
}
placement="bottom"
offsetX={0}
offsetY={-100}
style={{ display: 'block' }}
disableHover={!isDisabled}
>
<Tooltip
text={
<Box as="span" className={bodySmall} color="textPrimary">
{isSelected ? <Trans>Selected</Trans> : <Trans>Deselected</Trans>}
</Box>
}
show={showTooltip}
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>
</MouseoverTooltip>
)
}

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