1 Commits

Author SHA1 Message Date
sami7777 e02c03e83d UX polish: hamburger menu, copy buttons, skeletons, toasts, dark/light theme
- Add mobile hamburger menu with animated toggle (Header.js)
- Add CopyButton component with clipboard API + toast notifications
- Replace all loading spinners with skeleton loading screens
- Add custom Toast notification system (no new dependencies)
- Add dark/light theme toggle with localStorage persistence
- Use CSS variables for theme-aware colors across all components
- Build compiles successfully with zero warnings

New files: CopyButton.js, Skeleton.js, Toast.js, ThemeContext.js
Modified: All pages and components updated for UX improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:40:30 -07:00
17 changed files with 766 additions and 321 deletions
+70 -4
View File
@@ -2,6 +2,31 @@
@tailwind components;
@tailwind utilities;
/* Theme CSS Variables */
:root {
--bg-primary: #0f172a;
--bg-card: #1e293b;
--bg-secondary: #1f2937;
--bg-tertiary: #374151;
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--text-muted: #6b7280;
--border-color: #374151;
--border-light: #4b5563;
}
.light {
--bg-primary: #f8fafc;
--bg-card: #ffffff;
--bg-secondary: #f1f5f9;
--bg-tertiary: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--border-light: #cbd5e1;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -9,6 +34,8 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--bg-primary);
color: var(--text-primary);
}
code {
@@ -17,7 +44,9 @@ code {
}
.card {
@apply bg-maza-gray rounded-lg shadow-lg p-6 mb-4;
background-color: var(--bg-card);
border: 1px solid var(--border-color);
@apply rounded-lg shadow-lg p-6 mb-4;
}
.btn {
@@ -29,15 +58,22 @@ code {
}
.btn-secondary {
@apply bg-gray-600 text-white hover:bg-gray-700;
background-color: var(--bg-tertiary);
color: var(--text-primary);
@apply hover:opacity-80;
}
.input {
@apply w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 focus:outline-none focus:border-maza-blue;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
@apply w-full px-4 py-3 rounded-lg focus:outline-none focus:border-maza-blue;
}
.stat-card {
@apply bg-gradient-to-br from-maza-gray to-gray-800 rounded-lg p-4 shadow-lg;
background: linear-gradient(to bottom right, var(--bg-card), var(--bg-secondary));
border: 1px solid var(--border-color);
@apply rounded-lg p-4 shadow-lg;
}
.hash {
@@ -51,3 +87,33 @@ code {
.error {
@apply bg-red-900/30 border border-red-700 text-red-300 px-4 py-3 rounded-lg;
}
.light .error {
@apply bg-red-50 border-red-300 text-red-700;
}
/* Theme-aware utility classes */
.bg-theme-primary { background-color: var(--bg-primary); }
.bg-theme-card { background-color: var(--bg-card); }
.bg-theme-secondary { background-color: var(--bg-secondary); }
.bg-theme-tertiary { background-color: var(--bg-tertiary); }
.text-theme-primary { color: var(--text-primary); }
.text-theme-secondary { color: var(--text-secondary); }
.text-theme-muted { color: var(--text-muted); }
.border-theme { border-color: var(--border-color); }
/* Toast animation */
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-slide-in {
animation: slide-in 0.3s ease-out;
}
+7 -1
View File
@@ -1,5 +1,7 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider } from './contexts/ThemeContext';
import { ToastProvider } from './components/Toast';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
@@ -13,8 +15,10 @@ import './App.css';
function App() {
return (
<ThemeProvider>
<ToastProvider>
<Router>
<div className="min-h-screen bg-maza-dark text-gray-100 flex flex-col">
<div className="min-h-screen flex flex-col" style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-primary)' }}>
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
<Routes>
@@ -30,6 +34,8 @@ function App() {
<Footer />
</div>
</Router>
</ToastProvider>
</ThemeProvider>
);
}
+37
View File
@@ -0,0 +1,37 @@
import React, { useState } from 'react';
import { Copy, Check } from 'lucide-react';
import { useToast } from './Toast';
function CopyButton({ text, className = '' }) {
const [copied, setCopied] = useState(false);
const toast = useToast();
const handleCopy = async (e) => {
e.preventDefault();
e.stopPropagation();
try {
await navigator.clipboard.writeText(text);
setCopied(true);
toast('Copied to clipboard!', 'success');
setTimeout(() => setCopied(false), 2000);
} catch {
toast('Failed to copy', 'error');
}
};
return (
<button
onClick={handleCopy}
className={`inline-flex items-center justify-center p-1.5 rounded hover:bg-gray-600 transition-colors ${className}`}
title="Copy to clipboard"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-gray-400 hover:text-gray-200" />
)}
</button>
);
}
export default CopyButton;
+3 -3
View File
@@ -2,9 +2,9 @@ import React from 'react';
function Footer() {
return (
<footer className="bg-maza-gray border-t border-gray-700 py-6 mt-12">
<footer className="py-6 mt-12" style={{ backgroundColor: 'var(--bg-card)', borderTop: '1px solid var(--border-color)' }}>
<div className="container mx-auto px-4">
<div className="text-center text-gray-400 mb-4">
<div className="text-center mb-4" style={{ color: 'var(--text-secondary)' }}>
<p>Mazacoin Explorer &copy; 2026 | Built for the Mazacoin community</p>
<p className="text-sm mt-2">
<a href="https://mazacoin.org" target="_blank" rel="noopener noreferrer" className="hover:text-maza-blue">
@@ -12,7 +12,7 @@ function Footer() {
</a>
</p>
</div>
<div className="flex items-center justify-center gap-3 text-gray-400 text-sm">
<div className="flex items-center justify-center gap-3 text-sm" style={{ color: 'var(--text-secondary)' }}>
<span>Provided by</span>
<img
src="/samiahmed77777-logo.png"
+77 -19
View File
@@ -1,35 +1,93 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Activity, MapPin, TrendingUp } from 'lucide-react';
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Activity, MapPin, TrendingUp, Menu, X, Sun, Moon } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
function Header() {
const [menuOpen, setMenuOpen] = useState(false);
const { theme, toggleTheme } = useTheme();
const location = useLocation();
const closeMenu = () => setMenuOpen(false);
const navLinks = [
{ to: '/', label: 'Home', icon: null },
{ to: '/stats', label: 'Network Stats', icon: Activity },
{ to: '/nodes', label: 'Node Map', icon: MapPin },
{ to: '/richlist', label: 'Rich List', icon: TrendingUp },
];
const isActive = (path) => location.pathname === path;
return (
<header className="bg-maza-gray shadow-lg border-b border-gray-700">
<header className="shadow-lg border-b" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}>
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Link to="/" className="flex items-center space-x-3">
<Link to="/" className="flex items-center space-x-3" onClick={closeMenu}>
<img src="/maza-logo.png" alt="Mazacoin" className="w-8 h-8" />
<span className="text-2xl font-bold">Mazacoin Explorer</span>
</Link>
{/* Desktop nav */}
<div className="hidden md:flex items-center gap-6">
<nav className="flex items-center space-x-6">
<Link to="/" className="hover:text-maza-blue transition-colors">
Home
</Link>
<Link to="/stats" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<Activity className="w-4 h-4" />
Network Stats
</Link>
<Link to="/nodes" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<MapPin className="w-4 h-4" />
Node Map
</Link>
<Link to="/richlist" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Rich List
{navLinks.map(({ to, label, icon: Icon }) => (
<Link
key={to}
to={to}
className={`hover:text-maza-blue transition-colors flex items-center gap-2 ${isActive(to) ? 'text-maza-blue' : ''}`}
>
{Icon && <Icon className="w-4 h-4" />}
{label}
</Link>
))}
</nav>
<button
onClick={toggleTheme}
className="p-2 rounded-lg hover:opacity-80 transition-colors"
style={{ backgroundColor: 'var(--bg-secondary)' }}
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
>
{theme === 'dark' ? <Sun className="w-5 h-5 text-yellow-400" /> : <Moon className="w-5 h-5 text-blue-600" />}
</button>
</div>
{/* Mobile controls */}
<div className="flex items-center gap-2 md:hidden">
<button
onClick={toggleTheme}
className="p-2 rounded-lg hover:opacity-80 transition-colors"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
{theme === 'dark' ? <Sun className="w-5 h-5 text-yellow-400" /> : <Moon className="w-5 h-5 text-blue-600" />}
</button>
<button
onClick={() => setMenuOpen(!menuOpen)}
className="p-2 rounded-lg hover:opacity-80 transition-colors"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
{menuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile menu */}
{menuOpen && (
<nav className="md:hidden mt-4 pt-4 space-y-2" style={{ borderTop: '1px solid var(--border-color)' }}>
{navLinks.map(({ to, label, icon: Icon }) => (
<Link
key={to}
to={to}
onClick={closeMenu}
className={`flex items-center gap-3 px-3 py-3 rounded-lg transition-colors hover:opacity-80 ${isActive(to) ? 'text-maza-blue' : ''}`}
style={{ backgroundColor: isActive(to) ? 'var(--bg-secondary)' : 'transparent' }}
>
{Icon && <Icon className="w-5 h-5" />}
<span className="font-medium">{label}</span>
</Link>
))}
</nav>
)}
</div>
</header>
);
+21 -19
View File
@@ -3,6 +3,8 @@ import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale } from 'chart.js';
import 'chartjs-adapter-date-fns';
import axios from 'axios';
import { useTheme } from '../contexts/ThemeContext';
import { SkeletonChart } from './Skeleton';
ChartJS.register(CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale);
@@ -13,6 +15,7 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
const [loading, setLoading] = useState(true);
const [activeTimeframe, setActiveTimeframe] = useState(timeframe);
const [activeCurrency, setActiveCurrency] = useState(currency);
const { theme } = useTheme();
useEffect(() => {
fetchChartData();
@@ -56,6 +59,9 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
}
};
const gridColor = theme === 'light' ? '#e2e8f0' : '#1f2937';
const tickColor = theme === 'light' ? '#64748b' : '#9ca3af';
const options = {
responsive: true,
maintainAspectRatio: false,
@@ -63,7 +69,7 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
legend: {
display: true,
labels: {
color: '#9ca3af'
color: tickColor
}
},
title: {
@@ -99,19 +105,19 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
}
},
grid: {
color: '#1f2937'
color: gridColor
},
ticks: {
color: '#9ca3af'
color: tickColor
}
},
y: {
type: 'logarithmic',
grid: {
color: '#1f2937'
color: gridColor
},
ticks: {
color: '#9ca3af',
color: tickColor,
callback: function(value) {
return value.toFixed(activeCurrency === 'eth' ? 10 : 8);
}
@@ -140,28 +146,22 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
];
if (loading) {
return (
<div className="card">
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
</div>
);
return <SkeletonChart />;
}
if (!chartData) {
return (
<div className="card">
<p className="text-gray-400 text-center py-8">Building price history... Check back soon!</p>
<p className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>Building price history... Check back soon!</p>
</div>
);
}
return (
<div className="card">
<div className="flex items-center justify-between mb-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-4">
<h2 className="text-2xl font-bold">Price Chart</h2>
<div className="flex gap-4">
<div className="flex gap-4 flex-wrap">
{/* Currency selector */}
<div className="flex gap-2">
{currencies.map(curr => (
@@ -170,9 +170,10 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
onClick={() => setActiveCurrency(curr.value)}
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeCurrency === curr.value
? `${curr.color} bg-gray-700`
: 'text-gray-400 hover:text-gray-300'
? `${curr.color} bg-theme-secondary`
: 'hover:opacity-80'
}`}
style={activeCurrency !== curr.value ? { color: 'var(--text-secondary)' } : undefined}
>
{curr.label}
</button>
@@ -188,8 +189,9 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeTimeframe === tf.value
? 'bg-maza-blue text-white'
: 'text-gray-400 hover:text-gray-300'
: 'hover:opacity-80'
}`}
style={activeTimeframe !== tf.value ? { color: 'var(--text-secondary)' } : undefined}
>
{tf.label}
</button>
@@ -202,7 +204,7 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
<Line data={chartData} options={options} />
</div>
<div className="mt-4 text-sm text-gray-500 text-center">
<div className="mt-4 text-sm text-center" style={{ color: 'var(--text-muted)' }}>
Historical data from FreiExchange + CoinGecko. Data collection started March 2026.
</div>
</div>
+2 -1
View File
@@ -49,7 +49,8 @@ function SearchBar() {
/>
<button
type="submit"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-maza-blue transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 hover:text-maza-blue transition-colors"
style={{ color: 'var(--text-secondary)' }}
disabled={loading}
>
{loading ? (
+96
View File
@@ -0,0 +1,96 @@
import React from 'react';
export function SkeletonLine({ width = 'w-full', height = 'h-4', className = '' }) {
return (
<div className={`animate-pulse bg-gray-700 rounded ${width} ${height} ${className}`} />
);
}
export function SkeletonCard() {
return (
<div className="stat-card animate-pulse">
<div className="bg-gray-700 rounded h-3 w-24 mb-2" />
<div className="bg-gray-600 rounded h-8 w-32" />
</div>
);
}
export function SkeletonBlock() {
return (
<div className="p-4 bg-gray-800 rounded-lg animate-pulse">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="bg-gray-700 rounded-lg h-8 w-20" />
<div className="bg-gray-700 rounded h-4 w-40" />
</div>
<div className="flex items-center gap-6">
<div className="bg-gray-700 rounded h-4 w-24" />
<div className="bg-gray-700 rounded h-4 w-12" />
</div>
</div>
</div>
);
}
export function SkeletonTableRow({ cols = 4 }) {
return (
<tr className="animate-pulse">
{Array.from({ length: cols }).map((_, i) => (
<td key={i} className="px-6 py-4">
<div className="bg-gray-700 rounded h-4 w-full" />
</td>
))}
</tr>
);
}
export function SkeletonDetailCard() {
return (
<div className="card animate-pulse">
<div className="bg-gray-700 rounded h-6 w-48 mb-4" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i}>
<div className="bg-gray-700 rounded h-3 w-20 mb-2" />
<div className="bg-gray-600 rounded h-6 w-full" />
</div>
))}
<div className="md:col-span-2">
<div className="bg-gray-700 rounded h-3 w-16 mb-2" />
<div className="bg-gray-600 rounded h-10 w-full" />
</div>
</div>
</div>
);
}
export function SkeletonChart() {
return (
<div className="card animate-pulse">
<div className="flex items-center justify-between mb-6">
<div className="bg-gray-700 rounded h-7 w-32" />
<div className="flex gap-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-gray-700 rounded h-8 w-12" />
))}
</div>
</div>
<div className="bg-gray-700 rounded h-96 w-full" />
</div>
);
}
export function SkeletonTxList({ count = 3 }) {
return (
<div className="space-y-2">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="p-3 bg-gray-800 rounded animate-pulse">
<div className="flex items-center gap-3">
<div className="bg-gray-700 rounded h-4 w-8" />
<div className="bg-gray-700 rounded h-4 flex-1" />
</div>
</div>
))}
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { CheckCircle, XCircle, Info, X } from 'lucide-react';
const ToastContext = createContext();
let toastId = 0;
export function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const toast = useCallback((message, type = 'success') => {
const id = ++toastId;
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
}, []);
const removeToast = useCallback((id) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
return (
<ToastContext.Provider value={toast}>
{children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{toasts.map(t => (
<div
key={t.id}
className={`flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg text-sm font-medium animate-slide-in
${t.type === 'success' ? 'bg-green-600 text-white' : ''}
${t.type === 'error' ? 'bg-red-600 text-white' : ''}
${t.type === 'info' ? 'bg-blue-600 text-white' : ''}
`}
>
{t.type === 'success' && <CheckCircle className="w-4 h-4 shrink-0" />}
{t.type === 'error' && <XCircle className="w-4 h-4 shrink-0" />}
{t.type === 'info' && <Info className="w-4 h-4 shrink-0" />}
<span>{t.message}</span>
<button onClick={() => removeToast(t.id)} className="ml-2 hover:opacity-70">
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
+37
View File
@@ -0,0 +1,37 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('maza-theme') || 'dark';
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'light') {
root.classList.add('light');
} else {
root.classList.remove('light');
}
localStorage.setItem('maza-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
+19 -22
View File
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
import { Copy, TrendingUp } from 'lucide-react';
import { TrendingUp } from 'lucide-react';
import axios from 'axios';
import CopyButton from '../components/CopyButton';
import { SkeletonLine } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -27,10 +29,6 @@ function AddressDetail() {
}
};
const copyToClipboard = () => {
navigator.clipboard.writeText(address);
};
const formatBalance = (balance) => {
return balance.toLocaleString('en-US', {
minimumFractionDigits: 2,
@@ -47,47 +45,46 @@ function AddressDetail() {
<div className="flex-1">
<h2 className="text-xl font-bold mb-4">Address</h2>
<div className="bg-gray-800 p-4 rounded mb-4">
<div className="hash text-lg break-all mb-2">{address}</div>
<button
onClick={copyToClipboard}
className="btn btn-secondary text-sm flex items-center gap-2"
>
<Copy className="w-4 h-4" />
Copy Address
</button>
<div className="p-4 rounded mb-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center gap-2">
<div className="hash text-lg break-all flex-1">{address}</div>
<CopyButton text={address} />
</div>
</div>
{/* Balance Info */}
<div className="bg-gray-800 p-4 rounded mb-4">
<div className="p-4 rounded mb-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h3 className="text-lg font-bold mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-yellow-400" />
Balance
</h3>
{loadingBalance ? (
<div className="text-gray-400 text-sm">Loading balance...</div>
<div className="space-y-2">
<SkeletonLine width="w-48" height="h-8" />
<SkeletonLine width="w-32" height="h-4" />
</div>
) : balanceData?.balance !== null && balanceData?.balance !== undefined ? (
<>
<div className="text-3xl font-bold text-green-400 mb-2">
{formatBalance(balanceData.balance)} MAZA
</div>
{balanceData.rank && (
<div className="text-sm text-gray-400">
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Rank #{balanceData.rank} in rich list
</div>
)}
{balanceData.lastSeen && (
<div className="text-xs text-gray-500 mt-1">
<div className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>
Last seen in block: {balanceData.lastSeen.toLocaleString()}
</div>
)}
</>
) : (
<div className="text-sm text-gray-400">
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{balanceData?.message || 'Balance data not available yet'}
{balanceData?.lastScannedBlock && (
<div className="text-xs text-gray-500 mt-1">
<div className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>
Scanner progress: block {balanceData.lastScannedBlock.toLocaleString()}
</div>
)}
@@ -103,7 +100,7 @@ function AddressDetail() {
<li>Balance is calculated by our blockchain scanner tracking all transaction outputs</li>
<li>Scanner is running in the background and processes blocks continuously</li>
<li>Top addresses appear in the rich list as they're discovered</li>
<li>Full transaction history requires <code className="bg-gray-800 px-1 rounded">txindex=1</code> on the node</li>
<li>Full transaction history requires <code className="px-1 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>txindex=1</code> on the node</li>
</ul>
</div>
</div>
@@ -119,7 +116,7 @@ function AddressDetail() {
<div className="card">
<h2 className="text-xl font-bold mb-4">Transaction History</h2>
<div className="text-gray-400 text-center py-8">
<div className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>
<p className="mb-2">Transaction history for addresses requires txindex to be enabled on the Mazacoin node.</p>
<p className="text-sm">This feature will be available once the node is configured with txindex and fully synced.</p>
</div>
+46 -29
View File
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Clock, Hash, Layers } from 'lucide-react';
import { ChevronLeft, ChevronRight, Clock, Layers } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
import CopyButton from '../components/CopyButton';
import { SkeletonDetailCard, SkeletonTxList } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -14,6 +16,7 @@ function BlockDetail() {
useEffect(() => {
fetchBlock();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hashOrHeight]);
const fetchBlock = async () => {
@@ -32,18 +35,21 @@ function BlockDetail() {
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-6">
<div className="flex items-center justify-center">
<div className="animate-pulse bg-gray-700 rounded h-10 w-48" />
</div>
<SkeletonDetailCard />
<div className="card animate-pulse">
<div className="bg-gray-700 rounded h-6 w-40 mb-4" />
<SkeletonTxList count={3} />
</div>
</div>
);
}
if (error) {
return (
<div className="error">
{error}
</div>
);
return <div className="error">{error}</div>;
}
if (!block) return null;
@@ -80,12 +86,12 @@ function BlockDetail() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="text-gray-400 text-sm mb-1">Height</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Height</div>
<div className="text-lg font-bold">{block.height}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Timestamp</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Timestamp</div>
<div className="text-lg flex items-center gap-2">
<Clock className="w-4 h-4" />
{format(new Date(block.time * 1000), 'PPpp')}
@@ -93,51 +99,60 @@ function BlockDetail() {
</div>
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Hash</div>
<div className="hash text-lg bg-gray-800 p-3 rounded">{block.hash}</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Hash</div>
<div className="flex items-center gap-2 p-3 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="hash text-lg flex-1">{block.hash}</div>
<CopyButton text={block.hash} />
</div>
</div>
{block.previousblockhash && (
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Previous Block Hash</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Previous Block Hash</div>
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<Link
to={`/block/${block.previousblockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
className="hash text-lg flex-1 text-maza-blue hover:underline"
>
{block.previousblockhash}
</Link>
<CopyButton text={block.previousblockhash} />
</div>
</div>
)}
{block.nextblockhash && (
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Next Block Hash</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Next Block Hash</div>
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<Link
to={`/block/${block.nextblockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
className="hash text-lg flex-1 text-maza-blue hover:underline"
>
{block.nextblockhash}
</Link>
<CopyButton text={block.nextblockhash} />
</div>
</div>
)}
<div>
<div className="text-gray-400 text-sm mb-1">Difficulty</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Difficulty</div>
<div className="text-lg">{block.difficulty?.toFixed(4)}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Size</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Size</div>
<div className="text-lg">{(block.size / 1024).toFixed(2)} KB</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Confirmations</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Confirmations</div>
<div className="text-lg text-green-400">{block.confirmations}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Version</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Version</div>
<div className="text-lg">{block.version}</div>
</div>
</div>
@@ -157,25 +172,27 @@ function BlockDetail() {
{block.tx && block.tx.length > 0 ? (
<div className="space-y-2">
{block.tx.map((txid, index) => (
<Link
<div
key={txid}
to={`/tx/${txid}`}
className="block p-3 bg-gray-800 hover:bg-gray-700 rounded transition-colors"
className="flex items-center gap-2 p-3 rounded transition-colors"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="text-gray-400 text-sm shrink-0">#{index}</div>
<Link
to={`/tx/${txid}`}
className="flex items-center gap-3 flex-1 min-w-0 hover:text-maza-blue transition-colors"
>
<div className="text-sm shrink-0" style={{ color: 'var(--text-secondary)' }}>#{index}</div>
<div className="hash flex-1 truncate">{txid}</div>
</div>
</Link>
<CopyButton text={txid} />
{index === 0 && (
<div className="text-xs text-yellow-400 shrink-0">Coinbase</div>
)}
</div>
</Link>
))}
</div>
) : (
<div className="text-gray-400 text-center py-8">
<div className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>
No transactions in this block
</div>
)}
+30 -20
View File
@@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
import { Clock, Layers } from 'lucide-react';
import SearchBar from '../components/SearchBar';
import PriceChart from '../components/PriceChart';
import CopyButton from '../components/CopyButton';
import { SkeletonCard, SkeletonBlock } from '../components/Skeleton';
import axios from 'axios';
import { formatDistance } from 'date-fns';
import io from 'socket.io-client';
@@ -37,7 +39,6 @@ function Home() {
axios.get(`${API_URL}/api/price`, { timeout: 30000 })
]);
// Ensure we got valid data
if (Array.isArray(blocksRes.data)) {
setLatestBlocks(blocksRes.data);
}
@@ -49,7 +50,6 @@ function Home() {
}
} catch (error) {
console.error('Error fetching data:', error);
// Set empty/fallback data on error
setLatestBlocks([]);
} finally {
setLoading(false);
@@ -61,18 +61,22 @@ function Home() {
{/* Hero Section */}
<div className="text-center py-12">
<h1 className="text-5xl font-bold mb-4">Mazacoin Blockchain Explorer</h1>
<p className="text-xl text-gray-400 mb-8">
<p className="text-xl mb-8" style={{ color: 'var(--text-secondary)' }}>
Explore blocks, transactions, and addresses on the Mazacoin network
</p>
<SearchBar />
</div>
{/* Network Stats */}
{stats && stats.blockHeight !== undefined && (
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
) : stats && stats.blockHeight !== undefined && (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{price && price.btc !== undefined && (
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">MAZA Price</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>MAZA Price</div>
<div className="text-lg font-bold text-yellow-400">
{price.btc?.toFixed(8) || '0.00000000'} BTC
</div>
@@ -82,31 +86,31 @@ function Home() {
<div className="text-sm font-bold text-blue-400">
{price.eth?.toFixed(10) || '0.0000000000'} ETH
</div>
<div className="text-xs text-gray-500 mt-1">
<div className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>
<a href="https://freiexchange.com/market/MAZA/BTC" target="_blank" rel="noreferrer" className="text-maza-blue hover:underline">FreiExchange</a>
</div>
</div>
)}
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Block Height</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Block Height</div>
<div className="text-2xl font-bold text-maza-blue">
{stats.blockHeight?.toLocaleString() || '0'}
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Difficulty</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Difficulty</div>
<div className="text-2xl font-bold">
{stats.difficulty?.toFixed(2) || '0.00'}
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Network Hashrate</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Network Hashrate</div>
<div className="text-2xl font-bold">
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Connections</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Connections</div>
<div className="text-2xl font-bold text-green-400">
{stats.connections || 0}
</div>
@@ -125,27 +129,33 @@ function Home() {
</h2>
{loading ? (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => <SkeletonBlock key={i} />)}
</div>
) : (
<div className="space-y-2">
{latestBlocks.map((block) => (
<Link
<div
key={block.height}
to={`/block/${block.height}`}
className="block p-4 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors"
className="p-4 rounded-lg transition-colors"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="bg-maza-blue/20 text-maza-blue px-3 py-1 rounded-lg font-bold">
<Link
to={`/block/${block.height}`}
className="bg-maza-blue/20 text-maza-blue px-3 py-1 rounded-lg font-bold hover:bg-maza-blue/30 transition-colors"
>
{block.height}
</div>
<div className="hash text-gray-400">
</Link>
<div className="flex items-center gap-1">
<span className="hash" style={{ color: 'var(--text-secondary)' }}>
{block.hash.substring(0, 16)}...
</span>
<CopyButton text={block.hash} />
</div>
</div>
<div className="flex items-center gap-6 text-sm text-gray-400">
<div className="flex items-center gap-6 text-sm" style={{ color: 'var(--text-secondary)' }}>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{formatDistance(new Date(block.time * 1000), new Date(), { addSuffix: true })}
@@ -160,7 +170,7 @@ function Home() {
)}
</div>
</div>
</Link>
</div>
))}
</div>
)}
+16 -12
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Activity, Server, Zap, Clock } from 'lucide-react';
import axios from 'axios';
import { SkeletonCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -10,7 +11,7 @@ function NetworkStats() {
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 30000); // Update every 30s
const interval = setInterval(fetchStats, 30000);
return () => clearInterval(interval);
}, []);
@@ -27,8 +28,11 @@ function NetworkStats() {
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-6">
<div className="animate-pulse bg-gray-700 rounded h-10 w-64" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
</div>
);
}
@@ -44,7 +48,7 @@ function NetworkStats() {
<Activity className="w-8 h-8 text-maza-blue" />
Network Statistics
</h1>
<div className="text-sm text-gray-400">
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
</div>
</div>
@@ -57,7 +61,7 @@ function NetworkStats() {
<Server className="w-6 h-6 text-maza-blue" />
</div>
<div>
<div className="text-gray-400 text-sm">Block Height</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Block Height</div>
<div className="text-3xl font-bold">{stats.blockHeight?.toLocaleString() || '0'}</div>
</div>
</div>
@@ -69,7 +73,7 @@ function NetworkStats() {
<Zap className="w-6 h-6 text-purple-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Network Hashrate</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Network Hashrate</div>
<div className="text-3xl font-bold">
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
</div>
@@ -83,7 +87,7 @@ function NetworkStats() {
<Activity className="w-6 h-6 text-green-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Active Connections</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Active Connections</div>
<div className="text-3xl font-bold text-green-400">{stats.connections}</div>
</div>
</div>
@@ -95,19 +99,19 @@ function NetworkStats() {
<Clock className="w-6 h-6 text-yellow-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Difficulty</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Difficulty</div>
<div className="text-2xl font-bold">{stats.difficulty?.toFixed(2) || '0.00'}</div>
</div>
</div>
</div>
<div className="card">
<div className="text-gray-400 text-sm mb-1">Protocol Version</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Protocol Version</div>
<div className="text-2xl font-bold">{stats.protocolVersion}</div>
</div>
<div className="card">
<div className="text-gray-400 text-sm mb-1">Client Version</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Client Version</div>
<div className="text-2xl font-bold">{stats.version}</div>
</div>
</div>
@@ -115,12 +119,12 @@ function NetworkStats() {
{/* Additional Info */}
<div className="card">
<h2 className="text-xl font-bold mb-4">About Mazacoin</h2>
<div className="text-gray-300 space-y-2">
<div className="space-y-2">
<p>
Mazacoin (MAZA) is a cryptocurrency designed for the Oglala Lakota Nation.
It is based on the Bitcoin protocol and uses Proof-of-Work consensus.
</p>
<p className="text-sm text-gray-400">
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
This explorer provides real-time blockchain data directly from Mazacoin nodes.
</p>
</div>
+36 -24
View File
@@ -2,18 +2,20 @@ import React, { useState, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps';
import { MapPin, Globe } from 'lucide-react';
import axios from 'axios';
import { useTheme } from '../contexts/ThemeContext';
import { SkeletonCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
// World map TopoJSON URL
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
function NodeMap() {
const [nodes, setNodes] = useState([]);
const [nodes, setNodes] = useState([]); // eslint-disable-line no-unused-vars
const [geoNodes, setGeoNodes] = useState([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({ total: 0, active: 0, countries: {} });
const [price, setPrice] = useState(null);
const { theme } = useTheme();
useEffect(() => {
fetchNodes();
@@ -21,7 +23,7 @@ function NodeMap() {
const interval = setInterval(() => {
fetchNodes();
fetchPrice();
}, 60000); // Update every minute
}, 60000);
return () => clearInterval(interval);
}, []);
@@ -36,7 +38,6 @@ function NodeMap() {
const fetchNodes = async () => {
try {
// Fetch nodes with geolocation from backend API
const response = await axios.get(`${API_URL}/api/nodes`);
const data = response.data;
@@ -54,10 +55,21 @@ function NodeMap() {
}
};
const mapFill = theme === 'light' ? '#e2e8f0' : '#1f2937';
const mapStroke = theme === 'light' ? '#cbd5e1' : '#374151';
const mapHover = theme === 'light' ? '#cbd5e1' : '#2d3748';
const mapBg = theme === 'light' ? '#f1f5f9' : undefined;
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-6">
<div className="animate-pulse bg-gray-700 rounded h-10 w-48" />
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
<div className="card">
<div className="animate-pulse bg-gray-700 rounded h-[600px] w-full" />
</div>
</div>
);
}
@@ -69,7 +81,7 @@ function NodeMap() {
<MapPin className="w-8 h-8 text-maza-blue" />
Live Node Map
</h1>
<div className="text-sm text-gray-400">
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Showing all nodes discovered in the last 24 hours
</div>
</div>
@@ -78,7 +90,7 @@ function NodeMap() {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{price && price.btc !== undefined && (
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">MAZA Price</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>MAZA Price</div>
<div className="text-lg font-bold text-yellow-400">
{price.btc?.toFixed(8) || '0.00000000'} BTC
</div>
@@ -92,20 +104,20 @@ function NodeMap() {
)}
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Nodes (24h)</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Nodes (24h)</div>
<div className="text-3xl font-bold text-yellow-400">{stats.total}</div>
<div className="text-sm text-gray-400 mt-1">
<div className="text-sm mt-1" style={{ color: 'var(--text-secondary)' }}>
<span className="text-yellow-400 font-semibold">{stats.active} active</span>
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Mapped Nodes</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Mapped Nodes</div>
<div className="text-3xl font-bold text-maza-blue">{geoNodes.length}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">IP Protocol</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>IP Protocol</div>
<div className="text-sm">
<div className="text-lg font-bold text-blue-400">
IPv4: {geoNodes.filter(n => n.ipVersion === 4).length}
@@ -117,13 +129,13 @@ function NodeMap() {
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Countries</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Countries</div>
<div className="text-3xl font-bold">{Object.keys(stats.countries).length}</div>
</div>
</div>
{/* Legend */}
<div className="card bg-gray-800/50 border border-gray-700">
<div className="card" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-center gap-8 py-2">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-yellow-400 shadow-lg" style={{ filter: 'drop-shadow(0 0 8px rgba(251, 191, 36, 0.8))' }}></div>
@@ -131,14 +143,14 @@ function NodeMap() {
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-gray-500 opacity-60"></div>
<span className="text-sm font-medium text-gray-400">Seen in Last 24h ({geoNodes.filter(n => !n.isActive).length})</span>
<span className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Seen in Last 24h ({geoNodes.filter(n => !n.isActive).length})</span>
</div>
</div>
</div>
{/* Map */}
<div className="card">
<div className="h-[600px] rounded-lg overflow-hidden bg-gray-900">
<div className="h-[600px] rounded-lg overflow-hidden" style={{ backgroundColor: mapBg || 'rgb(17, 24, 39)' }}>
<ComposableMap
projection="geoMercator"
projectionConfig={{
@@ -153,12 +165,12 @@ function NodeMap() {
<Geography
key={geo.rsmKey}
geography={geo}
fill="#1f2937"
stroke="#374151"
fill={mapFill}
stroke={mapStroke}
strokeWidth={0.5}
style={{
default: { outline: "none" },
hover: { fill: "#2d3748", outline: "none" },
hover: { fill: mapHover, outline: "none" },
pressed: { outline: "none" },
}}
/>
@@ -168,7 +180,7 @@ function NodeMap() {
{geoNodes.map((node, index) => {
const isActive = node.isActive;
const markerColor = isActive ? '#fbbf24' : '#6b7280'; // yellow for active, gray for inactive
const markerColor = isActive ? '#fbbf24' : '#6b7280';
const glowColor = isActive ? 'rgba(251, 191, 36, 0.8)' : 'rgba(107, 114, 128, 0.4)';
return (
@@ -193,7 +205,7 @@ function NodeMap() {
/>
</g>
<title>
{isActive ? '🟡 ACTIVE' : 'Seen in last 24h'}
{isActive ? 'ACTIVE' : 'Seen in last 24h'}
{'\n'}{node.city}, {node.country}
{'\n'}IP: {node.ip || node.addr?.split(':')[0]} (IPv{node.ipVersion || '4'})
{node.subver ? `\nClient: ${node.subver}` : ''}
@@ -208,7 +220,7 @@ function NodeMap() {
</div>
{geoNodes.length === 0 && (
<div className="text-center text-gray-400 py-8">
<div className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>
<p>No nodes could be geolocated yet.</p>
<p className="text-sm mt-2">Geolocation is in progress...</p>
</div>
@@ -227,7 +239,7 @@ function NodeMap() {
{Object.entries(stats.countries)
.sort(([, a], [, b]) => b - a)
.map(([country, count]) => (
<div key={country} className="bg-gray-800 p-3 rounded">
<div key={country} className="p-3 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-between">
<div className="font-medium">{country}</div>
<div className="text-maza-blue font-bold">{count}</div>
@@ -236,7 +248,7 @@ function NodeMap() {
))}
</div>
) : (
<div className="text-gray-400 text-center py-8">
<div className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>
No country data available yet.
</div>
)}
+49 -24
View File
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Trophy, TrendingUp, AlertCircle } from 'lucide-react';
import axios from 'axios';
import CopyButton from '../components/CopyButton';
import { SkeletonCard, SkeletonTableRow } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -11,7 +14,7 @@ function RichList() {
useEffect(() => {
fetchRichList();
const interval = setInterval(fetchRichList, 60000); // Update every minute
const interval = setInterval(fetchRichList, 60000);
return () => clearInterval(interval);
}, []);
@@ -45,10 +48,10 @@ function RichList() {
};
const getRankColor = (rank) => {
if (rank === 1) return 'text-yellow-400'; // Gold
if (rank === 2) return 'text-gray-300'; // Silver
if (rank === 3) return 'text-yellow-600'; // Bronze
return 'text-gray-400';
if (rank === 1) return 'text-yellow-400';
if (rank === 2) return 'text-gray-300';
if (rank === 3) return 'text-yellow-600';
return '';
};
const getRankIcon = (rank) => {
@@ -60,8 +63,26 @@ function RichList() {
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-6">
<div className="animate-pulse bg-gray-700 rounded h-10 w-48" />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
<div className="card overflow-hidden">
<table className="min-w-full">
<thead className="border-b" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border-color)' }}>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>Rank</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>Address</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>Balance (MAZA)</th>
<th className="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>Last Seen Block</th>
</tr>
</thead>
<tbody>
{Array.from({ length: 10 }).map((_, i) => <SkeletonTableRow key={i} cols={4} />)}
</tbody>
</table>
</div>
</div>
);
}
@@ -101,22 +122,22 @@ function RichList() {
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Total Addresses</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Total Addresses</div>
<div className="text-3xl font-bold text-yellow-400">{data?.totalAddresses || 0}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Last Scanned Block</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Last Scanned Block</div>
<div className="text-2xl font-bold text-maza-blue">{formatBlockNumber(data?.lastScannedBlock)}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Top 100 Total Balance</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Top 100 Total Balance</div>
<div className="text-2xl font-bold text-green-400">{formatBalance(getTotalBalance())} MAZA</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Status</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Status</div>
<div className={`text-xl font-bold ${data?.isScanning ? 'text-yellow-400' : 'text-green-400'}`}>
{data?.isScanning ? 'Scanning...' : 'Up to date'}
</div>
@@ -143,27 +164,28 @@ function RichList() {
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-800 border-b border-gray-700">
<thead className="border-b" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border-color)' }}>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>
Rank
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>
Address
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>
Balance (MAZA)
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
<th className="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-secondary)' }}>
Last Seen Block
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody style={{ borderColor: 'var(--border-color)' }}>
{data?.addresses?.map((addr) => (
<tr
key={addr.address}
className="hover:bg-gray-800/50 transition-colors"
className="transition-colors hover:opacity-80"
style={{ borderBottom: '1px solid var(--border-color)' }}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
@@ -174,19 +196,22 @@ function RichList() {
</div>
</td>
<td className="px-6 py-4">
<a
href={`/address/${addr.address}`}
<div className="flex items-center gap-1">
<Link
to={`/address/${addr.address}`}
className="text-maza-blue hover:text-blue-300 font-mono text-sm break-all"
>
{addr.address}
</a>
</Link>
<CopyButton text={addr.address} />
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<span className="text-green-400 font-bold">
{formatBalance(addr.balance)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center text-gray-400 text-sm">
<td className="px-6 py-4 whitespace-nowrap text-center text-sm" style={{ color: 'var(--text-secondary)' }}>
{formatBlockNumber(addr.lastSeen)}
</td>
</tr>
@@ -198,8 +223,8 @@ function RichList() {
{!data?.addresses || data.addresses.length === 0 ? (
<div className="card">
<div className="text-center text-gray-400 py-12">
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-gray-500" />
<div className="text-center py-12" style={{ color: 'var(--text-secondary)' }}>
<AlertCircle className="w-12 h-12 mx-auto mb-4" style={{ color: 'var(--text-muted)' }} />
<p className="text-lg mb-2">No data available yet</p>
<p className="text-sm">
The blockchain scanner is starting up. Please check back in a few minutes.
+47 -26
View File
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowRight, Clock } from 'lucide-react';
import { Clock } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
import CopyButton from '../components/CopyButton';
import { SkeletonDetailCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -14,6 +16,7 @@ function TransactionDetail() {
useEffect(() => {
fetchTransaction();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [txid]);
const fetchTransaction = async () => {
@@ -32,8 +35,13 @@ function TransactionDetail() {
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
<div className="space-y-6">
<div className="animate-pulse bg-gray-700 rounded h-10 w-64" />
<SkeletonDetailCard />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<SkeletonDetailCard />
<SkeletonDetailCard />
</div>
</div>
);
}
@@ -44,7 +52,6 @@ function TransactionDetail() {
if (!tx) return null;
// Use backend-calculated totals if available, otherwise calculate
const totalInput = tx.totalInput !== undefined ? tx.totalInput :
tx.vin?.reduce((sum, input) => sum + (input.value || 0), 0) || 0;
const totalOutput = tx.totalOutput !== undefined ? tx.totalOutput :
@@ -61,33 +68,39 @@ function TransactionDetail() {
<div className="grid grid-cols-1 gap-6">
<div>
<div className="text-gray-400 text-sm mb-1">Transaction ID</div>
<div className="hash text-lg bg-gray-800 p-3 rounded">{tx.txid}</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Transaction ID</div>
<div className="flex items-center gap-2 p-3 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="hash text-lg flex-1">{tx.txid}</div>
<CopyButton text={tx.txid} />
</div>
</div>
{tx.blockhash && (
<div>
<div className="text-gray-400 text-sm mb-1">Block Hash</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Block Hash</div>
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<Link
to={`/block/${tx.blockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
className="hash text-lg flex-1 text-maza-blue hover:underline"
>
{tx.blockhash}
</Link>
<CopyButton text={tx.blockhash} />
</div>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{tx.confirmations !== undefined && (
<div>
<div className="text-gray-400 text-sm mb-1">Confirmations</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Confirmations</div>
<div className="text-lg text-green-400">{tx.confirmations}</div>
</div>
)}
{tx.time && (
<div>
<div className="text-gray-400 text-sm mb-1">Time</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Time</div>
<div className="text-sm flex items-center gap-1">
<Clock className="w-4 h-4" />
{format(new Date(tx.time * 1000), 'PPpp')}
@@ -97,14 +110,14 @@ function TransactionDetail() {
{tx.size && (
<div>
<div className="text-gray-400 text-sm mb-1">Size</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Size</div>
<div className="text-lg">{tx.size} bytes</div>
</div>
)}
{fee > 0 && (
<div>
<div className="text-gray-400 text-sm mb-1">Fee</div>
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Fee</div>
<div className="text-lg">{fee.toFixed(8)} MAZA</div>
</div>
)}
@@ -124,35 +137,41 @@ function TransactionDetail() {
<>
<div className="space-y-3 mb-4">
{tx.vin.map((input, index) => (
<div key={index} className="bg-gray-800 p-3 rounded">
<div key={index} className="p-3 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{input.coinbase || input.isCoinbase ? (
<div className="text-sm text-gray-400">Coinbase (Newly Generated Coins)</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Coinbase (Newly Generated Coins)</div>
) : (
<>
{input.txid && (
<div className="flex items-center gap-1 mb-1">
<Link
to={`/tx/${input.txid}`}
className="hash text-xs text-maza-blue hover:underline block mb-1"
className="hash text-xs text-maza-blue hover:underline"
>
{input.txid}:{input.vout}
</Link>
<CopyButton text={input.txid} />
</div>
)}
{input.value !== undefined && input.value !== null ? (
<>
<div className="text-lg font-bold">{input.value.toFixed(8)} MAZA</div>
{input.address && input.address !== 'Unknown' && (
<div className="flex items-center gap-1 mt-1">
<Link
to={`/address/${input.address}`}
className="hash text-xs text-maza-blue hover:underline block mt-1"
className="hash text-xs text-maza-blue hover:underline"
>
{input.address}
</Link>
<CopyButton text={input.address} />
</div>
)}
</>
) : input.error ? (
<div className="text-xs text-yellow-400">{input.error}</div>
) : (
<div className="text-xs text-gray-500">Amount unknown</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Amount unknown</div>
)}
</>
)}
@@ -160,16 +179,16 @@ function TransactionDetail() {
))}
</div>
{totalInput > 0 && (
<div className="pt-3 border-t border-gray-700">
<div className="pt-3" style={{ borderTop: '1px solid var(--border-color)' }}>
<div className="flex justify-between items-center">
<span className="text-gray-400">Total Input:</span>
<span style={{ color: 'var(--text-secondary)' }}>Total Input:</span>
<span className="text-xl font-bold text-green-400">{totalInput.toFixed(8)} MAZA</span>
</div>
</div>
)}
</>
) : (
<div className="text-gray-400 text-center py-4">No inputs</div>
<div className="text-center py-4" style={{ color: 'var(--text-secondary)' }}>No inputs</div>
)}
</div>
@@ -183,31 +202,33 @@ function TransactionDetail() {
<>
<div className="space-y-3 mb-4">
{tx.vout.map((output, index) => (
<div key={index} className="bg-gray-800 p-3 rounded">
<div key={index} className="p-3 rounded" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="text-lg font-bold mb-1">{output.value.toFixed(8)} MAZA</div>
{output.scriptPubKey?.addresses?.map((address, i) => (
<div key={i} className="flex items-center gap-1">
<Link
key={i}
to={`/address/${address}`}
className="hash text-xs text-maza-blue hover:underline block"
className="hash text-xs text-maza-blue hover:underline"
>
{address}
</Link>
<CopyButton text={address} />
</div>
))}
</div>
))}
</div>
{totalOutput > 0 && (
<div className="pt-3 border-t border-gray-700">
<div className="pt-3" style={{ borderTop: '1px solid var(--border-color)' }}>
<div className="flex justify-between items-center">
<span className="text-gray-400">Total Output:</span>
<span style={{ color: 'var(--text-secondary)' }}>Total Output:</span>
<span className="text-xl font-bold text-blue-400">{totalOutput.toFixed(8)} MAZA</span>
</div>
</div>
)}
</>
) : (
<div className="text-gray-400 text-center py-4">No outputs</div>
<div className="text-center py-4" style={{ color: 'var(--text-secondary)' }}>No outputs</div>
)}
</div>
</div>