diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6e3115..93898a3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,29 +17,32 @@ import { SearchResultsPage, AddressesPage, } from './pages'; +import { ThemeProvider } from './context/ThemeContext'; export default function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); } diff --git a/frontend/src/components/ContractTypeBadge.tsx b/frontend/src/components/ContractTypeBadge.tsx index 98d6231..908df0e 100644 --- a/frontend/src/components/ContractTypeBadge.tsx +++ b/frontend/src/components/ContractTypeBadge.tsx @@ -13,10 +13,8 @@ export default function ContractTypeBadge({ type, className = '' }: Props) { : 'EOA'; // Use a consistent pill style across types for visual cohesion - const base = 'bg-dark-600 text-white border-dark-500'; - return ( - + {label} ); diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index 277e55a..45b6c45 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -25,7 +25,7 @@ export default function CopyButton({ text, className = '' }: CopyButtonProps) { title={copied ? 'Copied!' : 'Copy to clipboard'} > {copied ? ( - + ) : ( diff --git a/frontend/src/components/EventLogs.tsx b/frontend/src/components/EventLogs.tsx index 055ee0f..3318f97 100644 --- a/frontend/src/components/EventLogs.tsx +++ b/frontend/src/components/EventLogs.tsx @@ -50,7 +50,7 @@ function LogCard({ log, showTxHash, showAddress }: { log: EventLog | DecodedEven
#{log.log_index} {decoded?.event_name ? ( - + {decoded.event_name} ) : ( @@ -61,7 +61,7 @@ function LogCard({ log, showTxHash, showAddress }: { log: EventLog | DecodedEven
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index a67bbc7..39642e1 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -5,6 +5,7 @@ import useLatestBlockHeight from '../hooks/useLatestBlockHeight'; import SmoothCounter from './SmoothCounter'; import logoImg from '../assets/logo.png'; import { BlockStatsContext } from '../context/BlockStatsContext'; +import { useTheme } from '../hooks/useTheme'; export default function Layout() { const location = useLocation(); @@ -17,6 +18,7 @@ export default function Layout() { const displayRafRef = useRef(null); const lastFrameRef = useRef(0); const displayedRef = useRef(0); + const displayInitializedRef = useRef(false); useEffect(() => { const id = window.setInterval(() => setNow(Date.now()), 1000); @@ -31,19 +33,21 @@ export default function Layout() { } displayRafRef.current = window.requestAnimationFrame(() => { setDisplayedHeight(null); + displayInitializedRef.current = false; displayRafRef.current = null; }); return; } // Initialize displayed to at least current height on first run - if (displayedHeight == null || height > displayedRef.current) { + if (!displayInitializedRef.current || height > displayedRef.current) { displayedRef.current = Math.max(displayedRef.current || 0, height); if (displayRafRef.current !== null) { cancelAnimationFrame(displayRafRef.current); } displayRafRef.current = window.requestAnimationFrame(() => { setDisplayedHeight(displayedRef.current); + displayInitializedRef.current = true; displayRafRef.current = null; }); } @@ -94,9 +98,11 @@ export default function Layout() { const navLinkClass = ({ isActive }: { isActive: boolean }) => `inline-flex items-center h-10 px-4 rounded-full leading-none transition-colors duration-150 ${ isActive - ? 'bg-dark-700/70 text-white' - : 'text-gray-400 hover:text-white hover:bg-dark-700/40' + ? 'bg-dark-700/70 text-fg' + : 'text-gray-400 hover:text-fg hover:bg-dark-700/40' }`; + const { theme, toggleTheme } = useTheme(); + const isDark = theme === 'dark'; return (
@@ -132,6 +138,41 @@ export default function Layout() { {/* Right status: latest height + live pulse */}
+
NFTs +
diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx index e448c5b..2cfdc2d 100644 --- a/frontend/src/components/Pagination.tsx +++ b/frontend/src/components/Pagination.tsx @@ -63,7 +63,7 @@ export default function Pagination({ currentPage, totalPages, onPageChange }: Pa page === currentPage ? 'btn-primary' : page === '...' - ? 'bg-transparent cursor-default text-gray-500' + ? 'bg-transparent cursor-default text-fg-subtle' : 'btn-secondary' }`} > diff --git a/frontend/src/components/ProxyBadge.tsx b/frontend/src/components/ProxyBadge.tsx index bdd3bb8..abc23ce 100644 --- a/frontend/src/components/ProxyBadge.tsx +++ b/frontend/src/components/ProxyBadge.tsx @@ -28,10 +28,6 @@ function getProxyTypeLabel(proxyType?: string): string { } } -function getProxyTypeColor(): string { - return 'bg-dark-600 text-white border-dark-500'; -} - export default function ProxyBadge({ address, showImplementation = true, @@ -44,12 +40,10 @@ export default function ProxyBadge({ } const typeLabel = getProxyTypeLabel(proxyInfo.proxy_type); - const typeColor = getProxyTypeColor(); - return (
- + Proxy @@ -82,11 +76,9 @@ export function ProxyIndicator({ address }: ProxyIndicatorProps) { return null; } - const typeColor = getProxyTypeColor(); - return ( Proxy diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 066596f..4cc4c36 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -192,7 +192,7 @@ export default function SearchBar() { onChange={(e) => setQuery(e.target.value)} onKeyDown={onKeyDown} placeholder="Search Address / Tx Hash / Block / Token / NFT" - className="w-full bg-dark-700/80 backdrop-blur border border-dark-500 px-4 py-2 pl-10 text-sm text-white placeholder-gray-500 rounded-full shadow-md shadow-black/20 focus:outline-none focus:border-accent-primary focus:ring-2 focus:ring-accent-primary/40 transition" + className="w-full bg-dark-700/80 backdrop-blur border border-dark-500 px-4 py-2 pl-10 text-sm text-fg placeholder-gray-500 rounded-full shadow-md shadow-black/20 focus:outline-none focus:border-accent-primary focus:ring-2 focus:ring-accent-primary/40 transition" />
-
+
{getPrimaryText(r)}
@@ -262,7 +262,7 @@ export default function SearchBar() {
-
View all results for “{query.trim()}”
+
View all results for “{query.trim()}”
diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx index d2a201e..dca0fca 100644 --- a/frontend/src/components/StatusBadge.tsx +++ b/frontend/src/components/StatusBadge.tsx @@ -5,9 +5,7 @@ interface StatusBadgeProps { export default function StatusBadge({ status }: StatusBadgeProps) { return ( {status ? 'Success' : 'Failed'} diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx new file mode 100644 index 0000000..b0b3685 --- /dev/null +++ b/frontend/src/context/ThemeContext.tsx @@ -0,0 +1,31 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { ThemeContext, STORAGE_KEY, type Theme, type ThemeContextValue } from './theme-context'; + +const getInitialTheme = (): Theme => { + if (typeof window === 'undefined') { + return 'dark'; + } + const stored = window.localStorage.getItem(STORAGE_KEY) as Theme | null; + if (stored === 'dark' || stored === 'light') { + return stored; + } + const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; + return prefersLight ? 'light' : 'dark'; +}; + +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useState(getInitialTheme); + + useEffect(() => { + document.documentElement.dataset.theme = theme; + window.localStorage.setItem(STORAGE_KEY, theme); + }, [theme]); + + const value = useMemo(() => ({ + theme, + setTheme, + toggleTheme: () => setTheme((prev) => (prev === 'dark' ? 'light' : 'dark')), + }), [theme]); + + return {children}; +}; diff --git a/frontend/src/context/theme-context.ts b/frontend/src/context/theme-context.ts new file mode 100644 index 0000000..67342ed --- /dev/null +++ b/frontend/src/context/theme-context.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +export type Theme = 'dark' | 'light'; + +export type ThemeContextValue = { + theme: Theme; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +}; + +export const STORAGE_KEY = 'atlas-theme'; + +export const ThemeContext = createContext(undefined); diff --git a/frontend/src/hooks/useAddresses.ts b/frontend/src/hooks/useAddresses.ts index ae9a7bf..cc51556 100644 --- a/frontend/src/hooks/useAddresses.ts +++ b/frontend/src/hooks/useAddresses.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { Address, ApiError, PaginatedResponse } from '../types'; import { getAddresses, type GetAddressesParams } from '../api/addresses'; @@ -15,12 +15,18 @@ export function useAddresses(params: GetAddressesParams = {}): UseAddressesResul const [pagination, setPagination] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); const fetchAddresses = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getAddresses(params); + const response = await getAddresses(paramsRef.current); setAddresses(response.data); setPagination(response); } catch (err) { @@ -28,11 +34,11 @@ export function useAddresses(params: GetAddressesParams = {}): UseAddressesResul } finally { setLoading(false); } - }, [params.page, params.limit, params.address_type, params.from_block, params.to_block]); + }, []); useEffect(() => { fetchAddresses(); - }, [fetchAddresses]); + }, [fetchAddresses, paramsKey]); return { addresses, pagination, loading, error, refetch: fetchAddresses }; } diff --git a/frontend/src/hooks/useBlocks.ts b/frontend/src/hooks/useBlocks.ts index d729d30..1338e1a 100644 --- a/frontend/src/hooks/useBlocks.ts +++ b/frontend/src/hooks/useBlocks.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { Block, Transaction, PaginatedResponse, ApiError } from '../types'; import { getBlocks, getBlockByNumber, getBlockTransactions } from '../api/blocks'; import type { GetBlocksParams } from '../api/blocks'; @@ -16,12 +16,18 @@ export function useBlocks(params: GetBlocksParams = {}): UseBlocksResult { const [pagination, setPagination] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); const fetchBlocks = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getBlocks(params); + const response = await getBlocks(paramsRef.current); setBlocks(response.data); setPagination(response); } catch (err) { @@ -29,11 +35,11 @@ export function useBlocks(params: GetBlocksParams = {}): UseBlocksResult { } finally { setLoading(false); } - }, [params.page, params.limit]); + }, []); useEffect(() => { fetchBlocks(); - }, [fetchBlocks]); + }, [fetchBlocks, paramsKey]); return { blocks, pagination, loading, error, refetch: fetchBlocks }; } @@ -92,6 +98,13 @@ export function useBlockTransactions( const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const txParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTransactions = useCallback(async () => { if (blockNumber === undefined) { setLoading(false); @@ -101,7 +114,7 @@ export function useBlockTransactions( setLoading(true); setError(null); try { - const response = await getBlockTransactions(blockNumber, params); + const response = await getBlockTransactions(blockNumber, paramsRef.current); setTransactions(response.data); setPagination(response); } catch (err) { @@ -109,11 +122,11 @@ export function useBlockTransactions( } finally { setLoading(false); } - }, [blockNumber, params.page, params.limit]); + }, [blockNumber]); useEffect(() => { fetchTransactions(); - }, [fetchTransactions]); + }, [fetchTransactions, txParamsKey]); return { transactions, pagination, loading, error, refetch: fetchTransactions }; } diff --git a/frontend/src/hooks/useEthPrice.ts b/frontend/src/hooks/useEthPrice.ts index db3358c..a6a50c0 100644 --- a/frontend/src/hooks/useEthPrice.ts +++ b/frontend/src/hooks/useEthPrice.ts @@ -9,7 +9,7 @@ export default function useEthPrice({ refreshMs = 60000 }: UseEthPriceOptions = const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const loadFromCache = () => { + const loadFromCache = useCallback(() => { try { const raw = localStorage.getItem('price:eth_usd'); if (!raw) return null; @@ -19,7 +19,7 @@ export default function useEthPrice({ refreshMs = 60000 }: UseEthPriceOptions = } catch { return null; } - }; + }, [refreshMs]); const saveToCache = (v: number) => { try { @@ -76,7 +76,7 @@ export default function useEthPrice({ refreshMs = 60000 }: UseEthPriceOptions = } finally { setLoading(false); } - }, [refreshMs]); + }, [loadFromCache]); useEffect(() => { fetchPrice(); diff --git a/frontend/src/hooks/useLogs.ts b/frontend/src/hooks/useLogs.ts index f251710..8b4b193 100644 --- a/frontend/src/hooks/useLogs.ts +++ b/frontend/src/hooks/useLogs.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { EventLog, DecodedEventLog, ApiError } from '../types'; import { getTransactionLogs, @@ -21,6 +21,13 @@ export function useTransactionLogs(txHash: string | undefined, params: GetTransa const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const txLogsParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchLogs = useCallback(async () => { if (!txHash) { setLoading(false); @@ -30,7 +37,7 @@ export function useTransactionLogs(txHash: string | undefined, params: GetTransa setLoading(true); setError(null); try { - const response = await getTransactionLogs(txHash, params); + const response = await getTransactionLogs(txHash, paramsRef.current); setLogs(response.data); setPagination({ page: response.page, @@ -43,11 +50,11 @@ export function useTransactionLogs(txHash: string | undefined, params: GetTransa } finally { setLoading(false); } - }, [txHash, params.page, params.limit]); + }, [txHash]); useEffect(() => { fetchLogs(); - }, [fetchLogs]); + }, [fetchLogs, txLogsParamsKey]); return { logs, pagination, loading, error, refetch: fetchLogs }; } @@ -66,6 +73,13 @@ export function useTransactionDecodedLogs(txHash: string | undefined, params: Ge const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const decodedParamsRef = useRef(params); + const decodedParamsKey = JSON.stringify(params); + + useEffect(() => { + decodedParamsRef.current = params; + }, [params]); + const fetchLogs = useCallback(async () => { if (!txHash) { setLoading(false); @@ -75,7 +89,7 @@ export function useTransactionDecodedLogs(txHash: string | undefined, params: Ge setLoading(true); setError(null); try { - const response = await getTransactionDecodedLogs(txHash, params); + const response = await getTransactionDecodedLogs(txHash, decodedParamsRef.current); setLogs(response.data); setPagination({ page: response.page, @@ -88,11 +102,11 @@ export function useTransactionDecodedLogs(txHash: string | undefined, params: Ge } finally { setLoading(false); } - }, [txHash, params.page, params.limit]); + }, [txHash]); useEffect(() => { fetchLogs(); - }, [fetchLogs]); + }, [fetchLogs, decodedParamsKey]); return { logs, pagination, loading, error, refetch: fetchLogs }; } @@ -111,6 +125,13 @@ export function useAddressLogs(address: string | undefined, params: GetAddressLo const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const addressParamsRef = useRef(params); + const addressParamsKey = JSON.stringify(params); + + useEffect(() => { + addressParamsRef.current = params; + }, [params]); + const fetchLogs = useCallback(async () => { if (!address) { setLoading(false); @@ -120,7 +141,7 @@ export function useAddressLogs(address: string | undefined, params: GetAddressLo setLoading(true); setError(null); try { - const response = await getAddressLogs(address, params); + const response = await getAddressLogs(address, addressParamsRef.current); setLogs(response.data); setPagination({ page: response.page, @@ -133,11 +154,11 @@ export function useAddressLogs(address: string | undefined, params: GetAddressLo } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); useEffect(() => { fetchLogs(); - }, [fetchLogs]); + }, [fetchLogs, addressParamsKey]); return { logs, pagination, loading, error, refetch: fetchLogs }; } diff --git a/frontend/src/hooks/useNFTs.ts b/frontend/src/hooks/useNFTs.ts index 4a330b5..fbad5f7 100644 --- a/frontend/src/hooks/useNFTs.ts +++ b/frontend/src/hooks/useNFTs.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { NftContract, NftToken, ApiError } from '../types'; import { getNftContracts, getNftContract, getNftTokens, getNftToken, getAddressNfts, getNftTransfers, getNftTokenTransfers } from '../api/nfts'; import type { GetNftContractsParams, GetNftTokensParams, GetAddressNftsParams, GetNftTransfersParams } from '../api/nfts'; @@ -16,12 +16,18 @@ export function useNftContracts(params: GetNftContractsParams = {}): UseNftContr const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; total_pages: number } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); const fetchContracts = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getNftContracts(params); + const response = await getNftContracts(paramsRef.current); setContracts(response.data); setPagination({ page: response.page, @@ -34,11 +40,11 @@ export function useNftContracts(params: GetNftContractsParams = {}): UseNftContr } finally { setLoading(false); } - }, [params.page, params.limit]); + }, []); useEffect(() => { fetchContracts(); - }, [fetchContracts]); + }, [fetchContracts, paramsKey]); return { contracts, pagination, loading, error, refetch: fetchContracts }; } @@ -94,6 +100,13 @@ export function useNftTokens(contractAddress: string | undefined, params: GetNft const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const tokenParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTokens = useCallback(async () => { if (!contractAddress) { setLoading(false); @@ -103,7 +116,7 @@ export function useNftTokens(contractAddress: string | undefined, params: GetNft setLoading(true); setError(null); try { - const response = await getNftTokens(contractAddress, params); + const response = await getNftTokens(contractAddress, paramsRef.current); setTokens(response.data); setPagination({ page: response.page, @@ -116,11 +129,11 @@ export function useNftTokens(contractAddress: string | undefined, params: GetNft } finally { setLoading(false); } - }, [contractAddress, params.page, params.limit, params.owner]); + }, [contractAddress]); useEffect(() => { fetchTokens(); - }, [fetchTokens]); + }, [fetchTokens, tokenParamsKey]); return { tokens, pagination, loading, error, refetch: fetchTokens }; } @@ -176,12 +189,19 @@ export function useNftTokenTransfers(contractAddress: string | undefined, tokenI const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const transferParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTransfers = useCallback(async () => { if (!contractAddress || !tokenId) { setLoading(false); return; } setLoading(true); setError(null); try { - const response = await getNftTokenTransfers(contractAddress, tokenId, params); + const response = await getNftTokenTransfers(contractAddress, tokenId, paramsRef.current); setTransfers(response.data); setPagination({ page: response.page, limit: response.limit, total: response.total, total_pages: response.total_pages }); } catch (err) { @@ -189,9 +209,9 @@ export function useNftTokenTransfers(contractAddress: string | undefined, tokenI } finally { setLoading(false); } - }, [contractAddress, tokenId, params.page, params.limit]); + }, [contractAddress, tokenId]); - useEffect(() => { fetchTransfers(); }, [fetchTransfers]); + useEffect(() => { fetchTransfers(); }, [fetchTransfers, transferParamsKey]); return { transfers, pagination, loading, error, refetch: fetchTransfers }; } @@ -210,12 +230,19 @@ export function useNftCollectionTransfers(contractAddress: string | undefined, p const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const collectionParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTransfers = useCallback(async () => { if (!contractAddress) { setLoading(false); return; } setLoading(true); setError(null); try { - const response = await getNftTransfers(contractAddress, params); + const response = await getNftTransfers(contractAddress, paramsRef.current); setTransfers(response.data); setPagination({ page: response.page, limit: response.limit, total: response.total, total_pages: response.total_pages }); } catch (err) { @@ -223,9 +250,9 @@ export function useNftCollectionTransfers(contractAddress: string | undefined, p } finally { setLoading(false); } - }, [contractAddress, params.page, params.limit]); + }, [contractAddress]); - useEffect(() => { fetchTransfers(); }, [fetchTransfers]); + useEffect(() => { fetchTransfers(); }, [fetchTransfers, collectionParamsKey]); return { transfers, pagination, loading, error, refetch: fetchTransfers }; } @@ -244,12 +271,19 @@ export function useAddressNfts(address: string | undefined, params: GetAddressNf const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const addressParamsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTokens = useCallback(async () => { if (!address) { setLoading(false); return; } setLoading(true); setError(null); try { - const response = await getAddressNfts(address, params); + const response = await getAddressNfts(address, paramsRef.current); setTokens(response.data); setPagination({ page: response.page, limit: response.limit, total: response.total, total_pages: response.total_pages }); } catch (err) { @@ -257,9 +291,9 @@ export function useAddressNfts(address: string | undefined, params: GetAddressNf } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); - useEffect(() => { fetchTokens(); }, [fetchTokens]); + useEffect(() => { fetchTokens(); }, [fetchTokens, addressParamsKey]); return { tokens, pagination, loading, error, refetch: fetchTokens }; } diff --git a/frontend/src/hooks/useProxies.ts b/frontend/src/hooks/useProxies.ts index 1c5bd99..67b3853 100644 --- a/frontend/src/hooks/useProxies.ts +++ b/frontend/src/hooks/useProxies.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { ProxyInfo, CombinedAbi, ApiError } from '../types'; import { getProxies, getContractProxy, getContractCombinedAbi } from '../api/proxies'; import type { GetProxiesParams } from '../api/proxies'; @@ -17,11 +17,18 @@ export function useProxies(params: GetProxiesParams = {}): UseProxiesResult { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchProxies = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getProxies(params); + const response = await getProxies(paramsRef.current); setProxies(response.data); setPagination({ page: response.page, @@ -34,11 +41,11 @@ export function useProxies(params: GetProxiesParams = {}): UseProxiesResult { } finally { setLoading(false); } - }, [params.page, params.limit]); + }, []); useEffect(() => { fetchProxies(); - }, [fetchProxies]); + }, [fetchProxies, paramsKey]); return { proxies, pagination, loading, error, refetch: fetchProxies }; } diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts new file mode 100644 index 0000000..c7e8075 --- /dev/null +++ b/frontend/src/hooks/useTheme.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { ThemeContext } from '../context/theme-context'; + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/frontend/src/hooks/useTokens.ts b/frontend/src/hooks/useTokens.ts index cf0be21..b2f03ff 100644 --- a/frontend/src/hooks/useTokens.ts +++ b/frontend/src/hooks/useTokens.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { Token, TokenHolder, TokenTransfer, AddressTokenBalance, ApiError } from '../types'; import { getTokens, @@ -28,11 +28,18 @@ export function useTokens(params: GetTokensParams = {}): UseTokensResult { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTokens = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getTokens(params); + const response = await getTokens(paramsRef.current); setTokens(response.data); setPagination({ page: response.page, @@ -45,11 +52,11 @@ export function useTokens(params: GetTokensParams = {}): UseTokensResult { } finally { setLoading(false); } - }, [params.page, params.limit]); + }, []); useEffect(() => { fetchTokens(); - }, [fetchTokens]); + }, [fetchTokens, paramsKey]); return { tokens, pagination, loading, error, refetch: fetchTokens }; } @@ -105,6 +112,13 @@ export function useTokenHolders(address: string | undefined, params: GetTokenHol const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const holdersParamsRef = useRef(params); + const holdersParamsKey = JSON.stringify(params); + + useEffect(() => { + holdersParamsRef.current = params; + }, [params]); + const fetchHolders = useCallback(async () => { if (!address) { setLoading(false); @@ -114,7 +128,7 @@ export function useTokenHolders(address: string | undefined, params: GetTokenHol setLoading(true); setError(null); try { - const response = await getTokenHolders(address, params); + const response = await getTokenHolders(address, holdersParamsRef.current); setHolders(response.data); setPagination({ page: response.page, @@ -127,11 +141,11 @@ export function useTokenHolders(address: string | undefined, params: GetTokenHol } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); useEffect(() => { fetchHolders(); - }, [fetchHolders]); + }, [fetchHolders, holdersParamsKey]); return { holders, pagination, loading, error, refetch: fetchHolders }; } @@ -150,6 +164,13 @@ export function useTokenTransfers(address: string | undefined, params: GetTokenT const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const transfersParamsRef = useRef(params); + const transfersParamsKey = JSON.stringify(params); + + useEffect(() => { + transfersParamsRef.current = params; + }, [params]); + const fetchTransfers = useCallback(async () => { if (!address) { setLoading(false); @@ -159,7 +180,7 @@ export function useTokenTransfers(address: string | undefined, params: GetTokenT setLoading(true); setError(null); try { - const response = await getTokenTransfers(address, params); + const response = await getTokenTransfers(address, transfersParamsRef.current); setTransfers(response.data); setPagination({ page: response.page, @@ -172,11 +193,11 @@ export function useTokenTransfers(address: string | undefined, params: GetTokenT } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); useEffect(() => { fetchTransfers(); - }, [fetchTransfers]); + }, [fetchTransfers, transfersParamsKey]); return { transfers, pagination, loading, error, refetch: fetchTransfers }; } @@ -195,6 +216,13 @@ export function useAddressTokens(address: string | undefined, params: GetAddress const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const addressParamsRef = useRef(params); + const addressParamsKey = JSON.stringify(params); + + useEffect(() => { + addressParamsRef.current = params; + }, [params]); + const fetchBalances = useCallback(async () => { if (!address) { setLoading(false); @@ -204,7 +232,7 @@ export function useAddressTokens(address: string | undefined, params: GetAddress setLoading(true); setError(null); try { - const response = await getAddressTokens(address, params); + const response = await getAddressTokens(address, addressParamsRef.current); setBalances(response.data); setPagination({ page: response.page, @@ -217,11 +245,11 @@ export function useAddressTokens(address: string | undefined, params: GetAddress } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); useEffect(() => { fetchBalances(); - }, [fetchBalances]); + }, [fetchBalances, addressParamsKey]); return { balances, pagination, loading, error, refetch: fetchBalances }; } diff --git a/frontend/src/hooks/useTransactions.ts b/frontend/src/hooks/useTransactions.ts index cc1c640..714c37b 100644 --- a/frontend/src/hooks/useTransactions.ts +++ b/frontend/src/hooks/useTransactions.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { Transaction, ApiError } from '../types'; import { getTransactions, getTransactionByHash, getTransactionsByAddress } from '../api/transactions'; import type { GetTransactionsParams } from '../api/transactions'; @@ -17,11 +17,18 @@ export function useTransactions(params: GetTransactionsParams = {}): UseTransact const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTransactions = useCallback(async () => { setLoading(true); setError(null); try { - const response = await getTransactions(params); + const response = await getTransactions(paramsRef.current); setTransactions(response.data); setPagination({ page: response.page, @@ -34,11 +41,11 @@ export function useTransactions(params: GetTransactionsParams = {}): UseTransact } finally { setLoading(false); } - }, [params.page, params.limit, params.block_number, params.address]); + }, []); useEffect(() => { fetchTransactions(); - }, [fetchTransactions]); + }, [fetchTransactions, paramsKey]); return { transactions, pagination, loading, error, refetch: fetchTransactions }; } @@ -49,6 +56,13 @@ export function useAddressTransactions(address: string | undefined, params: { pa const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const addressParamsRef = useRef(params); + const addressParamsKey = JSON.stringify(params); + + useEffect(() => { + addressParamsRef.current = params; + }, [params]); + const fetchTransactions = useCallback(async () => { if (!address) { setLoading(false); @@ -58,7 +72,7 @@ export function useAddressTransactions(address: string | undefined, params: { pa setLoading(true); setError(null); try { - const response = await getTransactionsByAddress(address, params); + const response = await getTransactionsByAddress(address, addressParamsRef.current); setTransactions(response.data); setPagination({ page: response.page, @@ -71,11 +85,11 @@ export function useAddressTransactions(address: string | undefined, params: { pa } finally { setLoading(false); } - }, [address, params.page, params.limit]); + }, [address]); useEffect(() => { fetchTransactions(); - }, [fetchTransactions]); + }, [fetchTransactions, addressParamsKey]); return { transactions, pagination, loading, error, refetch: fetchTransactions }; } diff --git a/frontend/src/hooks/useTransfers.ts b/frontend/src/hooks/useTransfers.ts index f09fa2e..973b9eb 100644 --- a/frontend/src/hooks/useTransfers.ts +++ b/frontend/src/hooks/useTransfers.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import type { AddressTransfer, ApiError } from '../types'; import { getAddressTransfers, type GetAddressTransfersParams } from '../api/addresses'; @@ -16,12 +16,19 @@ export function useAddressTransfers(address: string | undefined, params: GetAddr const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const paramsRef = useRef(params); + const paramsKey = JSON.stringify(params); + + useEffect(() => { + paramsRef.current = params; + }, [params]); + const fetchTransfers = useCallback(async () => { if (!address) { setLoading(false); return; } setLoading(true); setError(null); try { - const response = await getAddressTransfers(address, params); + const response = await getAddressTransfers(address, paramsRef.current); setTransfers(response.data); setPagination({ page: response.page, limit: response.limit, total: response.total, total_pages: response.total_pages }); } catch (err) { @@ -29,10 +36,9 @@ export function useAddressTransfers(address: string | undefined, params: GetAddr } finally { setLoading(false); } - }, [address, params.page, params.limit, params.transfer_type]); + }, [address]); - useEffect(() => { fetchTransfers(); }, [fetchTransfers]); + useEffect(() => { fetchTransfers(); }, [fetchTransfers, paramsKey]); return { transfers, pagination, loading, error, refetch: fetchTransfers }; } - diff --git a/frontend/src/index.css b/frontend/src/index.css index d88b555..d6f3cd8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,12 +3,61 @@ @tailwind utilities; @layer base { + :root { + --color-surface-900: 6 6 8; + --color-surface-800: 12 12 16; + --color-surface-700: 20 20 28; + --color-surface-600: 34 34 46; + --color-surface-500: 48 48 60; + --color-body-bg: #050505; + --color-body-text: #f8fafc; + --color-text-primary: 248 250 252; + --color-text-secondary: 229 231 235; + --color-text-muted: 203 213 225; + --color-text-subtle: 148 163 184; + --color-text-faint: 100 116 139; + --color-border: 45 45 58; + --color-gray-200: 226 232 240; + --color-gray-300: 203 213 225; + --color-gray-400: 148 163 184; + --color-gray-500: 107 114 128; + --color-gray-600: 75 85 99; + --color-gray-700: 55 65 81; + } + + :root[data-theme='light'] { + --color-surface-900: 244 237 230; + --color-surface-800: 238 230 224; + --color-surface-700: 230 222 214; + --color-surface-600: 219 209 200; + --color-surface-500: 205 195 186; + --color-body-bg: #f4ede6; + --color-body-text: #2f241b; + --color-text-primary: 31 28 24; + --color-text-secondary: 54 45 38; + --color-text-muted: 88 73 63; + --color-text-subtle: 120 102 90; + --color-text-faint: 137 122 112; + --color-border: 210 197 185; + --color-gray-200: 47 36 27; + --color-gray-300: 63 49 38; + --color-gray-400: 86 66 51; + --color-gray-500: 115 89 67; + --color-gray-600: 148 117 92; + --color-gray-700: 175 143 118; + } + body { - @apply bg-dark-900 text-white antialiased font-sans; + background-color: var(--color-body-bg); + color: var(--color-body-text); } } @layer components { + body { + @apply font-sans antialiased transition-colors duration-200; + } + .hash { @apply font-mono text-sm break-all; } @@ -32,7 +81,7 @@ } .btn-secondary { - @apply bg-dark-700/70 hover:bg-dark-600/70 text-white border border-dark-500/80 shadow-sm shadow-black/10; + @apply bg-dark-700/70 hover:bg-dark-600/70 text-fg border border-dark-500/80 shadow-sm shadow-black/10; } .table-header { @@ -47,6 +96,59 @@ .row-highlight { animation: highlightFade 0.9s ease-out forwards; } + + .badge-chip { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + border: 1px solid rgb(var(--color-surface-500)); + font-size: 0.65rem; + font-weight: 600; + background-color: rgb(var(--color-dark-600)); + color: #e2e8f0; + } + + .status-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.65rem; + border-radius: 9999px; + border-width: 1px; + font-size: 0.75rem; + font-weight: 600; + line-height: 1; + } + + .status-badge--success { + color: #4ade80; + border-color: rgba(74, 222, 128, 0.35); + background-color: rgba(34, 197, 94, 0.12); + } + + .status-badge--error { + color: #f87171; + border-color: rgba(248, 113, 113, 0.4); + background-color: rgba(248, 113, 113, 0.12); + } + + [data-theme='light'] .badge-chip { + border-color: #ccbcae; + background-color: #ede0d4; + color: #2f241b; + } + + [data-theme='light'] .status-badge--success { + color: #166534; + border-color: #86efac; + background-color: #d1fae5; + } + + [data-theme='light'] .status-badge--error { + color: #7f1d1d; + border-color: #fca5a5; + background-color: #fee2e2; + } } @keyframes highlightFade { diff --git a/frontend/src/pages/AddressPage.tsx b/frontend/src/pages/AddressPage.tsx index 451a01d..9c0811a 100644 --- a/frontend/src/pages/AddressPage.tsx +++ b/frontend/src/pages/AddressPage.tsx @@ -68,7 +68,7 @@ export default function AddressPage() { return (
-

Address

+

Address

{addressParam && (
{addressParam} @@ -92,7 +92,7 @@ export default function AddressPage() { {/* Header with Label */}
-

+

{address?.address_type === 'erc20' ? 'Token Contract' : address?.address_type === 'nft' @@ -127,19 +127,19 @@ export default function AddressPage() {

Transactions

-

{address ? formatNumber(address.tx_count) : '---'}

+

{address ? formatNumber(address.tx_count) : '---'}

First Seen Block

-

{address ? formatNumber(address.first_seen_block) : '---'}

+

{address ? formatNumber(address.first_seen_block) : '---'}

ETH Balance

-

{balanceWei ? `${formatEtherExact(balanceWei)} ETH` : '---'}

+

{balanceWei ? `${formatEtherExact(balanceWei)} ETH` : '---'}

Address Type

-
+
{address?.address_type === 'erc20' ? 'ERC-20 Token' : address?.address_type === 'nft' @@ -208,7 +208,7 @@ export default function AddressPage() { const self = addressParam?.toLowerCase(); const isSender = tx.from_address.toLowerCase() === self; const badge = ( - + {isSender ? 'Sent to' : 'Received from'} ); @@ -277,7 +277,7 @@ export default function AddressPage() {
- {balance.name || 'Unknown Token'} + {balance.name || 'Unknown Token'} {balance.symbol || '---'}
@@ -324,7 +324,7 @@ export default function AddressPage() { )}
-
{displayName}
+
{displayName}
{t.contract_address}
@@ -388,7 +388,7 @@ export default function AddressPage() { {isErc20 ? 'ERC-20' : 'NFT'}
- {t.token_name || (isErc20 ? 'ERC-20' : 'NFT')} + {t.token_name || (isErc20 ? 'ERC-20' : 'NFT')} {t.token_symbol || ''}
diff --git a/frontend/src/pages/AddressesPage.tsx b/frontend/src/pages/AddressesPage.tsx index 453d887..743c01a 100644 --- a/frontend/src/pages/AddressesPage.tsx +++ b/frontend/src/pages/AddressesPage.tsx @@ -98,7 +98,7 @@ export default function AddressesPage() { return (
-

Addresses

+

Addresses

- - - - - - -
)}
-

Transaction

+

Transaction