Skip to content

Commit

Permalink
Merge pull request #1635 from blockscout/add-info-to-dapp-page
Browse files Browse the repository at this point in the history
Add a top bar to the dapp page
  • Loading branch information
maxaleks authored Mar 4, 2024
2 parents 421e2a3 + 1ea0b6e commit 1d91b29
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 20 deletions.
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
55 changes: 55 additions & 0 deletions ui/marketplace/MarketplaceAppAlert.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Alert
status={ message.status }
borderRadius="base"
px={ 3 }
py={{ base: 3, md: 1.5 }}
fontSize="sm"
lineHeight={ 5 }
>
<IconSvg
name={ message.icon }
color={ message.status === 'success' ? 'green.600' : 'current' }
boxSize={ 5 }
flexShrink={ 0 }
mr={ 2 }
/>
{ message.text }
</Alert>
);
};

export default MarketplaceAppAlert;
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 | undefined;
}

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 | undefined;
}

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);
36 changes: 36 additions & 0 deletions ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={ url }
target="_blank"
display="inline-flex"
alignItems="center"
columnGap={ 1 }
mt={ 3 }
>
<IconSvg name="link" boxSize={ 5 } color="text_secondary"/>
<span>{ getHostname(url) }</span>
</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
69 changes: 69 additions & 0 deletions ui/marketplace/MarketplaceAppTopBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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" isLoading={ isLoading }>
<IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/>
</LinkInternal>
</Tooltip>
<Skeleton width={{ base: '100%', md: 'auto' }} order={{ base: 4, md: 2 }} isLoaded={ !isLoading }>
<MarketplaceAppAlert internalWallet={ data?.internalWallet }/>
</Skeleton>
<Skeleton order={{ base: 2, md: 3 }} isLoaded={ !isLoading }>
<MarketplaceAppInfo data={ data }/>
</Skeleton>
<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"
isLoading={ isLoading }
>
<chakra.span isTruncated>
{ getHostname(data?.url) }
</chakra.span>
</LinkExternal>
</Flex>
);
};

export default MarketplaceAppTopBar;
Loading

0 comments on commit 1d91b29

Please sign in to comment.