Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
LazyChain,
LazyContact,
LazyDevTools,
LazyGasTracker,
LazyHome,
LazyMempool,
LazyProfile,
Expand Down Expand Up @@ -114,6 +115,7 @@ function AppContent() {
<Route path="profile/:profileType/:profileId" element={<LazyProfile />} />
<Route path="supporters" element={<LazySupporters />} />
<Route path=":networkId" element={<LazyChain />} />
<Route path=":networkId/gastracker" element={<LazyGasTracker />} />
<Route path=":networkId/blocks" element={<LazyBlocks />} />
<Route path=":networkId/block/:filter" element={<LazyBlock />} />
<Route path=":networkId/txs" element={<LazyTxs />} />
Expand Down
2 changes: 2 additions & 0 deletions src/components/LazyComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Profile = lazy(() => import("./pages/profile"));
const Supporters = lazy(() => import("./pages/supporters"));
const Contact = lazy(() => import("./pages/contact"));
const Search = lazy(() => import("./pages/search"));
const GasTracker = lazy(() => import("./pages/gastracker"));

// Higher-order component to wrap lazy components with Suspense
// biome-ignore lint/suspicious/noExplicitAny: <TODO>
Expand Down Expand Up @@ -51,5 +52,6 @@ export const LazyProfile = withSuspense(Profile);
export const LazySupporters = withSuspense(Supporters);
export const LazyContact = withSuspense(Contact);
export const LazySearch = withSuspense(Search);
export const LazyGasTracker = withSuspense(GasTracker);
// Default exports for backward compatibility
export { Home };
88 changes: 77 additions & 11 deletions src/components/navbar/NetworkBlockIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { getRPCUrls } from "../../config/rpcConfig";
import { AppContext, useNetworks } from "../../context/AppContext";
import { RpcClient } from "@openscan/network-connectors";
import { NetworkIcon } from "../common/NetworkIcon";
import { useDataService } from "../../hooks/useDataService";
import { formatGasPrice } from "../../utils/formatUtils";

interface NetworkBlockIndicatorProps {
className?: string;
}

export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps) {
const location = useLocation();
const navigate = useNavigate();
const { rpcUrls } = useContext(AppContext);
const { getNetwork } = useNetworks();
const [blockNumber, setBlockNumber] = useState<number | null>(null);
const [gasPrice, setGasPrice] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

// Extract networkId from the pathname (e.g., /1/blocks -> 1)
Expand All @@ -25,17 +28,19 @@ export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps)
}, [location.pathname]);

const network = networkId ? getNetwork(networkId) : undefined;
const dataService = useDataService(networkId || 1);

useEffect(() => {
if (!networkId) {
setBlockNumber(null);
setGasPrice(null);
return;
}

let isMounted = true;
let intervalId: NodeJS.Timeout | null = null;

const fetchBlockNumber = async () => {
const fetchData = async () => {
try {
const urls = getRPCUrls(networkId, rpcUrls);
const client = new RpcClient(urls[0] || "");
Expand All @@ -50,21 +55,33 @@ export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps)
setIsLoading(false);
}
}

// Fetch gas price
if (dataService && rpcUrls[networkId]) {
try {
const gasPricesResult = await dataService.networkAdapter.getGasPrices();
if (isMounted && gasPricesResult.data) {
setGasPrice(gasPricesResult.data.average);
}
} catch (error) {
console.error("Failed to fetch gas price:", error);
}
}
};

setIsLoading(true);
fetchBlockNumber();
fetchData();

// Poll for new blocks every 12 seconds (Ethereum average block time)
intervalId = setInterval(fetchBlockNumber, 12000);
// Poll every 12 seconds
intervalId = setInterval(fetchData, 12000);

return () => {
isMounted = false;
if (intervalId) {
clearInterval(intervalId);
}
};
}, [networkId, rpcUrls]);
}, [networkId, rpcUrls, dataService]);

if (!networkId || !network) return null;

Expand All @@ -74,13 +91,62 @@ export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps)
style={{ "--network-color": network.color } as React.CSSProperties}
title={network.name}
>
<div className="network-block-pulse" />
<div className="network-block-logo">
<NetworkIcon network={network} size={20} />
</div>
<div className="network-block-pulse"></div>
<span className="network-block-number">
{isLoading ? "..." : blockNumber !== null ? `#${blockNumber.toLocaleString()}` : "---"}
</span>
{/* biome-ignore lint/a11y/useSemanticElements: using div for styling consistency */}
<div
className="network-block-number network-gas-tracker"
id="gas-tracker"
title={
gasPrice
? `${formatGasPrice(gasPrice).value} ${formatGasPrice(gasPrice).unit}`
: "Gas Tracker"
}
role="button"
tabIndex={0}
onClick={() => navigate(`/${networkId}/gastracker`)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
navigate(`/${networkId}/gastracker`);
}
}}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M3 22V6a2 2 0 012-2h8a2 2 0 012 2v16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M3 22h12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path
d="M13 10h2a2 2 0 012 2v3a2 2 0 002 2h0a2 2 0 002-2V9l-3-3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 10h4v4H7z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>{gasPrice ? formatGasPrice(gasPrice).value : "..."}</span>
</div>
</div>
);
}
Expand Down
32 changes: 32 additions & 0 deletions src/components/navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,38 @@ const Navbar = () => {
</svg>
<span>Transactions</span>
</button>
<button
type="button"
className="navbar-mobile-menu-item"
onClick={() => handleMobileNavigation(`/${networkId}/gastracker`)}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<title>Gas Tracker</title>
<path
d="M3 22V6a2 2 0 012-2h8a2 2 0 012 2v16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M3 22h12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path
d="M13 10h2a2 2 0 012 2v3a2 2 0 002 2h0a2 2 0 002-2V9l-3-3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 10h4v4H7z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Gas Tracker</span>
</button>
</>
)}

Expand Down
Loading
Loading