diff --git a/.env.example b/.env.example index fec4611..9607267 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +NEXT_PUBLIC_API_URL= # Example: https://api.example.com NEXT_PUBLIC_RPC_URL= # Example: https://localhost:8545 NEXT_PUBLIC_PROJECT_ID= # ProjectID from WalletConnect NEXT_PUBLIC_ALCHEMY_KEY= # API key from Alchemy \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea0cf54..2d3da82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: echo "NEXT_PUBLIC_RPC_URL=${{ secrets.NEXT_PUBLIC_RPC_URL }}" >> .env echo "NEXT_PUBLIC_PROJECT_ID=${{ secrets.NEXT_PUBLIC_PROJECT_ID }}" >> .env echo "NEXT_PUBLIC_ALCHEMY_KEY=${{ secrets.NEXT_PUBLIC_ALCHEMY_KEY }}" >> .env + echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" >> .env - name: run Cypress and Jest uses: cypress-io/github-action@v6 diff --git a/src/components/CustomHead.tsx b/src/components/CustomHead.tsx new file mode 100644 index 0000000..46a146b --- /dev/null +++ b/src/components/CustomHead.tsx @@ -0,0 +1,18 @@ +import Head from 'next/head'; + +interface MetadataProps { + title: string; + description?: string; + image?: string; + type?: string; +} + +export const CustomHead = ({ title }: MetadataProps) => { + return ( + + {`${title} - ZKchainHub`} + + + + ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 0fe1f0b..a25d295 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,3 @@ export * from './Theme'; export * from './Disclaimer'; +export * from './CustomHead'; diff --git a/src/config/env.ts b/src/config/env.ts index 4d3ba54..dd425c6 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -4,10 +4,12 @@ export const getEnv = (): Env => { const NEXT_PUBLIC_RPC_URL = process.env.NEXT_PUBLIC_RPC_URL; const NEXT_PUBLIC_PROJECT_ID = process.env.NEXT_PUBLIC_PROJECT_ID; const NEXT_PUBLIC_ALCHEMY_KEY = process.env.NEXT_PUBLIC_ALCHEMY_KEY; + const NEXT_PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_BASE_URL; return { RPC_URL: NEXT_PUBLIC_RPC_URL as string, PROJECT_ID: NEXT_PUBLIC_PROJECT_ID as string, ALCHEMY_KEY: NEXT_PUBLIC_ALCHEMY_KEY as string, + API_URL: NEXT_PUBLIC_API_URL as string, }; }; diff --git a/src/containers/Footer/index.tsx b/src/containers/Footer/index.tsx index 6fc8f48..5d98840 100644 --- a/src/containers/Footer/index.tsx +++ b/src/containers/Footer/index.tsx @@ -1,5 +1,5 @@ import { styled } from '@mui/material/styles'; -import { useCustomTheme } from '~/hooks/useTheme'; +import { useCustomTheme } from '~/hooks/useContext/useTheme'; import { FOOTER_HEIGHT } from '~/utils'; diff --git a/src/containers/Header/index.tsx b/src/containers/Header/index.tsx index 2caa8a9..6afb5cd 100644 --- a/src/containers/Header/index.tsx +++ b/src/containers/Header/index.tsx @@ -4,7 +4,7 @@ import { IconButton } from '@mui/material'; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; -import { useCustomTheme } from '~/hooks/useTheme'; +import { useCustomTheme } from '~/hooks/useContext/useTheme'; import { zIndex, HEADER_HEIGHT } from '~/utils'; export const Header = () => { diff --git a/src/data/chainMockData.json b/src/data/chainMockData.json new file mode 100644 index 0000000..767de09 --- /dev/null +++ b/src/data/chainMockData.json @@ -0,0 +1,60 @@ +[ + { + "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" + }, + { + "status": "Active", + "url": "https://blastapi.com" + }, + { + "status": "Inactive", + "url": "https://llamarpc.com" + }, + { + "status": "Active", + "url": "https://alchemy.com" + } + ], + "feeParams": { + "batchOverheadL1Gas": 1234567890, + "computeOverheadPart": 1234567890, + "maxGasPerBatch": 123456788 + } + } +] diff --git a/src/data/ecosystemMockData.json b/src/data/ecosystemMockData.json new file mode 100644 index 0000000..f738e79 --- /dev/null +++ b/src/data/ecosystemMockData.json @@ -0,0 +1,91 @@ +{ + "chains": [ + { + "name": "zkSync Era", + "id": 324, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "ZKRollup" + }, + { + "name": "Teva Chain", + "id": 100, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "ZKRollup" + }, + { + "name": "Cronos zkEVM", + "id": 101, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "Validium" + }, + { + "name": "GRVT", + "id": 102, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "Validium" + }, + { + "name": "Lens", + "id": 103, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "Validium" + }, + { + "name": "ZKChain 104", + "id": 104, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "ZKRollup" + }, + { + "name": "ZKChain 105", + "id": 105, + "nativeToken": "ETH", + "tvl": 1000000, + "type": "Validium" + } + ], + "tvl": [ + { + "token": "ETH", + "value": 557596566 + }, + { + "token": "USDC", + "value": 90091851 + }, + { + "token": "KOI", + "value": 32757850 + }, + { + "token": "USDT", + "value": 18021853 + }, + { + "token": "WBTC", + "value": 12620248 + }, + { + "token": "wstETH", + "value": 3552439 + }, + { + "token": "MUTE", + "value": 2071481 + }, + { + "token": "rETH", + "value": 1404096 + }, + { + "token": "DAI", + "value": 1080375 + } + ] +} diff --git a/src/hooks/ScrollToTop.tsx b/src/hooks/ScrollToTop.tsx deleted file mode 100644 index 1f720d7..0000000 --- a/src/hooks/ScrollToTop.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react'; -import { useRouter } from 'next/router'; - -export function ScrollToTop() { - const router = useRouter(); - - useEffect(() => { - const handleRouteChange = () => { - window.scrollTo(0, 0); - }; - router.events.on('routeChangeComplete', handleRouteChange); - - return () => { - router.events.off('routeChangeComplete', handleRouteChange); - }; - }, [router.events]); - - return null; -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3ffc027..af3d0f4 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1 @@ -export * from './ScrollToTop'; -export * from './useStateContext'; +export * from './useContext'; diff --git a/src/hooks/useContext/index.ts b/src/hooks/useContext/index.ts new file mode 100644 index 0000000..92f170c --- /dev/null +++ b/src/hooks/useContext/index.ts @@ -0,0 +1 @@ +export * from './useStateContext'; diff --git a/src/hooks/useContext/useData.tsx b/src/hooks/useContext/useData.tsx new file mode 100644 index 0000000..43e0a8a --- /dev/null +++ b/src/hooks/useContext/useData.tsx @@ -0,0 +1,13 @@ +import { useContext } from 'react'; + +import { DataContext } from '~/providers/DataProvider'; + +export const useData = () => { + const context = useContext(DataContext); + + if (context === undefined) { + throw new Error('useData must be used within a StateProvider'); + } + + return context; +}; diff --git a/src/hooks/useStateContext.tsx b/src/hooks/useContext/useStateContext.tsx similarity index 100% rename from src/hooks/useStateContext.tsx rename to src/hooks/useContext/useStateContext.tsx diff --git a/src/hooks/useTheme.tsx b/src/hooks/useContext/useTheme.tsx similarity index 100% rename from src/hooks/useTheme.tsx rename to src/hooks/useContext/useTheme.tsx diff --git a/src/pages/[chain]/index.tsx b/src/pages/[chain]/index.tsx new file mode 100644 index 0000000..a472e99 --- /dev/null +++ b/src/pages/[chain]/index.tsx @@ -0,0 +1,14 @@ +import { CustomHead } from '~/components'; + +const Chain = () => { + const title = 'Chain placeholder'; + + return ( + <> + + {/* TODO: Add chain page containers */} + + ); +}; + +export default Chain; diff --git a/src/pages/error.tsx b/src/pages/error.tsx new file mode 100644 index 0000000..cb4879f --- /dev/null +++ b/src/pages/error.tsx @@ -0,0 +1,5 @@ +const ErrorPage = () => { + return
Sorry, something went wrong.
; +}; + +export default ErrorPage; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2283214..d753b21 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,15 +2,15 @@ import Head from 'next/head'; import { Landing } from '~/containers'; -const Home = () => { +const Ecosystem = () => { return ( <> - Web3 Boilerplate + ZKchainHub ); }; -export default Home; +export default Ecosystem; diff --git a/src/providers/DataProvider.tsx b/src/providers/DataProvider.tsx new file mode 100644 index 0000000..f8d02a3 --- /dev/null +++ b/src/providers/DataProvider.tsx @@ -0,0 +1,71 @@ +import { createContext, useState, useEffect, ReactNode } from 'react'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; + +import { ChainData, EcosystemData } from '~/types'; +import { fetchEcosystemData, fetchChainData } from '~/utils'; + +type ContextType = { + selectedChain?: ChainData; + setSelectedChain: (val: ChainData) => void; + + isEcosystemLoading: boolean; + isChainLoading: boolean; + refetchChainData: (options: { throwOnError: boolean; cancelRefetch: boolean }) => Promise; + + ecosystemData: EcosystemData; + chainData: ChainData; +}; + +interface DataProps { + children: ReactNode; +} + +export const DataContext = createContext({} as ContextType); + +export const DataProvider = ({ children }: DataProps) => { + const [selectedChain, setSelectedChain] = useState(); + const router = useRouter(); + + const { + isLoading: isEcosystemLoading, + data: ecosystemData, + isError: isEcosystemError, + } = useQuery({ + queryKey: ['ecosystem'], + queryFn: fetchEcosystemData, + }); + + const { + isLoading: isChainLoading, + data: chainData, + isError: isChainError, + refetch: refetchChainData, + } = useQuery({ + queryKey: ['chainData', selectedChain?.chainId], + queryFn: () => fetchChainData(selectedChain!.chainId!), + enabled: !!selectedChain?.chainId, + }); + + useEffect(() => { + if (isEcosystemError || isChainError) { + router.push('/error'); + } + }, [isEcosystemError, isChainError, router]); + + return ( + + {children} + + ); +}; diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 3047956..6bfc7e8 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from 'react'; import { StateProvider } from './StateProvider'; import { ThemeProvider } from './ThemeProvider'; import { WalletProvider } from './WalletProvider'; +import { DataProvider } from './DataProvider'; type Props = { children: ReactNode; @@ -12,7 +13,9 @@ export const Providers = ({ children }: Props) => { return ( - {children} + + {children} + ); diff --git a/src/types/Config.ts b/src/types/Config.ts index 7537f5c..3bd98b1 100644 --- a/src/types/Config.ts +++ b/src/types/Config.ts @@ -2,6 +2,7 @@ export interface Env { RPC_URL: string; PROJECT_ID: string; ALCHEMY_KEY: string; + API_URL: string; } export interface Constants { diff --git a/src/types/Data.ts b/src/types/Data.ts new file mode 100644 index 0000000..4598036 --- /dev/null +++ b/src/types/Data.ts @@ -0,0 +1,47 @@ +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; + }; + }; + rpcs: { + status: string; + url: string; + }[]; + feeParams: { + batchOverheadL1Gas: number; + computeOverheadPart: number; + maxGasPerBatch: number; + }; +} + +export interface EcosystemData { + chain: { + name: string; + id: number; + nativeToken: string; + tvl: { + [token: string]: number; + }; + type: string; + }; + tvl: { + [token: string]: number; + }[]; +} diff --git a/src/types/index.ts b/src/types/index.ts index 54d9f8d..f5e98b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,3 @@ export * from './Config'; export * from './Theme'; +export * from './Data'; diff --git a/src/utils/index.ts b/src/utils/index.ts index a54ea12..2b5edbb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './getTheme'; export * from './Variables'; export * from './config'; export * from './format'; +export * from './services'; diff --git a/src/utils/services.ts b/src/utils/services.ts new file mode 100644 index 0000000..6c9490b --- /dev/null +++ b/src/utils/services.ts @@ -0,0 +1,29 @@ +import { getConfig } from '~/config'; +import ecosystemMockData from '~/data/ecosystemMockData.json'; +import chainMockData from '~/data/chainMockData.json'; + +const { API_URL } = getConfig(); + +export const fetchEcosystemData = async () => { + // temporary for mock data + if (!API_URL) { + return Promise.resolve(ecosystemMockData); + } + const res = await fetch(`${API_URL}/ecosystem`); + if (!res.ok) { + throw new Error('Failed to fetch ecosystem data'); + } + return res.json(); +}; + +export const fetchChainData = async (chainId: number) => { + // temporary for mock data + if (!API_URL) { + return Promise.resolve(chainMockData); + } + const res = await fetch(`${API_URL}/zkchain/${chainId}`); + if (!res.ok) { + throw new Error('Failed to fetch chain data'); + } + return res.json(); +}; diff --git a/tsconfig.json b/tsconfig.json index 08b37f1..91e94bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "~/*": ["src/*"] } }, - "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts", "./cypress/**/*.ts", "./jest.config.ts"], + "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts", "./cypress/**/*.ts", "./jest.config.ts", "src/data/chainMockData.ts"], "exclude": ["./node_modules"] } \ No newline at end of file