+
{/* Currency selector */}
{currencies.map(curr => (
@@ -170,15 +170,16 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
onClick={() => setActiveCurrency(curr.value)}
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeCurrency === curr.value
- ? `${curr.color} bg-gray-700`
- : 'text-gray-400 hover:text-gray-300'
+ ? `${curr.color} bg-theme-secondary`
+ : 'hover:opacity-80'
}`}
+ style={activeCurrency !== curr.value ? { color: 'var(--text-secondary)' } : undefined}
>
{curr.label}
))}
-
+
{/* Timeframe selector */}
{timeframes.map(tf => (
@@ -188,8 +189,9 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeTimeframe === tf.value
? 'bg-maza-blue text-white'
- : 'text-gray-400 hover:text-gray-300'
+ : 'hover:opacity-80'
}`}
+ style={activeTimeframe !== tf.value ? { color: 'var(--text-secondary)' } : undefined}
>
{tf.label}
@@ -197,12 +199,12 @@ function PriceChart({ currency = 'btc', timeframe = '30d' }) {
diff --git a/frontend/src/components/SearchBar.js b/frontend/src/components/SearchBar.js
index 5663621..56ca366 100644
--- a/frontend/src/components/SearchBar.js
+++ b/frontend/src/components/SearchBar.js
@@ -49,7 +49,8 @@ function SearchBar() {
/>
{error}
diff --git a/frontend/src/components/Skeleton.js b/frontend/src/components/Skeleton.js
new file mode 100644
index 0000000..d6c61e9
--- /dev/null
+++ b/frontend/src/components/Skeleton.js
@@ -0,0 +1,96 @@
+import React from 'react';
+
+export function SkeletonLine({ width = 'w-full', height = 'h-4', className = '' }) {
+ return (
+
+ );
+}
+
+export function SkeletonCard() {
+ return (
+
+ );
+}
+
+export function SkeletonBlock() {
+ return (
+
+ );
+}
+
+export function SkeletonTableRow({ cols = 4 }) {
+ return (
+
+ {Array.from({ length: cols }).map((_, i) => (
+ |
+
+ |
+ ))}
+
+ );
+}
+
+export function SkeletonDetailCard() {
+ return (
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function SkeletonChart() {
+ return (
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+export function SkeletonTxList({ count = 3 }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/Toast.js b/frontend/src/components/Toast.js
new file mode 100644
index 0000000..024773c
--- /dev/null
+++ b/frontend/src/components/Toast.js
@@ -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 (
+
+ {children}
+
+ {toasts.map(t => (
+
+ {t.type === 'success' && }
+ {t.type === 'error' && }
+ {t.type === 'info' && }
+ {t.message}
+
+
+ ))}
+
+
+ );
+}
+
+export function useToast() {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within a ToastProvider');
+ }
+ return context;
+}
diff --git a/frontend/src/contexts/ThemeContext.js b/frontend/src/contexts/ThemeContext.js
new file mode 100644
index 0000000..f38d5c1
--- /dev/null
+++ b/frontend/src/contexts/ThemeContext.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+}
diff --git a/frontend/src/pages/AddressDetail.js b/frontend/src/pages/AddressDetail.js
index 0cc376f..385f0e2 100644
--- a/frontend/src/pages/AddressDetail.js
+++ b/frontend/src/pages/AddressDetail.js
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
-import { Copy, TrendingUp } from 'lucide-react';
+import { TrendingUp } from 'lucide-react';
import axios from 'axios';
+import CopyButton from '../components/CopyButton';
+import { SkeletonLine } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -27,10 +29,6 @@ function AddressDetail() {
}
};
- const copyToClipboard = () => {
- navigator.clipboard.writeText(address);
- };
-
const formatBalance = (balance) => {
return balance.toLocaleString('en-US', {
minimumFractionDigits: 2,
@@ -46,48 +44,47 @@ function AddressDetail() {
Address
-
-
-
{address}
-
+
+
{/* Balance Info */}
-
+
Balance
-
+
{loadingBalance ? (
-
Loading balance...
+
+
+
+
) : balanceData?.balance !== null && balanceData?.balance !== undefined ? (
<>
{formatBalance(balanceData.balance)} MAZA
{balanceData.rank && (
-
+
Rank #{balanceData.rank} in rich list
)}
{balanceData.lastSeen && (
-
+
Last seen in block: {balanceData.lastSeen.toLocaleString()}
)}
>
) : (
-
+
{balanceData?.message || 'Balance data not available yet'}
{balanceData?.lastScannedBlock && (
-
+
Scanner progress: block {balanceData.lastScannedBlock.toLocaleString()}
)}
@@ -103,7 +100,7 @@ function AddressDetail() {
Balance is calculated by our blockchain scanner tracking all transaction outputs
Scanner is running in the background and processes blocks continuously
Top addresses appear in the rich list as they're discovered
-
Full transaction history requires txindex=1 on the node
+
Full transaction history requires txindex=1 on the node
@@ -119,7 +116,7 @@ function AddressDetail() {
Transaction History
-
+
Transaction history for addresses requires txindex to be enabled on the Mazacoin node.
This feature will be available once the node is configured with txindex and fully synced.
diff --git a/frontend/src/pages/BlockDetail.js b/frontend/src/pages/BlockDetail.js
index 15034ff..55f5df8 100644
--- a/frontend/src/pages/BlockDetail.js
+++ b/frontend/src/pages/BlockDetail.js
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
-import { ChevronLeft, ChevronRight, Clock, Hash, Layers } from 'lucide-react';
+import { ChevronLeft, ChevronRight, Clock, Layers } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
+import CopyButton from '../components/CopyButton';
+import { SkeletonDetailCard, SkeletonTxList } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -14,12 +16,13 @@ function BlockDetail() {
useEffect(() => {
fetchBlock();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hashOrHeight]);
const fetchBlock = async () => {
setLoading(true);
setError('');
-
+
try {
const response = await axios.get(`${API_URL}/api/block/${hashOrHeight}`);
setBlock(response.data);
@@ -32,18 +35,21 @@ function BlockDetail() {
if (loading) {
return (
-
-
+
);
}
if (error) {
- return (
-
- {error}
-
- );
+ return
{error}
;
}
if (!block) return null;
@@ -59,12 +65,12 @@ function BlockDetail() {
Previous Block
-
+
Block #{block.height}
-
+
Block Information
-
+
-
Height
+
Height
{block.height}
-
+
-
Timestamp
+
Timestamp
{format(new Date(block.time * 1000), 'PPpp')}
-
+
-
Hash
-
{block.hash}
+
Hash
+
-
+
{block.previousblockhash && (
-
Previous Block Hash
-
- {block.previousblockhash}
-
+
Previous Block Hash
+
+
+ {block.previousblockhash}
+
+
+
)}
-
+
{block.nextblockhash && (
-
Next Block Hash
-
- {block.nextblockhash}
-
+
Next Block Hash
+
+
+ {block.nextblockhash}
+
+
+
)}
-
+
-
Difficulty
+
Difficulty
{block.difficulty?.toFixed(4)}
-
+
-
Size
+
Size
{(block.size / 1024).toFixed(2)} KB
-
+
-
Confirmations
+
Confirmations
{block.confirmations}
-
+
-
Version
+
Version
{block.version}
@@ -153,29 +168,31 @@ function BlockDetail() {
)}
-
+
{block.tx && block.tx.length > 0 ? (
{block.tx.map((txid, index) => (
-
-
-
- {index === 0 && (
-
Coinbase
- )}
-
-
+
+
#{index}
+
{txid}
+
+
+ {index === 0 && (
+
Coinbase
+ )}
+
))}
) : (
-
+
No transactions in this block
)}
diff --git a/frontend/src/pages/Home.js b/frontend/src/pages/Home.js
index 88dea1c..70f5e65 100644
--- a/frontend/src/pages/Home.js
+++ b/frontend/src/pages/Home.js
@@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
import { Clock, Layers } from 'lucide-react';
import SearchBar from '../components/SearchBar';
import PriceChart from '../components/PriceChart';
+import CopyButton from '../components/CopyButton';
+import { SkeletonCard, SkeletonBlock } from '../components/Skeleton';
import axios from 'axios';
import { formatDistance } from 'date-fns';
import io from 'socket.io-client';
@@ -17,15 +19,15 @@ function Home() {
useEffect(() => {
fetchData();
-
+
// WebSocket for real-time updates
const socket = io(API_URL);
socket.emit('subscribe:blocks');
-
+
socket.on('block:new', (block) => {
setLatestBlocks(prev => [block, ...prev].slice(0, 10));
});
-
+
return () => socket.disconnect();
}, []);
@@ -36,8 +38,7 @@ function Home() {
axios.get(`${API_URL}/api/stats`, { timeout: 30000 }),
axios.get(`${API_URL}/api/price`, { timeout: 30000 })
]);
-
- // Ensure we got valid data
+
if (Array.isArray(blocksRes.data)) {
setLatestBlocks(blocksRes.data);
}
@@ -49,7 +50,6 @@ function Home() {
}
} catch (error) {
console.error('Error fetching data:', error);
- // Set empty/fallback data on error
setLatestBlocks([]);
} finally {
setLoading(false);
@@ -61,18 +61,22 @@ function Home() {
{/* Hero Section */}
Mazacoin Blockchain Explorer
-
+
Explore blocks, transactions, and addresses on the Mazacoin network
{/* Network Stats */}
- {stats && stats.blockHeight !== undefined && (
+ {loading ? (
+
+ {Array.from({ length: 5 }).map((_, i) => )}
+
+ ) : stats && stats.blockHeight !== undefined && (
{price && price.btc !== undefined && (
-
MAZA Price
+
MAZA Price
{price.btc?.toFixed(8) || '0.00000000'} BTC
@@ -82,31 +86,31 @@ function Home() {
{price.eth?.toFixed(10) || '0.0000000000'} ETH
-
)}
-
Block Height
+
Block Height
{stats.blockHeight?.toLocaleString() || '0'}
-
Difficulty
+
Difficulty
{stats.difficulty?.toFixed(2) || '0.00'}
-
Network Hashrate
+
Network Hashrate
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
-
Connections
+
Connections
{stats.connections || 0}
@@ -123,29 +127,35 @@ function Home() {
Latest Blocks
-
+
{loading ? (
-
-
+
+ {Array.from({ length: 3 }).map((_, i) => )}
) : (
{latestBlocks.map((block) => (
-
-
+
{block.height}
-
-
- {block.hash.substring(0, 16)}...
+
+
+
+ {block.hash.substring(0, 16)}...
+
+
-
+
{formatDistance(new Date(block.time * 1000), new Date(), { addSuffix: true })}
@@ -160,7 +170,7 @@ function Home() {
)}
-
+
))}
)}
diff --git a/frontend/src/pages/NetworkStats.js b/frontend/src/pages/NetworkStats.js
index 6b05c54..49bd03a 100644
--- a/frontend/src/pages/NetworkStats.js
+++ b/frontend/src/pages/NetworkStats.js
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Activity, Server, Zap, Clock } from 'lucide-react';
import axios from 'axios';
+import { SkeletonCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -10,7 +11,7 @@ function NetworkStats() {
useEffect(() => {
fetchStats();
- const interval = setInterval(fetchStats, 30000); // Update every 30s
+ const interval = setInterval(fetchStats, 30000);
return () => clearInterval(interval);
}, []);
@@ -27,8 +28,11 @@ function NetworkStats() {
if (loading) {
return (
-
-
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => )}
+
);
}
@@ -44,7 +48,7 @@ function NetworkStats() {
Network Statistics
-
+
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
@@ -57,7 +61,7 @@ function NetworkStats() {
-
Block Height
+
Block Height
{stats.blockHeight?.toLocaleString() || '0'}
@@ -69,7 +73,7 @@ function NetworkStats() {
-
Network Hashrate
+
Network Hashrate
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
@@ -83,7 +87,7 @@ function NetworkStats() {
-
Active Connections
+
Active Connections
{stats.connections}
@@ -95,19 +99,19 @@ function NetworkStats() {
-
Difficulty
+
Difficulty
{stats.difficulty?.toFixed(2) || '0.00'}
-
Protocol Version
+
Protocol Version
{stats.protocolVersion}
-
Client Version
+
Client Version
{stats.version}
@@ -115,12 +119,12 @@ function NetworkStats() {
{/* Additional Info */}
About Mazacoin
-
+
- 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.
-
+
This explorer provides real-time blockchain data directly from Mazacoin nodes.
diff --git a/frontend/src/pages/NodeMap.js b/frontend/src/pages/NodeMap.js
index da3c1f2..08cff29 100644
--- a/frontend/src/pages/NodeMap.js
+++ b/frontend/src/pages/NodeMap.js
@@ -2,18 +2,20 @@ import React, { useState, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps';
import { MapPin, Globe } from 'lucide-react';
import axios from 'axios';
+import { useTheme } from '../contexts/ThemeContext';
+import { SkeletonCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
-// World map TopoJSON URL
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
function NodeMap() {
- const [nodes, setNodes] = useState([]);
+ const [nodes, setNodes] = useState([]); // eslint-disable-line no-unused-vars
const [geoNodes, setGeoNodes] = useState([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({ total: 0, active: 0, countries: {} });
const [price, setPrice] = useState(null);
+ const { theme } = useTheme();
useEffect(() => {
fetchNodes();
@@ -21,7 +23,7 @@ function NodeMap() {
const interval = setInterval(() => {
fetchNodes();
fetchPrice();
- }, 60000); // Update every minute
+ }, 60000);
return () => clearInterval(interval);
}, []);
@@ -36,10 +38,9 @@ function NodeMap() {
const fetchNodes = async () => {
try {
- // Fetch nodes with geolocation from backend API
const response = await axios.get(`${API_URL}/api/nodes`);
const data = response.data;
-
+
setNodes(data.nodes || []);
setGeoNodes(data.nodes || []);
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) {
return (
-
-
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => )}
+
+
);
}
@@ -69,7 +81,7 @@ function NodeMap() {
Live Node Map
-
+
Showing all nodes discovered in the last 24 hours
@@ -78,7 +90,7 @@ function NodeMap() {
{price && price.btc !== undefined && (
-
MAZA Price
+
MAZA Price
{price.btc?.toFixed(8) || '0.00000000'} BTC
@@ -90,22 +102,22 @@ function NodeMap() {
)}
-
+
-
Nodes (24h)
+
Nodes (24h)
{stats.total}
-
-
+
-
Mapped Nodes
+
Mapped Nodes
{geoNodes.length}
-
+
-
IP Protocol
+
IP Protocol
IPv4: {geoNodes.filter(n => n.ipVersion === 4).length}
@@ -115,15 +127,15 @@ function NodeMap() {
-
+
-
Countries
+
Countries
{Object.keys(stats.countries).length}
{/* Legend */}
-
+
@@ -131,14 +143,14 @@ function NodeMap() {
-
Seen in Last 24h ({geoNodes.filter(n => !n.isActive).length})
+
Seen in Last 24h ({geoNodes.filter(n => !n.isActive).length})
{/* Map */}
-
+
))
}
-
+
{geoNodes.map((node, index) => {
const isActive = node.isActive;
- const markerColor = isActive ? '#fbbf24' : '#6b7280'; // yellow for active, gray for inactive
+ const markerColor = isActive ? '#fbbf24' : '#6b7280';
const glowColor = isActive ? 'rgba(251, 191, 36, 0.8)' : 'rgba(107, 114, 128, 0.4)';
-
+
return (
-
-
- {isActive ? '🟡 ACTIVE' : '⚪ Seen in last 24h'}
+ {isActive ? 'ACTIVE' : 'Seen in last 24h'}
{'\n'}{node.city}, {node.country}
{'\n'}IP: {node.ip || node.addr?.split(':')[0]} (IPv{node.ipVersion || '4'})
{node.subver ? `\nClient: ${node.subver}` : ''}
@@ -206,9 +218,9 @@ function NodeMap() {
-
+
{geoNodes.length === 0 && (
-
+
No nodes could be geolocated yet.
Geolocation is in progress...
@@ -221,13 +233,13 @@ function NodeMap() {
Node Distribution by Country
-
+
{Object.keys(stats.countries).length > 0 ? (
{Object.entries(stats.countries)
.sort(([, a], [, b]) => b - a)
.map(([country, count]) => (
-
+
{country}
{count}
@@ -236,7 +248,7 @@ function NodeMap() {
))}
) : (
-
+
No country data available yet.
)}
diff --git a/frontend/src/pages/RichList.js b/frontend/src/pages/RichList.js
index c98c7e5..eff791d 100644
--- a/frontend/src/pages/RichList.js
+++ b/frontend/src/pages/RichList.js
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
import { Trophy, TrendingUp, AlertCircle } from 'lucide-react';
import axios from 'axios';
+import CopyButton from '../components/CopyButton';
+import { SkeletonCard, SkeletonTableRow } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -11,7 +14,7 @@ function RichList() {
useEffect(() => {
fetchRichList();
- const interval = setInterval(fetchRichList, 60000); // Update every minute
+ const interval = setInterval(fetchRichList, 60000);
return () => clearInterval(interval);
}, []);
@@ -45,10 +48,10 @@ function RichList() {
};
const getRankColor = (rank) => {
- if (rank === 1) return 'text-yellow-400'; // Gold
- if (rank === 2) return 'text-gray-300'; // Silver
- if (rank === 3) return 'text-yellow-600'; // Bronze
- return 'text-gray-400';
+ if (rank === 1) return 'text-yellow-400';
+ if (rank === 2) return 'text-gray-300';
+ if (rank === 3) return 'text-yellow-600';
+ return '';
};
const getRankIcon = (rank) => {
@@ -60,8 +63,26 @@ function RichList() {
if (loading) {
return (
-
-
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => )}
+
+
+
+
+
+ | Rank |
+ Address |
+ Balance (MAZA) |
+ Last Seen Block |
+
+
+
+ {Array.from({ length: 10 }).map((_, i) => )}
+
+
+
);
}
@@ -101,22 +122,22 @@ function RichList() {
{/* Stats */}
-
Total Addresses
+
Total Addresses
{data?.totalAddresses || 0}
-
+
-
Last Scanned Block
+
Last Scanned Block
{formatBlockNumber(data?.lastScannedBlock)}
-
+
-
Top 100 Total Balance
+
Top 100 Total Balance
{formatBalance(getTotalBalance())} MAZA
-
+
-
Status
+
Status
{data?.isScanning ? 'Scanning...' : 'Up to date'}
@@ -130,9 +151,9 @@ function RichList() {
About the Rich List
- 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
- and processes blocks incrementally. Large exchanges and mining pools typically appear
+ 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
+ and processes blocks incrementally. Large exchanges and mining pools typically appear
at the top of this list.
@@ -143,27 +164,28 @@ function RichList() {
-
+
- |
+ |
Rank
|
-
+ |
Address
|
-
+ |
Balance (MAZA)
|
-
+ |
Last Seen Block
|
-
+
{data?.addresses?.map((addr) => (
-
|
@@ -174,19 +196,22 @@ function RichList() {
|
-
- {addr.address}
-
+
+
+ {addr.address}
+
+
+
|
{formatBalance(addr.balance)}
|
-
+ |
{formatBlockNumber(addr.lastSeen)}
|
@@ -198,8 +223,8 @@ function RichList() {
{!data?.addresses || data.addresses.length === 0 ? (
-
-
+
+
No data available yet
The blockchain scanner is starting up. Please check back in a few minutes.
diff --git a/frontend/src/pages/TransactionDetail.js b/frontend/src/pages/TransactionDetail.js
index 2b4b544..e1b0e9f 100644
--- a/frontend/src/pages/TransactionDetail.js
+++ b/frontend/src/pages/TransactionDetail.js
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
-import { ArrowRight, Clock } from 'lucide-react';
+import { Clock } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
+import CopyButton from '../components/CopyButton';
+import { SkeletonDetailCard } from '../components/Skeleton';
const API_URL = process.env.REACT_APP_API_URL || '';
@@ -14,12 +16,13 @@ function TransactionDetail() {
useEffect(() => {
fetchTransaction();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [txid]);
const fetchTransaction = async () => {
setLoading(true);
setError('');
-
+
try {
const response = await axios.get(`${API_URL}/api/tx/${txid}`);
setTx(response.data);
@@ -32,8 +35,13 @@ function TransactionDetail() {
if (loading) {
return (
-
-
+
);
}
@@ -44,8 +52,7 @@ function TransactionDetail() {
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;
const totalOutput = tx.totalOutput !== undefined ? tx.totalOutput :
tx.vout?.reduce((sum, output) => sum + (output.value || 0), 0) || 0;
@@ -58,53 +65,59 @@ function TransactionDetail() {
{/* Transaction Info */}
Transaction Information
-
+
-
Transaction ID
-
{tx.txid}
+
Transaction ID
+
-
+
{tx.blockhash && (
-
Block Hash
-
- {tx.blockhash}
-
+
Block Hash
+
+
+ {tx.blockhash}
+
+
+
)}
-
+
{tx.confirmations !== undefined && (
-
Confirmations
+
Confirmations
{tx.confirmations}
)}
-
+
{tx.time && (
-
Time
+
Time
{format(new Date(tx.time * 1000), 'PPpp')}
)}
-
+
{tx.size && (
-
Size
+
Size
{tx.size} bytes
)}
-
+
{fee > 0 && (
-
Fee
+
Fee
{fee.toFixed(8)} MAZA
)}
@@ -119,40 +132,46 @@ function TransactionDetail() {
Inputs ({tx.vin?.length || 0})
-
+
{tx.vin && tx.vin.length > 0 ? (
<>
{tx.vin.map((input, index) => (
-
+
{input.coinbase || input.isCoinbase ? (
-
Coinbase (Newly Generated Coins)
+
Coinbase (Newly Generated Coins)
) : (
<>
{input.txid && (
-
- {input.txid}:{input.vout}
-
+
+
+ {input.txid}:{input.vout}
+
+
+
)}
{input.value !== undefined && input.value !== null ? (
<>
{input.value.toFixed(8)} MAZA
{input.address && input.address !== 'Unknown' && (
-
- {input.address}
-
+
+
+ {input.address}
+
+
+
)}
>
) : input.error ? (
{input.error}
) : (
-
Amount unknown
+
Amount unknown
)}
>
)}
@@ -160,16 +179,16 @@ function TransactionDetail() {
))}
{totalInput > 0 && (
-
+
- Total Input:
+ Total Input:
{totalInput.toFixed(8)} MAZA
)}
>
) : (
-
No inputs
+
No inputs
)}
@@ -178,36 +197,38 @@ function TransactionDetail() {
Outputs ({tx.vout?.length || 0})
-
+
{tx.vout && tx.vout.length > 0 ? (
<>
{tx.vout.map((output, index) => (
-
+
{output.value.toFixed(8)} MAZA
{output.scriptPubKey?.addresses?.map((address, i) => (
-
- {address}
-
+
+
+ {address}
+
+
+
))}
))}
{totalOutput > 0 && (
-
+
- Total Output:
+ Total Output:
{totalOutput.toFixed(8)} MAZA
)}
>
) : (
-
No outputs
+
No outputs
)}