diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index e06a2986de..e831df7a6c 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -6,6 +6,7 @@ import { getEnvValue, getExternalAssetFilePath } from '../utils'; // config file will be downloaded at run-time and saved in the public folder const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL'); const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM'); +const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM'); const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); @@ -14,7 +15,7 @@ const title = 'Marketplace'; const config: Feature<( { configUrl: string } | { api: { endpoint: string; basePath: string } } -) & { submitFormUrl: string; categoriesUrl: string | undefined } +) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined } > = (() => { if (chain.rpcUrl && submitFormUrl) { if (configUrl) { @@ -24,6 +25,7 @@ const config: Feature<( configUrl, submitFormUrl, categoriesUrl, + suggestIdeasFormUrl, }); } else if (adminServiceApiHost) { return Object.freeze({ @@ -31,6 +33,7 @@ const config: Feature<( isEnabled: true, submitFormUrl, categoriesUrl, + suggestIdeasFormUrl, api: { endpoint: adminServiceApiHost, basePath: '', diff --git a/configs/envs/.env.main b/configs/envs/.env.main index 424e66c7af..837f72fefb 100644 --- a/configs/envs/.env.main +++ b/configs/envs/.env.main @@ -48,6 +48,7 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form 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_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 6855672bab..10a5c0bec5 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -95,6 +95,14 @@ const marketplaceSchema = yup // eslint-disable-next-line max-len otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_CONFIG_URL or NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'), }), + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: yup + .string() + .when([ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST' ], { + is: (config: Array, apiHost: string) => config.length > 0 || Boolean(apiHost), + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_CONFIG_URL or NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'), + }), }); const beaconChainSchema = yup diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index da66418e34..220bb815e7 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -25,6 +25,7 @@ NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='Hello' NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether diff --git a/deploy/values/l2-optimism-goerli/values.yaml b/deploy/values/l2-optimism-goerli/values.yaml index e376a6e30c..b0382e94a4 100644 --- a/deploy/values/l2-optimism-goerli/values.yaml +++ b/deploy/values/l2-optimism-goerli/values.yaml @@ -180,6 +180,7 @@ frontend: NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-goerli.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json diff --git a/deploy/values/main/values.yaml b/deploy/values/main/values.yaml index 4d3bd4bed7..742f773709 100644 --- a/deploy/values/main/values.yaml +++ b/deploy/values/main/values.yaml @@ -149,6 +149,7 @@ frontend: NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_APP_ENV: development NEXT_PUBLIC_APP_INSTANCE: main NEXT_PUBLIC_STATS_API_HOST: https://stats-test.k8s-dev.blockscout.com/ diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index afeb1d00ad..2c8a0b79a0 100644 --- a/deploy/values/review-l2/values.yaml.gotmpl +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -53,6 +53,7 @@ frontend: NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/base-goerli.json NEXT_PUBLIC_API_HOST: blockscout-optimism-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_STATS_API_HOST: https://stats-optimism-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index b4e3ff67d9..85d0e57486 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -59,6 +59,7 @@ frontend: NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli diff --git a/docs/ENVS.md b/docs/ENVS.md index 93bfe9870f..041f21d790 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -432,6 +432,7 @@ This feature is **always enabled**, but you can configure its behavior by passin | NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app. Can be replaced with NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | Required | - | `https://example.com/marketplace_config.json` | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | +| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | | 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 markeplace 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` | diff --git a/pages/apps/index.tsx b/pages/apps/index.tsx index 17f75586ac..aa5c0bae15 100644 --- a/pages/apps/index.tsx +++ b/pages/apps/index.tsx @@ -4,31 +4,13 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; -import config from 'configs/app'; -import LinkExternal from 'ui/shared/LinkExternal'; -import PageTitle from 'ui/shared/Page/PageTitle'; - -const feature = config.features.marketplace; - const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false }); -const Page: NextPage = () => { - return ( - - <> - - Submit app - - ) } - /> - - - - ); -}; +const Page: NextPage = () => ( + + + +); export default Page; diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 06c40c59e4..0093cdd201 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -1,4 +1,4 @@ -import { Box } from '@chakra-ui/react'; +import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react'; import React from 'react'; import { MarketplaceCategory } from 'types/client/marketplace'; @@ -7,18 +7,40 @@ import type { TabItem } from 'ui/shared/Tabs/types'; import config from 'configs/app'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useFeatureValue from 'lib/growthbook/useFeatureValue'; +import useIsMobile from 'lib/hooks/useIsMobile'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceList from 'ui/marketplace/MarketplaceList'; import FilterInput from 'ui/shared/filters/FilterInput'; import IconSvg from 'ui/shared/IconSvg'; +import type { IconName } from 'ui/shared/IconSvg'; +import LinkExternal from 'ui/shared/LinkExternal'; +import PageTitle from 'ui/shared/Page/PageTitle'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import useMarketplace from '../marketplace/useMarketplace'; const feature = config.features.marketplace; +const links: Array<{ label: string; href: string; icon: IconName }> = []; +if (feature.isEnabled) { + if (feature.submitFormUrl) { + links.push({ + label: 'Submit app', + href: feature.submitFormUrl, + icon: 'plus' as IconName, + }); + } + if (feature.suggestIdeasFormUrl) { + links.push({ + label: 'Suggest ideas', + href: feature.suggestIdeasFormUrl, + icon: 'edit' as IconName, + }); + } +} + const Marketplace = () => { const { isPlaceholderData, @@ -41,7 +63,7 @@ const Marketplace = () => { appsTotal, isCategoriesPlaceholderData, } = useMarketplace(); - + const isMobile = useIsMobile(); const { value: isExperiment } = useFeatureValue('marketplace_exp', false); const categoryTabs = React.useMemo(() => { @@ -88,6 +110,39 @@ const Marketplace = () => { return ( <> + 1) ? ( + + } + /> + + { links.map(({ label, href, icon }) => ( + + + { label } + + + )) } + + + ) : ( + + { links.map(({ label, href }) => ( + + { label } + + )) } + + ) } + /> { isExperiment && ( { (isCategoriesPlaceholderData) ? (