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
45 changes: 24 additions & 21 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,32 @@ import {
SearchResultsPage,
AddressesPage,
} from './pages';
import { ThemeProvider } from './context/ThemeContext';

export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<WelcomePage />} />
<Route path="blocks" element={<BlocksPage />} />
<Route path="blocks/:number" element={<BlockDetailPage />} />
<Route path="blocks/:number/transactions" element={<BlockTransactionsPage />} />
<Route path="transactions" element={<TransactionsPage />} />
<Route path="search" element={<SearchResultsPage />} />
<Route path="addresses" element={<AddressesPage />} />
<Route path="tx/:hash" element={<TransactionDetailPage />} />
<Route path="address/:address" element={<AddressPage />} />
<Route path="nfts" element={<NFTsPage />} />
<Route path="nfts/:contract" element={<NFTContractPage />} />
<Route path="nfts/:contract/:tokenId" element={<NFTTokenPage />} />
<Route path="tokens" element={<TokensPage />} />
<Route path="tokens/:address" element={<TokenDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
<ThemeProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<WelcomePage />} />
<Route path="blocks" element={<BlocksPage />} />
<Route path="blocks/:number" element={<BlockDetailPage />} />
<Route path="blocks/:number/transactions" element={<BlockTransactionsPage />} />
<Route path="transactions" element={<TransactionsPage />} />
<Route path="search" element={<SearchResultsPage />} />
<Route path="addresses" element={<AddressesPage />} />
<Route path="tx/:hash" element={<TransactionDetailPage />} />
<Route path="address/:address" element={<AddressPage />} />
<Route path="nfts" element={<NFTsPage />} />
<Route path="nfts/:contract" element={<NFTContractPage />} />
<Route path="nfts/:contract/:tokenId" element={<NFTTokenPage />} />
<Route path="tokens" element={<TokensPage />} />
<Route path="tokens/:address" element={<TokenDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
4 changes: 1 addition & 3 deletions frontend/src/components/ContractTypeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium ${base} ${className}`}>
<span className={`badge-chip uppercase tracking-wide ${className}`}>
{label}
</span>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function CopyButton({ text, className = '' }: CopyButtonProps) {
title={copied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-4 h-4 text-fg" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/EventLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function LogCard({ log, showTxHash, showAddress }: { log: EventLog | DecodedEven
<div className="flex items-center gap-4">
<span className="text-gray-500 text-sm">#{log.log_index}</span>
{decoded?.event_name ? (
<span className="text-white text-sm font-medium">
<span className="text-fg text-sm font-medium">
{decoded.event_name}
</span>
) : (
Expand All @@ -61,7 +61,7 @@ function LogCard({ log, showTxHash, showAddress }: { log: EventLog | DecodedEven
</div>
<button
onClick={() => setExpanded(!expanded)}
className="text-gray-400 hover:text-white text-sm transition-colors"
className="text-gray-400 hover:text-fg text-sm transition-colors"
>
{expanded ? 'Collapse' : 'Expand'}
</button>
Expand Down
82 changes: 79 additions & 3 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -17,6 +18,7 @@ export default function Layout() {
const displayRafRef = useRef<number | null>(null);
const lastFrameRef = useRef<number>(0);
const displayedRef = useRef<number>(0);
const displayInitializedRef = useRef(false);

useEffect(() => {
const id = window.setInterval(() => setNow(Date.now()), 1000);
Expand All @@ -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;
});
}
Expand Down Expand Up @@ -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 (
<div className="min-h-screen flex flex-col">
Expand Down Expand Up @@ -132,6 +138,41 @@ export default function Layout() {

{/* Right status: latest height + live pulse */}
<div className="hidden md:flex items-center justify-end">
<button
type="button"
onClick={toggleTheme}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
className="inline-flex items-center justify-center w-10 h-10 rounded-full border border-transparent hover:border-dark-600/60 bg-transparent hover:bg-dark-700/40 transition-colors mr-4"
>
{isDark ? (
<svg
className="w-5 h-5 text-gray-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 14.5a8.5 8.5 0 01-11.5-11.5 8.5 8.5 0 1011.5 11.5z" />
</svg>
) : (
<svg
className="w-5 h-5 text-gray-700"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2m0 16v2M20 12h2M2 12h2M17.657 6.343l-1.414 1.414M7.757 16.243l-1.414 1.414M6.343 6.343l1.414 1.414M16.243 16.243l1.414 1.414" />
</svg>
)}
</button>
<div className="flex items-center gap-3 text-sm text-gray-300">
<span
className={`inline-block w-2.5 h-2.5 rounded-full ${recentlyUpdated ? 'bg-red-500 live-dot' : 'bg-gray-600'}`}
Expand Down Expand Up @@ -165,6 +206,41 @@ export default function Layout() {
<NavLink to="/nfts" className={navLinkClass}>
NFTs
</NavLink>
<button
type="button"
onClick={toggleTheme}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
className="inline-flex items-center justify-center w-10 h-10 rounded-full border border-transparent hover:border-dark-600/60 bg-transparent hover:bg-dark-700/40 transition-colors"
>
{isDark ? (
<svg
className="w-5 h-5 text-gray-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 14.5a8.5 8.5 0 01-11.5-11.5 8.5 8.5 0 1011.5 11.5z" />
</svg>
) : (
<svg
className="w-5 h-5 text-gray-700"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2m0 16v2M20 12h2M2 12h2M17.657 6.343l-1.414 1.414M7.757 16.243l-1.414 1.414M6.343 6.343l1.414 1.414M16.243 16.243l1.414 1.414" />
</svg>
)}
</button>
</nav>
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}`}
>
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/components/ProxyBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,12 +40,10 @@ export default function ProxyBadge({
}

const typeLabel = getProxyTypeLabel(proxyInfo.proxy_type);
const typeColor = getProxyTypeColor();

return (
<div className={`flex flex-col gap-2 ${className}`}>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded border text-xs font-medium ${typeColor}`}>
<span className="badge-chip text-[0.65rem]">
Proxy
</span>
<span className="text-gray-400 text-xs">
Expand Down Expand Up @@ -82,11 +76,9 @@ export function ProxyIndicator({ address }: ProxyIndicatorProps) {
return null;
}

const typeColor = getProxyTypeColor();

return (
<span
className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${typeColor}`}
className="badge-chip text-[0.65rem]"
title={`${getProxyTypeLabel(proxyInfo.proxy_type)} Proxy - Implementation: ${proxyInfo.implementation_address}`}
>
Proxy
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
<svg
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500"
Expand Down Expand Up @@ -235,7 +235,7 @@ export default function SearchBar() {
>
<TypeIcon type={r.type} />
<div className="min-w-0">
<div className="text-sm text-white truncate">
<div className="text-sm text-fg truncate">
{getPrimaryText(r)}
</div>
<div className="text-xs text-gray-500 truncate">
Expand All @@ -262,7 +262,7 @@ export default function SearchBar() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<div className="min-w-0">
<div className="text-sm text-white truncate">View all results for “{query.trim()}”</div>
<div className="text-sm text-fg truncate">View all results for “{query.trim()}”</div>
</div>
</li>
</ul>
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ interface StatusBadgeProps {
export default function StatusBadge({ status }: StatusBadgeProps) {
return (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full border text-xs font-semibold leading-none bg-dark-600 border-dark-500 ${
status ? 'text-accent-success' : 'text-accent-error'
}`}
className={`status-badge ${status ? 'status-badge--success' : 'status-badge--error'}`}
title={status ? 'Transaction succeeded' : 'Transaction failed'}
>
{status ? 'Success' : 'Failed'}
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -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<Theme>(getInitialTheme);

useEffect(() => {
document.documentElement.dataset.theme = theme;
window.localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);

const value = useMemo<ThemeContextValue>(() => ({
theme,
setTheme,
toggleTheme: () => setTheme((prev) => (prev === 'dark' ? 'light' : 'dark')),
}), [theme]);

return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
13 changes: 13 additions & 0 deletions frontend/src/context/theme-context.ts
Original file line number Diff line number Diff line change
@@ -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<ThemeContextValue | undefined>(undefined);
Loading