a
This commit is contained in:
parent
c32622f34f
commit
f08eb965c2
15 changed files with 1306 additions and 189 deletions
|
|
@ -33,7 +33,7 @@ import {
|
|||
Eye,
|
||||
ListPlus,
|
||||
Activity,
|
||||
Vault,
|
||||
Timer,
|
||||
ShieldCheck,
|
||||
// Phase 7-16 icons
|
||||
UserX,
|
||||
|
|
@ -97,7 +97,7 @@ const governanceNavItems = [
|
|||
const toolsNavItems = [
|
||||
{ to: '/watch-only', label: 'Watch-Only', icon: Eye },
|
||||
{ to: '/fee-analytics', label: 'Fee Analytics', icon: Activity },
|
||||
{ to: '/vaults', label: 'Time Vaults', icon: Vault },
|
||||
{ to: '/vaults', label: 'Time Vaults', icon: Timer },
|
||||
{ to: '/recovery', label: 'Recovery', icon: ShieldCheck },
|
||||
{ to: '/decoy', label: 'Decoy Wallets', icon: UserX },
|
||||
{ to: '/mixer', label: 'Mixer', icon: Shuffle },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,43 @@
|
|||
import { Bell, Info, AlertTriangle, Plus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Bell, Info, AlertCircle, Plus, RefreshCw, Loader2, X, Trash2, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import { useAlertsStore } from '../../store/alerts';
|
||||
|
||||
export default function AlertsDashboard() {
|
||||
const {
|
||||
alerts,
|
||||
isLoading,
|
||||
error,
|
||||
listAlerts,
|
||||
createAlert,
|
||||
deleteAlert,
|
||||
toggleAlert,
|
||||
clearError,
|
||||
} = useAlertsStore();
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [asset, setAsset] = useState('SYN');
|
||||
const [condition, setCondition] = useState<'above' | 'below'>('above');
|
||||
const [targetPrice, setTargetPrice] = useState('');
|
||||
const [notificationMethod, setNotificationMethod] = useState<'push' | 'email' | 'both'>('push');
|
||||
|
||||
useEffect(() => {
|
||||
listAlerts();
|
||||
}, [listAlerts]);
|
||||
|
||||
const handleCreateAlert = async () => {
|
||||
if (!targetPrice) return;
|
||||
try {
|
||||
await createAlert(asset, condition, parseFloat(targetPrice), notificationMethod);
|
||||
setShowCreateModal(false);
|
||||
setTargetPrice('');
|
||||
} catch {
|
||||
// Error handled by store
|
||||
}
|
||||
};
|
||||
|
||||
const activeAlerts = alerts.filter(a => a.isEnabled && !a.isTriggered);
|
||||
const triggeredAlerts = alerts.filter(a => a.isTriggered);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -11,31 +48,125 @@ export default function AlertsDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">Get notified when tokens hit your targets</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={listAlerts}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 hover:bg-synor-700"
|
||||
>
|
||||
<Plus size={18} />
|
||||
New Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-yellow-400 mt-0.5" />
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertCircle className="text-red-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-red-200">Error</p>
|
||||
<p className="text-sm text-red-200/70">{error}</p>
|
||||
</div>
|
||||
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Alerts */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Active Alerts ({activeAlerts.length})</h3>
|
||||
{activeAlerts.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Bell size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>No active price alerts</p>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="mt-2 text-synor-400 hover:text-synor-300 text-sm"
|
||||
>
|
||||
Create your first alert
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{activeAlerts.map((alert) => (
|
||||
<div key={alert.id} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
alert.condition === 'above' ? 'bg-green-500/20' : 'bg-red-500/20'
|
||||
}`}>
|
||||
<span className={alert.condition === 'above' ? 'text-green-400' : 'text-red-400'}>
|
||||
{alert.condition === 'above' ? '↑' : '↓'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-yellow-200">Coming Soon</p>
|
||||
<p className="text-sm text-yellow-200/70">
|
||||
Set price targets and receive desktop notifications when your tokens
|
||||
reach those prices.
|
||||
<p className="font-medium">{alert.asset}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{alert.condition === 'above' ? 'Above' : 'Below'} ${alert.targetPrice.toFixed(4)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
Current: ${alert.currentPrice.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => toggleAlert(alert.id, !alert.isEnabled)}
|
||||
className="p-2 hover:bg-gray-700 rounded"
|
||||
>
|
||||
{alert.isEnabled ? (
|
||||
<ToggleRight size={20} className="text-synor-400" />
|
||||
) : (
|
||||
<ToggleLeft size={20} className="text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteAlert(alert.id)}
|
||||
className="p-2 text-red-400 hover:bg-red-500/20 rounded"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Triggered Alerts */}
|
||||
{triggeredAlerts.length > 0 && (
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Active Alerts</h3>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Bell size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>No price alerts set</p>
|
||||
<h3 className="font-medium mb-4 text-yellow-400">Triggered Alerts ({triggeredAlerts.length})</h3>
|
||||
<div className="space-y-2">
|
||||
{triggeredAlerts.map((alert) => (
|
||||
<div key={alert.id} className="p-4 bg-yellow-500/10 rounded-lg flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<Bell className="text-yellow-400" />
|
||||
<div>
|
||||
<p className="font-medium">{alert.asset}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Reached ${alert.targetPrice.toFixed(4)} on{' '}
|
||||
{alert.triggeredAt ? new Date(alert.triggeredAt * 1000).toLocaleString() : 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteAlert(alert.id)}
|
||||
className="p-2 text-gray-400 hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alert Types Info */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Alert Types</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
|
@ -48,12 +179,91 @@ export default function AlertsDashboard() {
|
|||
<p className="text-xs text-gray-500">Alert when price drops below target</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800 rounded-lg">
|
||||
<p className="font-medium text-yellow-400">% Change</p>
|
||||
<p className="text-xs text-gray-500">Alert on percentage movement</p>
|
||||
<p className="font-medium text-yellow-400">Notification Methods</p>
|
||||
<p className="text-xs text-gray-500">Push, Email, or Both</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Alert Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">Create Price Alert</h3>
|
||||
<button onClick={() => setShowCreateModal(false)} className="text-gray-400 hover:text-white">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Asset</label>
|
||||
<select
|
||||
value={asset}
|
||||
onChange={(e) => setAsset(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
>
|
||||
<option value="SYN">SYN</option>
|
||||
<option value="BTC">BTC</option>
|
||||
<option value="ETH">ETH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Condition</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCondition('above')}
|
||||
className={`flex-1 py-2 rounded-lg font-medium ${
|
||||
condition === 'above' ? 'bg-green-600 text-white' : 'bg-gray-800 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Above Price
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('below')}
|
||||
className={`flex-1 py-2 rounded-lg font-medium ${
|
||||
condition === 'below' ? 'bg-red-600 text-white' : 'bg-gray-800 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Below Price
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Target Price (USD)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={targetPrice}
|
||||
onChange={(e) => setTargetPrice(e.target.value)}
|
||||
placeholder="0.0000"
|
||||
step="0.0001"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Notification Method</label>
|
||||
<select
|
||||
value={notificationMethod}
|
||||
onChange={(e) => setNotificationMethod(e.target.value as 'push' | 'email' | 'both')}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
>
|
||||
<option value="push">Desktop Push</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateAlert}
|
||||
disabled={isLoading || !targetPrice}
|
||||
className="w-full py-3 bg-synor-600 rounded-lg font-medium hover:bg-synor-700 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Create Alert'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
|
||||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
<p className="text-sm text-gray-400">
|
||||
|
|
|
|||
|
|
@ -1,53 +1,55 @@
|
|||
import { useState } from 'react';
|
||||
import { Terminal, Info, Send } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Terminal, Info, Send, Loader2 } from 'lucide-react';
|
||||
import { useCliStore } from '../../store/cli';
|
||||
|
||||
export default function CliDashboard() {
|
||||
const {
|
||||
history,
|
||||
isExecuting,
|
||||
execute,
|
||||
loadHistory,
|
||||
clearOutput,
|
||||
navigateHistory,
|
||||
} = useCliStore();
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [history, setHistory] = useState<string[]>([
|
||||
'> Welcome to Synor CLI Mode',
|
||||
'> Type "help" for available commands',
|
||||
'>',
|
||||
]);
|
||||
const outputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleCommand = () => {
|
||||
if (!input.trim()) return;
|
||||
useEffect(() => {
|
||||
loadHistory();
|
||||
}, [loadHistory]);
|
||||
|
||||
const cmd = input.trim().toLowerCase();
|
||||
let output = '';
|
||||
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
output = `Available commands:
|
||||
help - Show this help
|
||||
balance - Show wallet balance
|
||||
address - Show wallet address
|
||||
send - Send transaction (coming soon)
|
||||
status - Network status
|
||||
clear - Clear history`;
|
||||
break;
|
||||
case 'balance':
|
||||
output = 'Balance: 0 SYN (feature coming soon)';
|
||||
break;
|
||||
case 'address':
|
||||
output = 'Primary address: synor1... (feature coming soon)';
|
||||
break;
|
||||
case 'status':
|
||||
output = 'Network: Testnet\nConnected: No\nBlock Height: -';
|
||||
break;
|
||||
case 'clear':
|
||||
setHistory(['> Cleared', '>']);
|
||||
setInput('');
|
||||
return;
|
||||
default:
|
||||
output = `Unknown command: ${cmd}. Type "help" for available commands.`;
|
||||
// Auto-scroll to bottom when history changes
|
||||
useEffect(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
setHistory([...history, `> ${input}`, output, '>']);
|
||||
const handleCommand = async () => {
|
||||
if (!input.trim() || isExecuting) return;
|
||||
const cmd = input.trim();
|
||||
setInput('');
|
||||
await execute(cmd);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCommand();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const prev = navigateHistory('up');
|
||||
if (prev !== null) setInput(prev);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const next = navigateHistory('down');
|
||||
if (next !== null) setInput(next);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-3">
|
||||
<Terminal className="text-synor-400" />
|
||||
|
|
@ -55,14 +57,47 @@ export default function CliDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">Terminal interface for power users</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearOutput}
|
||||
className="px-3 py-1 bg-gray-800 rounded text-sm hover:bg-gray-700"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
||||
<div className="p-4 font-mono text-sm bg-black/50 h-96 overflow-y-auto">
|
||||
{history.map((line, i) => (
|
||||
<div key={i} className={line.startsWith('>') ? 'text-synor-400' : 'text-gray-300'}>
|
||||
{line}
|
||||
<div
|
||||
ref={outputRef}
|
||||
className="p-4 font-mono text-sm bg-black/50 h-96 overflow-y-auto"
|
||||
>
|
||||
{/* Welcome message if no history */}
|
||||
{history.length === 0 && (
|
||||
<div className="text-gray-500">
|
||||
<p className="text-synor-400">{'>'} Welcome to Synor CLI Mode</p>
|
||||
<p className="text-synor-400">{'>'} Type "help" for available commands</p>
|
||||
<p className="text-synor-400">{'>'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Command history */}
|
||||
{history.map((result, i) => (
|
||||
<div key={i} className="mb-2">
|
||||
<div className="text-synor-400">
|
||||
{'>'} {result.command}
|
||||
</div>
|
||||
<div className={`whitespace-pre-wrap ${result.isError ? 'text-red-400' : 'text-gray-300'}`}>
|
||||
{result.output}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isExecuting && (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span>Executing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 p-3 flex gap-2">
|
||||
|
|
@ -71,25 +106,76 @@ export default function CliDashboard() {
|
|||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCommand()}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter command..."
|
||||
className="flex-1 bg-transparent text-white font-mono outline-none"
|
||||
autoFocus
|
||||
disabled={isExecuting}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCommand}
|
||||
className="p-2 bg-synor-600 rounded-lg hover:bg-synor-700"
|
||||
disabled={isExecuting || !input.trim()}
|
||||
className="p-2 bg-synor-600 rounded-lg hover:bg-synor-700 disabled:opacity-50"
|
||||
>
|
||||
<Send size={16} />
|
||||
{isExecuting ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Commands */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-3">Quick Commands</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['help', 'balance', 'address', 'status', 'utxos', 'peers'].map((cmd) => (
|
||||
<button
|
||||
key={cmd}
|
||||
onClick={() => { setInput(cmd); }}
|
||||
className="px-3 py-1 bg-gray-800 rounded font-mono text-sm hover:bg-gray-700"
|
||||
>
|
||||
{cmd}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Command Reference */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-3">Command Reference</h3>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="font-mono text-synor-400">help</div>
|
||||
<div className="text-gray-400">Show all available commands</div>
|
||||
|
||||
<div className="font-mono text-synor-400">balance</div>
|
||||
<div className="text-gray-400">Show wallet balance</div>
|
||||
|
||||
<div className="font-mono text-synor-400">address</div>
|
||||
<div className="text-gray-400">Show wallet addresses</div>
|
||||
|
||||
<div className="font-mono text-synor-400">send <addr> <amount></div>
|
||||
<div className="text-gray-400">Send SYN to address</div>
|
||||
|
||||
<div className="font-mono text-synor-400">utxos</div>
|
||||
<div className="text-gray-400">List unspent outputs</div>
|
||||
|
||||
<div className="font-mono text-synor-400">history</div>
|
||||
<div className="text-gray-400">Transaction history</div>
|
||||
|
||||
<div className="font-mono text-synor-400">status</div>
|
||||
<div className="text-gray-400">Network status</div>
|
||||
|
||||
<div className="font-mono text-synor-400">peers</div>
|
||||
<div className="text-gray-400">Connected peers</div>
|
||||
|
||||
<div className="font-mono text-synor-400">clear</div>
|
||||
<div className="text-gray-400">Clear output</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
|
||||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
<p className="text-sm text-gray-400">
|
||||
CLI mode provides a terminal interface for advanced users who prefer keyboard-driven
|
||||
interaction. All wallet operations are available through commands.
|
||||
interaction. Use ↑/↓ arrows to navigate command history.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,51 @@
|
|||
import { ArrowUpDown, Info, AlertTriangle, Plus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArrowUpDown, Info, AlertCircle, Plus, RefreshCw, Loader2, X, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import { useLimitOrdersStore, formatAmount } from '../../store/limitOrders';
|
||||
|
||||
export default function LimitOrdersDashboard() {
|
||||
const {
|
||||
orders,
|
||||
orderBook,
|
||||
isLoading,
|
||||
error,
|
||||
listOrders,
|
||||
getOrderBook,
|
||||
createOrder,
|
||||
cancelOrder,
|
||||
clearError,
|
||||
} = useLimitOrdersStore();
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [orderType, setOrderType] = useState<'buy' | 'sell'>('buy');
|
||||
const [tradingPair, setTradingPair] = useState('SYN/USDT');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
listOrders();
|
||||
getOrderBook('SYN/USDT');
|
||||
}, [listOrders, getOrderBook]);
|
||||
|
||||
const handleCreateOrder = async () => {
|
||||
if (!amount || !price) return;
|
||||
try {
|
||||
await createOrder(
|
||||
orderType,
|
||||
tradingPair,
|
||||
parseFloat(amount) * 100_000_000,
|
||||
parseFloat(price)
|
||||
);
|
||||
setShowCreateModal(false);
|
||||
setAmount('');
|
||||
setPrice('');
|
||||
} catch {
|
||||
// Error handled by store
|
||||
}
|
||||
};
|
||||
|
||||
const activeOrders = orders.filter(o => o.status === 'open');
|
||||
const filledOrders = orders.filter(o => o.status === 'filled');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -11,33 +56,217 @@ export default function LimitOrdersDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">Set buy/sell orders at specific prices</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => { listOrders(); getOrderBook('SYN/USDT'); }}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 hover:bg-synor-700"
|
||||
>
|
||||
<Plus size={18} />
|
||||
New Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-yellow-400 mt-0.5" />
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertCircle className="text-red-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-red-200">Error</p>
|
||||
<p className="text-sm text-red-200/70">{error}</p>
|
||||
</div>
|
||||
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Book */}
|
||||
{orderBook && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-3 text-green-400 flex items-center gap-2">
|
||||
<TrendingUp size={16} />
|
||||
Buy Orders ({orderBook.bids.length})
|
||||
</h3>
|
||||
{orderBook.bids.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No buy orders</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{orderBook.bids.slice(0, 5).map((bid, i) => (
|
||||
<div key={i} className="flex justify-between text-sm p-2 bg-green-500/10 rounded">
|
||||
<span className="text-green-400">${bid.price.toFixed(4)}</span>
|
||||
<span className="text-gray-400">{formatAmount(bid.amount)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-3 text-red-400 flex items-center gap-2">
|
||||
<TrendingDown size={16} />
|
||||
Sell Orders ({orderBook.asks.length})
|
||||
</h3>
|
||||
{orderBook.asks.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No sell orders</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{orderBook.asks.slice(0, 5).map((ask, i) => (
|
||||
<div key={i} className="flex justify-between text-sm p-2 bg-red-500/10 rounded">
|
||||
<span className="text-red-400">${ask.price.toFixed(4)}</span>
|
||||
<span className="text-gray-400">{formatAmount(ask.amount)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Orders */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Active Orders ({activeOrders.length})</h3>
|
||||
{activeOrders.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No active orders</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{activeOrders.map((order) => (
|
||||
<div key={order.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
order.orderType === 'buy' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
{order.orderType.toUpperCase()}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-yellow-200">Coming Soon</p>
|
||||
<p className="text-sm text-yellow-200/70">
|
||||
Limit orders let you automate trades - set your target price and the order
|
||||
executes automatically when the market reaches it.
|
||||
<p className="font-medium">{order.pair}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{formatAmount(order.amount)} @ ${order.price.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-400">
|
||||
{((order.filledAmount / order.amount) * 100).toFixed(0)}% filled
|
||||
</span>
|
||||
<button
|
||||
onClick={() => cancelOrder(order.id)}
|
||||
className="p-1 text-red-400 hover:bg-red-500/20 rounded"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Order History */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-2 text-green-400">Buy Orders</h3>
|
||||
<p className="text-sm text-gray-500">No active buy orders</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-2 text-red-400">Sell Orders</h3>
|
||||
<p className="text-sm text-gray-500">No active sell orders</p>
|
||||
<h3 className="font-medium mb-4">Filled Orders ({filledOrders.length})</h3>
|
||||
{filledOrders.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No filled orders</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filledOrders.slice(0, 10).map((order) => (
|
||||
<div key={order.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
order.orderType === 'buy' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
{order.orderType.toUpperCase()}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium">{order.pair}</p>
|
||||
<p className="text-xs text-gray-500">{formatAmount(order.amount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-green-400 text-sm">Filled</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create Order Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">Create Limit Order</h3>
|
||||
<button onClick={() => setShowCreateModal(false)} className="text-gray-400 hover:text-white">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setOrderType('buy')}
|
||||
className={`flex-1 py-2 rounded-lg font-medium ${
|
||||
orderType === 'buy' ? 'bg-green-600 text-white' : 'bg-gray-800 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOrderType('sell')}
|
||||
className={`flex-1 py-2 rounded-lg font-medium ${
|
||||
orderType === 'sell' ? 'bg-red-600 text-white' : 'bg-gray-800 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Trading Pair</label>
|
||||
<select
|
||||
value={tradingPair}
|
||||
onChange={(e) => setTradingPair(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
>
|
||||
<option value="SYN/USDT">SYN/USDT</option>
|
||||
<option value="SYN/BTC">SYN/BTC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Amount (SYN)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Price (USD)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
placeholder="0.0000"
|
||||
step="0.0001"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateOrder}
|
||||
disabled={isLoading || !amount || !price}
|
||||
className={`w-full py-3 rounded-lg font-medium disabled:opacity-50 ${
|
||||
orderType === 'buy' ? 'bg-green-600 hover:bg-green-700' : 'bg-red-600 hover:bg-red-700'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : `Place ${orderType.toUpperCase()} Order`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
|
||||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,60 @@
|
|||
import { Shuffle, Info, AlertTriangle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Shuffle, Info, AlertCircle, RefreshCw, Loader2, X } from 'lucide-react';
|
||||
import { useMixerStore, formatAmount, formatDenomination } from '../../store/mixer';
|
||||
|
||||
export default function MixerDashboard() {
|
||||
const {
|
||||
denominations,
|
||||
poolStatus,
|
||||
requests,
|
||||
isLoading,
|
||||
error,
|
||||
loadDenominations,
|
||||
getPoolStatus,
|
||||
createRequest,
|
||||
listRequests,
|
||||
cancelRequest,
|
||||
clearError,
|
||||
} = useMixerStore();
|
||||
|
||||
const [selectedDenom, setSelectedDenom] = useState<number | null>(null);
|
||||
const [outputAddress, setOutputAddress] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadDenominations();
|
||||
listRequests();
|
||||
}, [loadDenominations, listRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load pool status for first denomination when available
|
||||
if (denominations.length > 0 && !selectedDenom) {
|
||||
setSelectedDenom(denominations[0]);
|
||||
getPoolStatus(denominations[0]);
|
||||
}
|
||||
}, [denominations, selectedDenom, getPoolStatus]);
|
||||
|
||||
const handleSelectDenom = (denom: number) => {
|
||||
setSelectedDenom(denom);
|
||||
getPoolStatus(denom);
|
||||
};
|
||||
|
||||
const handleCreateRequest = async () => {
|
||||
if (!selectedDenom || !outputAddress) return;
|
||||
try {
|
||||
await createRequest(selectedDenom, selectedDenom, outputAddress);
|
||||
setOutputAddress('');
|
||||
} catch {
|
||||
// Error handled by store
|
||||
}
|
||||
};
|
||||
|
||||
const pendingRequests = requests.filter(r => r.status === 'pending' || r.status === 'mixing');
|
||||
const completedRequests = requests.filter(r => r.status === 'completed');
|
||||
const currentPool = selectedDenom ? poolStatus[selectedDenom] : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-3">
|
||||
<Shuffle className="text-synor-400" />
|
||||
|
|
@ -10,36 +62,189 @@ export default function MixerDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">Enhanced privacy through coin mixing</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { loadDenominations(); listRequests(); }}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-yellow-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-yellow-200">Coming Soon</p>
|
||||
<p className="text-sm text-yellow-200/70">
|
||||
Transaction mixing will allow you to break the link between source and destination
|
||||
addresses, enhancing privacy for your transactions.
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertCircle className="text-red-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-red-200">Error</p>
|
||||
<p className="text-sm text-red-200/70">{error}</p>
|
||||
</div>
|
||||
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Denomination Selection */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-3">Select Mixing Denomination</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{denominations.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Loading denominations...</p>
|
||||
) : (
|
||||
denominations.map((denom) => (
|
||||
<button
|
||||
key={denom}
|
||||
onClick={() => handleSelectDenom(denom)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||
selectedDenom === denom
|
||||
? 'bg-synor-600 text-white'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{formatDenomination(denom)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pool Status */}
|
||||
{currentPool && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Denomination</p>
|
||||
<p className="text-xl font-bold">{formatDenomination(currentPool.denomination)}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Participants</p>
|
||||
<p className="text-xl font-bold">{currentPool.participants}/{currentPool.requiredParticipants}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Status</p>
|
||||
<p className={`text-xl font-bold ${
|
||||
currentPool.status === 'mixing' ? 'text-yellow-400' :
|
||||
currentPool.status === 'completed' ? 'text-green-400' : 'text-gray-400'
|
||||
}`}>
|
||||
{currentPool.status.charAt(0).toUpperCase() + currentPool.status.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Est. Time</p>
|
||||
<p className="text-xl font-bold text-synor-400">
|
||||
{currentPool.estimatedTimeSecs ? `${Math.ceil(currentPool.estimatedTimeSecs / 60)}m` : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Mix Request */}
|
||||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Join Mixing Pool</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Selected Amount</label>
|
||||
<div className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-lg font-bold">
|
||||
{selectedDenom ? formatDenomination(selectedDenom) : 'Select a denomination above'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Output Address</label>
|
||||
<input
|
||||
type="text"
|
||||
value={outputAddress}
|
||||
onChange={(e) => setOutputAddress(e.target.value)}
|
||||
placeholder="synor1... (fresh address for privacy)"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-synor-500 outline-none font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Use a new address that hasn't received funds before</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateRequest}
|
||||
disabled={isLoading || !selectedDenom || !outputAddress}
|
||||
className="w-full py-3 bg-synor-600 rounded-lg font-medium hover:bg-synor-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <Shuffle size={18} />}
|
||||
Join Pool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Requests */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4 text-yellow-400">Pending Mixes ({pendingRequests.length})</h3>
|
||||
{pendingRequests.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No pending mix requests</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{pendingRequests.map((req) => (
|
||||
<div key={req.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div>
|
||||
<p className="font-mono text-sm">{formatAmount(req.amount)}</p>
|
||||
<p className="text-xs text-gray-500">Status: {req.status}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
req.status === 'mixing' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-gray-700 text-gray-400'
|
||||
}`}>
|
||||
{req.status}
|
||||
</span>
|
||||
{req.status === 'pending' && (
|
||||
<button
|
||||
onClick={() => cancelRequest(req.id)}
|
||||
className="p-1 text-red-400 hover:bg-red-500/20 rounded"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Completed Requests */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4 text-green-400">Completed Mixes ({completedRequests.length})</h3>
|
||||
{completedRequests.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No completed mixes</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{completedRequests.map((req) => (
|
||||
<div key={req.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div>
|
||||
<p className="font-mono text-sm">{formatAmount(req.amount)}</p>
|
||||
<p className="text-xs text-gray-500 font-mono truncate max-w-xs">→ {req.outputAddress}</p>
|
||||
{req.txId && (
|
||||
<p className="text-xs text-synor-400 font-mono truncate max-w-xs">TX: {req.txId}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs">
|
||||
Complete
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* How It Works */}
|
||||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">How Mixing Works</h3>
|
||||
<ol className="space-y-3 text-sm text-gray-400">
|
||||
<li className="flex gap-3">
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">1</span>
|
||||
<span>Deposit funds into the mixing pool</span>
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">1</span>
|
||||
<span>Select a denomination and provide an output address</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">2</span>
|
||||
<span>Your funds are combined with others</span>
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">2</span>
|
||||
<span>Wait for enough participants to join the pool</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">3</span>
|
||||
<span>After a delay, withdraw to a fresh address</span>
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">3</span>
|
||||
<span>All participants' coins are mixed together cryptographically</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">4</span>
|
||||
<span>Transaction history is broken</span>
|
||||
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">4</span>
|
||||
<span>Receive coins at your output address with broken transaction history</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
@ -48,7 +253,7 @@ export default function MixerDashboard() {
|
|||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
<p className="text-sm text-gray-400">
|
||||
Mixing uses cryptographic techniques to make transactions untraceable while remaining
|
||||
fully on-chain and trustless.
|
||||
fully on-chain and trustless. Fixed denominations ensure all outputs look identical.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,48 @@
|
|||
import { PieChart, Info, AlertTriangle, Download } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PieChart, Info, AlertCircle, Download, RefreshCw, Loader2, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import { usePortfolioStore, formatUSD, formatAmount } from '../../store/portfolio';
|
||||
|
||||
export default function PortfolioDashboard() {
|
||||
const {
|
||||
summary,
|
||||
holdings,
|
||||
taxReport,
|
||||
isLoading,
|
||||
error,
|
||||
loadSummary,
|
||||
loadHoldings,
|
||||
loadTaxReport,
|
||||
exportTaxReport,
|
||||
clearError,
|
||||
} = usePortfolioStore();
|
||||
|
||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
||||
|
||||
useEffect(() => {
|
||||
loadSummary();
|
||||
loadHoldings();
|
||||
}, [loadSummary, loadHoldings]);
|
||||
|
||||
const handleLoadTaxReport = async () => {
|
||||
await loadTaxReport(selectedYear);
|
||||
};
|
||||
|
||||
const handleExportTaxReport = async () => {
|
||||
try {
|
||||
const data = await exportTaxReport(selectedYear, 'csv');
|
||||
// Create a download link
|
||||
const blob = new Blob([data], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `tax-report-${selectedYear}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
// Error handled by store
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -11,62 +53,184 @@ export default function PortfolioDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">P&L tracking and tax reports</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-gray-800 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => { loadSummary(); loadHoldings(); }}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportTaxReport}
|
||||
className="px-4 py-2 bg-gray-800 rounded-lg flex items-center gap-2 hover:bg-gray-700"
|
||||
>
|
||||
<Download size={18} />
|
||||
Export Report
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-yellow-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-yellow-200">Coming Soon</p>
|
||||
<p className="text-sm text-yellow-200/70">
|
||||
Track your profit & loss, view portfolio allocation, and generate tax reports
|
||||
for your crypto holdings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertCircle className="text-red-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-red-200">Error</p>
|
||||
<p className="text-sm text-red-200/70">{error}</p>
|
||||
</div>
|
||||
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Stats */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Total Value</p>
|
||||
<p className="text-2xl font-bold">$0.00</p>
|
||||
<p className="text-2xl font-bold">{formatUSD(summary.totalValueUsd)}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">24h Change</p>
|
||||
<p className="text-2xl font-bold text-gray-500">0%</p>
|
||||
<p className={`text-2xl font-bold flex items-center gap-1 ${
|
||||
summary.dayChangePercent >= 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{summary.dayChangePercent >= 0 ? <TrendingUp size={20} /> : <TrendingDown size={20} />}
|
||||
{summary.dayChangePercent >= 0 ? '+' : ''}{summary.dayChangePercent.toFixed(2)}%
|
||||
</p>
|
||||
<p className={`text-xs ${summary.dayChangeUsd >= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
|
||||
{formatUSD(summary.dayChangeUsd)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Unrealized P&L</p>
|
||||
<p className="text-2xl font-bold text-gray-500">$0.00</p>
|
||||
<p className="text-sm text-gray-400 mb-1">Total P&L</p>
|
||||
<p className={`text-2xl font-bold ${
|
||||
summary.totalPnlUsd >= 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{summary.totalPnlUsd >= 0 ? '+' : ''}{formatUSD(summary.totalPnlUsd)}
|
||||
</p>
|
||||
<p className={`text-xs ${summary.totalPnlPercent >= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
|
||||
{summary.totalPnlPercent >= 0 ? '+' : ''}{summary.totalPnlPercent.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<p className="text-sm text-gray-400 mb-1">Realized P&L</p>
|
||||
<p className="text-2xl font-bold text-gray-500">$0.00</p>
|
||||
<p className="text-sm text-gray-400 mb-1">Cost Basis</p>
|
||||
<p className="text-2xl font-bold">{formatUSD(summary.totalCostBasisUsd)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Holdings */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Asset Holdings ({holdings.length})</h3>
|
||||
{holdings.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No holdings found</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{holdings.map((holding) => (
|
||||
<div key={holding.asset} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-synor-600 rounded-full flex items-center justify-center font-bold">
|
||||
{holding.symbol.substring(0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{holding.asset}</p>
|
||||
<p className="text-sm text-gray-500">{holding.balanceFormatted}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{formatUSD(holding.valueUsd)}</p>
|
||||
<p className={`text-sm ${holding.pnlPercent >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{holding.pnlPercent >= 0 ? '+' : ''}{holding.pnlPercent.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-400">Allocation</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-synor-500 rounded-full"
|
||||
style={{ width: `${holding.allocationPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm">{holding.allocationPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Tax Report Section */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Asset Allocation</h3>
|
||||
<div className="h-48 flex items-center justify-center text-gray-500">
|
||||
Chart coming soon
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-medium">Tax Report</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="px-3 py-1 bg-gray-800 border border-gray-700 rounded-lg text-sm"
|
||||
>
|
||||
{[2026, 2025, 2024, 2023].map(year => (
|
||||
<option key={year} value={year}>{year}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={handleLoadTaxReport}
|
||||
className="px-3 py-1 bg-synor-600 rounded-lg text-sm hover:bg-synor-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin" size={14} /> : 'Load'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Performance History</h3>
|
||||
<div className="h-48 flex items-center justify-center text-gray-500">
|
||||
Chart coming soon
|
||||
{taxReport.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-gray-500 mb-2">Select a year and click Load to generate your tax report</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-6 text-xs text-gray-500 p-2">
|
||||
<span>Date</span>
|
||||
<span>Type</span>
|
||||
<span>Asset</span>
|
||||
<span>Amount</span>
|
||||
<span>Total</span>
|
||||
<span>Gain/Loss</span>
|
||||
</div>
|
||||
{taxReport.slice(0, 10).map((tx) => (
|
||||
<div key={tx.id} className="grid grid-cols-6 text-sm p-2 bg-gray-800 rounded items-center">
|
||||
<span>{new Date(tx.timestamp * 1000).toLocaleDateString()}</span>
|
||||
<span className={
|
||||
tx.txType === 'sell' ? 'text-red-400' :
|
||||
tx.txType === 'buy' ? 'text-green-400' :
|
||||
'text-yellow-400'
|
||||
}>
|
||||
{tx.txType.toUpperCase()}
|
||||
</span>
|
||||
<span>{tx.asset}</span>
|
||||
<span>{formatAmount(tx.amount)}</span>
|
||||
<span>{formatUSD(tx.totalUsd)}</span>
|
||||
<span className={tx.gainLossUsd && tx.gainLossUsd >= 0 ? 'text-green-400' : 'text-red-400'}>
|
||||
{tx.gainLossUsd !== undefined ? formatUSD(tx.gainLossUsd) : '--'}
|
||||
{tx.isLongTerm && <span className="text-xs ml-1">(LT)</span>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{taxReport.length > 10 && (
|
||||
<p className="text-center text-sm text-gray-500 pt-2">
|
||||
Showing 10 of {taxReport.length} transactions. Export to see all.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
|
||||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
<p className="text-sm text-gray-400">
|
||||
Portfolio analytics helps you understand your investment performance and simplifies
|
||||
tax reporting with exportable transaction history.
|
||||
tax reporting with exportable transaction history. LT = Long-term capital gains.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Vault as VaultIcon,
|
||||
Plus,
|
||||
Clock,
|
||||
Lock,
|
||||
Unlock,
|
||||
Trash2,
|
||||
|
|
@ -13,6 +11,9 @@ import {
|
|||
Timer,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
|
||||
// Using Timer as VaultIcon since lucide-react doesn't export Vault
|
||||
const VaultIcon = Timer;
|
||||
import {
|
||||
useVaultsStore,
|
||||
Vault,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,51 @@
|
|||
import { TrendingUp, Info, AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { TrendingUp, Info, AlertCircle, RefreshCw, Loader2, Plus, Percent, X } from 'lucide-react';
|
||||
import { useYieldStore, formatAmount } from '../../store/yield';
|
||||
|
||||
export default function YieldDashboard() {
|
||||
const {
|
||||
opportunities,
|
||||
positions,
|
||||
isLoading,
|
||||
error,
|
||||
loadOpportunities,
|
||||
listPositions,
|
||||
deposit,
|
||||
withdraw,
|
||||
clearError,
|
||||
} = useYieldStore();
|
||||
|
||||
const [selectedOpportunity, setSelectedOpportunity] = useState<string | null>(null);
|
||||
const [depositAmount, setDepositAmount] = useState('');
|
||||
const [autoCompound, setAutoCompound] = useState(true);
|
||||
const [showDepositModal, setShowDepositModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadOpportunities();
|
||||
listPositions();
|
||||
}, [loadOpportunities, listPositions]);
|
||||
|
||||
const handleDeposit = async () => {
|
||||
if (!selectedOpportunity || !depositAmount) return;
|
||||
try {
|
||||
await deposit(selectedOpportunity, parseFloat(depositAmount) * 100_000_000, autoCompound);
|
||||
setShowDepositModal(false);
|
||||
setDepositAmount('');
|
||||
setSelectedOpportunity(null);
|
||||
} catch {
|
||||
// Error handled by store
|
||||
}
|
||||
};
|
||||
|
||||
const totalDeposited = positions.reduce((sum, p) => sum + p.depositedAmount, 0);
|
||||
const totalEarned = positions.reduce((sum, p) => sum + p.rewardsEarned, 0);
|
||||
const bestApy = opportunities.length > 0
|
||||
? Math.max(...opportunities.map(o => o.apy))
|
||||
: 0;
|
||||
|
||||
// Helper to get opportunity details for a position
|
||||
const getOpportunity = (oppId: string) => opportunities.find(o => o.id === oppId);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -11,43 +56,193 @@ export default function YieldDashboard() {
|
|||
</h1>
|
||||
<p className="text-gray-400 mt-1">Auto-compound and find the best APY</p>
|
||||
</div>
|
||||
<button className="p-2 bg-gray-800 rounded-lg">
|
||||
<RefreshCw size={18} />
|
||||
<button
|
||||
onClick={() => { loadOpportunities(); listPositions(); }}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-yellow-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-yellow-200">Coming Soon</p>
|
||||
<p className="text-sm text-yellow-200/70">
|
||||
The yield aggregator automatically finds the best yields across DeFi protocols
|
||||
and compounds your earnings.
|
||||
</p>
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
|
||||
<AlertCircle className="text-red-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-red-200">Error</p>
|
||||
<p className="text-sm text-red-200/70">{error}</p>
|
||||
</div>
|
||||
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
|
||||
<p className="text-sm text-gray-400 mb-1">Total Deposited</p>
|
||||
<p className="text-2xl font-bold">0 SYN</p>
|
||||
<p className="text-2xl font-bold">{formatAmount(totalDeposited)}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
|
||||
<p className="text-sm text-gray-400 mb-1">Total Earned</p>
|
||||
<p className="text-2xl font-bold text-green-400">0 SYN</p>
|
||||
<p className="text-2xl font-bold text-green-400">{formatAmount(totalEarned)}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
|
||||
<p className="text-sm text-gray-400 mb-1">Best APY</p>
|
||||
<p className="text-2xl font-bold text-synor-400">--%</p>
|
||||
<p className="text-2xl font-bold text-synor-400">{bestApy.toFixed(2)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Positions */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Available Strategies</h3>
|
||||
<p className="text-sm text-gray-500 text-center py-4">
|
||||
No yield strategies available yet
|
||||
</p>
|
||||
<h3 className="font-medium mb-4">Your Positions ({positions.length})</h3>
|
||||
{positions.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No active positions</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{positions.map((position) => {
|
||||
const opp = getOpportunity(position.opportunityId);
|
||||
return (
|
||||
<div key={position.id} className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<p className="font-medium">{opp?.protocol || 'Unknown Protocol'}</p>
|
||||
<p className="text-xs text-gray-500">{opp?.asset || 'Unknown Asset'}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{position.autoCompound && (
|
||||
<span className="px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">Auto</span>
|
||||
)}
|
||||
<span className="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs flex items-center gap-1">
|
||||
<Percent size={12} />
|
||||
{opp?.apy.toFixed(2) || '--'}% APY
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">Deposited</p>
|
||||
<p className="font-mono">{formatAmount(position.depositedAmount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Current Value</p>
|
||||
<p className="font-mono">{formatAmount(position.currentValue)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Earned</p>
|
||||
<p className="font-mono text-green-400">+{formatAmount(position.rewardsEarned)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => withdraw(position.id)}
|
||||
className="mt-3 w-full py-2 bg-gray-700 rounded text-sm hover:bg-gray-600"
|
||||
>
|
||||
Withdraw All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Available Strategies */}
|
||||
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||
<h3 className="font-medium mb-4">Available Strategies ({opportunities.length})</h3>
|
||||
{opportunities.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No yield strategies available yet</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{opportunities.map((opp) => (
|
||||
<div key={opp.id} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium">{opp.protocol}</p>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
opp.riskLevel === 'low' ? 'bg-green-500/20 text-green-400' :
|
||||
opp.riskLevel === 'medium' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
{opp.riskLevel} risk
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{opp.asset}</p>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
<span>TVL: ${(opp.tvl / 1_000_000).toFixed(2)}M</span>
|
||||
{opp.lockupPeriodDays > 0 && (
|
||||
<span className="ml-2">• {opp.lockupPeriodDays}d lockup</span>
|
||||
)}
|
||||
<span className="ml-2">• Min: {formatAmount(opp.minDeposit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-green-400">{opp.apy.toFixed(2)}%</p>
|
||||
<p className="text-xs text-gray-500">APY</p>
|
||||
<button
|
||||
onClick={() => { setSelectedOpportunity(opp.id); setShowDepositModal(true); }}
|
||||
className="mt-2 px-4 py-1 bg-synor-600 rounded text-sm hover:bg-synor-700 flex items-center gap-1"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Deposit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Deposit Modal */}
|
||||
{showDepositModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">Deposit to Yield Strategy</h3>
|
||||
<button onClick={() => setShowDepositModal(false)} className="text-gray-400 hover:text-white">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Amount (SYN)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={depositAmount}
|
||||
onChange={(e) => setDepositAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoCompound"
|
||||
checked={autoCompound}
|
||||
onChange={(e) => setAutoCompound(e.target.checked)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<label htmlFor="autoCompound" className="text-sm text-gray-400">
|
||||
Enable auto-compounding
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowDepositModal(false)}
|
||||
className="flex-1 py-2 bg-gray-700 rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeposit}
|
||||
disabled={isLoading || !depositAmount}
|
||||
className="flex-1 py-2 bg-synor-600 rounded-lg disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Deposit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
|
||||
<Info className="text-gray-500 mt-0.5" size={18} />
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function transformDecoy(data: any): DecoyWallet {
|
|||
};
|
||||
}
|
||||
|
||||
export const useDecoyStore = create<DecoyState>()((set, get) => ({
|
||||
export const useDecoyStore = create<DecoyState>()((set) => ({
|
||||
isEnabled: false,
|
||||
decoys: [],
|
||||
isLoading: false,
|
||||
|
|
|
|||
|
|
@ -130,3 +130,9 @@ export const useLimitOrdersStore = create<LimitOrdersState & LimitOrdersActions>
|
|||
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
|
||||
// Helper to format sompi to SYN
|
||||
export const formatAmount = (sompi: number): string => {
|
||||
const syn = sompi / 100_000_000;
|
||||
return `${syn.toFixed(8)} SYN`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const transformPoolStatus = (data: Record<string, unknown>): MixPoolStatus => ({
|
|||
estimatedTimeSecs: data.estimated_time_secs as number | undefined,
|
||||
});
|
||||
|
||||
export const useMixerStore = create<MixerState & MixerActions>((set, get) => ({
|
||||
export const useMixerStore = create<MixerState & MixerActions>((set) => ({
|
||||
denominations: [],
|
||||
poolStatus: {},
|
||||
requests: [],
|
||||
|
|
@ -155,3 +155,9 @@ export const formatDenomination = (sompi: number): string => {
|
|||
const syn = sompi / 100_000_000;
|
||||
return `${syn} SYN`;
|
||||
};
|
||||
|
||||
// Helper to format sompi to SYN
|
||||
export const formatAmount = (sompi: number): string => {
|
||||
const syn = sompi / 100_000_000;
|
||||
return `${syn.toFixed(8)} SYN`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -178,3 +178,12 @@ export const formatPercent = (value: number): string => {
|
|||
const sign = value >= 0 ? '+' : '';
|
||||
return `${sign}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// Alias for formatting USD
|
||||
export const formatCurrency = formatUSD;
|
||||
|
||||
// Helper to format sompi to SYN
|
||||
export const formatAmount = (sompi: number): string => {
|
||||
const syn = sompi / 100_000_000;
|
||||
return `${syn.toFixed(8)} SYN`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function transformRequest(data: any): RecoveryRequest {
|
|||
};
|
||||
}
|
||||
|
||||
export const useRecoveryStore = create<RecoveryState>()((set, get) => ({
|
||||
export const useRecoveryStore = create<RecoveryState>()((set) => ({
|
||||
// Initial state
|
||||
config: null,
|
||||
requests: [],
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const transformProfile = (data: Record<string, unknown>): RpcProfile => ({
|
|||
isHealthy: data.is_healthy as boolean,
|
||||
});
|
||||
|
||||
export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set, get) => ({
|
||||
export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set) => ({
|
||||
profiles: [],
|
||||
activeProfile: null,
|
||||
isLoading: false,
|
||||
|
|
|
|||
|
|
@ -158,3 +158,9 @@ export const formatTVL = (sompi: number): string => {
|
|||
if (syn >= 1_000) return `${(syn / 1_000).toFixed(2)}K SYN`;
|
||||
return `${syn.toFixed(2)} SYN`;
|
||||
};
|
||||
|
||||
// Helper to format sompi to SYN
|
||||
export const formatAmount = (sompi: number): string => {
|
||||
const syn = sompi / 100_000_000;
|
||||
return `${syn.toFixed(8)} SYN`;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue