Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a top bar to the dapp page #1635

Merged
merged 6 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = 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(),
});
Expand Down
12 changes: 8 additions & 4 deletions types/client/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ export type MarketplaceAppPreview = {
priority?: number;
}

export type MarketplaceAppOverview = MarketplaceAppPreview & {
export type MarketplaceAppSocialInfo = {
twitter?: string;
telegram?: string;
github?: string | Array<string>;
discord?: string;
}

export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocialInfo & {
author: string;
description: string;
site?: string;
twitter?: string;
telegram?: string;
github?: string;
}

export enum MarketplaceCategory {
Expand Down
50 changes: 50 additions & 0 deletions ui/marketplace/MarketplaceAppInfo.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

const MarketplaceAppInfo = ({ data }: Props) => {
const isMobile = useIsMobile();
const { isOpen, onToggle, onClose } = useDisclosure();

if (isMobile) {
return (
<>
<TriggerButton onClick={ onToggle }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent>
<ModalCloseButton/>
<Content data={ data }/>
</ModalContent>
</Modal>
</>
);
}

return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<TriggerButton onClick={ onToggle }/>
</PopoverTrigger>
<PopoverContent w="500px">
<PopoverBody px={ 6 } py={ 5 }>
<Content data={ data }/>
</PopoverBody>
</PopoverContent>
</Popover>
);
};

export default React.memo(MarketplaceAppInfo);
53 changes: 53 additions & 0 deletions ui/marketplace/MarketplaceAppInfo/Content.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

const SOCIAL_LINKS: Array<Omit<SocialLinkProps, 'href'>> = [
{ 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<SocialLinkProps> = [];
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 (
<Flex fontSize="sm" flexDir="column" rowGap={ 5 }>
<div>
<Text variant="secondary" fontSize="xs">Project info</Text>
<Text fontSize="sm" mt={ 3 }>{ data.shortDescription }</Text>
<WebsiteLink url={ data.site }/>
</div>
{ socialLinks.length > 0 && (
<div>
<Text variant="secondary" fontSize="xs">Links</Text>
<Grid templateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} columnGap={ 4 } rowGap={ 3 } mt={ 3 }>
{ socialLinks.map((link, index) => <SocialLink key={ index } { ...link }/>) }
</Grid>
</div>
) }
</Flex>
);
};

export default Content;
32 changes: 32 additions & 0 deletions ui/marketplace/MarketplaceAppInfo/SocialLink.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={ href }
aria-label={ title }
title={ title }
target="_blank"
display="inline-flex"
alignItems="center"
>
<IconSvg name={ icon } boxSize={ 5 } mr={ 2 } color="text_secondary"/>
<span>{ title }</span>
</Link>
);
};

export default SocialLink;
29 changes: 29 additions & 0 deletions ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>) => {
return (
<Button
ref={ ref }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onClick }
aria-label="Show project info"
fontWeight={ 500 }
px={ 2 }
h="32px"
>
<IconSvg name="info" boxSize={ 6 } mr={ 1 }/>
<span>Info</span>
</Button>
);
};

export default React.forwardRef(TriggerButton);
29 changes: 29 additions & 0 deletions ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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;
}
return (
<Link
href={ url }
target="_blank"
display="inline-flex"
alignItems="center"
columnGap={ 1 }
mt={ 3 }
>
<IconSvg name="link" boxSize={ 5 } color="text_secondary"/>
<span>{ (new URL(url)).hostname }</span>
maxaleks marked this conversation as resolved.
Show resolved Hide resolved
</Link>
);
};

export default WebsiteLink;
12 changes: 8 additions & 4 deletions ui/marketplace/MarketplaceAppModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]);
Expand Down
106 changes: 106 additions & 0 deletions ui/marketplace/MarketplaceAppTopBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { chakra, Flex, Tooltip, Skeleton, Box } 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 type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';

import MarketplaceAppInfo from './MarketplaceAppInfo';

type Props = {
isWalletConnected: boolean;
data: MarketplaceAppOverview | undefined;
isPending: boolean;
tom2drum marked this conversation as resolved.
Show resolved Hide resolved
}

const MarketplaceAppTopBar = ({ data, isPending, isWalletConnected }: 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 ]);

const message = React.useMemo(() => {
let icon: IconName = 'wallet';
let iconColor = 'blackAlpha.800';
let bgColor = 'orange.100';
let text = 'Connect your wallet to Blockscout for full-featured access';

if (isWalletConnected && data?.internalWallet) {
icon = 'integration/full';
iconColor = 'green.600';
bgColor = 'green.100';
text = 'Your wallet is connected with Blockscout';
} else if (isWalletConnected) {
icon = 'integration/partial';
text = 'Connect your wallet in the app below';
}

return { icon, iconColor, bgColor, text };
}, [ isWalletConnected, data?.internalWallet ]);
tom2drum marked this conversation as resolved.
Show resolved Hide resolved

if (isPending) {
return (
<Flex alignItems="center" mb={ 2 } gap={ 2 }>
<Skeleton w="25px" h="32px" borderRadius="base"/>
<Skeleton w="300px" h="32px" borderRadius="base"/>
<Skeleton w="75px" h="32px" borderRadius="base"/>
<Skeleton w="150px" h="32px" borderRadius="base"/>
</Flex>
);
}
tom2drum marked this conversation as resolved.
Show resolved Hide resolved

if (!data) {
return null;
}

return (
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 6, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
<Tooltip label="Back to dApps list" order={ 1 }>
<LinkInternal display="inline-flex" href={ goBackUrl } h="32px" mr={{ base: 'auto', md: 0 }}>
<IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/>
</LinkInternal>
</Tooltip>
<Flex
flex={{ base: 1, md: 'none' }}
alignItems="center"
bgColor={ message.bgColor }
color="gray.700"
minHeight={ 8 }
borderRadius="base"
px={ 3 }
py={{ base: 3, md: 1.5 }}
order={{ base: 4, md: 2 }}
>
<IconSvg name={ message.icon } color={ message.iconColor } boxSize={ 5 } flexShrink={ 0 }/>
<chakra.span ml={ 2 } fontSize="sm" lineHeight={ 5 }>{ message.text }</chakra.span>
</Flex>
<Box order={{ base: 2, md: 3 }}>
<MarketplaceAppInfo data={ data }/>
</Box>
<LinkExternal
order={{ base: 3, md: 4 }}
href={ data.url }
variant="subtle"
fontSize="sm"
lineHeight={ 5 }
minW={ 0 }
maxW={{ base: 'calc(100% - 114px)', md: 'auto' }}
display="flex"
>
<chakra.span isTruncated>{ (new URL(data.url)).hostname }</chakra.span>
</LinkExternal>
</Flex>
);
};

export default MarketplaceAppTopBar;
24 changes: 14 additions & 10 deletions ui/pages/MarketplaceApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -139,16 +140,19 @@ const MarketplaceApp = () => {
throwOnResourceLoadError(query);

return (
<DappscoutIframeProvider
address={ address }
appUrl={ data?.url }
rpcUrl={ config.chain.rpcUrl }
sendTransaction={ sendTransaction }
signMessage={ signMessage }
signTypedData={ signTypedData }
>
<MarketplaceAppContent address={ address } data={ data } isPending={ isPending }/>
</DappscoutIframeProvider>
<>
<MarketplaceAppTopBar isWalletConnected={ Boolean(address) } data={ data } isPending={ isPending }/>
<DappscoutIframeProvider
address={ address }
appUrl={ data?.url }
rpcUrl={ config.chain.rpcUrl }
sendTransaction={ sendTransaction }
signMessage={ signMessage }
signTypedData={ signTypedData }
>
<MarketplaceAppContent address={ address } data={ data } isPending={ isPending }/>
</DappscoutIframeProvider>
</>
);
};

Expand Down
Loading
Loading