This commit is contained in:
Gulshan Yadav 2026-02-02 15:00:13 +05:30
parent c32622f34f
commit f08eb965c2
15 changed files with 1306 additions and 189 deletions

View file

@ -33,7 +33,7 @@ import {
Eye, Eye,
ListPlus, ListPlus,
Activity, Activity,
Vault, Timer,
ShieldCheck, ShieldCheck,
// Phase 7-16 icons // Phase 7-16 icons
UserX, UserX,
@ -97,7 +97,7 @@ const governanceNavItems = [
const toolsNavItems = [ const toolsNavItems = [
{ to: '/watch-only', label: 'Watch-Only', icon: Eye }, { to: '/watch-only', label: 'Watch-Only', icon: Eye },
{ to: '/fee-analytics', label: 'Fee Analytics', icon: Activity }, { 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: '/recovery', label: 'Recovery', icon: ShieldCheck },
{ to: '/decoy', label: 'Decoy Wallets', icon: UserX }, { to: '/decoy', label: 'Decoy Wallets', icon: UserX },
{ to: '/mixer', label: 'Mixer', icon: Shuffle }, { to: '/mixer', label: 'Mixer', icon: Shuffle },

View file

@ -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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -11,31 +48,125 @@ export default function AlertsDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">Get notified when tokens hit your targets</p> <p className="text-gray-400 mt-1">Get notified when tokens hit your targets</p>
</div> </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} /> <Plus size={18} />
New Alert New Alert
</button> </button>
</div> </div>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3"> {error && (
<AlertTriangle className="text-yellow-400 mt-0.5" /> <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> <div>
<p className="font-medium text-yellow-200">Coming Soon</p> <p className="font-medium">{alert.asset}</p>
<p className="text-sm text-yellow-200/70"> <p className="text-sm text-gray-500">
Set price targets and receive desktop notifications when your tokens {alert.condition === 'above' ? 'Above' : 'Below'} ${alert.targetPrice.toFixed(4)}
reach those prices. </p>
<p className="text-xs text-gray-600">
Current: ${alert.currentPrice.toFixed(4)}
</p> </p>
</div> </div>
</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"> <div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Active Alerts</h3> <h3 className="font-medium mb-4 text-yellow-400">Triggered Alerts ({triggeredAlerts.length})</h3>
<div className="text-center py-8 text-gray-500"> <div className="space-y-2">
<Bell size={32} className="mx-auto mb-2 opacity-50" /> {triggeredAlerts.map((alert) => (
<p>No price alerts set</p> <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>
</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"> <div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Alert Types</h3> <h3 className="font-medium mb-4">Alert Types</h3>
<div className="grid grid-cols-3 gap-4"> <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> <p className="text-xs text-gray-500">Alert when price drops below target</p>
</div> </div>
<div className="p-3 bg-gray-800 rounded-lg"> <div className="p-3 bg-gray-800 rounded-lg">
<p className="font-medium text-yellow-400">% Change</p> <p className="font-medium text-yellow-400">Notification Methods</p>
<p className="text-xs text-gray-500">Alert on percentage movement</p> <p className="text-xs text-gray-500">Push, Email, or Both</p>
</div> </div>
</div> </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"> <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} /> <Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">

View file

@ -1,53 +1,55 @@
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Terminal, Info, Send } from 'lucide-react'; import { Terminal, Info, Send, Loader2 } from 'lucide-react';
import { useCliStore } from '../../store/cli';
export default function CliDashboard() { export default function CliDashboard() {
const {
history,
isExecuting,
execute,
loadHistory,
clearOutput,
navigateHistory,
} = useCliStore();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [history, setHistory] = useState<string[]>([ const outputRef = useRef<HTMLDivElement>(null);
'> Welcome to Synor CLI Mode',
'> Type "help" for available commands',
'>',
]);
const handleCommand = () => { useEffect(() => {
if (!input.trim()) return; loadHistory();
}, [loadHistory]);
const cmd = input.trim().toLowerCase(); // Auto-scroll to bottom when history changes
let output = ''; useEffect(() => {
if (outputRef.current) {
switch (cmd) { outputRef.current.scrollTop = outputRef.current.scrollHeight;
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.`;
} }
}, [history]);
setHistory([...history, `> ${input}`, output, '>']); const handleCommand = async () => {
if (!input.trim() || isExecuting) return;
const cmd = input.trim();
setInput(''); 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-3"> <h1 className="text-2xl font-bold flex items-center gap-3">
<Terminal className="text-synor-400" /> <Terminal className="text-synor-400" />
@ -55,14 +57,47 @@ export default function CliDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">Terminal interface for power users</p> <p className="text-gray-400 mt-1">Terminal interface for power users</p>
</div> </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="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"> <div
{history.map((line, i) => ( ref={outputRef}
<div key={i} className={line.startsWith('>') ? 'text-synor-400' : 'text-gray-300'}> className="p-4 font-mono text-sm bg-black/50 h-96 overflow-y-auto"
{line} >
{/* 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> </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>
<div className="border-t border-gray-800 p-3 flex gap-2"> <div className="border-t border-gray-800 p-3 flex gap-2">
@ -71,25 +106,76 @@ export default function CliDashboard() {
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCommand()} onKeyDown={handleKeyDown}
placeholder="Enter command..." placeholder="Enter command..."
className="flex-1 bg-transparent text-white font-mono outline-none" className="flex-1 bg-transparent text-white font-mono outline-none"
autoFocus autoFocus
disabled={isExecuting}
/> />
<button <button
onClick={handleCommand} 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> </button>
</div> </div>
</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 &lt;addr&gt; &lt;amount&gt;</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"> <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} /> <Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
CLI mode provides a terminal interface for advanced users who prefer keyboard-driven 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> </p>
</div> </div>
</div> </div>

View file

@ -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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -11,33 +56,217 @@ export default function LimitOrdersDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">Set buy/sell orders at specific prices</p> <p className="text-gray-400 mt-1">Set buy/sell orders at specific prices</p>
</div> </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} /> <Plus size={18} />
New Order New Order
</button> </button>
</div> </div>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3"> {error && (
<AlertTriangle className="text-yellow-400 mt-0.5" /> <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> <div>
<p className="font-medium text-yellow-200">Coming Soon</p> <p className="font-medium">{order.pair}</p>
<p className="text-sm text-yellow-200/70"> <p className="text-xs text-gray-500">
Limit orders let you automate trades - set your target price and the order {formatAmount(order.amount)} @ ${order.price.toFixed(4)}
executes automatically when the market reaches it.
</p> </p>
</div> </div>
</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"> <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> <h3 className="font-medium mb-4">Filled Orders ({filledOrders.length})</h3>
<p className="text-sm text-gray-500">No active buy orders</p> {filledOrders.length === 0 ? (
</div> <p className="text-sm text-gray-500 text-center py-4">No filled orders</p>
<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> <div className="space-y-2">
<p className="text-sm text-gray-500">No active sell orders</p> {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>
</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"> <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} /> <Info className="text-gray-500 mt-0.5" size={18} />

View file

@ -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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-3"> <h1 className="text-2xl font-bold flex items-center gap-3">
<Shuffle className="text-synor-400" /> <Shuffle className="text-synor-400" />
@ -10,36 +62,189 @@ export default function MixerDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">Enhanced privacy through coin mixing</p> <p className="text-gray-400 mt-1">Enhanced privacy through coin mixing</p>
</div> </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"> {error && (
<AlertTriangle className="text-yellow-400 mt-0.5" /> <div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<div> <AlertCircle className="text-red-400 mt-0.5" />
<p className="font-medium text-yellow-200">Coming Soon</p> <div className="flex-1">
<p className="text-sm text-yellow-200/70"> <p className="font-medium text-red-200">Error</p>
Transaction mixing will allow you to break the link between source and destination <p className="text-sm text-red-200/70">{error}</p>
addresses, enhancing privacy for your transactions. </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> </p>
</div> </div>
</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"> <div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h3 className="font-medium mb-4">How Mixing Works</h3> <h3 className="font-medium mb-4">How Mixing Works</h3>
<ol className="space-y-3 text-sm text-gray-400"> <ol className="space-y-3 text-sm text-gray-400">
<li className="flex gap-3"> <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 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>Deposit funds into the mixing pool</span> <span>Select a denomination and provide an output address</span>
</li> </li>
<li className="flex gap-3"> <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 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>Your funds are combined with others</span> <span>Wait for enough participants to join the pool</span>
</li> </li>
<li className="flex gap-3"> <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 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>After a delay, withdraw to a fresh address</span> <span>All participants' coins are mixed together cryptographically</span>
</li> </li>
<li className="flex gap-3"> <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 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>Transaction history is broken</span> <span>Receive coins at your output address with broken transaction history</span>
</li> </li>
</ol> </ol>
</div> </div>
@ -48,7 +253,7 @@ export default function MixerDashboard() {
<Info className="text-gray-500 mt-0.5" size={18} /> <Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
Mixing uses cryptographic techniques to make transactions untraceable while remaining 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> </p>
</div> </div>
</div> </div>

View file

@ -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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -11,62 +53,184 @@ export default function PortfolioDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">P&L tracking and tax reports</p> <p className="text-gray-400 mt-1">P&L tracking and tax reports</p>
</div> </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} /> <Download size={18} />
Export Report Export CSV
</button> </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" />
<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> </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="grid grid-cols-4 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> <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-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>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> <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-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>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> <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-sm text-gray-400 mb-1">Total P&L</p>
<p className="text-2xl font-bold text-gray-500">$0.00</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>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> <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-sm text-gray-400 mb-1">Cost Basis</p>
<p className="text-2xl font-bold text-gray-500">$0.00</p> <p className="text-2xl font-bold">{formatUSD(summary.totalCostBasisUsd)}</p>
</div> </div>
</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"> <div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Asset Allocation</h3> <div className="flex justify-between items-center mb-4">
<div className="h-48 flex items-center justify-center text-gray-500"> <h3 className="font-medium">Tax Report</h3>
Chart coming soon <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> </div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> {taxReport.length === 0 ? (
<h3 className="font-medium mb-4">Performance History</h3> <div className="text-center py-4">
<div className="h-48 flex items-center justify-center text-gray-500"> <p className="text-sm text-gray-500 mb-2">Select a year and click Load to generate your tax report</p>
Chart coming soon
</div> </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> </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>
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3"> <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} /> <Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
Portfolio analytics helps you understand your investment performance and simplifies 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> </p>
</div> </div>
</div> </div>

View file

@ -1,8 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Vault as VaultIcon,
Plus, Plus,
Clock,
Lock, Lock,
Unlock, Unlock,
Trash2, Trash2,
@ -13,6 +11,9 @@ import {
Timer, Timer,
Info, Info,
} from 'lucide-react'; } from 'lucide-react';
// Using Timer as VaultIcon since lucide-react doesn't export Vault
const VaultIcon = Timer;
import { import {
useVaultsStore, useVaultsStore,
Vault, Vault,

View file

@ -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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -11,43 +56,193 @@ export default function YieldDashboard() {
</h1> </h1>
<p className="text-gray-400 mt-1">Auto-compound and find the best APY</p> <p className="text-gray-400 mt-1">Auto-compound and find the best APY</p>
</div> </div>
<button className="p-2 bg-gray-800 rounded-lg"> <button
<RefreshCw size={18} /> 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> </button>
</div> </div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3"> {error && (
<AlertTriangle className="text-yellow-400 mt-0.5" /> <div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<div> <AlertCircle className="text-red-400 mt-0.5" />
<p className="font-medium text-yellow-200">Coming Soon</p> <div className="flex-1">
<p className="text-sm text-yellow-200/70"> <p className="font-medium text-red-200">Error</p>
The yield aggregator automatically finds the best yields across DeFi protocols <p className="text-sm text-red-200/70">{error}</p>
and compounds your earnings.
</p>
</div> </div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div> </div>
)}
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center"> <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-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>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center"> <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-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>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center"> <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-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>
</div> </div>
{/* Active Positions */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> <div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Available Strategies</h3> <h3 className="font-medium mb-4">Your Positions ({positions.length})</h3>
<p className="text-sm text-gray-500 text-center py-4"> {positions.length === 0 ? (
No yield strategies available yet <p className="text-sm text-gray-500 text-center py-4">No active positions</p>
</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>
<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"> <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} /> <Info className="text-gray-500 mt-0.5" size={18} />

View file

@ -49,7 +49,7 @@ function transformDecoy(data: any): DecoyWallet {
}; };
} }
export const useDecoyStore = create<DecoyState>()((set, get) => ({ export const useDecoyStore = create<DecoyState>()((set) => ({
isEnabled: false, isEnabled: false,
decoys: [], decoys: [],
isLoading: false, isLoading: false,

View file

@ -130,3 +130,9 @@ export const useLimitOrdersStore = create<LimitOrdersState & LimitOrdersActions>
clearError: () => set({ error: null }), 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`;
};

View file

@ -61,7 +61,7 @@ const transformPoolStatus = (data: Record<string, unknown>): MixPoolStatus => ({
estimatedTimeSecs: data.estimated_time_secs as number | undefined, estimatedTimeSecs: data.estimated_time_secs as number | undefined,
}); });
export const useMixerStore = create<MixerState & MixerActions>((set, get) => ({ export const useMixerStore = create<MixerState & MixerActions>((set) => ({
denominations: [], denominations: [],
poolStatus: {}, poolStatus: {},
requests: [], requests: [],
@ -155,3 +155,9 @@ export const formatDenomination = (sompi: number): string => {
const syn = sompi / 100_000_000; const syn = sompi / 100_000_000;
return `${syn} SYN`; 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`;
};

View file

@ -178,3 +178,12 @@ export const formatPercent = (value: number): string => {
const sign = value >= 0 ? '+' : ''; const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(2)}%`; 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`;
};

View file

@ -102,7 +102,7 @@ function transformRequest(data: any): RecoveryRequest {
}; };
} }
export const useRecoveryStore = create<RecoveryState>()((set, get) => ({ export const useRecoveryStore = create<RecoveryState>()((set) => ({
// Initial state // Initial state
config: null, config: null,
requests: [], requests: [],

View file

@ -44,7 +44,7 @@ const transformProfile = (data: Record<string, unknown>): RpcProfile => ({
isHealthy: data.is_healthy as boolean, isHealthy: data.is_healthy as boolean,
}); });
export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set, get) => ({ export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set) => ({
profiles: [], profiles: [],
activeProfile: null, activeProfile: null,
isLoading: false, isLoading: false,

View file

@ -158,3 +158,9 @@ export const formatTVL = (sompi: number): string => {
if (syn >= 1_000) return `${(syn / 1_000).toFixed(2)}K SYN`; if (syn >= 1_000) return `${(syn / 1_000).toFixed(2)}K SYN`;
return `${syn.toFixed(2)} 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`;
};