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 all 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
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
Loading