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