Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e02c03e83d |
+70
-4
@@ -2,6 +2,31 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
@@ -9,6 +34,8 @@ body {
|
|||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@@ -17,7 +44,9 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.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 {
|
.btn {
|
||||||
@@ -29,15 +58,22 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.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 {
|
.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 {
|
.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 {
|
.hash {
|
||||||
@@ -51,3 +87,33 @@ code {
|
|||||||
.error {
|
.error {
|
||||||
@apply bg-red-900/30 border border-red-700 text-red-300 px-4 py-3 rounded-lg;
|
@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;
|
||||||
|
}
|
||||||
|
|||||||
+23
-17
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
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 Header from './components/Header';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
@@ -13,23 +15,27 @@ import './App.css';
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<ThemeProvider>
|
||||||
<div className="min-h-screen bg-maza-dark text-gray-100 flex flex-col">
|
<ToastProvider>
|
||||||
<Header />
|
<Router>
|
||||||
<main className="flex-grow container mx-auto px-4 py-8">
|
<div className="min-h-screen flex flex-col" style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-primary)' }}>
|
||||||
<Routes>
|
<Header />
|
||||||
<Route path="/" element={<Home />} />
|
<main className="flex-grow container mx-auto px-4 py-8">
|
||||||
<Route path="/block/:hashOrHeight" element={<BlockDetail />} />
|
<Routes>
|
||||||
<Route path="/tx/:txid" element={<TransactionDetail />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/address/:address" element={<AddressDetail />} />
|
<Route path="/block/:hashOrHeight" element={<BlockDetail />} />
|
||||||
<Route path="/stats" element={<NetworkStats />} />
|
<Route path="/tx/:txid" element={<TransactionDetail />} />
|
||||||
<Route path="/nodes" element={<NodeMap />} />
|
<Route path="/address/:address" element={<AddressDetail />} />
|
||||||
<Route path="/richlist" element={<RichList />} />
|
<Route path="/stats" element={<NetworkStats />} />
|
||||||
</Routes>
|
<Route path="/nodes" element={<NodeMap />} />
|
||||||
</main>
|
<Route path="/richlist" element={<RichList />} />
|
||||||
<Footer />
|
</Routes>
|
||||||
</div>
|
</main>
|
||||||
</Router>
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
</ToastProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -2,9 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
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="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 © 2026 | Built for the Mazacoin community</p>
|
<p>Mazacoin Explorer © 2026 | Built for the Mazacoin community</p>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
<a href="https://mazacoin.org" target="_blank" rel="noopener noreferrer" className="hover:text-maza-blue">
|
<a href="https://mazacoin.org" target="_blank" rel="noopener noreferrer" className="hover:text-maza-blue">
|
||||||
@@ -12,11 +12,11 @@ function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<span>Provided by</span>
|
||||||
<img
|
<img
|
||||||
src="/samiahmed77777-logo.png"
|
src="/samiahmed77777-logo.png"
|
||||||
alt="samiahmed77777"
|
alt="samiahmed77777"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,93 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Activity, MapPin, TrendingUp } from 'lucide-react';
|
import { Activity, MapPin, TrendingUp, Menu, X, Sun, Moon } from 'lucide-react';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
function Header() {
|
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 (
|
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="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<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" />
|
<img src="/maza-logo.png" alt="Mazacoin" className="w-8 h-8" />
|
||||||
<span className="text-2xl font-bold">Mazacoin Explorer</span>
|
<span className="text-2xl font-bold">Mazacoin Explorer</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="flex items-center space-x-6">
|
{/* Desktop nav */}
|
||||||
<Link to="/" className="hover:text-maza-blue transition-colors">
|
<div className="hidden md:flex items-center gap-6">
|
||||||
Home
|
<nav className="flex items-center space-x-6">
|
||||||
</Link>
|
{navLinks.map(({ to, label, icon: Icon }) => (
|
||||||
<Link to="/stats" className="hover:text-maza-blue transition-colors flex items-center gap-2">
|
<Link
|
||||||
<Activity className="w-4 h-4" />
|
key={to}
|
||||||
Network Stats
|
to={to}
|
||||||
</Link>
|
className={`hover:text-maza-blue transition-colors flex items-center gap-2 ${isActive(to) ? 'text-maza-blue' : ''}`}
|
||||||
<Link to="/nodes" className="hover:text-maza-blue transition-colors flex items-center gap-2">
|
>
|
||||||
<MapPin className="w-4 h-4" />
|
{Icon && <Icon className="w-4 h-4" />}
|
||||||
Node Map
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/richlist" className="hover:text-maza-blue transition-colors flex items-center gap-2">
|
))}
|
||||||
<TrendingUp className="w-4 h-4" />
|
</nav>
|
||||||
Rich List
|
<button
|
||||||
</Link>
|
onClick={toggleTheme}
|
||||||
</nav>
|
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>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { Chart as ChartJS, CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale } from 'chart.js';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { SkeletonChart } from './Skeleton';
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [activeTimeframe, setActiveTimeframe] = useState(timeframe);
|
const [activeTimeframe, setActiveTimeframe] = useState(timeframe);
|
||||||
const [activeCurrency, setActiveCurrency] = useState(currency);
|
const [activeCurrency, setActiveCurrency] = useState(currency);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchChartData();
|
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 = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -63,7 +69,7 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
labels: {
|
labels: {
|
||||||
color: '#9ca3af'
|
color: tickColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
@@ -99,19 +105,19 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: '#1f2937'
|
color: gridColor
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#9ca3af'
|
color: tickColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
type: 'logarithmic',
|
type: 'logarithmic',
|
||||||
grid: {
|
grid: {
|
||||||
color: '#1f2937'
|
color: gridColor
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#9ca3af',
|
color: tickColor,
|
||||||
callback: function(value) {
|
callback: function(value) {
|
||||||
return value.toFixed(activeCurrency === 'eth' ? 10 : 8);
|
return value.toFixed(activeCurrency === 'eth' ? 10 : 8);
|
||||||
}
|
}
|
||||||
@@ -140,28 +146,22 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <SkeletonChart />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chartData) {
|
if (!chartData) {
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<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>
|
<h2 className="text-2xl font-bold">Price Chart</h2>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4 flex-wrap">
|
||||||
{/* Currency selector */}
|
{/* Currency selector */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{currencies.map(curr => (
|
{currencies.map(curr => (
|
||||||
@@ -170,15 +170,16 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
onClick={() => setActiveCurrency(curr.value)}
|
onClick={() => setActiveCurrency(curr.value)}
|
||||||
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
|
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
|
||||||
activeCurrency === curr.value
|
activeCurrency === curr.value
|
||||||
? `${curr.color} bg-gray-700`
|
? `${curr.color} bg-theme-secondary`
|
||||||
: 'text-gray-400 hover:text-gray-300'
|
: 'hover:opacity-80'
|
||||||
}`}
|
}`}
|
||||||
|
style={activeCurrency !== curr.value ? { color: 'var(--text-secondary)' } : undefined}
|
||||||
>
|
>
|
||||||
{curr.label}
|
{curr.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeframe selector */}
|
{/* Timeframe selector */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{timeframes.map(tf => (
|
{timeframes.map(tf => (
|
||||||
@@ -188,8 +189,9 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
|
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
|
||||||
activeTimeframe === tf.value
|
activeTimeframe === tf.value
|
||||||
? 'bg-maza-blue text-white'
|
? '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}
|
{tf.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -197,12 +199,12 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-96">
|
<div className="h-96">
|
||||||
<Line data={chartData} options={options} />
|
<Line data={chartData} options={options} />
|
||||||
</div>
|
</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.
|
Historical data from FreiExchange + CoinGecko. Data collection started March 2026.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ function SearchBar() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -59,7 +60,7 @@ function SearchBar() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error mt-4">
|
<div className="error mt-4">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { Copy, TrendingUp } from 'lucide-react';
|
import { TrendingUp } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import { SkeletonLine } from '../components/Skeleton';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || '';
|
const API_URL = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@@ -27,10 +29,6 @@ function AddressDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBalance = (balance) => {
|
const formatBalance = (balance) => {
|
||||||
return balance.toLocaleString('en-US', {
|
return balance.toLocaleString('en-US', {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
@@ -46,48 +44,47 @@ function AddressDetail() {
|
|||||||
<div className="flex flex-col md:flex-row gap-8">
|
<div className="flex flex-col md:flex-row gap-8">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-xl font-bold mb-4">Address</h2>
|
<h2 className="text-xl font-bold mb-4">Address</h2>
|
||||||
|
|
||||||
<div className="bg-gray-800 p-4 rounded mb-4">
|
<div className="p-4 rounded mb-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
<div className="hash text-lg break-all mb-2">{address}</div>
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<div className="hash text-lg break-all flex-1">{address}</div>
|
||||||
onClick={copyToClipboard}
|
<CopyButton text={address} />
|
||||||
className="btn btn-secondary text-sm flex items-center gap-2"
|
</div>
|
||||||
>
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
Copy Address
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Balance Info */}
|
{/* 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">
|
<h3 className="text-lg font-bold mb-3 flex items-center gap-2">
|
||||||
<TrendingUp className="w-5 h-5 text-yellow-400" />
|
<TrendingUp className="w-5 h-5 text-yellow-400" />
|
||||||
Balance
|
Balance
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{loadingBalance ? (
|
{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 ? (
|
) : balanceData?.balance !== null && balanceData?.balance !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-3xl font-bold text-green-400 mb-2">
|
<div className="text-3xl font-bold text-green-400 mb-2">
|
||||||
{formatBalance(balanceData.balance)} MAZA
|
{formatBalance(balanceData.balance)} MAZA
|
||||||
</div>
|
</div>
|
||||||
{balanceData.rank && (
|
{balanceData.rank && (
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Rank #{balanceData.rank} in rich list
|
Rank #{balanceData.rank} in rich list
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{balanceData.lastSeen && (
|
{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()}
|
Last seen in block: {balanceData.lastSeen.toLocaleString()}
|
||||||
</div>
|
</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?.message || 'Balance data not available yet'}
|
||||||
{balanceData?.lastScannedBlock && (
|
{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()}
|
Scanner progress: block {balanceData.lastScannedBlock.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -103,7 +100,7 @@ function AddressDetail() {
|
|||||||
<li>Balance is calculated by our blockchain scanner tracking all transaction outputs</li>
|
<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>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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +116,7 @@ function AddressDetail() {
|
|||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 className="text-xl font-bold mb-4">Transaction History</h2>
|
<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="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>
|
<p className="text-sm">This feature will be available once the node is configured with txindex and fully synced.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
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 axios from 'axios';
|
||||||
import { format } from 'date-fns';
|
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 || '';
|
const API_URL = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@@ -14,12 +16,13 @@ function BlockDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBlock();
|
fetchBlock();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hashOrHeight]);
|
}, [hashOrHeight]);
|
||||||
|
|
||||||
const fetchBlock = async () => {
|
const fetchBlock = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/api/block/${hashOrHeight}`);
|
const response = await axios.get(`${API_URL}/api/block/${hashOrHeight}`);
|
||||||
setBlock(response.data);
|
setBlock(response.data);
|
||||||
@@ -32,18 +35,21 @@ function BlockDetail() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="space-y-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return <div className="error">{error}</div>;
|
||||||
<div className="error">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!block) return null;
|
if (!block) return null;
|
||||||
@@ -59,12 +65,12 @@ function BlockDetail() {
|
|||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
Previous Block
|
Previous Block
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||||
<Layers className="w-8 h-8 text-maza-blue" />
|
<Layers className="w-8 h-8 text-maza-blue" />
|
||||||
Block #{block.height}
|
Block #{block.height}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/block/${block.height + 1}`}
|
to={`/block/${block.height + 1}`}
|
||||||
className="btn btn-secondary flex items-center gap-2"
|
className="btn btn-secondary flex items-center gap-2"
|
||||||
@@ -77,67 +83,76 @@ function BlockDetail() {
|
|||||||
{/* Block Details */}
|
{/* Block Details */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 className="text-xl font-bold mb-4">Block Information</h2>
|
<h2 className="text-xl font-bold mb-4">Block Information</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<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 className="text-lg font-bold">{block.height}</div>
|
||||||
</div>
|
</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">
|
<div className="text-lg flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
{format(new Date(block.time * 1000), 'PPpp')}
|
{format(new Date(block.time * 1000), 'PPpp')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<div className="text-gray-400 text-sm mb-1">Hash</div>
|
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Hash</div>
|
||||||
<div className="hash text-lg bg-gray-800 p-3 rounded">{block.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>
|
</div>
|
||||||
|
|
||||||
{block.previousblockhash && (
|
{block.previousblockhash && (
|
||||||
<div className="md:col-span-2">
|
<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>
|
||||||
<Link
|
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
to={`/block/${block.previousblockhash}`}
|
<Link
|
||||||
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
|
to={`/block/${block.previousblockhash}`}
|
||||||
>
|
className="hash text-lg flex-1 text-maza-blue hover:underline"
|
||||||
{block.previousblockhash}
|
>
|
||||||
</Link>
|
{block.previousblockhash}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={block.previousblockhash} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{block.nextblockhash && (
|
{block.nextblockhash && (
|
||||||
<div className="md:col-span-2">
|
<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>
|
||||||
<Link
|
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
to={`/block/${block.nextblockhash}`}
|
<Link
|
||||||
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
|
to={`/block/${block.nextblockhash}`}
|
||||||
>
|
className="hash text-lg flex-1 text-maza-blue hover:underline"
|
||||||
{block.nextblockhash}
|
>
|
||||||
</Link>
|
{block.nextblockhash}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={block.nextblockhash} />
|
||||||
|
</div>
|
||||||
</div>
|
</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 className="text-lg">{block.difficulty?.toFixed(4)}</div>
|
||||||
</div>
|
</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 className="text-lg">{(block.size / 1024).toFixed(2)} KB</div>
|
||||||
</div>
|
</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 className="text-lg text-green-400">{block.confirmations}</div>
|
||||||
</div>
|
</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 className="text-lg">{block.version}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,29 +168,31 @@ function BlockDetail() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{block.tx && block.tx.length > 0 ? (
|
{block.tx && block.tx.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{block.tx.map((txid, index) => (
|
{block.tx.map((txid, index) => (
|
||||||
<Link
|
<div
|
||||||
key={txid}
|
key={txid}
|
||||||
to={`/tx/${txid}`}
|
className="flex items-center gap-2 p-3 rounded transition-colors"
|
||||||
className="block p-3 bg-gray-800 hover:bg-gray-700 rounded transition-colors"
|
style={{ backgroundColor: 'var(--bg-secondary)' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<Link
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
to={`/tx/${txid}`}
|
||||||
<div className="text-gray-400 text-sm shrink-0">#{index}</div>
|
className="flex items-center gap-3 flex-1 min-w-0 hover:text-maza-blue transition-colors"
|
||||||
<div className="hash flex-1 truncate">{txid}</div>
|
>
|
||||||
</div>
|
<div className="text-sm shrink-0" style={{ color: 'var(--text-secondary)' }}>#{index}</div>
|
||||||
{index === 0 && (
|
<div className="hash flex-1 truncate">{txid}</div>
|
||||||
<div className="text-xs text-yellow-400 shrink-0">Coinbase</div>
|
</Link>
|
||||||
)}
|
<CopyButton text={txid} />
|
||||||
</div>
|
{index === 0 && (
|
||||||
</Link>
|
<div className="text-xs text-yellow-400 shrink-0">Coinbase</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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
|
No transactions in this block
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+36
-26
@@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Clock, Layers } from 'lucide-react';
|
import { Clock, Layers } from 'lucide-react';
|
||||||
import SearchBar from '../components/SearchBar';
|
import SearchBar from '../components/SearchBar';
|
||||||
import PriceChart from '../components/PriceChart';
|
import PriceChart from '../components/PriceChart';
|
||||||
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import { SkeletonCard, SkeletonBlock } from '../components/Skeleton';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { formatDistance } from 'date-fns';
|
import { formatDistance } from 'date-fns';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
@@ -17,15 +19,15 @@ function Home() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
||||||
// WebSocket for real-time updates
|
// WebSocket for real-time updates
|
||||||
const socket = io(API_URL);
|
const socket = io(API_URL);
|
||||||
socket.emit('subscribe:blocks');
|
socket.emit('subscribe:blocks');
|
||||||
|
|
||||||
socket.on('block:new', (block) => {
|
socket.on('block:new', (block) => {
|
||||||
setLatestBlocks(prev => [block, ...prev].slice(0, 10));
|
setLatestBlocks(prev => [block, ...prev].slice(0, 10));
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => socket.disconnect();
|
return () => socket.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -36,8 +38,7 @@ function Home() {
|
|||||||
axios.get(`${API_URL}/api/stats`, { timeout: 30000 }),
|
axios.get(`${API_URL}/api/stats`, { timeout: 30000 }),
|
||||||
axios.get(`${API_URL}/api/price`, { timeout: 30000 })
|
axios.get(`${API_URL}/api/price`, { timeout: 30000 })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Ensure we got valid data
|
|
||||||
if (Array.isArray(blocksRes.data)) {
|
if (Array.isArray(blocksRes.data)) {
|
||||||
setLatestBlocks(blocksRes.data);
|
setLatestBlocks(blocksRes.data);
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,6 @@ function Home() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching data:', error);
|
console.error('Error fetching data:', error);
|
||||||
// Set empty/fallback data on error
|
|
||||||
setLatestBlocks([]);
|
setLatestBlocks([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -61,18 +61,22 @@ function Home() {
|
|||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<h1 className="text-5xl font-bold mb-4">Mazacoin Blockchain Explorer</h1>
|
<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
|
Explore blocks, transactions, and addresses on the Mazacoin network
|
||||||
</p>
|
</p>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Network Stats */}
|
{/* 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">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
{price && price.btc !== undefined && (
|
{price && price.btc !== undefined && (
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-lg font-bold text-yellow-400">
|
||||||
{price.btc?.toFixed(8) || '0.00000000'} BTC
|
{price.btc?.toFixed(8) || '0.00000000'} BTC
|
||||||
</div>
|
</div>
|
||||||
@@ -82,31 +86,31 @@ function Home() {
|
|||||||
<div className="text-sm font-bold text-blue-400">
|
<div className="text-sm font-bold text-blue-400">
|
||||||
{price.eth?.toFixed(10) || '0.0000000000'} ETH
|
{price.eth?.toFixed(10) || '0.0000000000'} ETH
|
||||||
</div>
|
</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>
|
<a href="https://freiexchange.com/market/MAZA/BTC" target="_blank" rel="noreferrer" className="text-maza-blue hover:underline">FreiExchange</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-2xl font-bold text-maza-blue">
|
||||||
{stats.blockHeight?.toLocaleString() || '0'}
|
{stats.blockHeight?.toLocaleString() || '0'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-2xl font-bold">
|
||||||
{stats.difficulty?.toFixed(2) || '0.00'}
|
{stats.difficulty?.toFixed(2) || '0.00'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-2xl font-bold">
|
||||||
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
|
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-2xl font-bold text-green-400">
|
||||||
{stats.connections || 0}
|
{stats.connections || 0}
|
||||||
</div>
|
</div>
|
||||||
@@ -123,29 +127,35 @@ function Home() {
|
|||||||
<Layers className="w-6 h-6 text-maza-blue" />
|
<Layers className="w-6 h-6 text-maza-blue" />
|
||||||
Latest Blocks
|
Latest Blocks
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">
|
<div className="space-y-2">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
{Array.from({ length: 3 }).map((_, i) => <SkeletonBlock key={i} />)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{latestBlocks.map((block) => (
|
{latestBlocks.map((block) => (
|
||||||
<Link
|
<div
|
||||||
key={block.height}
|
key={block.height}
|
||||||
to={`/block/${block.height}`}
|
className="p-4 rounded-lg transition-colors"
|
||||||
className="block p-4 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors"
|
style={{ backgroundColor: 'var(--bg-secondary)' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<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}
|
{block.height}
|
||||||
</div>
|
</Link>
|
||||||
<div className="hash text-gray-400">
|
<div className="flex items-center gap-1">
|
||||||
{block.hash.substring(0, 16)}...
|
<span className="hash" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{block.hash.substring(0, 16)}...
|
||||||
|
</span>
|
||||||
|
<CopyButton text={block.hash} />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
{formatDistance(new Date(block.time * 1000), new Date(), { addSuffix: true })}
|
{formatDistance(new Date(block.time * 1000), new Date(), { addSuffix: true })}
|
||||||
@@ -160,7 +170,7 @@ function Home() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Activity, Server, Zap, Clock } from 'lucide-react';
|
import { Activity, Server, Zap, Clock } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { SkeletonCard } from '../components/Skeleton';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || '';
|
const API_URL = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ function NetworkStats() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
const interval = setInterval(fetchStats, 30000); // Update every 30s
|
const interval = setInterval(fetchStats, 30000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -27,8 +28,11 @@ function NetworkStats() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="space-y-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -44,7 +48,7 @@ function NetworkStats() {
|
|||||||
<Activity className="w-8 h-8 text-maza-blue" />
|
<Activity className="w-8 h-8 text-maza-blue" />
|
||||||
Network Statistics
|
Network Statistics
|
||||||
</h1>
|
</h1>
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
|
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +61,7 @@ function NetworkStats() {
|
|||||||
<Server className="w-6 h-6 text-maza-blue" />
|
<Server className="w-6 h-6 text-maza-blue" />
|
||||||
</div>
|
</div>
|
||||||
<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 className="text-3xl font-bold">{stats.blockHeight?.toLocaleString() || '0'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +73,7 @@ function NetworkStats() {
|
|||||||
<Zap className="w-6 h-6 text-purple-400" />
|
<Zap className="w-6 h-6 text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div className="text-3xl font-bold">
|
||||||
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
|
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +87,7 @@ function NetworkStats() {
|
|||||||
<Activity className="w-6 h-6 text-green-400" />
|
<Activity className="w-6 h-6 text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<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 className="text-3xl font-bold text-green-400">{stats.connections}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,19 +99,19 @@ function NetworkStats() {
|
|||||||
<Clock className="w-6 h-6 text-yellow-400" />
|
<Clock className="w-6 h-6 text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
<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 className="text-2xl font-bold">{stats.difficulty?.toFixed(2) || '0.00'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<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 className="text-2xl font-bold">{stats.protocolVersion}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<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 className="text-2xl font-bold">{stats.version}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,12 +119,12 @@ function NetworkStats() {
|
|||||||
{/* Additional Info */}
|
{/* Additional Info */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 className="text-xl font-bold mb-4">About Mazacoin</h2>
|
<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>
|
<p>
|
||||||
Mazacoin (MAZA) is a cryptocurrency designed for the Oglala Lakota Nation.
|
Mazacoin (MAZA) is a cryptocurrency designed for the Oglala Lakota Nation.
|
||||||
It is based on the Bitcoin protocol and uses Proof-of-Work consensus.
|
It is based on the Bitcoin protocol and uses Proof-of-Work consensus.
|
||||||
</p>
|
</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.
|
This explorer provides real-time blockchain data directly from Mazacoin nodes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps';
|
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps';
|
||||||
import { MapPin, Globe } from 'lucide-react';
|
import { MapPin, Globe } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { SkeletonCard } from '../components/Skeleton';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || '';
|
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";
|
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
|
||||||
|
|
||||||
function NodeMap() {
|
function NodeMap() {
|
||||||
const [nodes, setNodes] = useState([]);
|
const [nodes, setNodes] = useState([]); // eslint-disable-line no-unused-vars
|
||||||
const [geoNodes, setGeoNodes] = useState([]);
|
const [geoNodes, setGeoNodes] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [stats, setStats] = useState({ total: 0, active: 0, countries: {} });
|
const [stats, setStats] = useState({ total: 0, active: 0, countries: {} });
|
||||||
const [price, setPrice] = useState(null);
|
const [price, setPrice] = useState(null);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchNodes();
|
fetchNodes();
|
||||||
@@ -21,7 +23,7 @@ function NodeMap() {
|
|||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchNodes();
|
fetchNodes();
|
||||||
fetchPrice();
|
fetchPrice();
|
||||||
}, 60000); // Update every minute
|
}, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -36,10 +38,9 @@ function NodeMap() {
|
|||||||
|
|
||||||
const fetchNodes = async () => {
|
const fetchNodes = async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch nodes with geolocation from backend API
|
|
||||||
const response = await axios.get(`${API_URL}/api/nodes`);
|
const response = await axios.get(`${API_URL}/api/nodes`);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
setNodes(data.nodes || []);
|
setNodes(data.nodes || []);
|
||||||
setGeoNodes(data.nodes || []);
|
setGeoNodes(data.nodes || []);
|
||||||
setStats({
|
setStats({
|
||||||
@@ -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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="space-y-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,7 +81,7 @@ function NodeMap() {
|
|||||||
<MapPin className="w-8 h-8 text-maza-blue" />
|
<MapPin className="w-8 h-8 text-maza-blue" />
|
||||||
Live Node Map
|
Live Node Map
|
||||||
</h1>
|
</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
|
Showing all nodes discovered in the last 24 hours
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +90,7 @@ function NodeMap() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
{price && price.btc !== undefined && (
|
{price && price.btc !== undefined && (
|
||||||
<div className="stat-card">
|
<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">
|
<div className="text-lg font-bold text-yellow-400">
|
||||||
{price.btc?.toFixed(8) || '0.00000000'} BTC
|
{price.btc?.toFixed(8) || '0.00000000'} BTC
|
||||||
</div>
|
</div>
|
||||||
@@ -90,22 +102,22 @@ function NodeMap() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="stat-card">
|
<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-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>
|
<span className="text-yellow-400 font-semibold">{stats.active} active</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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 className="text-3xl font-bold text-maza-blue">{geoNodes.length}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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-sm">
|
||||||
<div className="text-lg font-bold text-blue-400">
|
<div className="text-lg font-bold text-blue-400">
|
||||||
IPv4: {geoNodes.filter(n => n.ipVersion === 4).length}
|
IPv4: {geoNodes.filter(n => n.ipVersion === 4).length}
|
||||||
@@ -115,15 +127,15 @@ function NodeMap() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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 className="text-3xl font-bold">{Object.keys(stats.countries).length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* 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 justify-center gap-8 py-2">
|
||||||
<div className="flex items-center gap-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>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 rounded-full bg-gray-500 opacity-60"></div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map */}
|
{/* Map */}
|
||||||
<div className="card">
|
<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
|
<ComposableMap
|
||||||
projection="geoMercator"
|
projection="geoMercator"
|
||||||
projectionConfig={{
|
projectionConfig={{
|
||||||
@@ -153,29 +165,29 @@ function NodeMap() {
|
|||||||
<Geography
|
<Geography
|
||||||
key={geo.rsmKey}
|
key={geo.rsmKey}
|
||||||
geography={geo}
|
geography={geo}
|
||||||
fill="#1f2937"
|
fill={mapFill}
|
||||||
stroke="#374151"
|
stroke={mapStroke}
|
||||||
strokeWidth={0.5}
|
strokeWidth={0.5}
|
||||||
style={{
|
style={{
|
||||||
default: { outline: "none" },
|
default: { outline: "none" },
|
||||||
hover: { fill: "#2d3748", outline: "none" },
|
hover: { fill: mapHover, outline: "none" },
|
||||||
pressed: { outline: "none" },
|
pressed: { outline: "none" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</Geographies>
|
</Geographies>
|
||||||
|
|
||||||
{geoNodes.map((node, index) => {
|
{geoNodes.map((node, index) => {
|
||||||
const isActive = node.isActive;
|
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)';
|
const glowColor = isActive ? 'rgba(251, 191, 36, 0.8)' : 'rgba(107, 114, 128, 0.4)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker key={index} coordinates={[node.lon, node.lat]}>
|
<Marker key={index} coordinates={[node.lon, node.lat]}>
|
||||||
<g>
|
<g>
|
||||||
<circle
|
<circle
|
||||||
r={6}
|
r={6}
|
||||||
fill={markerColor}
|
fill={markerColor}
|
||||||
stroke={isActive ? '#f59e0b' : '#4b5563'}
|
stroke={isActive ? '#f59e0b' : '#4b5563'}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -184,8 +196,8 @@ function NodeMap() {
|
|||||||
filter: `drop-shadow(0 0 8px ${glowColor})`
|
filter: `drop-shadow(0 0 8px ${glowColor})`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
r={12}
|
r={12}
|
||||||
fill={markerColor}
|
fill={markerColor}
|
||||||
fillOpacity={0.2}
|
fillOpacity={0.2}
|
||||||
stroke="none"
|
stroke="none"
|
||||||
@@ -193,7 +205,7 @@ function NodeMap() {
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<title>
|
<title>
|
||||||
{isActive ? '🟡 ACTIVE' : '⚪ Seen in last 24h'}
|
{isActive ? 'ACTIVE' : 'Seen in last 24h'}
|
||||||
{'\n'}{node.city}, {node.country}
|
{'\n'}{node.city}, {node.country}
|
||||||
{'\n'}IP: {node.ip || node.addr?.split(':')[0]} (IPv{node.ipVersion || '4'})
|
{'\n'}IP: {node.ip || node.addr?.split(':')[0]} (IPv{node.ipVersion || '4'})
|
||||||
{node.subver ? `\nClient: ${node.subver}` : ''}
|
{node.subver ? `\nClient: ${node.subver}` : ''}
|
||||||
@@ -206,9 +218,9 @@ function NodeMap() {
|
|||||||
</ZoomableGroup>
|
</ZoomableGroup>
|
||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{geoNodes.length === 0 && (
|
{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>No nodes could be geolocated yet.</p>
|
||||||
<p className="text-sm mt-2">Geolocation is in progress...</p>
|
<p className="text-sm mt-2">Geolocation is in progress...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,13 +233,13 @@ function NodeMap() {
|
|||||||
<Globe className="w-6 h-6" />
|
<Globe className="w-6 h-6" />
|
||||||
Node Distribution by Country
|
Node Distribution by Country
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{Object.keys(stats.countries).length > 0 ? (
|
{Object.keys(stats.countries).length > 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{Object.entries(stats.countries)
|
{Object.entries(stats.countries)
|
||||||
.sort(([, a], [, b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.map(([country, count]) => (
|
.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="flex items-center justify-between">
|
||||||
<div className="font-medium">{country}</div>
|
<div className="font-medium">{country}</div>
|
||||||
<div className="text-maza-blue font-bold">{count}</div>
|
<div className="text-maza-blue font-bold">{count}</div>
|
||||||
@@ -236,7 +248,7 @@ function NodeMap() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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.
|
No country data available yet.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { Trophy, TrendingUp, AlertCircle } from 'lucide-react';
|
import { Trophy, TrendingUp, AlertCircle } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import { SkeletonCard, SkeletonTableRow } from '../components/Skeleton';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || '';
|
const API_URL = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@@ -11,7 +14,7 @@ function RichList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRichList();
|
fetchRichList();
|
||||||
const interval = setInterval(fetchRichList, 60000); // Update every minute
|
const interval = setInterval(fetchRichList, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -45,10 +48,10 @@ function RichList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRankColor = (rank) => {
|
const getRankColor = (rank) => {
|
||||||
if (rank === 1) return 'text-yellow-400'; // Gold
|
if (rank === 1) return 'text-yellow-400';
|
||||||
if (rank === 2) return 'text-gray-300'; // Silver
|
if (rank === 2) return 'text-gray-300';
|
||||||
if (rank === 3) return 'text-yellow-600'; // Bronze
|
if (rank === 3) return 'text-yellow-600';
|
||||||
return 'text-gray-400';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRankIcon = (rank) => {
|
const getRankIcon = (rank) => {
|
||||||
@@ -60,8 +63,26 @@ function RichList() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="space-y-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,22 +122,22 @@ function RichList() {
|
|||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div className="stat-card">
|
<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 className="text-3xl font-bold text-yellow-400">{data?.totalAddresses || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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 className="text-2xl font-bold text-maza-blue">{formatBlockNumber(data?.lastScannedBlock)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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 className="text-2xl font-bold text-green-400">{formatBalance(getTotalBalance())} MAZA</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<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'}`}>
|
<div className={`text-xl font-bold ${data?.isScanning ? 'text-yellow-400' : 'text-green-400'}`}>
|
||||||
{data?.isScanning ? 'Scanning...' : 'Up to date'}
|
{data?.isScanning ? 'Scanning...' : 'Up to date'}
|
||||||
</div>
|
</div>
|
||||||
@@ -130,9 +151,9 @@ function RichList() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="font-semibold mb-1">About the Rich List</p>
|
<p className="font-semibold mb-1">About the Rich List</p>
|
||||||
<p>
|
<p>
|
||||||
This list shows the top 100 addresses by balance. Balances are calculated by tracking
|
This list shows the top 100 addresses by balance. Balances are calculated by tracking
|
||||||
transaction outputs as the blockchain is scanned. The scanner runs in the background
|
transaction outputs as the blockchain is scanned. The scanner runs in the background
|
||||||
and processes blocks incrementally. Large exchanges and mining pools typically appear
|
and processes blocks incrementally. Large exchanges and mining pools typically appear
|
||||||
at the top of this list.
|
at the top of this list.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,27 +164,28 @@ function RichList() {
|
|||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full">
|
<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>
|
<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
|
Rank
|
||||||
</th>
|
</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
|
Address
|
||||||
</th>
|
</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)
|
Balance (MAZA)
|
||||||
</th>
|
</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
|
Last Seen Block
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody style={{ borderColor: 'var(--border-color)' }}>
|
||||||
{data?.addresses?.map((addr) => (
|
{data?.addresses?.map((addr) => (
|
||||||
<tr
|
<tr
|
||||||
key={addr.address}
|
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">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -174,19 +196,22 @@ function RichList() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<a
|
<div className="flex items-center gap-1">
|
||||||
href={`/address/${addr.address}`}
|
<Link
|
||||||
className="text-maza-blue hover:text-blue-300 font-mono text-sm break-all"
|
to={`/address/${addr.address}`}
|
||||||
>
|
className="text-maza-blue hover:text-blue-300 font-mono text-sm break-all"
|
||||||
{addr.address}
|
>
|
||||||
</a>
|
{addr.address}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={addr.address} />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
<span className="text-green-400 font-bold">
|
<span className="text-green-400 font-bold">
|
||||||
{formatBalance(addr.balance)}
|
{formatBalance(addr.balance)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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)}
|
{formatBlockNumber(addr.lastSeen)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -198,8 +223,8 @@ function RichList() {
|
|||||||
|
|
||||||
{!data?.addresses || data.addresses.length === 0 ? (
|
{!data?.addresses || data.addresses.length === 0 ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="text-center text-gray-400 py-12">
|
<div className="text-center py-12" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-gray-500" />
|
<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-lg mb-2">No data available yet</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
The blockchain scanner is starting up. Please check back in a few minutes.
|
The blockchain scanner is starting up. Please check back in a few minutes.
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { ArrowRight, Clock } from 'lucide-react';
|
import { Clock } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import { SkeletonDetailCard } from '../components/Skeleton';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || '';
|
const API_URL = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@@ -14,12 +16,13 @@ function TransactionDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTransaction();
|
fetchTransaction();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [txid]);
|
}, [txid]);
|
||||||
|
|
||||||
const fetchTransaction = async () => {
|
const fetchTransaction = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/api/tx/${txid}`);
|
const response = await axios.get(`${API_URL}/api/tx/${txid}`);
|
||||||
setTx(response.data);
|
setTx(response.data);
|
||||||
@@ -32,8 +35,13 @@ function TransactionDetail() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="space-y-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -44,8 +52,7 @@ function TransactionDetail() {
|
|||||||
|
|
||||||
if (!tx) return null;
|
if (!tx) return null;
|
||||||
|
|
||||||
// Use backend-calculated totals if available, otherwise calculate
|
const totalInput = tx.totalInput !== undefined ? tx.totalInput :
|
||||||
const totalInput = tx.totalInput !== undefined ? tx.totalInput :
|
|
||||||
tx.vin?.reduce((sum, input) => sum + (input.value || 0), 0) || 0;
|
tx.vin?.reduce((sum, input) => sum + (input.value || 0), 0) || 0;
|
||||||
const totalOutput = tx.totalOutput !== undefined ? tx.totalOutput :
|
const totalOutput = tx.totalOutput !== undefined ? tx.totalOutput :
|
||||||
tx.vout?.reduce((sum, output) => sum + (output.value || 0), 0) || 0;
|
tx.vout?.reduce((sum, output) => sum + (output.value || 0), 0) || 0;
|
||||||
@@ -58,53 +65,59 @@ function TransactionDetail() {
|
|||||||
{/* Transaction Info */}
|
{/* Transaction Info */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 className="text-xl font-bold mb-4">Transaction Information</h2>
|
<h2 className="text-xl font-bold mb-4">Transaction Information</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6">
|
<div className="grid grid-cols-1 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-sm mb-1">Transaction ID</div>
|
<div className="text-sm mb-1" style={{ color: 'var(--text-secondary)' }}>Transaction ID</div>
|
||||||
<div className="hash text-lg bg-gray-800 p-3 rounded">{tx.txid}</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>
|
</div>
|
||||||
|
|
||||||
{tx.blockhash && (
|
{tx.blockhash && (
|
||||||
<div>
|
<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>
|
||||||
<Link
|
<div className="flex items-center gap-2 p-3 rounded transition-colors" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
to={`/block/${tx.blockhash}`}
|
<Link
|
||||||
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
|
to={`/block/${tx.blockhash}`}
|
||||||
>
|
className="hash text-lg flex-1 text-maza-blue hover:underline"
|
||||||
{tx.blockhash}
|
>
|
||||||
</Link>
|
{tx.blockhash}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={tx.blockhash} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{tx.confirmations !== undefined && (
|
{tx.confirmations !== undefined && (
|
||||||
<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">{tx.confirmations}</div>
|
<div className="text-lg text-green-400">{tx.confirmations}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tx.time && (
|
{tx.time && (
|
||||||
<div>
|
<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">
|
<div className="text-sm flex items-center gap-1">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
{format(new Date(tx.time * 1000), 'PPpp')}
|
{format(new Date(tx.time * 1000), 'PPpp')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tx.size && (
|
{tx.size && (
|
||||||
<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">{tx.size} bytes</div>
|
<div className="text-lg">{tx.size} bytes</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fee > 0 && (
|
{fee > 0 && (
|
||||||
<div>
|
<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 className="text-lg">{fee.toFixed(8)} MAZA</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -119,40 +132,46 @@ function TransactionDetail() {
|
|||||||
<h3 className="text-lg font-bold mb-4">
|
<h3 className="text-lg font-bold mb-4">
|
||||||
Inputs ({tx.vin?.length || 0})
|
Inputs ({tx.vin?.length || 0})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{tx.vin && tx.vin.length > 0 ? (
|
{tx.vin && tx.vin.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
{tx.vin.map((input, index) => (
|
{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 ? (
|
{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 && (
|
{input.txid && (
|
||||||
<Link
|
<div className="flex items-center gap-1 mb-1">
|
||||||
to={`/tx/${input.txid}`}
|
<Link
|
||||||
className="hash text-xs text-maza-blue hover:underline block mb-1"
|
to={`/tx/${input.txid}`}
|
||||||
>
|
className="hash text-xs text-maza-blue hover:underline"
|
||||||
{input.txid}:{input.vout}
|
>
|
||||||
</Link>
|
{input.txid}:{input.vout}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={input.txid} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{input.value !== undefined && input.value !== null ? (
|
{input.value !== undefined && input.value !== null ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-lg font-bold">{input.value.toFixed(8)} MAZA</div>
|
<div className="text-lg font-bold">{input.value.toFixed(8)} MAZA</div>
|
||||||
{input.address && input.address !== 'Unknown' && (
|
{input.address && input.address !== 'Unknown' && (
|
||||||
<Link
|
<div className="flex items-center gap-1 mt-1">
|
||||||
to={`/address/${input.address}`}
|
<Link
|
||||||
className="hash text-xs text-maza-blue hover:underline block mt-1"
|
to={`/address/${input.address}`}
|
||||||
>
|
className="hash text-xs text-maza-blue hover:underline"
|
||||||
{input.address}
|
>
|
||||||
</Link>
|
{input.address}
|
||||||
|
</Link>
|
||||||
|
<CopyButton text={input.address} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : input.error ? (
|
) : input.error ? (
|
||||||
<div className="text-xs text-yellow-400">{input.error}</div>
|
<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>
|
</div>
|
||||||
{totalInput > 0 && (
|
{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">
|
<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>
|
<span className="text-xl font-bold text-green-400">{totalInput.toFixed(8)} MAZA</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@@ -178,36 +197,38 @@ function TransactionDetail() {
|
|||||||
<h3 className="text-lg font-bold mb-4">
|
<h3 className="text-lg font-bold mb-4">
|
||||||
Outputs ({tx.vout?.length || 0})
|
Outputs ({tx.vout?.length || 0})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{tx.vout && tx.vout.length > 0 ? (
|
{tx.vout && tx.vout.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
{tx.vout.map((output, index) => (
|
{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>
|
<div className="text-lg font-bold mb-1">{output.value.toFixed(8)} MAZA</div>
|
||||||
{output.scriptPubKey?.addresses?.map((address, i) => (
|
{output.scriptPubKey?.addresses?.map((address, i) => (
|
||||||
<Link
|
<div key={i} className="flex items-center gap-1">
|
||||||
key={i}
|
<Link
|
||||||
to={`/address/${address}`}
|
to={`/address/${address}`}
|
||||||
className="hash text-xs text-maza-blue hover:underline block"
|
className="hash text-xs text-maza-blue hover:underline"
|
||||||
>
|
>
|
||||||
{address}
|
{address}
|
||||||
</Link>
|
</Link>
|
||||||
|
<CopyButton text={address} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{totalOutput > 0 && (
|
{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">
|
<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>
|
<span className="text-xl font-bold text-blue-400">{totalOutput.toFixed(8)} MAZA</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user