From f26b09537d25c000f83a70baa9dbbcd5743183b7 Mon Sep 17 00:00:00 2001 From: Charles Bachmeier Date: Mon, 6 Feb 2023 12:45:13 -0800 Subject: [PATCH] feat: [ListV2] Success modal + Twitter Share (#5924) * added success screen with images * add listing proceeds * working return * working single tweet * working tweet multiple * update how we parse twitterName * add scrollbar styles * usestablecoinvalue * math.min * add collection name backup to tweet --------- Co-authored-by: Charles Bachmeier --- .../data/__generated__/types-and-hooks.ts | 3 +- src/graphql/data/nft/NftBalance.ts | 8 +- .../profile/list/Modal/ListModal.tsx | 10 +- .../profile/list/Modal/SuccessScreen.tsx | 129 ++++++++++++++++++ src/nft/components/profile/list/shared.tsx | 6 + src/nft/utils/asset.ts | 37 ++++- 6 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 src/nft/components/profile/list/Modal/SuccessScreen.tsx diff --git a/src/graphql/data/__generated__/types-and-hooks.ts b/src/graphql/data/__generated__/types-and-hooks.ts index 5b2552810e..fc0c05ea09 100644 --- a/src/graphql/data/__generated__/types-and-hooks.ts +++ b/src/graphql/data/__generated__/types-and-hooks.ts @@ -1004,7 +1004,7 @@ export type NftBalanceQueryVariables = Exact<{ }>; -export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } }; +export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } }; export const TokenDocument = gql` @@ -1622,6 +1622,7 @@ export const NftBalanceDocument = gql` url } name + twitterName nftContracts { address chain diff --git a/src/graphql/data/nft/NftBalance.ts b/src/graphql/data/nft/NftBalance.ts index 53fa030827..0fdb289d69 100644 --- a/src/graphql/data/nft/NftBalance.ts +++ b/src/graphql/data/nft/NftBalance.ts @@ -34,6 +34,7 @@ gql` url } name + twitterName nftContracts { address chain @@ -166,7 +167,12 @@ export function useNftBalance( image_url: asset?.collection?.image?.url, payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress, }, - collection: asset?.collection as unknown as GenieCollection, + collection: { + name: asset.collection?.name, + isVerified: asset.collection?.isVerified, + imageUrl: asset.collection?.image?.url, + twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined, + } as GenieCollection, collectionIsVerified: asset?.collection?.isVerified, lastPrice: queryAsset.node.lastPrice?.value, floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value, diff --git a/src/nft/components/profile/list/Modal/ListModal.tsx b/src/nft/components/profile/list/Modal/ListModal.tsx index 7f3905d4d2..ffca24d5ce 100644 --- a/src/nft/components/profile/list/Modal/ListModal.tsx +++ b/src/nft/components/profile/list/Modal/ListModal.tsx @@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro' import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics' import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events' import { useWeb3React } from '@web3-react/core' -import Row from 'components/Row' import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils' import { Portal } from 'nft/components/common/Portal' import { Overlay } from 'nft/components/modals/Overlay' @@ -15,7 +14,9 @@ import styled from 'styled-components/macro' import { BREAKPOINTS, ThemedText } from 'theme' import { Z_INDEX } from 'theme/zIndex' +import { TitleRow } from '../shared' import { ListModalSection, Section } from './ListModalSection' +import { SuccessScreen } from './SuccessScreen' const ListModalWrapper = styled.div` position: fixed; @@ -39,11 +40,6 @@ const ListModalWrapper = styled.div` } ` -const TitleRow = styled(Row)` - justify-content: space-between; - margin-bottom: 8px; -` - export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => { const { provider } = useWeb3React() const signer = provider?.getSigner() @@ -101,7 +97,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => { {listingStatus === ListingStatus.APPROVED ? ( - <>TODO Success Screen + ) : ( <> diff --git a/src/nft/components/profile/list/Modal/SuccessScreen.tsx b/src/nft/components/profile/list/Modal/SuccessScreen.tsx new file mode 100644 index 0000000000..a704b8612a --- /dev/null +++ b/src/nft/components/profile/list/Modal/SuccessScreen.tsx @@ -0,0 +1,129 @@ +import { Trans } from '@lingui/macro' +import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format' +import Column from 'components/Column' +import { ScrollBarStyles } from 'components/Common' +import Row from 'components/Row' +import { useStablecoinValue } from 'hooks/useStablecoinPrice' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { getTotalEthValue } from 'nft/components/bag/profile/utils' +import { useSellAsset } from 'nft/hooks' +import { formatEth, generateTweetForList, pluralize } from 'nft/utils' +import { useMemo } from 'react' +import { Twitter, X } from 'react-feather' +import styled, { css, useTheme } from 'styled-components/macro' +import { BREAKPOINTS, ThemedText } from 'theme' + +import { TitleRow } from '../shared' + +const SuccessImage = styled.img<{ numImages: number }>` + width: calc(${({ numImages }) => (numImages > 1 ? (numImages > 2 ? '33%' : '50%') : '100%')} - 12px); + border-radius: 12px; +` + +const SuccessImageWrapper = styled(Row)` + flex-wrap: wrap; + gap: 12px; + justify-content: center; + overflow-y: scroll; + margin-bottom: 16px; + ${ScrollBarStyles} +` + +const ProceedsColumn = styled(Column)` + text-align: right; +` + +const buttonStyle = css` + width: 182px; + cursor: pointer; + padding: 12px 0px; + text-align: center; + font-weight: 600; + font-size: 16px; + line-height: 20px; + border-radius: 12px; + border: none; + + &:hover { + opacity: 0.6; + } + + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + width: 100%; + margin-bottom: 8px; + } +` + +const ReturnButton = styled.button` + background-color: ${({ theme }) => theme.backgroundInteractive}; + color: ${({ theme }) => theme.textPrimary}; + ${buttonStyle} +` + +const TweetButton = styled.a` + background-color: ${({ theme }) => theme.accentAction}; + color: ${({ theme }) => theme.textPrimary}; + text-decoration: none; + ${buttonStyle} +` + +const TweetRow = styled(Row)` + justify-content: center; + gap: 4px; +` + +export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => { + const theme = useTheme() + const sellAssets = useSellAsset((state) => state.sellAssets) + const nativeCurrency = useNativeCurrency() + + const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets]) + const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency) + const usdcValue = useStablecoinValue(parsedAmount) + + return ( + <> + + + Successfully listed {sellAssets.length > 1 ? ` ${sellAssets.length} ` : ''} + NFT{pluralize(sellAssets.length)}! + + + + + {sellAssets.map((asset) => ( + + ))} + + + + Proceeds if sold + + + {formatEth(totalEthListingValue)} ETH + {usdcValue && ( + + {formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)} + + )} + + + + window.location.reload()}> + Return to My NFTs + + + + + Share on Twitter + + + + + ) +} diff --git a/src/nft/components/profile/list/shared.tsx b/src/nft/components/profile/list/shared.tsx index 0a9495fa82..073052f5c9 100644 --- a/src/nft/components/profile/list/shared.tsx +++ b/src/nft/components/profile/list/shared.tsx @@ -1,3 +1,4 @@ +import Row from 'components/Row' import styled from 'styled-components/macro' export const RemoveIconWrap = styled.div<{ hovered: boolean }>` @@ -8,3 +9,8 @@ export const RemoveIconWrap = styled.div<{ hovered: boolean }>` width: 32px; visibility: ${({ hovered }) => (hovered ? 'visible' : 'hidden')}; ` + +export const TitleRow = styled(Row)` + justify-content: space-between; + margin-bottom: 8px; +` diff --git a/src/nft/utils/asset.ts b/src/nft/utils/asset.ts index 83525b0dd7..7617efe5d3 100644 --- a/src/nft/utils/asset.ts +++ b/src/nft/utils/asset.ts @@ -1,4 +1,4 @@ -import { DetailsOrigin, GenieAsset, UpdatedGenieAsset, WalletAsset } from 'nft/types' +import { DetailsOrigin, GenieAsset, Listing, UpdatedGenieAsset, WalletAsset } from 'nft/types' export function getRarityStatus( rarityStatusCache: Map, @@ -44,3 +44,38 @@ export const generateTweetForPurchase = (assets: UpdatedGenieAsset[], txHashUrl: } with @Uniswap 🦄\n\nhttps://app.uniswap.org/#/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769\n${txHashUrl}` return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}` } + +function getMinListingPrice(listings: Listing[]): number { + return Math.min(...listings.map((listing) => listing.price ?? 0)) ?? 0 +} + +function mapAssetsToCollections(assets: WalletAsset[]): { collection: string; items: string[] }[] { + const collections = assets.map((asset) => asset.collection?.twitterUrl ?? asset.collection?.name ?? '') + const uniqueCollections = [...new Set(collections)] + return uniqueCollections.map((collection) => { + return { + collection, + items: assets + .filter((asset) => asset.collection?.twitterUrl === collection || asset.collection?.name === collection) + .map((asset) => asset.name ?? ''), + } + }) +} + +export const generateTweetForList = (assets: WalletAsset[]): string => { + const tweetText = + assets.length == 1 + ? `I just listed ${ + assets[0].collection?.twitterUrl + ? `${assets[0].collection?.twitterUrl} ` + : `${assets[0].collection?.name} ` ?? '' + }${assets[0].name} for ${getMinListingPrice(assets[0].newListings ?? [])} ETH on ${assets[0].marketplaces + ?.map((market) => market.name) + .join(', ')}. Buy it on @Uniswap at https://app.uniswap.org/#${getAssetHref(assets[0])}` + : `I just listed ${ + assets.length + } items on @Uniswap at https://app.uniswap.org/#/nfts/profile\n\nCollections: ${mapAssetsToCollections(assets) + .map(({ collection, items }) => `${collection} ${items.map((item) => item).join(', ')}`) + .join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}` + return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}` +}