import { faArrowLeft, faChevronDown, faRefresh, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import * as Tabs from '@radix-ui/react-tabs' import { TokenMedia, useAttributes, useBids, useCollections, useDynamicTokens, useListings, useTokenActivity, useUserTokens, } from '@reservoir0x/reservoir-kit-ui' import { paths } from '@reservoir0x/reservoir-sdk' import { ActivityFilters } from 'components/token/ActivityFilters' import { spin } from 'components/common/LoadingSpinner' import { MobileActivityFilters } from 'components/common/MobileActivityFilters' import { OpenSeaVerified } from 'components/common/OpenSeaVerified' import Layout from 'components/Layout' import { Anchor, Box, Button, Flex, Grid, Text } from 'components/primitives' import { Dropdown } from 'components/primitives/Dropdown' import { TabsContent, TabsList, TabsTrigger } from 'components/primitives/Tab' import AttributeCard from 'components/token/AttributeCard' import FullscreenMedia from 'components/token/FullscreenMedia' import { PriceData } from 'components/token/PriceData' import RarityRank from 'components/token/RarityRank' import { TokenActions } from 'components/token/TokenActions' import { TokenActivityTable } from 'components/token/ActivityTable' import { TokenInfo } from 'components/token/TokenInfo' import { ToastContext } from 'context/ToastContextProvider' import { useENSResolver, useMarketplaceChain, useMounted } from 'hooks' import { GetServerSideProps, InferGetServerSidePropsType, NextPage } from 'next' import Link from 'next/link' import { useRouter } from 'next/router' import { NORMALIZE_ROYALTIES } from 'pages/_app' import { useContext, useEffect, useState } from 'react' import { jsNumberForAddress } from 'react-jazzicon' import Jazzicon from 'react-jazzicon/dist/Jazzicon' import { useMediaQuery } from 'react-responsive' import supportedChains, { DefaultChain } from 'utils/chains' import fetcher from 'utils/fetcher' import { DATE_REGEX, timeTill } from 'utils/till' import titleCase from 'utils/titleCase' import { useAccount } from 'wagmi' import { Head } from 'components/Head' import { OffersTable } from 'components/token/OffersTable' import { ListingsTable } from 'components/token/ListingsTable' type Props = InferGetServerSidePropsType type ActivityTypes = Exclude< NonNullable< NonNullable< Exclude['1'], boolean> >['types'] >, string > const IndexPage: NextPage = ({ assetId, ssr }) => { const assetIdPieces = assetId ? assetId.toString().split(':') : [] let collectionId = assetIdPieces[0] const id = assetIdPieces[1] const router = useRouter() const { addToast } = useContext(ToastContext) const account = useAccount() const isMounted = useMounted() const isSmallDevice = useMediaQuery({ maxWidth: 900 }) && isMounted const [tabValue, setTabValue] = useState('info') const [isRefreshing, setIsRefreshing] = useState(false) const [activityFiltersOpen, setActivityFiltersOpen] = useState(true) const [activityTypes, setActivityTypes] = useState([]) const [activityNames, setActivityNames] = useState([]) const { proxyApi } = useMarketplaceChain() const contract = collectionId ? collectionId?.split(':')[0] : undefined const { data: tokens, mutate } = useDynamicTokens( { tokens: [`${contract}:${id}`], includeAttributes: true, includeTopBid: true, includeQuantity: true, }, { fallbackData: [ssr.tokens ? ssr.tokens : {}], } ) const token = tokens && tokens[0] ? tokens[0] : undefined const is1155 = token?.token?.kind === 'erc1155' const { data: collections } = useCollections( { includeSecurityConfigs: true, includeMintStages: true, id: token?.token?.collection?.id, }, { fallbackData: [ssr.collection ? ssr.collection : {}], } ) const collection = collections && collections[0] ? collections[0] : null const { data: userTokens } = useUserTokens( is1155 ? account.address : undefined, { tokens: [`${contract}:${id}`], limit: 20, } ) const { data: offers } = useBids({ token: `${token?.token?.collection?.id}:${token?.token?.tokenId}`, includeRawData: true, sortBy: 'price', limit: 1, }) const { data: listings } = useListings({ token: `${token?.token?.collection?.id}:${token?.token?.tokenId}`, includeRawData: true, sortBy: 'price', limit: 1, }) const offer = offers && offers[0] ? offers[0] : undefined const listing = listings && listings[0] ? listings[0] : undefined const attributesData = useAttributes(collectionId) let countOwned = 0 if (is1155) { countOwned = Number(userTokens?.[0]?.ownership?.tokenCount || 0) } else { countOwned = token?.token?.owner?.toLowerCase() === account?.address?.toLowerCase() ? 1 : 0 } const isOwner = countOwned > 0 const owner = isOwner ? account?.address : token?.token?.owner const { displayName: ownerFormatted } = useENSResolver(token?.token?.owner) const tokenName = `${token?.token?.name || `#${token?.token?.tokenId}`}` const hasAttributes = token?.token?.attributes && token?.token?.attributes.length > 0 const trigger = ( ) useEffect(() => { let tab = tabValue const hasAttributesTab = isMounted && isSmallDevice && hasAttributes if (hasAttributesTab) { tab = 'attributes' } else { tab = 'info' } let deeplinkTab: string | null = null if (typeof window !== 'undefined') { const params = new URL(window.location.href).searchParams deeplinkTab = params.get('tab') } if (deeplinkTab) { switch (deeplinkTab) { case 'attributes': if (hasAttributesTab) { tab = 'attributes' } break case 'info': tab = 'info' break case 'activity': tab = 'activity' break case 'listings': tab = 'listings' break case 'offers': tab = 'offers' break } } setTabValue(tab) }, [isSmallDevice]) useEffect(() => { const updatedUrl = new URL(`${window.location.origin}${router.asPath}`) updatedUrl.searchParams.set('tab', tabValue) router.replace(updatedUrl, undefined, { shallow: true, }) }, [tabValue]) const pageTitle = token?.token?.name ? token.token.name : `${token?.token?.tokenId} - ${token?.token?.collection?.name}` return ( button': { height: 0, opacity: 0, transition: 'opacity .3s', }, }, ':hover >button': { opacity: 1, transition: 'opacity .3s', }, }} > { mutate?.() addToast?.({ title: 'Refresh token', description: 'Request to refresh this token was accepted.', }) }} /> {token?.token?.attributes && !isSmallDevice && ( {token?.token?.attributes?.map((attribute) => ( ))} )} {token?.token?.collection?.name} {tokenName} {token && ( <> {is1155 && countOwned > 0 && ( You own {countOwned} Sell )} {!is1155 && owner && ( Owner {isMounted ? ownerFormatted : ''} )} {isMounted && ( )} setTabValue(value)} style={{ paddingRight: isSmallDevice ? 0 : 15, }} > {isMounted && isSmallDevice && hasAttributes && ( Attributes )} Info Activity Listings Offers {token?.token?.attributes && ( {token?.token?.attributes?.map((attribute) => ( ))} )} {collection && ( )} {isSmallDevice ? ( ) : ( )} )} ) } type SSRProps = { collection?: | paths['/collections/v7']['get']['responses']['200']['schema'] | null tokens?: paths['/tokens/v6']['get']['responses']['200']['schema'] | null } export const getServerSideProps: GetServerSideProps<{ assetId?: string ssr: SSRProps }> = async ({ params, res }) => { const assetId = params?.assetId ? params.assetId.toString().split(':') : [] let collectionId = assetId[0] //preventing that users access non-greenlisted addresses: /* if (!filterContractsTheSphere.includes(collectionId as string)) { return { redirect: { destination: '/404', permanent: false, }, } } */ const id = assetId[1] const { reservoirBaseUrl } = supportedChains.find((chain) => params?.chain === chain.routePrefix) || DefaultChain const contract = collectionId ? collectionId?.split(':')[0] : undefined const headers = { headers: { 'x-api-key': process.env.RESERVOIR_API_KEY || '', }, } let tokensQuery: paths['/tokens/v6']['get']['parameters']['query'] = { tokens: [`${contract}:${id}`], includeAttributes: true, includeTopBid: true, normalizeRoyalties: NORMALIZE_ROYALTIES, includeDynamicPricing: true, } let tokens: SSRProps['tokens'] = null let collection: SSRProps['collection'] = null try { const tokensPromise = fetcher( `${reservoirBaseUrl}/tokens/v6`, tokensQuery, headers ) const tokensResponse = await tokensPromise tokens = tokensResponse.data ? (tokensResponse.data as Props['ssr']['tokens']) : {} let collectionQuery: paths['/collections/v7']['get']['parameters']['query'] = { id: tokens?.tokens?.[0]?.token?.collection?.id, normalizeRoyalties: NORMALIZE_ROYALTIES, } const collectionsPromise = fetcher( `${reservoirBaseUrl}/collections/v7`, collectionQuery, headers ) const collectionsResponse = await collectionsPromise collection = collectionsResponse.data ? (collectionsResponse.data as Props['ssr']['collection']) : {} res.setHeader( 'Cache-Control', 'public, s-maxage=30, stale-while-revalidate=60' ) } catch (e) {} return { props: { assetId: params?.assetId as string, ssr: { collection, tokens }, }, } } export default IndexPage