diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index fd2a2c656c..33fb8ab4f6 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -75,7 +75,12 @@ const marketplaceAppSchema: yup.ObjectSchema = yup site: yup.string().test(urlTest), twitter: yup.string().test(urlTest), telegram: yup.string().test(urlTest), - github: yup.string().test(urlTest), + github: yup.lazy(value => + Array.isArray(value) ? + yup.array().of(yup.string().required().test(urlTest)) : + yup.string().test(urlTest), + ), + discord: yup.string().test(urlTest), internalWallet: yup.boolean(), priority: yup.number(), }); diff --git a/types/client/marketplace.ts b/types/client/marketplace.ts index eb132aac20..58ede5b258 100644 --- a/types/client/marketplace.ts +++ b/types/client/marketplace.ts @@ -11,13 +11,17 @@ export type MarketplaceAppPreview = { priority?: number; } -export type MarketplaceAppOverview = MarketplaceAppPreview & { +export type MarketplaceAppSocialInfo = { + twitter?: string; + telegram?: string; + github?: string | Array; + discord?: string; +} + +export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocialInfo & { author: string; description: string; site?: string; - twitter?: string; - telegram?: string; - github?: string; } export enum MarketplaceCategory { diff --git a/ui/marketplace/MarketplaceAppAlert.tsx b/ui/marketplace/MarketplaceAppAlert.tsx new file mode 100644 index 0000000000..107a542e06 --- /dev/null +++ b/ui/marketplace/MarketplaceAppAlert.tsx @@ -0,0 +1,55 @@ +import { Alert } from '@chakra-ui/react'; +import React from 'react'; + +import type { IconName } from 'ui/shared/IconSvg'; +import IconSvg from 'ui/shared/IconSvg'; + +import useMarketplaceWallet from './useMarketplaceWallet'; + +type Props = { + internalWallet: boolean | undefined; +} + +const MarketplaceAppAlert = ({ internalWallet }: Props) => { + const { address } = useMarketplaceWallet(); + const isWalletConnected = Boolean(address); + + const message = React.useMemo(() => { + let icon: IconName = 'wallet'; + let text = 'Connect your wallet to Blockscout for full-featured access'; + let status: 'warning' | 'success' = 'warning'; + + if (isWalletConnected && internalWallet) { + icon = 'integration/full'; + text = 'Your wallet is connected with Blockscout'; + status = 'success'; + } else if (isWalletConnected) { + icon = 'integration/partial'; + text = 'Connect your wallet in the app below'; + } + + return { icon, text, status }; + }, [ isWalletConnected, internalWallet ]); + + return ( + + + { message.text } + + ); +}; + +export default MarketplaceAppAlert; diff --git a/ui/marketplace/MarketplaceAppInfo.tsx b/ui/marketplace/MarketplaceAppInfo.tsx new file mode 100644 index 0000000000..5b25aea641 --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo.tsx @@ -0,0 +1,50 @@ +import { + Popover, PopoverTrigger, PopoverContent, PopoverBody, + Modal, ModalContent, ModalCloseButton, useDisclosure, +} from '@chakra-ui/react'; +import React from 'react'; + +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +import useIsMobile from 'lib/hooks/useIsMobile'; + +import Content from './MarketplaceAppInfo/Content'; +import TriggerButton from './MarketplaceAppInfo/TriggerButton'; + +interface Props { + data: MarketplaceAppOverview | undefined; +} + +const MarketplaceAppInfo = ({ data }: Props) => { + const isMobile = useIsMobile(); + const { isOpen, onToggle, onClose } = useDisclosure(); + + if (isMobile) { + return ( + <> + + + + + + + + + ); + } + + return ( + + + + + + + + + + + ); +}; + +export default React.memo(MarketplaceAppInfo); diff --git a/ui/marketplace/MarketplaceAppInfo/Content.tsx b/ui/marketplace/MarketplaceAppInfo/Content.tsx new file mode 100644 index 0000000000..04b55b84b4 --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo/Content.tsx @@ -0,0 +1,53 @@ +import { Flex, Text, Grid } from '@chakra-ui/react'; +import React from 'react'; + +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +import SocialLink from './SocialLink'; +import type { Props as SocialLinkProps } from './SocialLink'; +import WebsiteLink from './WebsiteLink'; + +interface Props { + data: MarketplaceAppOverview | undefined; +} + +const SOCIAL_LINKS: Array> = [ + { field: 'github', icon: 'social/github_filled', title: 'Github' }, + { field: 'twitter', icon: 'social/twitter_filled', title: 'Twitter' }, + { field: 'telegram', icon: 'social/telegram_filled', title: 'Telegram' }, + { field: 'discord', icon: 'social/discord_filled', title: 'Discord' }, +]; + +const Content = ({ data }: Props) => { + const socialLinks: Array = []; + SOCIAL_LINKS.forEach((link) => { + const href = data?.[link.field]; + if (href) { + if (Array.isArray(href)) { + href.forEach((href) => socialLinks.push({ ...link, href })); + } else { + socialLinks.push({ ...link, href }); + } + } + }); + + return ( + +
+ Project info + { data?.shortDescription } + +
+ { socialLinks.length > 0 && ( +
+ Links + + { socialLinks.map((link, index) => ) } + +
+ ) } +
+ ); +}; + +export default Content; diff --git a/ui/marketplace/MarketplaceAppInfo/SocialLink.tsx b/ui/marketplace/MarketplaceAppInfo/SocialLink.tsx new file mode 100644 index 0000000000..da3464f6d6 --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo/SocialLink.tsx @@ -0,0 +1,32 @@ +import { Link } from '@chakra-ui/react'; +import React from 'react'; + +import type { MarketplaceAppSocialInfo } from 'types/client/marketplace'; + +import type { IconName } from 'ui/shared/IconSvg'; +import IconSvg from 'ui/shared/IconSvg'; + +export interface Props { + field: keyof MarketplaceAppSocialInfo; + icon: IconName; + title: string; + href?: string; +} + +const SocialLink = ({ href, icon, title }: Props) => { + return ( + + + { title } + + ); +}; + +export default SocialLink; diff --git a/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx b/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx new file mode 100644 index 0000000000..cef2f9c31e --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx @@ -0,0 +1,29 @@ +import { Button } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + onClick: () => void; +} + +const TriggerButton = ({ onClick }: Props, ref: React.ForwardedRef) => { + return ( + + ); +}; + +export default React.forwardRef(TriggerButton); diff --git a/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx b/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx new file mode 100644 index 0000000000..2a3dea9cca --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx @@ -0,0 +1,36 @@ +import { Link } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + url?: string | undefined; +} + +const WebsiteLink = ({ url }: Props) => { + if (!url) { + return null; + } + + function getHostname(url: string) { + try { + return new URL(url).hostname; + } catch (err) {} + } + + return ( + + + { getHostname(url) } + + ); +}; + +export default WebsiteLink; diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx index d997621f8a..297bcae1c3 100644 --- a/ui/marketplace/MarketplaceAppModal.tsx +++ b/ui/marketplace/MarketplaceAppModal.tsx @@ -50,12 +50,16 @@ const MarketplaceAppModal = ({ icon: 'social/tweet' as IconName, url: twitter, } : null, - github ? { - icon: 'social/git' as IconName, - url: github, - } : null, ].filter(Boolean); + if (github) { + if (Array.isArray(github)) { + github.forEach((url) => socialLinks.push({ icon: 'social/git', url })); + } else { + socialLinks.push({ icon: 'social/git', url: github }); + } + } + const handleFavoriteClick = useCallback(() => { onFavoriteClick(data.id, isFavorite); }, [ onFavoriteClick, data.id, isFavorite ]); diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx new file mode 100644 index 0000000000..cdf4c191f6 --- /dev/null +++ b/ui/marketplace/MarketplaceAppTopBar.tsx @@ -0,0 +1,69 @@ +import { chakra, Flex, Tooltip, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +import { route } from 'nextjs-routes'; + +import { useAppContext } from 'lib/contexts/app'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkExternal from 'ui/shared/LinkExternal'; +import LinkInternal from 'ui/shared/LinkInternal'; + +import MarketplaceAppAlert from './MarketplaceAppAlert'; +import MarketplaceAppInfo from './MarketplaceAppInfo'; + +type Props = { + data: MarketplaceAppOverview | undefined; + isLoading: boolean; +} + +const MarketplaceAppTopBar = ({ data, isLoading }: Props) => { + const appProps = useAppContext(); + + const goBackUrl = React.useMemo(() => { + if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) { + return appProps.referrer; + } + return route({ pathname: '/apps' }); + }, [ appProps.referrer ]); + + function getHostname(url: string | undefined) { + try { + return new URL(url || '').hostname; + } catch (err) {} + } + + return ( + + + + + + + + + + + + + + + { getHostname(data?.url) } + + + + ); +}; + +export default MarketplaceAppTopBar; diff --git a/ui/pages/MarketplaceApp.tsx b/ui/pages/MarketplaceApp.tsx index 98e65dcf57..f2ffb6c85c 100644 --- a/ui/pages/MarketplaceApp.tsx +++ b/ui/pages/MarketplaceApp.tsx @@ -17,6 +17,7 @@ import * as metadata from 'lib/metadata'; import getQueryParamString from 'lib/router/getQueryParamString'; import ContentLoader from 'ui/shared/ContentLoader'; +import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar'; import useAutoConnectWallet from '../marketplace/useAutoConnectWallet'; import useMarketplaceWallet from '../marketplace/useMarketplaceWallet'; @@ -139,16 +140,19 @@ const MarketplaceApp = () => { throwOnResourceLoadError(query); return ( - - - + <> + + + + + ); }; diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx index 7f02e34df8..8eaebf342a 100644 --- a/ui/shared/layout/LayoutApp.tsx +++ b/ui/shared/layout/LayoutApp.tsx @@ -20,7 +20,7 @@ const LayoutDefault = ({ children }: Props) => { > - + { children }