Skip to content

Commit

Permalink
Implement new stats/home page (#82)
Browse files Browse the repository at this point in the history
* ADd stats initial data

* Create icons map

* Improve stats cards

* Refactor blockCard to accept compact

* Pass grid props

* Refactor stats card

* Reuse component

* WIP Implement ChainCosts

* Use IncrementalStat

* Implement details grid info

* Implement show raw modal

* Create hint component

* Fix DetailsGrid layout

* Implement ChainCosts

* Minor layout fixes

* Mock txCosts

* Minor layout fixes

* Create wide layout

* Fix wide layout max width

* Fix dancing words

* Change read more

use an icon instead

* Fix image height

* Refactor to use right component

* Add close button to raw modal

* Refactor price factors

* Revert "Create wide layout"

This reverts commit 28fddc8.

* Refactor chain info card

* Fix prettier

* Change homepage layout

* Add blockTimestamp

* Add blockTimestamp tooltip

* Multiple string fixes

* Show last 3 blocks

* Fix prettier

* Update extended-sdk and remove mock

* Upgrade sdk commit

* Fix missing import

* Add error handling

It also refactors the `StatisticsCardWrapper` into a `StatsCardWrapper` component.

* Delete unused component

* Fix in sync badge color

* Use Raw IconButton

* Implement StatsModalWrapper

This way the price factors and VOC tokens price are shown inside two separated modals that can be opened from the Blockhain info card

* Minor fixes

- Delete commented
- Comment useEffect

---------

Co-authored-by: Γ’scar Casajuana <[email protected]>
  • Loading branch information
selankon and elboletaire authored Aug 20, 2024
1 parent 7724bb8 commit eeb658c
Show file tree
Hide file tree
Showing 22 changed files with 886 additions and 224 deletions.
48 changes: 22 additions & 26 deletions src/components/Blocks/BlockCard.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
import { Box, Card, CardBody, Flex, HStack, Icon, Text } from '@chakra-ui/react'
import { Box, Card, CardBody, Flex, Link, HStack, Icon, Text } from '@chakra-ui/react'
import { BlockError, BlockNotFoundError } from '@vocdoni/extended-sdk'
import { IChainBlockInfoResponse } from '@vocdoni/sdk'
import { Trans, useTranslation } from 'react-i18next'
import { BiTransferAlt } from 'react-icons/bi'
import { generatePath } from 'react-router-dom'

import { generatePath, Link as RouterLink } from 'react-router-dom'
import { ReducedTextAndCopy } from '~components/Layout/CopyButton'
import LinkCard from '~components/Layout/LinkCard'
import { RoutePath } from '~constants'
import { useDateFns } from '~i18n/use-date-fns'
import { Icons } from '~src/theme/components/Icons'

export const BlockCard = ({ block }: { block: IChainBlockInfoResponse | BlockError }) => {
if (block instanceof BlockError) return <BlockErrorCard error={block} height={block.height} />
return (
<BlockInfoCard
height={block.header.height}
time={block.header.time}
proposer={block.header.proposerAddress}
txn={block.data.txs.length}
/>
)
interface IBlockCardProps {
block: IChainBlockInfoResponse | BlockError
compact?: boolean
}

const BlockInfoCard = ({
height,
time,
proposer,
txn,
}: {
height: number
time: string
proposer: string
txn: number
}) => {
const date = new Date(time)
export const BlockCard = ({ block, compact = false }: IBlockCardProps) => {
const { formatDistance } = useDateFns()
if (block instanceof BlockError) return <BlockErrorCard error={block} height={block.height} />

const height = block.header.height
const time = block.header.time
const proposer = block.header.proposerAddress
const txn = block.data.txs.length

const date = new Date(time)

return (
<LinkCard to={generatePath(RoutePath.Block, { height: height.toString(), tab: null, page: null })}>
Expand All @@ -42,7 +34,7 @@ const BlockInfoCard = ({
<Flex gap={3}>
<Text fontWeight='bold'># {height}</Text>
<HStack spacing={1}>
<Icon as={BiTransferAlt} boxSize={5} />
<Icon as={Icons.TxIcon} boxSize={5} />
<Text fontSize={'sm'} fontWeight={'bold'}>
{txn}
</Text>
Expand All @@ -54,7 +46,11 @@ const BlockInfoCard = ({
<Box fontSize={'sm'}>
<Flex gap={2} align={'center'}>
<Trans i18nKey='blocks.proposer'>Proposer:</Trans>
<ReducedTextAndCopy color={'textAccent1'} toCopy={proposer}>
<ReducedTextAndCopy
breakPoint={compact ? { base: true } : undefined}
color={'textAccent1'}
toCopy={proposer}
>
{proposer}
</ReducedTextAndCopy>
</Flex>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Home/FeaturedContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const FeaturedContent = () => {

const icons = [
{
width: '96px',
width: '72px',
src: anonymous,
alt: t('featured.anonymous_image_alt'),
},
Expand Down Expand Up @@ -50,7 +50,7 @@ export const FeaturedContent = () => {
<Flex direction={'column'} gap={12}>
<Flex justify={'space-between'} wrap={'wrap'}>
{icons.map((icon, i) => (
<Image key={i} maxW={icon.width} src={icon.src} alt={icon.alt} />
<Image key={i} maxW={icon.width} height={'88px'} src={icon.src} alt={icon.alt} />
))}
</Flex>
<Box>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Layout/ContentError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export const NoResultsError = ({ msg }: { msg?: string }) => {
return <Text>{msg ?? t('errors.no_results', { defaultValue: 'Looks like there are no results to show.' })}</Text>
}

export const ContentError = ({ error }: { error: Error | undefined | null | string }) => {
export type ContentErrorType = Error | undefined | null | string

export const ContentError = ({ error }: { error: ContentErrorType }) => {
return (
<Stack spacing={4}>
<Alert status='warning'>
Expand Down
39 changes: 28 additions & 11 deletions src/components/Layout/DetailsGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import { Grid, GridItem, Text } from '@chakra-ui/react'
import { Flex, Grid, GridItem, GridProps, Text } from '@chakra-ui/react'
import { PropsWithChildren } from 'react'
import Hint from '~components/Layout/Hint'

export type GridItemProps = { label: string } & PropsWithChildren
export type GridItemProps = { label: string; info?: string; isNumber?: boolean } & PropsWithChildren

/**
* Util component used to render a grid of details with its label
* @param details String label, and the component that should render on the grid
* @constructor
*/
export const DetailsGrid = ({ details }: { details: GridItemProps[] }) => {
export const DetailsGrid = ({ details, ...rest }: { details: GridItemProps[] } & GridProps) => {
return (
<Grid templateColumns={{ base: '1fr', sm: '1fr 4fr' }} gap={4} alignItems={'baseline'}>
{details.map(({ label, children }, key) => (
<DetailRow key={label} label={label}>
<Grid templateColumns={{ base: '1fr', sm: '1fr 4fr' }} columnGap={3} rowGap={2} {...rest}>
{details.map(({ children, ...rest }, key) => (
<DetailRow key={key} {...rest}>
{children}
</DetailRow>
))}
</Grid>
)
}

const DetailRow = ({ label, children }: GridItemProps) => {
const gridProps = { display: 'flex', alignItems: 'center' }
const DetailRow = ({ label, info, isNumber, children }: GridItemProps) => {
return (
<>
<GridItem {...gridProps}>
<Text fontWeight={'bold'}>{label}</Text>
<GridItem py={1} lineHeight={{ base: 5, lg: 6 }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={2} alignItems='flex-start'>
{info && <Hint label={info} isLoading={false} my={{ lg: '2px' }} />}
<Text my={{ lg: '2px' }} lineHeight={{ base: 5, lg: 6 }} align={'left'}>
{label}
</Text>
</Flex>
</GridItem>
<GridItem
display='flex'
alignItems='center'
flexWrap='wrap'
rowGap={3}
pl={{ base: 7, lg: 0 }}
py={1}
lineHeight={{ base: 5, lg: 6 }}
whiteSpace='nowrap'
justifyContent={isNumber ? 'end' : 'start'}
>
{children}
</GridItem>
<GridItem {...gridProps}>{children}</GridItem>
</>
)
}
56 changes: 56 additions & 0 deletions src/components/Layout/Hint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Based from https://github.com/blockscout/frontend/blob/main/ui/shared/Hint.tsx
import type { TooltipProps } from '@chakra-ui/react'
import { chakra, IconButton, Skeleton, Tooltip, useDisclosure } from '@chakra-ui/react'
import React from 'react'
import { GrStatusInfo } from 'react-icons/gr'

interface Props {
label: string | React.ReactNode
className?: string
tooltipProps?: Partial<TooltipProps>
isLoading?: boolean
}

const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure()

const handleClick = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
onToggle()
},
[onToggle]
)

if (isLoading) {
return <Skeleton className={className} boxSize={5} borderRadius='sm' />
}

return (
<Tooltip
label={label}
placement='top'
maxW={{ base: 'calc(100vw - 8px)', lg: '320px' }}
isOpen={isOpen}
{...tooltipProps}
>
<IconButton
minW={2}
colorScheme='none'
aria-label='hint'
icon={<GrStatusInfo />}
boxSize={5}
variant='simple'
display='inline-block'
flexShrink={0}
className={className}
onMouseEnter={onOpen}
onMouseLeave={onClose}
onClick={handleClick}
/>
</Tooltip>
)
}

export default React.memo(chakra(Hint))
52 changes: 51 additions & 1 deletion src/components/Layout/ShowRawButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { Box, BoxProps, Button, ButtonProps, useDisclosure } from '@chakra-ui/react'
import {
Box,
BoxProps,
Button,
ButtonProps,
IconButton,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useDisclosure,
} from '@chakra-ui/react'
import { Trans } from 'react-i18next'
import { CopyButtonIcon } from '~components/Layout/CopyButton'
import { JsonViewer } from '~components/Layout/JsonViewer'
import { useTranslation } from 'react-i18next'
import { FaCode } from 'react-icons/fa'

const ShowRawButton = ({ obj, ...props }: { obj: object } & Omit<ButtonProps, 'onClick'>) => {
const { getDisclosureProps, getButtonProps } = useDisclosure()
Expand All @@ -27,4 +43,38 @@ export const RawContentBox = ({ obj, ...rest }: { obj: object } & BoxProps) => (
</Box>
)

export const RawModal = ({ obj, ...rest }: { obj: object } & Omit<ButtonProps, 'onClick'>) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslation()
return (
<>
<IconButton
onClick={onOpen}
icon={<FaCode />}
h={0}
p={0}
justifyContent={'end'}
aria-label={t('raw')}
size={'lg'}
variant={'text'}
/>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Trans i18nKey={'raw'}></Trans>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<RawContentBox obj={obj} />
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}

export default ShowRawButton
106 changes: 106 additions & 0 deletions src/components/Stats/ChainDetails/ChainInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Badge, HStack, Text, VStack } from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'
import { useChainInfo } from '~queries/stats'
import { useDateFns } from '~i18n/use-date-fns'
import { MdSpeed } from 'react-icons/md'
import { DetailsGrid, GridItemProps } from '~components/Layout/DetailsGrid'
import { StatsCardWrapper } from '~components/Stats/StatsCardWrapper'
import { TxCostsModal } from '~components/Stats/ChainDetails/TxCosts'
import { PriceFactorsModal } from '~components/Stats/ChainDetails/PriceFactors'

const SyncBadge = ({ syncing }: { syncing: boolean }) => {
const { t } = useTranslation()

const label = syncing ? t('stats.syncing') : t('stats.in_sync')
const color = syncing ? 'orange' : 'green'

return <Badge colorScheme={color}>{label}</Badge>
}

export const ChainInfo = () => {
const { t } = useTranslation()
const { data: stats, isError, error } = useChainInfo()
const { format } = useDateFns()

if (!stats) return null

const genesisBlockDate = format(new Date(stats.genesisTime), 'PPPpp')
const timestampInfo = format(new Date(stats?.blockTimestamp * 1000), 'hh:mm:ss')

const statsData: GridItemProps[] = [
{
label: t('stats.maxCensusSize', { defaultValue: 'Max Census Size' }),
children: stats.maxCensusSize,
isNumber: true,
},
{
label: t('stats.networkCapacity', { defaultValue: 'Capacity (votes/block)' }),
// Not typed on the SDK
// @ts-ignore
children: stats.networkCapacity,
isNumber: true,
},
{
label: t('stats.initial_height', { defaultValue: 'Epoch initial Height' }),
// Not typed on the SDK
// @ts-ignore
children: stats.initialHeight,
isNumber: true,
},
{
label: t('stats.blockTimestamp', { defaultValue: 'Block timestamp' }),
children: timestampInfo,
isNumber: true,
},
{
label: t('stats.blockTimestamp', { defaultValue: 'Block timestamp' }),
children: timestampInfo,
isNumber: true,
},
]

const tokensData: GridItemProps[] = [
{
label: t('stats.voc_tokens', { defaultValue: 'VOC Tokens' }),
children: <TxCostsModal />,
isNumber: true,
},
{
label: t('stats.price_factors', { defaultValue: 'Price factors' }),
children: <PriceFactorsModal />,
isNumber: true,
},
]

return (
<StatsCardWrapper
flex='2'
w={'full'}
icon={MdSpeed}
title={t('stats.blockchain_info')}
raw={stats}
isError={isError}
error={error}
>
<VStack pb={4} align={'start'} spacing={1}>
<HStack>
<Text fontSize='lg' fontWeight={'bold'}>
{stats.chainId}
</Text>
<SyncBadge syncing={stats.syncing} />
</HStack>
<Text color={'lightText'} fontSize='md'>
{t('stats.genesis', {
date: genesisBlockDate,
defaultValue: 'Chain Genesis Epoch from {{date}}',
})}
</Text>
</VStack>
<DetailsGrid templateColumns={{ base: '1fr', sm: '1fr 1fr' }} details={statsData} rowGap={0} />
<Text fontSize='lg' fontWeight={'bold'} pt={2}>
<Trans i18nKey={'stats.tokens'}>Tokens</Trans>
</Text>
<DetailsGrid templateColumns={{ base: '1fr', sm: '1fr 1fr' }} details={tokensData} rowGap={0} />
</StatsCardWrapper>
)
}
Loading

2 comments on commit eeb658c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.