Commit 8071589e authored by tom's avatar tom

skeletons for stats

parent 4f948605
...@@ -26,4 +26,4 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false ...@@ -26,4 +26,4 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
import type { HomeStats } from 'types/api/stats'; import type { HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = { export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346, average_block_time: 14346,
...@@ -18,3 +18,38 @@ export const HOMEPAGE_STATS: HomeStats = { ...@@ -18,3 +18,38 @@ export const HOMEPAGE_STATS: HomeStats = {
total_transactions: '193823272', total_transactions: '193823272',
transactions_today: '0', transactions_today: '0',
}; };
export const STATS_CHARTS_SECTION: StatsChartsSection = {
id: 'placeholder',
title: 'Placeholder',
charts: [
{
id: 'chart_0',
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
},
{
id: 'chart_1',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
},
{
id: 'chart_2',
title: 'New transactions',
description: 'New transactions number',
units: null,
},
{
id: 'chart_3',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: null,
},
],
};
export const STATS_CHARTS = {
sections: [ STATS_CHARTS_SECTION ],
};
...@@ -40,7 +40,7 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -40,7 +40,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } variant="secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
<Utilization <Utilization
colorScheme="gray" colorScheme="gray"
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() } value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }
...@@ -49,7 +49,7 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -49,7 +49,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } variant="secondary">{ totalReward.toFixed() }</Skeleton> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
</Flex> </Flex>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -25,7 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -25,7 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading } isLoading={ isLoading }
h="250px" h="300px"
/> />
); );
}; };
......
...@@ -57,7 +57,7 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -57,7 +57,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
) } ) }
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Age</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Age</Skeleton>
<Skeleton isLoaded={ !props.isLoading } variant="secondary">{ timeAgo }</Skeleton> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ timeAgo }</Skeleton>
</Flex> </Flex>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -12,14 +12,14 @@ import useStats from '../stats/useStats'; ...@@ -12,14 +12,14 @@ import useStats from '../stats/useStats';
const Stats = () => { const Stats = () => {
const { const {
isLoading, isPlaceholderData,
isError, isError,
sections, sections,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
filterQuery, filterQuery,
} = useStats(); } = useStats();
...@@ -39,14 +39,14 @@ const Stats = () => { ...@@ -39,14 +39,14 @@ const Stats = () => {
onSectionChange={ handleSectionChange } onSectionChange={ handleSectionChange }
interval={ interval } interval={ interval }
onIntervalChange={ handleIntervalChange } onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts } onFilterInputChange={ handleFilterChange }
/> />
</Box> </Box>
<ChartsWidgetsList <ChartsWidgetsList
filterQuery={ filterQuery } filterQuery={ filterQuery }
isError={ isError } isError={ isError }
isLoading={ isLoading } isPlaceholderData={ isPlaceholderData }
charts={ displayedCharts } charts={ displayedCharts }
interval={ interval } interval={ interval }
/> />
......
...@@ -3,13 +3,13 @@ import { ...@@ -3,13 +3,13 @@ import {
Center, Center,
chakra, chakra,
Flex, Flex,
Grid,
Icon, Icon,
IconButton, Link, IconButton, Link,
Menu, Menu,
MenuButton, MenuButton,
MenuItem, MenuItem,
MenuList, MenuList,
Skeleton,
Text, Text,
Tooltip, Tooltip,
useColorModeValue, useColorModeValue,
...@@ -30,7 +30,6 @@ import { apos } from 'lib/html-entities'; ...@@ -30,7 +30,6 @@ import { apos } from 'lib/html-entities';
import saveAsCSV from 'lib/saveAsCSV'; import saveAsCSV from 'lib/saveAsCSV';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import FullscreenChartModal from './FullscreenChartModal'; import FullscreenChartModal from './FullscreenChartModal';
export type Props = { export type Props = {
...@@ -110,10 +109,6 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -110,10 +109,6 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
} }
}, [ items, title ]); }, [ items, title ]);
if (isLoading) {
return <ChartWidgetSkeleton hasDescription={ Boolean(description) }/>;
}
const hasItems = items && items.length > 2; const hasItems = items && items.length > 2;
const content = (() => { const content = (() => {
...@@ -137,6 +132,10 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -137,6 +132,10 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
); );
} }
if (isLoading) {
return <Skeleton flexGrow={ 1 } w="100%"/>;
}
if (!hasItems) { if (!hasItems) {
return ( return (
<Center flexGrow={ 1 }> <Center flexGrow={ 1 }>
...@@ -146,7 +145,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -146,7 +145,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
} }
return ( return (
<Box h="100%" maxW="100%"> <Box flexGrow={ 1 } maxW="100%">
<ChartWidgetGraph <ChartWidgetGraph
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
...@@ -160,112 +159,104 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -160,112 +159,104 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
return ( return (
<> <>
<Box <Flex
height="100%" height="100%"
display="flex"
flexDirection="column"
ref={ ref } ref={ ref }
flexDir="column"
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
border="1px" border="1px"
borderColor={ borderColor } borderColor={ borderColor }
className={ className } className={ className }
> >
<Grid <Flex columnGap={ 6 } mb={ 1 } alignItems="flex-start">
gridTemplateColumns="auto auto 36px" <Flex flexGrow={ 1 } flexDir="column" alignItems="flex-start">
gridColumnGap={ 2 } <Skeleton
> isLoaded={ !isLoading }
<Text fontWeight={ 600 }
fontWeight={ 600 } size={{ base: 'xs', lg: 'sm' }}
fontSize="md"
lineHeight={ 6 }
as="p"
size={{ base: 'xs', lg: 'sm' }}
>
{ title }
</Text>
{ description && (
<Text
mb={ 1 }
gridColumn={ 1 }
as="p"
variant="secondary"
fontSize="xs"
> >
{ description } { title }
</Text> </Skeleton>
) }
<Tooltip label="Reset zoom">
<IconButton
hidden={ isZoomResetInitial }
aria-label="Reset zoom"
colorScheme="blue"
w={ 9 }
h={ 8 }
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
/>
</Tooltip>
{ hasItems && ( { description && (
<Menu> <Skeleton
<MenuButton isLoaded={ !isLoading }
gridColumn={ 3 } color="text_secondary"
gridRow="1/3" fontSize="xs"
justifySelf="end" mt={ 1 }
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
> >
<VisuallyHidden> <span>{ description }</span>
Open chart options menu </Skeleton>
</VisuallyHidden> ) }
</MenuButton> </Flex>
<MenuList>
<MenuItem <Flex ml="auto" columnGap={ 2 }>
display="flex" <Tooltip label="Reset zoom">
alignItems="center" <IconButton
onClick={ showChartFullscreen } hidden={ isZoomResetInitial }
> aria-label="Reset zoom"
<Icon as={ scopeIcon } boxSize={ 5 } mr={ 3 }/> colorScheme="blue"
w={ 9 }
h={ 8 }
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
/>
</Tooltip>
{ hasItems && (
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<MenuButton
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
>
<VisuallyHidden>
Open chart options menu
</VisuallyHidden>
</MenuButton>
</Skeleton>
<MenuList>
<MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<Icon as={ scopeIcon } boxSize={ 5 } mr={ 3 }/>
View fullscreen View fullscreen
</MenuItem> </MenuItem>
<MenuItem <MenuItem
display="flex" display="flex"
alignItems="center" alignItems="center"
onClick={ handleFileSaveClick } onClick={ handleFileSaveClick }
> >
<Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/> <Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/>
Save as PNG Save as PNG
</MenuItem> </MenuItem>
<MenuItem <MenuItem
display="flex" display="flex"
alignItems="center" alignItems="center"
onClick={ handleSVGSavingClick } onClick={ handleSVGSavingClick }
> >
<Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/> <Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/>
Save as CSV Save as CSV
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
) } ) }
</Grid> </Flex>
</Flex>
{ content } { content }
</Box> </Flex>
{ hasItems && ( { hasItems && (
<FullscreenChartModal <FullscreenChartModal
......
import { Box, Skeleton } from '@chakra-ui/react';
import React from 'react';
interface Props {
hasDescription: boolean;
}
const ChartWidgetSkeleton = ({ hasDescription }: Props) => {
return (
<Box
height="235px"
paddingY={{ base: 3, lg: 4 }}
>
<Skeleton w="75%" h="24px"/>
{ hasDescription && <Skeleton w="50%" h="18px" mt={ 1 }/> }
<Skeleton w="100%" h="150px" mt={ 5 }/>
</Box>
);
};
export default ChartWidgetSkeleton;
...@@ -14,13 +14,14 @@ type Props = { ...@@ -14,13 +14,14 @@ type Props = {
units?: string; units?: string;
interval: StatsIntervalIds; interval: StatsIntervalIds;
onLoadingError: () => void; onLoadingError: () => void;
isPlaceholderData: boolean;
} }
function formatDate(date: Date) { function formatDate(date: Date) {
return date.toISOString().substring(0, 10); return date.toISOString().substring(0, 10);
} }
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units }: Props) => { const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData }: Props) => {
const selectedInterval = STATS_INTERVALS[interval]; const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
...@@ -32,6 +33,10 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -32,6 +33,10 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
from: startDate, from: startDate,
to: endDate, to: endDate,
}, },
queryOptions: {
enabled: !isPlaceholderData,
refetchOnMount: false,
},
}); });
const items = useMemo(() => data?.chart?.map((item) => { const items = useMemo(() => data?.chart?.map((item) => {
...@@ -52,6 +57,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -52,6 +57,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
units={ units } units={ units }
description={ description } description={ description }
isLoading={ isLoading } isLoading={ isLoading }
minH="230px"
/> />
); );
}; };
......
import { Box, Grid, GridItem, Heading, List, ListItem, Skeleton } from '@chakra-ui/react'; import { Box, Grid, Heading, List, ListItem, Skeleton } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { StatsChartsSection } from 'types/api/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import ChartWidgetSkeleton from 'ui/shared/chart/ChartWidgetSkeleton';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert'; import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
...@@ -14,14 +13,12 @@ import ChartWidgetContainer from './ChartWidgetContainer'; ...@@ -14,14 +13,12 @@ import ChartWidgetContainer from './ChartWidgetContainer';
type Props = { type Props = {
filterQuery: string; filterQuery: string;
isError: boolean; isError: boolean;
isLoading: boolean; isPlaceholderData: boolean;
charts?: Array<StatsChartsSection>; charts?: Array<StatsChartsSection>;
interval: StatsIntervalIds; interval: StatsIntervalIds;
} }
const skeletonsCount = 4; const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval }: Props) => {
const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }: Props) => {
const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false); const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false);
const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed; const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
...@@ -30,29 +27,6 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval } ...@@ -30,29 +27,6 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
() => setIsSomeChartLoadingError(true), () => setIsSomeChartLoadingError(true),
[ setIsSomeChartLoadingError ]); [ setIsSomeChartLoadingError ]);
const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => (
<GridItem key={ i }>
<ChartWidgetSkeleton hasDescription={ true }/>
</GridItem>
));
if (isLoading) {
return (
<>
<Skeleton w="30%" h="32px" mb={ 4 }/>
<Grid
templateColumns={{
lg: 'repeat(2, minmax(0, 1fr))',
}}
gap={ 4 }
>
{ skeletonElement }
</Grid>
</>
);
}
if (isError) { if (isError) {
return <ChartsLoadingErrorAlert/>; return <ChartsLoadingErrorAlert/>;
} }
...@@ -77,32 +51,27 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval } ...@@ -77,32 +51,27 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
marginBottom: 0, marginBottom: 0,
}} }}
> >
<Heading <Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-block">
size="md" <Heading size="md" >
mb={ 4 } { section.title }
> </Heading>
{ section.title } </Skeleton>
</Heading>
<Grid <Grid
templateColumns={{ templateColumns={{ lg: 'repeat(2, minmax(0, 1fr))' }}
lg: 'repeat(2, minmax(0, 1fr))',
}}
gap={ 4 } gap={ 4 }
> >
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem <ChartWidgetContainer
key={ chart.id } key={ chart.id }
> id={ chart.id }
<ChartWidgetContainer title={ chart.title }
id={ chart.id } description={ chart.description }
title={ chart.title } interval={ interval }
description={ chart.description } units={ chart.units || undefined }
interval={ interval } isPlaceholderData={ isPlaceholderData }
units={ chart.units || undefined } onLoadingError={ handleChartLoadingError }
onLoadingError={ handleChartLoadingError } />
/>
</GridItem>
)) } )) }
</Grid> </Grid>
</ListItem> </ListItem>
......
import debounce from 'lodash/debounce'; import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats'; import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import { STATS_CHARTS } from 'stubs/stats';
function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean { function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean {
return currentSection === 'all' || section.id === currentSection; return currentSection === 'all' || section.id === currentSection;
...@@ -15,30 +16,30 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) { ...@@ -15,30 +16,30 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) {
} }
export default function useStats() { export default function useStats() {
const { data, isLoading, isError } = useApiQuery('stats_lines'); const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
queryOptions: {
placeholderData: STATS_CHARTS,
},
});
const [ currentSection, setCurrentSection ] = useState('all'); const [ currentSection, setCurrentSection ] = useState('all');
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const [ displayedCharts, setDisplayedCharts ] = useState(data?.sections);
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth'); const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]); const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]);
// eslint-disable-next-line react-hooks/exhaustive-deps const debouncedFilterQuery = useDebounce(filterQuery, 500);
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: string) => { const displayedCharts = React.useMemo(() => {
const charts = data?.sections return data?.sections
?.map((section) => { ?.map((section) => {
const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart)); const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(debouncedFilterQuery, chart));
return { return {
...section, ...section,
charts, charts,
}; };
}).filter((section) => section.charts.length > 0); }).filter((section) => section.charts.length > 0);
}, [ currentSection, data?.sections, debouncedFilterQuery ]);
setDisplayedCharts(charts || []);
}, [ data ]);
const handleSectionChange = useCallback((newSection: string) => { const handleSectionChange = useCallback((newSection: string) => {
setCurrentSection(newSection); setCurrentSection(newSection);
...@@ -48,33 +49,33 @@ export default function useStats() { ...@@ -48,33 +49,33 @@ export default function useStats() {
setInterval(newInterval); setInterval(newInterval);
}, []); }, []);
useEffect(() => { const handleFilterChange = useCallback((q: string) => {
filterCharts(filterQuery, currentSection); setFilterQuery(q);
}, [ filterQuery, currentSection, filterCharts ]); }, []);
return React.useMemo(() => ({ return React.useMemo(() => ({
sections: data?.sections, sections: data?.sections,
sectionIds, sectionIds,
isLoading, isPlaceholderData,
isError, isError,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
}), [ }), [
data, data,
sectionIds, sectionIds,
isLoading, isPlaceholderData,
isError, isError,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
]); ]);
} }
import { Flex, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { TokenVerifiedInfo } from 'types/api/token';
import iconDocs from 'icons/docs.svg';
import iconEmail from 'icons/email.svg';
import iconLink from 'icons/link.svg';
import iconCoinGecko from 'icons/social/coingecko.svg';
import iconCoinMarketCap from 'icons/social/coinmarketcap.svg';
import iconDefiLlama from 'icons/social/defi_llama.svg';
import iconDiscord from 'icons/social/discord_filled.svg';
import iconFacebook from 'icons/social/facebook_filled.svg';
import iconGithub from 'icons/social/github_filled.svg';
import iconLinkedIn from 'icons/social/linkedin_filled.svg';
import iconMedium from 'icons/social/medium_filled.svg';
import iconOpenSea from 'icons/social/opensea_filled.svg';
import iconReddit from 'icons/social/reddit_filled.svg';
import iconSlack from 'icons/social/slack_filled.svg';
import iconTelegram from 'icons/social/telegram_filled.svg';
import iconTwitter from 'icons/social/twitter_filled.svg';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkExternal from 'ui/shared/LinkExternal';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props {
data: TokenVerifiedInfo;
}
interface TServiceLink {
field: keyof TokenVerifiedInfo;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
hint: string;
}
const SOCIAL_LINKS: Array<TServiceLink> = [
{ field: 'github', icon: iconGithub, hint: 'Github account' },
{ field: 'twitter', icon: iconTwitter, hint: 'Twitter account' },
{ field: 'telegram', icon: iconTelegram, hint: 'Telegram account' },
{ field: 'openSea', icon: iconOpenSea, hint: 'OpenSea page' },
{ field: 'linkedin', icon: iconLinkedIn, hint: 'LinkedIn page' },
{ field: 'facebook', icon: iconFacebook, hint: 'Facebook account' },
{ field: 'discord', icon: iconDiscord, hint: 'Discord account' },
{ field: 'medium', icon: iconMedium, hint: 'Medium account' },
{ field: 'slack', icon: iconSlack, hint: 'Slack account' },
{ field: 'reddit', icon: iconReddit, hint: 'Reddit account' },
];
const PRICE_TICKERS: Array<TServiceLink> = [
{ field: 'coinGeckoTicker', icon: iconCoinGecko, hint: 'Coin Gecko' },
{ field: 'coinMarketCapTicker', icon: iconCoinMarketCap, hint: 'Coin Market Cap' },
{ field: 'defiLlamaTicker', icon: iconDefiLlama, hint: 'Defi Llama' },
];
const ServiceLink = ({ href, hint, icon }: TServiceLink & { href: string | undefined }) => (
<Link
href={ href }
variant="secondary"
boxSize={ 5 }
aria-label={ hint }
title={ hint }
target="_blank"
>
<Icon as={ icon } boxSize={ 5 }/>
</Link>
);
// todo_tom DELETE ME
const TokenDetailsVerifiedInfo = ({ data }: Props) => {
const websiteLink = (() => {
try {
const url = new URL(data.projectWebsite);
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconLink } boxSize={ 6 }/>
<LinkExternal href={ data.projectWebsite } fontSize="md">{ url.host }</LinkExternal>
</Flex>
);
} catch (error) {
return null;
}
})();
const docsLink = (() => {
if (!data.docs) {
return null;
}
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconDocs } boxSize={ 6 }/>
<LinkExternal href={ data.docs } fontSize="md">Documentation</LinkExternal>
</Flex>
);
})();
const supportLink = (() => {
if (!data.support) {
return null;
}
const isEmail = data.support.includes('@');
const href = isEmail ? `mailto:${ data.support }` : data.support;
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconEmail } boxSize={ 6 }/>
<Link href={ href } target="_blank">
{ data.support }
</Link>
</Flex>
);
})();
const socialLinks = SOCIAL_LINKS
.map((link) => ({ ...link, href: data[link.field] }))
.filter(({ href }) => href);
const priceTickersLinks = PRICE_TICKERS
.map((link) => ({ ...link, href: data[link.field] }))
.filter(({ href }) => href);
return (
<DetailsInfoItem
title="Links"
hint="Links to the project's official website and social media channels."
>
<Flex flexDir="column" rowGap={ 5 }>
<Flex
flexDir={{ base: 'column', lg: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
columnGap={ 6 }
rowGap={ 2 }
>
{ websiteLink }
{ docsLink }
{ supportLink }
</Flex>
{ (socialLinks.length > 0 || priceTickersLinks.length > 0) && (
<Flex
columnGap={ 2 }
rowGap={ 2 }
flexWrap="wrap"
>
{ socialLinks.map((link) => <ServiceLink key={ link.field } { ...link }/>) }
{ priceTickersLinks.length > 0 && (
<>
<TextSeparator color="divider"/>
{ priceTickersLinks.map((link) => <ServiceLink key={ link.field } { ...link }/>) }
</>
) }
</Flex>
) }
</Flex>
</DetailsInfoItem>
);
};
export default React.memo(TokenDetailsVerifiedInfo);
...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({ ...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({
<Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
Value Value
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ value } { value }
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
......
...@@ -77,12 +77,12 @@ const TokensTableItem = ({ ...@@ -77,12 +77,12 @@ const TokensTableItem = ({
{ totalValue?.usd && ( { totalValue?.usd && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>On-chain market cap</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>On-chain market cap</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary"><span>{ totalValue.usd }</span></Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ totalValue.usd }</span></Skeleton>
</HStack> </HStack>
) } ) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Holders</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Holders</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary"><span>{ Number(holders).toLocaleString() }</span></Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ Number(holders).toLocaleString() }</span></Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -45,13 +45,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -45,13 +45,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary"> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Skeleton> </Skeleton>
</HStack> </HStack>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Gas limit</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Gas limit</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">{ BigNumber(gasLimit).toFormat() }</Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment