Commit 973d9496 authored by tom goriunov's avatar tom goriunov Committed by GitHub

NFT collections and instances - link to markets (#1277)

Fixes #1213
parent eae41d10
export { default as address } from './address';
export { default as block } from './block';
export { default as nft } from './nft';
export { default as tx } from './tx';
import type { NftMarketplaceItem } from 'types/views/nft';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
const config = Object.freeze({
marketplaces: parseEnvJson<Array<NftMarketplaceItem>>(getEnvValue('NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES')) || [],
});
export default config;
......@@ -26,6 +26,8 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth.json
## footer
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
......
......@@ -29,6 +29,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
## footer
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
......
......@@ -30,6 +30,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
## footer
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
## views
......
......@@ -30,6 +30,7 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11
## views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=
......
......@@ -23,6 +23,7 @@ import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
import type { BlockFieldId } from '../../../types/views/block';
import type { NftMarketplaceItem } from '../../../types/views/nft';
import type { TxAdditionalFieldsId, TxFieldsId } from '../../../types/views/tx';
import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from '../../../types/views/tx';
......@@ -251,6 +252,14 @@ const networkExplorerSchema: yup.ObjectSchema<NetworkExplorer> = yup
}),
});
const nftMarketplaceSchema: yup.ObjectSchema<NftMarketplaceItem> = yup
.object({
name: yup.string().required(),
collection_url: yup.string().test(urlTest).required(),
instance_url: yup.string().test(urlTest).required(),
logo_url: yup.string().test(urlTest).required(),
});
const bridgedTokenChainSchema: yup.ObjectSchema<BridgedTokenChain> = yup
.object({
id: yup.string().required(),
......@@ -378,6 +387,11 @@ const schema = yup
.transform(replaceQuotes)
.json()
.of(yup.string<TxAdditionalFieldsId>().oneOf(TX_ADDITIONAL_FIELDS_IDS)),
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup
.array()
.transform(replaceQuotes)
.json()
.of(nftMarketplaceSchema),
// e. misc
NEXT_PUBLIC_NETWORK_EXPLORERS: yup
......
......@@ -159,6 +159,7 @@ frontend:
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......
......@@ -142,4 +142,6 @@ frontend:
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS:
_default: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY:
_default: true
\ No newline at end of file
_default: true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES:
_default: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
\ No newline at end of file
......@@ -18,6 +18,7 @@ The app instance could be customized by passing following variables to NodeJS en
- [Block](ENVS.md#block-views)
- [Address](ENVS.md#address-views)
- [Transaction](ENVS.md#transaction-views)
- [NFT](ENVS.md#nft-views)
- [Misc](ENVS.md#misc)
- [App features](ENVS.md#app-features)
- [My account](ENVS.md#my-account)
......@@ -219,6 +220,25 @@ Settings for meta tags and OG tags
&nbsp;
#### NFT views
| Variable | Type | Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES | `Array<NftMarketplace>` where `NftMarketplace` can have following [properties](#nft-marketplace-properties) | Used to build up links to NFT collections and NFT instances in external marketplaces. | - | - | `[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'}]` |
##### NFT marketplace properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Displayed name of the marketplace | Required | - | `OpenSea` |
| collection_url | `string` | URL template for NFT collection | Required | - | `https://opensea.io/assets/ethereum/{hash}` |
| instance_url | `string` | URL template for NFT instance | Required | - | `https://opensea.io/assets/ethereum/{hash}/{id}` |
| logo_url | `string` | URL of marketplace logo | Required | - | `https://opensea.io/static/images/logos/opensea-logo.svg` |
*Note* URL templates should contain placeholders of NFT hash (`{hash}`) and NFT id (`{id}`). This placeholders will be substituted with particular values for every collection or instance.
&nbsp;
### Misc
| Variable | Type| Description | Compulsoriness | Default value | Example value |
......
export interface NftMarketplaceItem {
name: string;
collection_url: string;
instance_url: string;
logo_url: string;
}
......@@ -15,21 +15,24 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import TruncatedValue from 'ui/shared/TruncatedValue';
import TokenNftMarketplaces from './TokenNftMarketplaces';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
}
const TokenDetails = ({ tokenQuery }: Props) => {
const router = useRouter();
const hash = router.query.hash?.toString();
const tokenCountersQuery = useApiQuery('token_counters', {
pathParams: { hash: router.query.hash?.toString() },
pathParams: { hash },
queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
});
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
router.push(
{ pathname: '/token/[hash]', query: { hash: router.query.hash?.toString() || '', tab } },
{ pathname: '/token/[hash]', query: { hash: hash || '', tab } },
undefined,
{ shallow: true },
);
......@@ -37,7 +40,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
duration: 500,
smooth: true,
});
}, [ router ]);
}, [ hash, router ]);
const countersItem = useCallback((item: 'token_holders_count' | 'transfers_count') => {
const itemValue = tokenCountersQuery.data?.[item];
......@@ -158,6 +161,9 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</Skeleton>
</DetailsInfoItem>
) }
{ type !== 'ERC-20' && <TokenNftMarketplaces hash={ hash } isLoading={ tokenQuery.isPlaceholderData }/> }
<DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid>
);
......
import { Image, Link, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props {
hash: string | undefined;
id?: string;
isLoading?: boolean;
}
const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
if (!hash || config.UI.views.nft.marketplaces.length === 0) {
return null;
}
return (
<DetailsInfoItem
title="Marketplaces"
hint="Marketplaces trading this NFT"
alignSelf="center"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } display="flex" columnGap={ 3 } flexWrap="wrap">
{ config.UI.views.nft.marketplaces.map((item) => {
const hrefTemplate = id ? item.instance_url : item.collection_url;
const href = hrefTemplate.replace('{id}', id || '').replace('{hash}', hash || '');
return (
<Tooltip label={ `View on ${ item.name }` } key={ item.name }>
<Link href={ href } target="_blank">
<Image
src={ item.logo_url }
alt={ `${ item.name } marketplace logo` }
boxSize={ 5 }
borderRadius="full"
/>
</Link>
</Tooltip>
);
}) }
</Skeleton>
</DetailsInfoItem>
);
};
export default React.memo(TokenNftMarketplaces);
......@@ -16,6 +16,10 @@ const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_coun
});
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route('http://localhost:3000/nft-marketplace-logo.png', (route) => route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
}));
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
......
......@@ -11,6 +11,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import NftMedia from 'ui/shared/nft/NftMedia';
import TokenNftMarketplaces from 'ui/token/TokenNftMarketplaces';
import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress';
import TokenInstanceMetadataInfo from './details/TokenInstanceMetadataInfo';
......@@ -81,6 +82,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
</Flex>
</DetailsInfoItem>
<TokenInstanceTransfersCount hash={ isLoading ? '' : data.token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/>
<TokenNftMarketplaces isLoading={ isLoading } hash={ data.token.address } id={ data.id }/>
</Grid>
<NftMedia
url={ data.animation_url || data.image_url }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment