Marketplace titles and ads (#3109)
@@ -1,11 +1,19 @@
|
||||
import type { Feature } from './types';
|
||||
import type { EssentialDappsConfig } from 'types/client/marketplace';
|
||||
import type { EssentialDappsConfig, MarketplaceTitles } from 'types/client/marketplace';
|
||||
|
||||
import apis from '../apis';
|
||||
import chain from '../chain';
|
||||
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from '../utils';
|
||||
import blockchainInteraction from './blockchainInteraction';
|
||||
|
||||
const defaultTitles: MarketplaceTitles = {
|
||||
entity_name: 'Dapp',
|
||||
menu_item: 'Dapps',
|
||||
title: 'Dappscout',
|
||||
subtitle_essential_dapps: 'Essential dapps',
|
||||
subtitle_list: 'Explore dapps',
|
||||
};
|
||||
|
||||
// config file will be downloaded at run-time and saved in the public folder
|
||||
const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED');
|
||||
const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL');
|
||||
@@ -17,6 +25,8 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE
|
||||
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
|
||||
const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL');
|
||||
const essentialDappsConfig = parseEnvJson<EssentialDappsConfig>(getEnvValue('NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG'));
|
||||
const essentialDappsAdEnabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED') !== 'false';
|
||||
const customTitles = parseEnvJson(getEnvValue('NEXT_PUBLIC_MARKETPLACE_TITLES')) || {};
|
||||
|
||||
const title = 'Marketplace';
|
||||
|
||||
@@ -31,6 +41,8 @@ const config: Feature<(
|
||||
banner: { contentUrl: string; linkUrl: string } | undefined;
|
||||
graphLinksUrl: string | undefined;
|
||||
essentialDapps: EssentialDappsConfig | undefined;
|
||||
essentialDappsAdEnabled: boolean;
|
||||
titles: MarketplaceTitles;
|
||||
}> = (() => {
|
||||
if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
|
||||
const props = {
|
||||
@@ -44,6 +56,8 @@ const config: Feature<(
|
||||
} : undefined,
|
||||
graphLinksUrl,
|
||||
essentialDapps: blockchainInteraction.isEnabled ? (essentialDappsConfig || undefined) : undefined,
|
||||
essentialDappsAdEnabled,
|
||||
titles: { ...defaultTitles, ...customTitles },
|
||||
};
|
||||
|
||||
if (configUrl) {
|
||||
|
||||
@@ -39,8 +39,8 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Launch your own fully functioning blockchain explorer in minutes. <a href="https://deploy.blockscout.com/?utm_source=blockscout_ad">Deploy now</a></p>
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/1a7395339d20b603b9c1d0ee929f23dc/raw/55e2eebef3ba3ef064842d15555b59d5ba8cbcc7/badges-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://badges.blockscout.com/badges-list?utm_source=blockscout-explorer&utm_medium=marketplace-banner
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/4888339a781c8384267bf4f37f33a2fc/raw/807c66bb2e4e7cd341309035984a1f2458d7e160/revokescout_banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://eth.blockscout.com/apps/revokescout?chainId=1
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '1923', '42793', '59144'], 'fee': '0.004', 'integrator': 'blockscout'}, 'multisend': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '59144', '7000'], 'posthogKey': 'phc_7O4WGsecqqDO1PeaKayHAxUWN1PjheOmQCiDxEMcmkx', 'posthogHost': 'https://us.i.posthog.com'}}
|
||||
@@ -77,4 +77,4 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
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://storage.googleapis.com/opensea-static/Logomark/Logomark-Blue.svg'},{'name':'Rarible','collection_url':'https://rarible.com/collection/{hash}/items','instance_url':'https://rarible.com/token/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'Blur','collection_url':'https://blur.io/eth/collection/{hash}','instance_url':'https://blur.io/eth/asset/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/blur.png'},{'name':'MagicEden','collection_url':'https://magiceden.io/collections/ethereum/{hash}','instance_url':'https://magiceden.io/item-details/ethereum/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}]
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
|
||||
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
|
||||
import { GAS_UNITS } from '../../../types/client/gasTracker';
|
||||
import type { GasUnit } from '../../../types/client/gasTracker';
|
||||
import type { MarketplaceAppBase, MarketplaceAppSocialInfo, EssentialDappsConfig } from '../../../types/client/marketplace';
|
||||
import type { MarketplaceAppBase, MarketplaceAppSocialInfo, EssentialDappsConfig, MarketplaceTitles } from '../../../types/client/marketplace';
|
||||
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
|
||||
import type { ApiDocsTabId } from '../../../types/views/apiDocs';
|
||||
import { API_DOCS_TABS } from '../../../types/views/apiDocs';
|
||||
@@ -216,6 +216,38 @@ const marketplaceSchema = yup
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_TITLES: yup
|
||||
.mixed()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MARKETPLACE_TITLES', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
const valueSchema = yup.object<MarketplaceTitles>().transform(replaceQuotes).json().shape({
|
||||
menu_item: yup.string(),
|
||||
title: yup.string(),
|
||||
subtitle_essential_dapps: yup.string(),
|
||||
subtitle_list: yup.string(),
|
||||
});
|
||||
|
||||
return isUndefined || valueSchema.isValidSync(data);
|
||||
}),
|
||||
otherwise: (schema) => schema.test(
|
||||
'not-exist',
|
||||
'NEXT_PUBLIC_MARKETPLACE_TITLES cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED',
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED: yup
|
||||
.boolean()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.test(
|
||||
'not-exist',
|
||||
'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED',
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const beaconChainSchema = yup
|
||||
|
||||
@@ -8,3 +8,5 @@ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app
|
||||
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '100', '11155111'], 'fee': '0.004', 'integrator': 'blockscout'}, 'revoke': {'chains': ['1', '10', '100', '11155111']}, 'multisend': {'chains': ['1', '10', '100', '11155111'], 'posthogKey': '123', 'posthogHost': 'https://example.com'}}
|
||||
NEXT_PUBLIC_MARKETPLACE_TITLES={'menu_item': 'Dapps', 'title': 'Dappscout'}
|
||||
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED=true
|
||||
|
||||
@@ -588,6 +588,8 @@ Ads are enabled by default on all self-hosted instances. If you would like to di
|
||||
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG | `EssentialDappsConfig`, see details [below](#essential-dapps-configuration-properties) | Configuration of the essential dapps to be displayed on the Marketplace page | - | - | `{'swap': {'chains': ['1', '10', '100', '11155111'], 'fee': '0.004', 'integrator': 'blockscout'}}` | v2.4.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED | `boolean` | The flag enables ad in essential dapps. *Feature is enabled by default; pass `false` to disable it.* | - | `true` | `false` | upcoming |
|
||||
| NEXT_PUBLIC_MARKETPLACE_TITLES | `{ entity_name?: string; menu_item?: string; title?: string; subtitle_essential_dapps?: string; subtitle_list?: string }` | Used to override default titles of the Marketplace and dapps | - | `{ 'entity_name': 'Dapp', 'menu_item': 'Dapps', 'title': 'Dappscout', 'subtitle_essential_dapps': 'Essential dapps', 'subtitle_list': 'Explore dapps' }` | `{ 'entity_name': 'App', 'menu_item': 'Apps', 'title': 'Marketplace', 'subtitle_essential_dapps': 'Essential apps', 'subtitle_list': 'Explore apps' }` | upcoming |
|
||||
|
||||
#### Marketplace app configuration properties
|
||||
|
||||
@@ -888,7 +890,7 @@ If the feature is enabled, a single button or a dropdown (if more than 1 item is
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in DAppscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
|
||||
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in Dappscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga
|
||||
import config from 'configs/app';
|
||||
import { rightLineArrow } from 'toolkit/utils/htmlEntities';
|
||||
|
||||
const marketplaceFeature = config.features.marketplace;
|
||||
|
||||
interface ReturnType {
|
||||
mainNavItems: Array<NavItem | NavGroupItem>;
|
||||
accountNavItems: Array<NavItem>;
|
||||
@@ -346,8 +348,8 @@ export default function useNavItems(): ReturnType {
|
||||
isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive),
|
||||
subItems: tokensNavItems,
|
||||
},
|
||||
config.features.marketplace.isEnabled ? {
|
||||
text: 'DApps',
|
||||
marketplaceFeature.isEnabled ? {
|
||||
text: marketplaceFeature.titles.menu_item,
|
||||
nextRoute: { pathname: '/apps' as const },
|
||||
icon: 'apps',
|
||||
isActive: pathname.startsWith('/app') || pathname.startsWith('/essential-dapps'),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const dappEntityName = (getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '').toLowerCase();
|
||||
|
||||
const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/': '%network_name% blockchain explorer - View %network_name% stats',
|
||||
'/txs': '%network_name% transactions - %network_name% explorer',
|
||||
@@ -21,8 +25,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/tokens': 'Tokens list - %network_name% explorer',
|
||||
'/token/[hash]': '%network_name% token details',
|
||||
'/token/[hash]/instance/[id]': '%network_name% NFT instance',
|
||||
'/apps': '%network_name% DApps - Explore top apps',
|
||||
'/apps/[id]': '%network_name% marketplace app',
|
||||
'/apps': `%network_name% ${ dappEntityName }s - Explore top ${ dappEntityName }s`,
|
||||
'/apps/[id]': `%network_name% marketplace ${ dappEntityName }`,
|
||||
'/essential-dapps/[id]': '%id_cap%',
|
||||
'/stats': '%network_name% stats - %network_name% network insights',
|
||||
'/stats/[id]': '%network_name% stats - %id% chart',
|
||||
|
||||
@@ -49,3 +49,11 @@ export type EssentialDappsConfig = {
|
||||
posthogHost?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface MarketplaceTitles {
|
||||
entity_name: string;
|
||||
menu_item: string;
|
||||
title: string;
|
||||
subtitle_essential_dapps: string;
|
||||
subtitle_list: string;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import AdBanner from 'ui/shared/ad/AdBanner';
|
||||
|
||||
import FeaturedApp from './Banner/FeaturedApp';
|
||||
import IframeBanner from './Banner/IframeBanner';
|
||||
@@ -21,17 +24,21 @@ type BannerProps = {
|
||||
};
|
||||
|
||||
const Banner = ({ apps = [], favoriteApps, isLoading, onInfoClick, onFavoriteClick, onAppClick }: BannerProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!feature.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let content = null;
|
||||
|
||||
if (feature.featuredApp) {
|
||||
const app = apps.find(app => app.id === feature.featuredApp);
|
||||
const isFavorite = favoriteApps.includes(feature.featuredApp);
|
||||
if (!isLoading && !app) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
content = (
|
||||
<FeaturedApp
|
||||
app={ app || appsMock[0] }
|
||||
isFavorite={ isFavorite }
|
||||
@@ -42,10 +49,27 @@ const Banner = ({ apps = [], favoriteApps, isLoading, onInfoClick, onFavoriteCli
|
||||
/>
|
||||
);
|
||||
} else if (feature.banner) {
|
||||
return <IframeBanner contentUrl={ feature.banner.contentUrl } linkUrl={ feature.banner.linkUrl }/>;
|
||||
content = <IframeBanner contentUrl={ feature.banner.contentUrl } linkUrl={ feature.banner.linkUrl }/>;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={ 6 }>
|
||||
{ content }
|
||||
{ !isMobile && (
|
||||
<AdBanner
|
||||
format="mobile"
|
||||
w="fit-content"
|
||||
flexShrink={ 0 }
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -4,17 +4,17 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import { Link, LinkBox, LinkOverlay } from 'toolkit/chakra/link';
|
||||
import { Link, LinkBox } from 'toolkit/chakra/link';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
|
||||
import FavoriteIcon from '../FavoriteIcon';
|
||||
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
import FeaturedAppMobile from './FeaturedAppMobile';
|
||||
|
||||
@@ -63,18 +63,16 @@ const FeaturedApp = ({
|
||||
return (
|
||||
<LinkBox>
|
||||
<Flex
|
||||
gap={ 6 }
|
||||
gap={ 4 }
|
||||
borderRadius="md"
|
||||
height="136px"
|
||||
padding={ 5 }
|
||||
height="100px"
|
||||
padding={ 3 }
|
||||
background={{ _light: 'purple.50', _dark: 'whiteAlpha.100' }}
|
||||
mb={ 2 }
|
||||
mt={ 6 }
|
||||
>
|
||||
<Skeleton
|
||||
loading={ isLoading }
|
||||
w="96px"
|
||||
h="96px"
|
||||
w="76px"
|
||||
h="76px"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -86,22 +84,18 @@ const FeaturedApp = ({
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
<Flex flexDirection="column" flex={ 1 } gap={ 2 }>
|
||||
<Flex flexDirection="column" flex={ 1 } gap={ 1 }>
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
<Skeleton
|
||||
loading={ isLoading }
|
||||
fontSize="30px"
|
||||
fontWeight="semibold"
|
||||
fontFamily="heading"
|
||||
lineHeight="36px"
|
||||
>
|
||||
<LinkOverlay
|
||||
href={ external ? url : route({ pathname: '/apps/[id]', query: { id } }) }
|
||||
marginRight={ 2 }
|
||||
external={ external }
|
||||
>
|
||||
{ title }
|
||||
</LinkOverlay>
|
||||
<Skeleton loading={ isLoading } display="flex" alignItems="center">
|
||||
<Heading level="3">
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
</Heading>
|
||||
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
|
||||
</Skeleton>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import { Link, LinkBox } from 'toolkit/chakra/link';
|
||||
@@ -47,8 +48,7 @@ const FeaturedAppMobile = ({
|
||||
borderRadius="md"
|
||||
padding={{ base: 3, sm: '20px' }}
|
||||
role="group"
|
||||
background={{ base: 'purple.50', sm: 'whiteAlpha.100' }}
|
||||
mt={ 6 }
|
||||
background={{ _light: 'purple.50', _dark: 'whiteAlpha.100' }}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="row"
|
||||
@@ -98,20 +98,19 @@ const FeaturedAppMobile = ({
|
||||
<Flex flexDirection="column" gap={ 2 }>
|
||||
<Skeleton
|
||||
loading={ isLoading }
|
||||
fontSize={{ base: 'sm', sm: 'lg' }}
|
||||
lineHeight={{ base: '20px', sm: '28px' }}
|
||||
paddingRight={{ base: '25px', sm: '110px' }}
|
||||
fontWeight="semibold"
|
||||
fontFamily="heading"
|
||||
display="inline-block"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
<Heading level="3">
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
</Heading>
|
||||
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
|
||||
</Skeleton>
|
||||
|
||||
|
||||
@@ -20,11 +20,9 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st
|
||||
<Skeleton
|
||||
loading={ isFrameLoading }
|
||||
position="relative"
|
||||
h="136px"
|
||||
h="100px"
|
||||
w="100%"
|
||||
borderRadius="md"
|
||||
mb={{ base: 0, sm: 2 }}
|
||||
mt={ 6 }
|
||||
overflow="hidden"
|
||||
>
|
||||
<Link
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { chakra, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
|
||||
import type { ButtonProps } from 'toolkit/chakra/button';
|
||||
@@ -18,7 +21,7 @@ interface Props extends ButtonProps {
|
||||
|
||||
const getTooltipText = (canRate: boolean) => {
|
||||
if (!canRate) {
|
||||
return <>Please log in to Blockscout to rate this DApp.</>;
|
||||
return <>Please log in to Blockscout to rate this { (getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '').toLowerCase() }.</>;
|
||||
}
|
||||
return <>Ratings come from verified users.<br/>Click here to rate!</>;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import type { TestFnArgs } from 'playwright/lib';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
import * as pwConfig from 'playwright/utils/config';
|
||||
|
||||
import Multisend from './Multisend';
|
||||
|
||||
@@ -9,7 +10,7 @@ const ESSENTIAL_DAPPS_CONFIG = JSON.stringify({
|
||||
multisend: { chains: [ '1' ] },
|
||||
});
|
||||
|
||||
test('base view +@dark-mode +@mobile', async({ render, mockEnvs }: TestFnArgs) => {
|
||||
test('base view +@dark-mode +@mobile', async({ render, mockEnvs, page }: TestFnArgs) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG', ESSENTIAL_DAPPS_CONFIG ],
|
||||
@@ -17,5 +18,8 @@ test('base view +@dark-mode +@mobile', async({ render, mockEnvs }: TestFnArgs) =
|
||||
|
||||
const component = await render(<Multisend/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(pwConfig.adsBannerSelector) ],
|
||||
maskColor: pwConfig.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,19 @@ import { Box } from '@chakra-ui/react';
|
||||
import { MultisenderWidget } from '@multisender.app/multisender-react-widget';
|
||||
import React from 'react';
|
||||
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import AdBanner from 'ui/shared/ad/AdBanner';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const dappConfig = feature.isEnabled ? feature.essentialDapps?.multisend : undefined;
|
||||
const feature = getFeaturePayload(config.features.marketplace);
|
||||
const dappConfig = feature?.essentialDapps?.multisend;
|
||||
|
||||
const Container = ({ children }: { children: React.ReactNode }) => (
|
||||
<Box
|
||||
w="full"
|
||||
maxW="670px"
|
||||
mx="auto"
|
||||
css={{
|
||||
@@ -510,19 +515,33 @@ const widgetConfig = Object.fromEntries(dappConfig?.chains.map((chainId) => {
|
||||
}) || []);
|
||||
|
||||
const Multisend = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<MultisenderWidget
|
||||
config={ widgetConfig }
|
||||
logoType="minified"
|
||||
posthogKey={ dappConfig?.posthogKey }
|
||||
posthogHost={ dappConfig?.posthogHost }
|
||||
classNames={{
|
||||
theme: 'multisenderTheme',
|
||||
mantineProvider: 'multisenderMantineProvider',
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<>
|
||||
<Container>
|
||||
<MultisenderWidget
|
||||
config={ widgetConfig }
|
||||
logoType="minified"
|
||||
posthogKey={ dappConfig?.posthogKey }
|
||||
posthogHost={ dappConfig?.posthogHost }
|
||||
classNames={{
|
||||
theme: 'multisenderTheme',
|
||||
mantineProvider: 'multisenderMantineProvider',
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
{ (feature?.essentialDappsAdEnabled && !isMobile) && (
|
||||
<AdBanner
|
||||
format="desktop"
|
||||
w="fit-content"
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
mx="auto"
|
||||
mt={ 10 }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
@@ -1,18 +1,22 @@
|
||||
import { Box, useToken } from '@chakra-ui/react';
|
||||
import { Box, Flex, useToken } from '@chakra-ui/react';
|
||||
import type { RouteExecutionUpdate, WidgetConfig } from '@lifi/widget';
|
||||
import { LiFiWidget, useWidgetEvents, WidgetEvent } from '@lifi/widget';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import useWeb3Wallet from 'lib/web3/useWallet';
|
||||
import { useColorMode } from 'toolkit/chakra/color-mode';
|
||||
import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography';
|
||||
import AdBanner from 'ui/shared/ad/AdBanner';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const dappConfig = feature.isEnabled ? feature.essentialDapps?.swap : undefined;
|
||||
const feature = getFeaturePayload(config.features.marketplace);
|
||||
const dappConfig = feature?.essentialDapps?.swap;
|
||||
|
||||
const defaultChainId = Number(
|
||||
dappConfig?.chains.includes(config.chain.id as string) ?
|
||||
@@ -84,6 +88,7 @@ const Widget = () => {
|
||||
};
|
||||
|
||||
const Swap = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
|
||||
const widgetEvents = useWidgetEvents();
|
||||
const eventParams = useRef<{
|
||||
@@ -128,7 +133,20 @@ const Swap = () => {
|
||||
return () => widgetEvents.all.clear();
|
||||
}, [ widgetEvents, trackTransaction, trackTransactionConfirm ]);
|
||||
|
||||
return <Widget/>;
|
||||
return (
|
||||
<Flex flex="1" flexDir="column" justifyContent="space-between" gap={ 6 }>
|
||||
<Widget/>
|
||||
{ (feature?.essentialDappsAdEnabled && !isMobile) && (
|
||||
<AdBanner
|
||||
format="mobile"
|
||||
w="fit-content"
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
ml="auto"
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swap;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import config from 'configs/app';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
import * as pwConfig from 'playwright/utils/config';
|
||||
|
||||
import Marketplace from './Marketplace';
|
||||
|
||||
@@ -40,10 +41,13 @@ test('with featured app +@dark-mode', async({ render, mockEnvs, page }) => {
|
||||
|
||||
const component = await render(<Marketplace/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(pwConfig.adsBannerSelector) ],
|
||||
maskColor: pwConfig.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => {
|
||||
test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ],
|
||||
@@ -51,10 +55,13 @@ test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse })
|
||||
await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true);
|
||||
const component = await render(<Marketplace/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(pwConfig.adsBannerSelector) ],
|
||||
maskColor: pwConfig.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => {
|
||||
test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigResponse, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG', ESSENTIAL_DAPPS_CONFIG ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ],
|
||||
@@ -62,7 +69,10 @@ test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigRes
|
||||
]);
|
||||
await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true);
|
||||
const component = await render(<Marketplace/>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(pwConfig.adsBannerSelector) ],
|
||||
maskColor: pwConfig.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
// I had a memory error while running tests in GH actions
|
||||
|
||||
@@ -143,7 +143,7 @@ const Marketplace = () => {
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title="DAppscout"
|
||||
title={ feature.titles.title }
|
||||
contentAfter={ (isMobile && links.length > 1) ? (
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
@@ -188,9 +188,13 @@ const Marketplace = () => {
|
||||
|
||||
{ feature.essentialDapps && (
|
||||
<>
|
||||
<Heading level="2" mt={ 8 } mb={ 6 }>Essential dapps</Heading>
|
||||
<Heading level="2" mb={ 6 } mt={ 8 }>
|
||||
{ feature.titles.subtitle_essential_dapps }
|
||||
</Heading>
|
||||
<EssentialDappsList/>
|
||||
<Heading level="2">Explore dapps</Heading>
|
||||
<Heading level="2">
|
||||
{ feature.titles.subtitle_list }
|
||||
</Heading>
|
||||
</>
|
||||
) }
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
@@ -42,10 +43,10 @@ const EssentialDapp = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex flexDir="column" h="full" w="full">
|
||||
<PageTitle title={ title || '' } alignItems={ isCentered ? 'center' : undefined }/>
|
||||
{ content }
|
||||
</>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
@@ -1,4 +1,5 @@
|
||||
import type { CctxListItem } from '@blockscout/zetachain-cctx-types';
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
import type { SearchResultItem } from 'types/client/search';
|
||||
|
||||
@@ -6,6 +7,8 @@ import config from 'configs/app';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
const dappEntityName = getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '';
|
||||
|
||||
export type ApiCategory =
|
||||
'token' |
|
||||
'nft' |
|
||||
@@ -31,7 +34,6 @@ export type SearchResultAppItem = {
|
||||
};
|
||||
|
||||
export const searchCategories: Array<{ id: Category; title: string; tabTitle: string }> = [
|
||||
{ id: 'app', title: 'DApps', tabTitle: 'DApps' },
|
||||
{ id: 'token', title: `Tokens (${ config.chain.tokenStandard }-20)`, tabTitle: 'Tokens' },
|
||||
{ id: 'nft', title: `NFTs (${ config.chain.tokenStandard }-721 & 1155)`, tabTitle: 'NFTs' },
|
||||
{ id: 'address', title: 'Addresses', tabTitle: 'Addresses' },
|
||||
@@ -50,6 +52,10 @@ if (config.features.dataAvailability.isEnabled) {
|
||||
searchCategories.push({ id: 'blob', title: 'Blobs', tabTitle: 'Blobs' });
|
||||
}
|
||||
|
||||
if (config.features.marketplace.isEnabled) {
|
||||
searchCategories.unshift({ id: 'app', title: `${ dappEntityName }s`, tabTitle: `${ dappEntityName }s` });
|
||||
}
|
||||
|
||||
if (nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled) {
|
||||
searchCategories.unshift({ id: 'domain', title: 'Names', tabTitle: 'Names' });
|
||||
}
|
||||
@@ -59,7 +65,7 @@ if (nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled) {
|
||||
}
|
||||
|
||||
export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleShort: string }> = {
|
||||
app: { itemTitle: 'DApp', itemTitleShort: 'App' },
|
||||
app: { itemTitle: dappEntityName, itemTitleShort: dappEntityName },
|
||||
domain: { itemTitle: 'Name', itemTitleShort: 'Name' },
|
||||
cluster: { itemTitle: 'Cluster', itemTitleShort: 'Cluster' },
|
||||
token: { itemTitle: 'Token', itemTitleShort: 'Token' },
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -93,7 +93,7 @@ test.describe('with tooltips', () => {
|
||||
|
||||
await component.locator('header').hover();
|
||||
await page.locator('svg[aria-label="Expand/Collapse menu"]').click();
|
||||
await page.locator('a[aria-label="DApps link"]').hover();
|
||||
await page.locator('a[aria-label="Dapps link"]').hover();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |