From afe39150c3cba68c6af43ec9ce9f79f1ce471a50 Mon Sep 17 00:00:00 2001 From: TITI <162849030+0xtiti@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:19:02 -0300 Subject: [PATCH] feat: chain page details (#9) closes ZKS-103 closes ZKS-114 closes ZKS-116 closes ZKS-117 closes ZKS-113 Description: - Add chain page details HTML structure - Add dashboard chain navigation FYI: Currently using mock data so all chains show data from chain id: 324. Since ecosystemData mock has all available chains, chain page change is shown in route but rendered data is just 324 temporary. --- public/locales/en/common.json | 9 +- public/locales/es/common.json | 9 +- src/components/ChainInformation.tsx | 36 +++++++ src/components/FeeParams.tsx | 19 ++++ src/components/InfoBox.tsx | 13 +++ src/components/RPC.tsx | 22 +++++ src/components/TVL.tsx | 20 ++++ src/components/Table.tsx | 8 +- src/components/index.ts | 5 + .../ChainDetail/ChainDescription.tsx | 12 +++ src/containers/ChainDetail/ChainMetadata.tsx | 49 ++++++++++ src/containers/ChainDetail/InfoCard.tsx | 16 ++++ src/containers/ChainDetail/index.tsx | 11 +++ src/containers/Header/index.tsx | 5 +- src/containers/index.ts | 1 + src/data/chainMockData.json | 93 ++++++++----------- src/pages/[chain]/index.tsx | 37 +++++--- src/types/Data.ts | 54 ++++++----- src/utils/format.ts | 4 + src/utils/misc.ts | 0 20 files changed, 326 insertions(+), 97 deletions(-) create mode 100644 src/components/ChainInformation.tsx create mode 100644 src/components/FeeParams.tsx create mode 100644 src/components/InfoBox.tsx create mode 100644 src/components/RPC.tsx create mode 100644 src/components/TVL.tsx create mode 100644 src/containers/ChainDetail/ChainDescription.tsx create mode 100644 src/containers/ChainDetail/ChainMetadata.tsx create mode 100644 src/containers/ChainDetail/InfoCard.tsx create mode 100644 src/containers/ChainDetail/index.tsx delete mode 100644 src/utils/misc.ts diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 58bc09c..fd7f356 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -15,7 +15,7 @@ "notFound": "Chain not found" } }, - "CHAINPAGE": { + "CHAIN": { "website": "Website", "explorer": "Explorer", "launchDate": "Launch date", @@ -28,10 +28,11 @@ "lastBlockVerified": "Last block verified", "transactionsPerSecond": "Transactions per second", "totalBatchesCommitted": "Total batches committed", + "totalBatchesExecuted": "Total batches executed", "totalBatchesVerified": "Total batches verified", "averageBlockTime": "Average block time" }, - "ZKCHAINTVL": { + "TVL": { "title": "ZKchain TVL" }, "RPC": { @@ -39,10 +40,12 @@ "status": "Status" }, "FEEPARAMS": { + "title": "Fee params", "batch": "Batch Overhead L1 Gas", "compute": "Compute Overhead Part", "maxGasBatch": "Max Gas per Batch" - } + }, + "backButton": "Go back" }, "FOOTER": { "docs": "Documentation", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index f85b9a8..bcd8170 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -15,7 +15,7 @@ "notFound": "Cadena no encontrada" } }, - "CHAINPAGE": { + "CHAIN": { "website": "Sitio web", "explorer": "Explorador", "launchDate": "Fecha de lanzamiento", @@ -28,10 +28,11 @@ "lastBlockVerified": "Último bloque verificado", "transactionsPerSecond": "Transacciones por segundo", "totalBatchesCommitted": "Total de lotes comprometidos", + "totalBatchesExecuted": "Total de lotes ejecutados", "totalBatchesVerified": "Total de lotes verificados", "averageBlockTime": "Tiempo promedio de bloque" }, - "ZKCHAINTVL": { + "TVL": { "title": "TVL de ZKchain" }, "RPC": { @@ -39,10 +40,12 @@ "status": "Estado" }, "FEEPARAMS": { + "title": "Parámetros de gas", "batch": "Sobrecarga de lote L1 Gas", "compute": "Parte de sobrecarga de cómputo", "maxGasBatch": "Máximo gas por lote" - } + }, + "backButton": "Volver" }, "FOOTER": { "docs": "Documentación", diff --git a/src/components/ChainInformation.tsx b/src/components/ChainInformation.tsx new file mode 100644 index 0000000..49bc4d0 --- /dev/null +++ b/src/components/ChainInformation.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from 'next-i18next'; +import { InfoBox, Title } from '~/components'; +import { useData } from '~/hooks'; + +export const ChainInformation = () => { + const { t } = useTranslation(); + const { chainData } = useData(); + + return ( +
+ + <div> + <InfoBox title={t('CHAIN.CHAININFORMATION.chainType')} description={chainData?.chainType} /> + <InfoBox title={t('CHAIN.CHAININFORMATION.lastBlock')} description={chainData?.l2ChainInfo.lastBlock} /> + <InfoBox + title={t('CHAIN.CHAININFORMATION.lastBlockVerified')} + description={chainData?.l2ChainInfo.lastBlockVerified} + /> + <InfoBox title={t('CHAIN.CHAININFORMATION.transactionsPerSecond')} description={chainData?.l2ChainInfo.tps} /> + <InfoBox + title={t('CHAIN.CHAININFORMATION.totalBatchesCommitted')} + description={chainData?.batchesInfo.commited} + /> + <InfoBox title={t('CHAIN.CHAININFORMATION.totalBatchesExecuted')} description={chainData?.batchesInfo.proved} /> + <InfoBox + title={t('CHAIN.CHAININFORMATION.totalBatchesVerified')} + description={chainData?.batchesInfo.verified} + /> + <InfoBox + title={t('CHAIN.CHAININFORMATION.averageBlockTime')} + description={chainData?.l2ChainInfo.avgBlockTime} + /> + </div> + </article> + ); +}; diff --git a/src/components/FeeParams.tsx b/src/components/FeeParams.tsx new file mode 100644 index 0000000..51f218b --- /dev/null +++ b/src/components/FeeParams.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from 'next-i18next'; +import { InfoBox, Title } from '~/components'; +import { useData } from '~/hooks'; + +export const FeeParams = () => { + const { t } = useTranslation(); + const { chainData } = useData(); + + return ( + <article> + <Title title={t('CHAIN.FEEPARAMS.title')} /> + <div> + <InfoBox title={t('CHAIN.FEEPARAMS.batch')} description={chainData?.feeParams.batchOverheadL1Gas} /> + <InfoBox title={t('CHAIN.FEEPARAMS.compute')} description={chainData?.feeParams.maxPubdataPerBatch} /> + <InfoBox title={t('CHAIN.FEEPARAMS.maxGasBatch')} description={chainData?.feeParams.maxL2GasPerBatch} /> + </div> + </article> + ); +}; diff --git a/src/components/InfoBox.tsx b/src/components/InfoBox.tsx new file mode 100644 index 0000000..8a9ead6 --- /dev/null +++ b/src/components/InfoBox.tsx @@ -0,0 +1,13 @@ +interface InfoBoxProps { + title: string; + description: string | number; +} + +export const InfoBox = ({ title, description }: InfoBoxProps) => { + return ( + <div> + <p>{title}</p> + <p>{description}</p> + </div> + ); +}; diff --git a/src/components/RPC.tsx b/src/components/RPC.tsx new file mode 100644 index 0000000..9a74845 --- /dev/null +++ b/src/components/RPC.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from 'next-i18next'; +import { InfoBox, Title } from '~/components'; +import { useData } from '~/hooks'; + +export const TVL = () => { + const { t } = useTranslation(); + const { chainData } = useData(); + + return ( + <article> + <Title title={t('CHAIN.RPC.title')} /> + <div> + {chainData?.metadata?.publicRpcs && + chainData.metadata.publicRpcs.map((rpc, index) => ( + <div key={index}> + <InfoBox title={t('CHAIN.RPC.status')} description={rpc.url} /> + </div> + ))} + </div> + </article> + ); +}; diff --git a/src/components/TVL.tsx b/src/components/TVL.tsx new file mode 100644 index 0000000..c841dcc --- /dev/null +++ b/src/components/TVL.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'next-i18next'; +import { InfoBox, Title } from '~/components'; +import { useData } from '~/hooks'; + +export const RPC = () => { + const { t } = useTranslation(); + const { chainData } = useData(); + + return ( + <article> + <Title title={t('CHAIN.TVL.title')} /> + <div> + {chainData?.tvl && + Object.entries(chainData.tvl).map(([token, value]) => ( + <InfoBox key={token} title={token} description={value.toString()} /> + ))} + </div> + </article> + ); +}; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 4aec5f6..11560e4 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; import { EcosystemChainData } from '~/types'; import { formatDataNumber } from '~/utils'; @@ -9,6 +10,11 @@ interface TableProps { export const Table = ({ chains }: TableProps) => { const { t } = useTranslation(); + const router = useRouter(); + + const handleChainNavigation = (id: number) => { + router.push(`/${id}`); + }; return ( <table> @@ -22,7 +28,7 @@ export const Table = ({ chains }: TableProps) => { {chains?.map((data, index) => { return ( - <tr key={index}> + <tr key={index} onClick={() => handleChainNavigation(data.id)}> <td>{data.name}</td> <td>{data.id}</td> <td>{data.nativeToken}</td> diff --git a/src/components/index.ts b/src/components/index.ts index 25f07aa..bd43e0e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,5 +6,10 @@ export * from './SearchBar'; export * from './TotalValueLocked'; export * from './Title'; export * from './TitleBanner'; +export * from './InfoBox'; +export * from './FeeParams'; +export * from './RPC'; +export * from './TVL'; +export * from './ChainInformation'; export * from './BasicSelect'; export * from './NotFound'; diff --git a/src/containers/ChainDetail/ChainDescription.tsx b/src/containers/ChainDetail/ChainDescription.tsx new file mode 100644 index 0000000..b1b4df5 --- /dev/null +++ b/src/containers/ChainDetail/ChainDescription.tsx @@ -0,0 +1,12 @@ +import { ChainInformation, FeeParams, RPC, TVL } from '~/components'; + +export const ChainDescription = () => { + return ( + <section> + <ChainInformation /> + <TVL /> + <RPC /> + <FeeParams /> + </section> + ); +}; diff --git a/src/containers/ChainDetail/ChainMetadata.tsx b/src/containers/ChainDetail/ChainMetadata.tsx new file mode 100644 index 0000000..35f1f91 --- /dev/null +++ b/src/containers/ChainDetail/ChainMetadata.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { InfoBox } from '~/components'; +import { useData } from '~/hooks'; +import { formatTimestampToDate } from '~/utils'; + +export const ChainMetadata = () => { + const { t } = useTranslation(); + const { chainData, ecosystemData } = useData(); + const router = useRouter(); + const data = chainData?.metadata; + + const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => { + const selectedChainId = event.target.value; + router.push(`/${selectedChainId}`); + }; + + const handleBack = () => { + router.back(); + }; + + return ( + <div> + <div> + <div> + {/* <img></img> */} + <select onChange={handleChange} value={data?.chainName || ''}> + {ecosystemData?.chains.map((chain) => ( + <option key={chain.id} value={chain.id}> + {chain.name} + </option> + ))} + </select> + <span>{data?.chainId}</span> + </div> + + <button onClick={handleBack}>{t('CHAIN.backButton')}</button> + </div> + + <div> + <InfoBox title={t('CHAIN.website')} description={data?.websiteUrl} /> + <InfoBox title={t('CHAIN.explorer')} description={data?.explorerUrl} /> + <InfoBox title={t('CHAIN.launchDate')} description={formatTimestampToDate(data?.launchDate)} /> + <InfoBox title={t('CHAIN.environment')} description={data?.environment} /> + <InfoBox title={t('CHAIN.nativeToken')} description={data?.nativeToken} /> + </div> + </div> + ); +}; diff --git a/src/containers/ChainDetail/InfoCard.tsx b/src/containers/ChainDetail/InfoCard.tsx new file mode 100644 index 0000000..1a455cf --- /dev/null +++ b/src/containers/ChainDetail/InfoCard.tsx @@ -0,0 +1,16 @@ +import { InfoBox } from '~/components'; + +interface InfoCardProps { + title: string; +} + +export const InfoCard = ({ title }: InfoCardProps) => { + return ( + <section> + <h2>{title}</h2> + <div> + <InfoBox title='Website' description='https://www.example.com' /> + </div> + </section> + ); +}; diff --git a/src/containers/ChainDetail/index.tsx b/src/containers/ChainDetail/index.tsx new file mode 100644 index 0000000..4ec3935 --- /dev/null +++ b/src/containers/ChainDetail/index.tsx @@ -0,0 +1,11 @@ +import { ChainMetadata } from './ChainMetadata'; +import { ChainDescription } from './ChainDescription'; + +export const ChainDetail = () => { + return ( + <div> + <ChainMetadata /> + <ChainDescription /> + </div> + ); +}; diff --git a/src/containers/Header/index.tsx b/src/containers/Header/index.tsx index 6519891..8a76ce0 100644 --- a/src/containers/Header/index.tsx +++ b/src/containers/Header/index.tsx @@ -2,6 +2,7 @@ import { styled } from '@mui/material/styles'; import { IconButton } from '@mui/material'; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; +import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; @@ -30,7 +31,9 @@ export const Header = () => { return ( <StyledHeader> - <Logo>ZKchainHub</Logo> + <Link href='/' passHref> + <Logo>ZKchainHub</Logo> + </Link> <SIconButton onClick={changeTheme}>{theme === 'dark' ? <LightModeIcon /> : <DarkModeIcon />}</SIconButton> <BasicSelect value={t(`LOCALES.${language}`)} setValue={handleChangeLanguage} list={Object.values(localesMap)} /> </StyledHeader> diff --git a/src/containers/index.ts b/src/containers/index.ts index ac2a1af..0698472 100644 --- a/src/containers/index.ts +++ b/src/containers/index.ts @@ -4,3 +4,4 @@ export * from './Layout'; export * from './Landing'; export * from './Dashboard'; export * from './LockedAssets'; +export * from './ChainDetail'; diff --git a/src/data/chainMockData.json b/src/data/chainMockData.json index 767de09..5b90d97 100644 --- a/src/data/chainMockData.json +++ b/src/data/chainMockData.json @@ -1,60 +1,49 @@ -[ - { - "name": "ZKSync Era", - "chainId": 324, - "website": "https://example.com", - "explorer": "https://example.com", - "launchDate": "2023-12-05", - "environment": "Production", - "nativeToken": "ETH", - "chainType": "ZKRollup", - "lastBlock": 123456789, - "lastBlockVerified": 123456788, - "transactionsPerSecond": 15, - "totalBatchesCommitted": 1234567890, - "totalBatchesExecuted": 1234567890, - "totalBatchesVerified": 123456788, - "averageBlockTime": 100000, - "tvl": { - "ETH": { - "value": 500000000, - "address": "0x79db...d692" - }, - "USDT": { - "value": 100000000, - "address": "0x79db...d692" - }, - "USDC": { - "value": 50000000, - "address": "0x79db...d692" - }, - "WBTC": { - "value": 30000000, - "address": "0x79db...d692" - } - }, - "rpcs": [ - { - "status": "Active", - "url": "https://lrpc.com" - }, +{ + "chainType": "Rollup", + "tvl": { + "ETH": 1000000, + "USDC": 500000 + }, + "batchesInfo": { + "commited": 100, + "verified": 90, + "proved": 80 + }, + "feeParams": { + "batchOverheadL1Gas": 50000, + "maxPubdataPerBatch": 120000, + "maxL2GasPerBatch": 10000000, + "priorityTxMaxPubdata": 15000, + "minimalL2GasPrice": 0.25 + }, + "metadata": { + "iconUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/24091.png", + "chainName": "ZKsyncERA", + "chainId": "324", + "publicRpcs": [ { - "status": "Active", - "url": "https://blastapi.com" + "url": "https://mainnet.era.zksync.io", + "status": true }, { - "status": "Inactive", - "url": "https://llamarpc.com" + "url": "https://1rpc.io/zksync2-era", + "status": true }, { - "status": "Active", - "url": "https://alchemy.com" + "url": "https://zksync.drpc.org", + "status": false } ], - "feeParams": { - "batchOverheadL1Gas": 1234567890, - "computeOverheadPart": 1234567890, - "maxGasPerBatch": 123456788 - } + "websiteUrl": "https://zksync.io/", + "explorerUrl": "https://explorer.zksync.io/", + "launchDate": 1679626800, + "environment": "mainnet", + "nativeToken": "ETH" + }, + "l2ChainInfo": { + "tps": 10000000, + "avgBlockTime": 12, + "lastBlock": 1000000, + "lastBlockVerified": 999999 } -] +} diff --git a/src/pages/[chain]/index.tsx b/src/pages/[chain]/index.tsx index 34eed58..c5b142c 100644 --- a/src/pages/[chain]/index.tsx +++ b/src/pages/[chain]/index.tsx @@ -1,27 +1,32 @@ import { useEffect } from 'react'; -import { GetStaticPaths, GetStaticProps } from 'next'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { GetStaticProps, GetStaticPaths, GetStaticPropsContext, InferGetStaticPropsType } from 'next'; import { EcosystemChainData } from '~/types'; import { CustomHead } from '~/components'; import { useData } from '~/hooks'; import { fetchEcosystemData } from '~/utils'; +import { ChainDetail } from '~/containers'; +import { getConfig } from '~/config'; + +const { DEFAULT_LANG, SUPPORTED_LANGUAGES } = getConfig(); interface ChainProps { chain: EcosystemChainData; } -const Chain = ({ chain }: ChainProps) => { - const { setSelectedChainId } = useData(); +const Chain = ({ chain }: InferGetStaticPropsType<typeof getStaticProps>) => { + const { setSelectedChainId, refetchChainData } = useData(); useEffect(() => { - setSelectedChainId(chain.id); - }, [chain.id, setSelectedChainId]); + setSelectedChainId(chain?.id); + refetchChainData({ throwOnError: true, cancelRefetch: false }); + }, [chain?.id, setSelectedChainId, refetchChainData]); return ( <> - <CustomHead title={chain.name} /> - <h1>{chain.name}</h1> - {/* TODO: Add chain page containers */} + <CustomHead title={chain?.name} /> + <ChainDetail /> </> ); }; @@ -30,14 +35,17 @@ export const getStaticPaths: GetStaticPaths = async () => { const ecosystemData = await fetchEcosystemData(); const chains = ecosystemData.chains; - const paths = chains.map((chain: EcosystemChainData) => ({ - params: { chain: chain.id.toString() }, - })); + const paths = SUPPORTED_LANGUAGES.flatMap((locale) => + chains.map((chain: EcosystemChainData) => ({ + params: { chain: chain.id.toString() }, + locale, + })), + ); - return { paths, fallback: false }; + return { paths, fallback: true }; }; -export const getStaticProps: GetStaticProps = async ({ params }) => { +export const getStaticProps: GetStaticProps<ChainProps> = async ({ params, locale }: GetStaticPropsContext) => { const ecosystemData = await fetchEcosystemData(); const chains = ecosystemData.chains; const chainId = parseInt(params?.chain as string); @@ -47,9 +55,12 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { return { notFound: true }; } + const i18Config = await serverSideTranslations(locale || DEFAULT_LANG, ['common'], null, SUPPORTED_LANGUAGES); + return { props: { chain, + ...i18Config, }, }; }; diff --git a/src/types/Data.ts b/src/types/Data.ts index edce979..21121c0 100644 --- a/src/types/Data.ts +++ b/src/types/Data.ts @@ -1,33 +1,39 @@ export interface ChainData { - name: string; - chainId: number; - website: string; - explorer: string; - launchDate: string; - environment: string; - nativeToken: string; chainType: string; - lastBlock: number; - lastBlockVerified: number; - transactionsPerSecond: number; - totalBatchesCommitted: number; - totalBatchesExecuted: number; - totalBatchesVerified: number; - averageBlockTime: number; tvl: { - [token: string]: { - value: number; - address: string; - }; + [token: string]: number; + }; + batchesInfo: { + commited: number; + verified: number; + proved: number; }; - rpcs: { - status: string; - url: string; - }[]; feeParams: { batchOverheadL1Gas: number; - computeOverheadPart: number; - maxGasPerBatch: number; + maxPubdataPerBatch: number; + maxL2GasPerBatch: number; + priorityTxMaxPubdata: number; + minimalL2GasPrice: number; + }; + metadata: { + iconUrl: string; + chainName: string; + chainId: number; + publicRpcs: { + url: string; + status: boolean; + }[]; + websiteUrl: string; + explorerUrl: string; + launchDate: number; + environment: string; + nativeToken: string; + }; + l2ChainInfo: { + tps: number; + avgBlockTime: number; + lastBlock: number; + lastBlockVerified: number; }; } diff --git a/src/utils/format.ts b/src/utils/format.ts index 1b39c3d..542020c 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -2,6 +2,10 @@ export const truncateAddress = (address: string) => { return `${address.slice(0, 6)}...${address.slice(-4)}`; }; +export const formatTimestampToDate = (timestamp: number): string => { + return new Date(timestamp * 1000).toLocaleDateString(); +}; + export function formatDataNumber(input: string | number, formatDecimal = 3, currency?: boolean, compact?: boolean) { const res: number = Number.parseFloat(input.toString()); diff --git a/src/utils/misc.ts b/src/utils/misc.ts deleted file mode 100644 index e69de29..0000000