Commit c0ff778f authored by Max Alekseenko's avatar Max Alekseenko

add featured app banner

parent cbd0bf04
...@@ -11,6 +11,9 @@ const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_F ...@@ -11,6 +11,9 @@ const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_F
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL'); const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP');
const bannerImageUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_IMAGE_URL');
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
const title = 'Marketplace'; const title = 'Marketplace';
...@@ -22,31 +25,38 @@ const config: Feature<( ...@@ -22,31 +25,38 @@ const config: Feature<(
categoriesUrl: string | undefined; categoriesUrl: string | undefined;
suggestIdeasFormUrl: string | undefined; suggestIdeasFormUrl: string | undefined;
securityReportsUrl: string | undefined; securityReportsUrl: string | undefined;
} featuredApp: string | undefined;
> = (() => { banner: { imageUrl: string; linkUrl: string } | undefined;
}> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
const props = {
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
securityReportsUrl,
featuredApp,
banner: bannerImageUrl && bannerLinkUrl ? {
imageUrl: bannerImageUrl,
linkUrl: bannerLinkUrl,
} : undefined,
};
if (configUrl) { if (configUrl) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
configUrl, configUrl,
submitFormUrl, ...props,
categoriesUrl,
suggestIdeasFormUrl,
securityReportsUrl,
}); });
} else if (adminServiceApiHost) { } else if (adminServiceApiHost) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
securityReportsUrl,
api: { api: {
endpoint: adminServiceApiHost, endpoint: adminServiceApiHost,
basePath: '', basePath: '',
}, },
...props,
}); });
} }
} }
......
...@@ -51,6 +51,7 @@ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blocksc ...@@ -51,6 +51,7 @@ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blocksc
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
......
...@@ -17,6 +17,7 @@ ASSETS_ENVS=( ...@@ -17,6 +17,7 @@ ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CONFIG_URL" "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
"NEXT_PUBLIC_MARKETPLACE_BANNER_IMAGE_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO" "NEXT_PUBLIC_NETWORK_LOGO"
......
...@@ -456,6 +456,9 @@ This feature is **always enabled**, but you can configure its behavior by passin ...@@ -456,6 +456,9 @@ This feature is **always enabled**, but you can configure its behavior by passin
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` |
| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | | NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` |
| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | | NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` |
| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` |
| NEXT_PUBLIC_MARKETPLACE_BANNER_IMAGE_URL | `string` | URL of the banner image. Must support the `_mobile` postfix | - | - | `https://example.com/banner_image` |
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` |
#### Marketplace app configuration properties #### Marketplace app configuration properties
......
...@@ -100,7 +100,7 @@ Type extends EventTypes.PAGE_WIDGET ? ( ...@@ -100,7 +100,7 @@ Type extends EventTypes.PAGE_WIDGET ? (
} | { } | {
'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts'; 'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts';
'Info': string; 'Info': string;
'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup'; 'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner';
} | { } | {
'Type': 'Security score'; 'Type': 'Security score';
'Source': 'Analyzed contracts popup'; 'Source': 'Analyzed contracts popup';
......
import { Link, Skeleton, useColorModeValue, LinkBox, Flex, Image, LinkOverlay, IconButton } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { apps as appsMock } from 'mocks/apps/apps';
import IconSvg from 'ui/shared/IconSvg';
import MarketplaceAppCard from './MarketplaceAppCard';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
const feature = config.features.marketplace;
type BannerProps = {
apps: Array<MarketplaceAppPreview>;
favoriteApps: Array<string>;
isLoading: boolean;
onInfoClick: (id: string) => void;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Banner') => void;
onAppClick: (event: MouseEvent, id: string) => void;
}
type FeaturedAppProps = {
app: MarketplaceAppPreview;
isFavorite: boolean;
isLoading: boolean;
onInfoClick: (id: string) => void;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Banner') => void;
onAppClick: (event: MouseEvent, id: string) => void;
}
const FeaturedApp = ({
app, isFavorite, isLoading, onAppClick,
onInfoClick: onInfoClickProp, onFavoriteClick: onFavoriteClickProp,
}: FeaturedAppProps) => {
const isMobile = useIsMobile();
const { id, url, external, title, logo, logoDarkMode, shortDescription, categories, internalWallet } = app;
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const categoriesLabel = categories.join(', ');
const onInfoClick = useCallback((id: string) => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Banner' });
onInfoClickProp(id);
}, [ onInfoClickProp ]);
const onFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
onFavoriteClickProp(id, isFavorite, 'Banner');
}, [ onFavoriteClickProp ]);
const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
if (isMobile) {
return (
<MarketplaceAppCard
{ ...app }
onInfoClick={ onInfoClick }
isFavorite={ isFavorite }
onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading }
onAppClick={ onAppClick }
_hover={{ boxShadow: 'none' }}
_focusWithin={{ boxShadow: 'none' }}
border="none"
background="purple.50"
mb={ 4 }
/>
);
}
return (
<LinkBox role="group">
<Flex
gap={{ base: 4, sm: 6 }}
borderRadius={{ base: '8px', sm: '12px' }}
height={{ base: '135px', sm: '136px' }}
padding={{ base: 3, sm: 5 }}
background="purple.50"
>
<Skeleton
isLoaded={ !isLoading }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
display="flex"
alignItems="center"
justifyContent="center"
>
<Image
src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` }
borderRadius={{ base: '8px', sm: '12px' }}
/>
</Skeleton>
<Flex flexDirection="column" flex={ 1 } gap={ 2 }>
<Flex alignItems="center" gap={ 3 }>
<Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'sm', sm: '30px' }}
fontWeight="semibold"
fontFamily="heading"
lineHeight="36px"
>
{ external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }>
{ title }
</LinkOverlay>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay marginRight={ 2 }>
{ title }
</LinkOverlay>
</NextLink>
) }
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
</Skeleton>
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
fontSize="xs"
flex={ 1 }
>
<span>{ categoriesLabel }</span>
</Skeleton>
{ !isLoading && (
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
fontWeight="500"
href="#"
onClick={ handleInfoClick }
>
More info
</Link>
) }
{ !isLoading && (
<IconButton
display="flex"
alignItems="center"
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
) }
</Flex>
<Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'xs', sm: 'sm' }}
lineHeight="20px"
noOfLines={ 2 }
>
{ shortDescription }
</Skeleton>
</Flex>
</Flex>
</LinkBox>
);
};
const Banner = ({ apps, favoriteApps, isLoading, onInfoClick, onFavoriteClick, onAppClick }: BannerProps) => {
if (!feature.isEnabled) {
return 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 (
<FeaturedApp
app={ app || appsMock[0] }
isFavorite={ isFavorite }
isLoading={ isLoading }
onInfoClick={ onInfoClick }
onFavoriteClick={ onFavoriteClick }
onAppClick={ onAppClick }
/>
);
}
return null;
};
export default Banner;
...@@ -43,7 +43,7 @@ export default function useMarketplace() { ...@@ -43,7 +43,7 @@ export default function useMarketplace() {
const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null); const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null);
const [ hasPreviousStep, setHasPreviousStep ] = React.useState<boolean>(false); const [ hasPreviousStep, setHasPreviousStep ] = React.useState<boolean>(false);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal') => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal' | 'Banner') => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source });
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
......
...@@ -9,6 +9,7 @@ import config from 'configs/app'; ...@@ -9,6 +9,7 @@ import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFeatureValue from 'lib/growthbook/useFeatureValue'; import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Banner from 'ui/marketplace/Banner';
import ContractListModal from 'ui/marketplace/ContractListModal'; import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
...@@ -167,6 +168,16 @@ const Marketplace = () => { ...@@ -167,6 +168,16 @@ const Marketplace = () => {
</Flex> </Flex>
) } ) }
/> />
<Banner
apps={ displayedApps }
favoriteApps={ favoriteApps }
isLoading={ isPlaceholderData }
onInfoClick={ showAppInfo }
onFavoriteClick={ onFavoriteClick }
onAppClick={ handleAppClick }
/>
<Box marginTop={{ base: 0, lg: 8 }}> <Box marginTop={{ base: 0, lg: 8 }}>
{ (isCategoriesPlaceholderData) ? ( { (isCategoriesPlaceholderData) ? (
<TabsSkeleton tabs={ categoryTabs }/> <TabsSkeleton tabs={ categoryTabs }/>
......
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