import { faCheck, faEdit, faEllipsis, faGasPump, faHand, faPlus, faRefresh, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { EditListingModal, EditListingStep, extractMediaType, TokenMedia, useDynamicTokens, useTokens, useUserTokens, } from '@reservoir0x/reservoir-kit-ui' import { AcceptBid } from 'components/buttons' import BuyNow from 'components/buttons/BuyNow' import CancelListing from 'components/buttons/CancelListing' import List from 'components/buttons/List' import { spin } from 'components/common/LoadingSpinner' import { Box, Button, Flex, FormatCryptoCurrency, Text, Tooltip, } from 'components/primitives' import { Dropdown, DropdownMenuItem } from 'components/primitives/Dropdown' import { ToastContext } from 'context/ToastContextProvider' import { useMarketplaceChain } from 'hooks' import Link from 'next/link' import { UserToken } from 'pages/portfolio/[[...address]]' import { Dispatch, SetStateAction, SyntheticEvent, useContext, useMemo, useState, } from 'react' import { MutatorCallback } from 'swr' import { useMediaQuery } from 'react-responsive' import fetcher from 'utils/fetcher' import { formatNumber } from 'utils/numbers' import { DATE_REGEX, timeTill } from 'utils/till' import { Address } from 'wagmi' import Image from 'next/image' import optimizeImage from 'utils/optimizeImage' type PortfolioTokenCardProps = { token: ReturnType['data'][0] address: Address isOwner: boolean rarityEnabled: boolean tokenCount?: string orderQuantity?: number selectedItems: UserToken[] setSelectedItems: Dispatch> mutate?: MutatorCallback onMediaPlayed?: ( e: SyntheticEvent ) => void } export default ({ token, address, isOwner, rarityEnabled = true, orderQuantity, tokenCount, selectedItems, setSelectedItems, mutate, onMediaPlayed, }: PortfolioTokenCardProps) => { const { addToast } = useContext(ToastContext) const [isRefreshing, setIsRefreshing] = useState(false) let dynamicToken = token as ReturnType['data'][0] const isSmallDevice = useMediaQuery({ maxWidth: 900 }) const mediaType = extractMediaType(dynamicToken?.token) const showPreview = mediaType === 'other' || mediaType === 'html' || mediaType === null const { routePrefix, proxyApi } = useMarketplaceChain() const collectionImage = useMemo(() => { return optimizeImage(token?.token?.collection?.imageUrl, 500) }, [token?.token?.collection?.imageUrl]) const isOracleOrder = token?.ownership?.floorAsk?.rawData?.isNativeOffChainCancellable const contract = token.token?.collection?.id ? token.token?.collection.id?.split(':')[0] : undefined const addSelectedItem = (item: UserToken) => { setSelectedItems([...selectedItems, item]) } const removeSelectedItem = (item: UserToken) => { setSelectedItems( selectedItems.filter( (selectedItem) => selectedItem?.token?.tokenId !== item?.token?.tokenId || selectedItem?.token?.contract !== item?.token?.contract ) ) } const isSelectedItem = useMemo(() => { return selectedItems.some( (selectedItem) => selectedItem?.token?.tokenId === token?.token?.tokenId && selectedItem?.token?.contract === token?.token?.contract ) }, [selectedItems]) return ( a > div > img': { transform: 'scale(1.1)', }, '@sm': { '&:hover .token-button-container': { bottom: 0, }, }, }} > {tokenCount && ( x{formatNumber(tokenCount, 0, true)} )} {isOwner && !isSmallDevice ? ( ) : null} { onMediaPlayed?.(e) }, }} videoOptions={{ onPlay: (e) => { onMediaPlayed?.(e) }, }} onRefreshToken={() => { mutate?.() addToast?.({ title: 'Refresh token', description: 'Request to refresh this token was accepted.', }) }} /> {collectionImage ? ( src} src={collectionImage} alt={`${token?.token?.name}`} width={24} height={24} /> ) : null} {token?.token?.name || '#' + token?.token?.tokenId}{' '} {rarityEnabled && token?.token?.kind !== 'erc1155' && token?.token?.rarityRank ? ( {formatNumber(token?.token?.rarityRank)} ) : null} {token?.ownership?.floorAsk?.price && ( )} <> {token?.ownership?.floorAsk?.source?.name && ( )} {token?.token?.lastSale?.price?.amount?.decimal ? ( Last Sale ) : null} {!isOwner && token?.ownership?.floorAsk?.price?.amount ? ( ) : null} {isOwner ? ( ['data'][0]} buttonCss={{ justifyContent: 'center', flex: 1, }} buttonProps={{ corners: 'square', }} buttonChildren="List" mutate={mutate} /> } contentProps={{ asChild: true, forceMount: true, align: 'start', alignOffset: -18, }} > {token?.token?.topBid?.price?.amount?.decimal && isOwner && ( Accept Best Offer } /> )} { if (isRefreshing) { e.preventDefault() return } setIsRefreshing(true) fetcher( `${window.location.origin}/${proxyApi}/tokens/refresh/v1`, undefined, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: `${contract}:${token.token?.tokenId}`, }), } ) .then(({ data, response }) => { if (response.status === 200) { addToast?.({ title: 'Refresh token', description: 'Request to refresh this token was accepted.', }) } else { throw data } setIsRefreshing(false) }) .catch((e) => { const ratelimit = DATE_REGEX.exec(e?.message)?.[0] addToast?.({ title: 'Refresh token failed', description: ratelimit ? `This token was recently refreshed. The next available refresh is ${timeTill( ratelimit )}.` : `This token was recently refreshed. Please try again later.`, }) setIsRefreshing(false) throw e }) }} > Refresh Token {isOracleOrder && token?.ownership?.floorAsk?.id && token?.token?.tokenId && token?.token?.collection?.id ? ( Edit Listing } listingId={token?.ownership?.floorAsk?.id} tokenId={token?.token?.tokenId} collectionId={token?.token?.collection?.id} onClose={(data, currentStep) => { if (mutate && currentStep == EditListingStep.Complete) mutate() }} /> ) : null} {token?.ownership?.floorAsk?.id ? ( {!isOracleOrder ? ( Canceling this order requires gas. } > Cancel Listing ) : ( Cancel )} } /> ) : null} ) : null} ) }