synor/apps/desktop-wallet/src/pages/Mining/MiningDashboard.tsx
Gulshan Yadav 5126c33113 ui/ux
2026-02-02 05:12:01 +05:30

388 lines
12 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
Hammer,
Play,
Pause,
Square,
Cpu,
Blocks,
TrendingUp,
Clock,
Zap,
} from 'lucide-react';
import { useMiningStore, formatHashrate } from '../../store/mining';
import { useNodeStore } from '../../store/node';
import { useWalletStore } from '../../store/wallet';
export default function MiningDashboard() {
const {
status,
recentBlocks,
hashrateHistory,
defaultThreads,
defaultCoinbaseAddress,
startMining,
stopMining,
pauseMining,
resumeMining,
setThreads,
refreshStatus,
refreshStats,
setupEventListeners,
cleanupEventListeners,
} = useMiningStore();
const nodeStatus = useNodeStore((state) => state.status);
const addresses = useWalletStore((state) => state.addresses);
const [threads, setThreadsLocal] = useState(defaultThreads);
const [coinbaseAddress, setCoinbaseAddress] = useState(
defaultCoinbaseAddress || addresses[0]?.address || ''
);
const [isStarting, setIsStarting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Max threads available
const maxThreads = navigator.hardwareConcurrency || 8;
// Setup event listeners on mount
useEffect(() => {
setupEventListeners();
return () => cleanupEventListeners();
}, [setupEventListeners, cleanupEventListeners]);
// Refresh stats periodically when mining
useEffect(() => {
if (!status.isMining) return;
const interval = setInterval(() => {
refreshStatus();
refreshStats();
}, 1000);
return () => clearInterval(interval);
}, [status.isMining, refreshStatus, refreshStats]);
// Update coinbase address when addresses change
useEffect(() => {
if (!coinbaseAddress && addresses.length > 0) {
setCoinbaseAddress(addresses[0].address);
}
}, [addresses, coinbaseAddress]);
const handleStart = async () => {
if (!nodeStatus.isConnected) {
setError('Please connect to a node first');
return;
}
if (!coinbaseAddress) {
setError('Please select a coinbase address for rewards');
return;
}
setIsStarting(true);
setError(null);
try {
await startMining(coinbaseAddress, threads);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start mining');
} finally {
setIsStarting(false);
}
};
const handleStop = async () => {
try {
await stopMining();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to stop mining');
}
};
const handlePauseResume = async () => {
try {
if (status.isPaused) {
await resumeMining();
} else {
await pauseMining();
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Action failed');
}
};
const handleThreadsChange = async (newThreads: number) => {
setThreadsLocal(newThreads);
if (status.isMining) {
try {
await setThreads(newThreads);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update threads');
}
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
<Hammer size={28} />
Mining
</h1>
<p className="text-gray-400 mt-1">
Mine SYN coins using your computer's CPU
</p>
</div>
{/* Control buttons */}
<div className="flex items-center gap-2">
{status.isMining ? (
<>
<button
onClick={handlePauseResume}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
status.isPaused
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-yellow-600 hover:bg-yellow-700 text-white'
}`}
>
{status.isPaused ? <Play size={18} /> : <Pause size={18} />}
{status.isPaused ? 'Resume' : 'Pause'}
</button>
<button
onClick={handleStop}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white transition-colors"
>
<Square size={18} />
Stop
</button>
</>
) : (
<button
onClick={handleStart}
disabled={isStarting || !nodeStatus.isConnected}
className="flex items-center gap-2 px-6 py-2 rounded-lg bg-synor-600 hover:bg-synor-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isStarting ? (
<Cpu size={18} className="animate-spin" />
) : (
<Play size={18} />
)}
Start Mining
</button>
)}
</div>
</div>
{/* Error display */}
{error && (
<div className="p-4 rounded-lg bg-red-900/30 border border-red-800 text-red-400">
{error}
</div>
)}
{/* Warning if not connected */}
{!nodeStatus.isConnected && (
<div className="p-4 rounded-lg bg-yellow-900/30 border border-yellow-800 text-yellow-400">
Please connect to a node before starting mining
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
icon={<Zap size={20} className={status.isMining && !status.isPaused ? 'animate-pulse' : ''} />}
label="Hashrate"
value={formatHashrate(status.hashrate)}
highlight={status.isMining && !status.isPaused}
/>
<StatCard
icon={<Blocks size={20} />}
label="Blocks Found"
value={status.blocksFound.toString()}
/>
<StatCard
icon={<Cpu size={20} />}
label="Threads"
value={`${status.threads || threads}/${maxThreads}`}
/>
<StatCard
icon={<Clock size={20} />}
label="Status"
value={
status.isMining
? status.isPaused
? 'Paused'
: 'Mining'
: 'Idle'
}
highlight={status.isMining && !status.isPaused}
/>
</div>
{/* Configuration (when not mining) */}
{!status.isMining && (
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
<h3 className="text-lg font-semibold text-white mb-4">
Mining Configuration
</h3>
<div className="space-y-4">
{/* Coinbase address */}
<div>
<label className="block text-sm text-gray-400 mb-2">
Reward Address
</label>
<select
value={coinbaseAddress}
onChange={(e) => setCoinbaseAddress(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-gray-800 border border-gray-700 text-white focus:outline-none focus:border-synor-500"
>
<option value="">Select address...</option>
{addresses.map((addr) => (
<option key={addr.address} value={addr.address}>
{addr.label || `Address ${addr.index}`} - {addr.address.slice(0, 20)}...
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Mining rewards will be sent to this address
</p>
</div>
{/* Thread slider */}
<div>
<label className="block text-sm text-gray-400 mb-2">
Mining Threads: {threads}
</label>
<input
type="range"
min={1}
max={maxThreads}
value={threads}
onChange={(e) => handleThreadsChange(parseInt(e.target.value))}
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-synor-500"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>1 (Low Power)</span>
<span>{maxThreads} (Max Performance)</span>
</div>
</div>
</div>
</div>
)}
{/* Hashrate Chart (simplified) */}
{status.isMining && hashrateHistory.length > 0 && (
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<TrendingUp size={20} />
Hashrate History
</h3>
<div className="h-32 flex items-end gap-0.5">
{hashrateHistory.slice(-60).map((point, i) => {
const maxHash = Math.max(...hashrateHistory.map((h) => h.hashrate));
const height = maxHash > 0 ? (point.hashrate / maxHash) * 100 : 0;
return (
<div
key={i}
className="flex-1 bg-synor-500 rounded-t transition-all duration-300"
style={{ height: `${height}%` }}
title={`${formatHashrate(point.hashrate)}`}
/>
);
})}
</div>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>60s ago</span>
<span>Now</span>
</div>
</div>
)}
{/* Recent Blocks */}
{recentBlocks.length > 0 && (
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Blocks size={20} />
Blocks Found
</h3>
<div className="space-y-2">
{recentBlocks.slice(0, 5).map((block, i) => (
<div
key={i}
className="flex items-center justify-between p-3 rounded-lg bg-gray-800"
>
<div>
<p className="text-sm text-white font-mono">
Block #{block.height.toLocaleString()}
</p>
<p className="text-xs text-gray-500">
{block.hash.slice(0, 24)}...
</p>
</div>
<div className="text-right">
<p className="text-sm text-synor-400 font-medium">
+{(block.reward / 100_000_000).toFixed(2)} SYN
</p>
<p className="text-xs text-gray-500">
{new Date(block.timestamp).toLocaleTimeString()}
</p>
</div>
</div>
))}
</div>
</div>
)}
{/* Mining Tips */}
<div className="p-6 rounded-xl bg-gray-900/50 border border-gray-800">
<h3 className="text-sm font-semibold text-gray-400 mb-3">Mining Tips</h3>
<ul className="space-y-2 text-sm text-gray-500">
<li>• Use fewer threads to keep your computer responsive</li>
<li>• Mining profitability depends on network difficulty</li>
<li>• Ensure adequate cooling for sustained mining</li>
<li>• For best results, use an embedded node for lower latency</li>
</ul>
</div>
</div>
);
}
// Stat card component
function StatCard({
icon,
label,
value,
highlight = false,
}: {
icon: React.ReactNode;
label: string;
value: string;
highlight?: boolean;
}) {
return (
<div
className={`p-4 rounded-xl border ${
highlight
? 'bg-synor-900/30 border-synor-700'
: 'bg-gray-900 border-gray-800'
}`}
>
<div className="flex items-center gap-2 text-gray-400 mb-2">
{icon}
<span className="text-sm">{label}</span>
</div>
<p
className={`text-xl font-bold ${
highlight ? 'text-synor-400' : 'text-white'
}`}
>
{value}
</p>
</div>
);
}