solvox / src /renderer /pages /SendPage.tsx
muthuk1's picture
🎨 Complete Coinbase design system rebuild: white canvas, single blue accent, pill CTAs, editorial spacing, hairline borders, dark hero bands, asset rows, mono numbers, 96px section rhythm
5f5514e verified
import React, { useState } from 'react';
import { Num, useToast } from '../components/ui/index';
interface Props { balance: { sol: number; usdt: number }; onSent: () => void; }
export default function SendPage({ balance, onSent }: Props) {
const [token, setToken] = useState<'SOL' | 'USDT'>('SOL');
const [to, setTo] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState<any>(null);
const [step, setStep] = useState<'form' | 'confirm' | 'done'>('form');
const [risk, setRisk] = useState<any>(null);
const { addToast } = useToast();
const max = token === 'SOL' ? balance.sol : balance.usdt;
const amt = parseFloat(amount) || 0;
const review = async () => {
setError('');
if (!to.trim()) return setError('Recipient address required');
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(to.trim())) return setError('Invalid Solana address');
if (!amt || amt <= 0) return setError('Enter a valid amount');
if (amt > max) return setError(`Insufficient balance. Max: ${max.toFixed(token === 'SOL' ? 4 : 2)} ${token}`);
// AI risk assessment before showing confirmation
if (window.solvox) {
try {
const r = await window.solvox.ai.assessRisk({ amount: amt, token, to: to.trim() });
if (r.success) setRisk(r.risk);
} catch {}
}
setStep('confirm');
};
const send = async () => {
setLoading(true); setError('');
try {
const r = window.solvox
? await window.solvox.ai.executeConfirmed({ token, amount: amt, to: to.trim() })
: { success: true, signature: 'dev_' + Date.now(), explorer: '#' };
if (r.success) {
setSuccess(r); setStep('done'); onSent();
addToast({ type: 'success', title: `${amount} ${token} sent` });
} else { setError(r.error || 'Transaction failed'); setStep('form'); }
} catch (e: any) { setError(e.message); setStep('form'); }
setLoading(false);
};
const reset = () => { setTo(''); setAmount(''); setError(''); setSuccess(null); setRisk(null); setStep('form'); };
return (
<div className="max-w-md mx-auto px-8 py-section">
<h2 className="text-title-lg display-text text-ink mb-2">Send {token}</h2>
<p className="text-body-sm text-body mb-8">Balance: <span className="number-mono"><Num value={max} decimals={token === 'SOL' ? 4 : 2} /></span> {token}</p>
{step === 'form' && (
<div className="card space-y-5">
{/* Token */}
<div className="flex gap-2 p-1 bg-surface-soft rounded-pill">
{(['SOL', 'USDT'] as const).map(t => (
<button key={t} onClick={() => setToken(t)} className={`flex-1 py-2 rounded-pill text-button transition-colors ${token === t ? 'bg-primary text-on-primary' : 'text-body hover:text-ink'}`}>
{t === 'SOL' ? '◎' : '₮'} {t}
</button>
))}
</div>
{/* Recipient */}
<div>
<label className="text-caption-strong text-muted uppercase tracking-wider block mb-1.5">Recipient</label>
<input value={to} onChange={e => setTo(e.target.value)} placeholder="Solana address" className="input-field font-mono text-body-sm" />
</div>
{/* Amount */}
<div>
<div className="flex justify-between items-center mb-1.5">
<label className="text-caption-strong text-muted uppercase tracking-wider">Amount</label>
<button onClick={() => setAmount(max.toString())} className="btn-text text-body-sm py-0 px-1">Max</button>
</div>
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" step="any" className="input-field number-mono text-title-lg text-center" />
<div className="flex gap-2 mt-2">
{[25, 50, 75, 100].map(p => (
<button key={p} onClick={() => setAmount((max * p / 100).toFixed(token === 'SOL' ? 4 : 2))} className="flex-1 py-1.5 rounded-pill text-caption-strong text-muted bg-surface-soft hover:bg-surface-strong transition-colors">{p}%</button>
))}
</div>
</div>
{error && <div className="text-body-sm text-semantic-down bg-semantic-down/5 rounded-md p-3">{error}</div>}
<button onClick={review} className="btn-primary w-full">Review transaction</button>
</div>
)}
{step === 'confirm' && (
<div className="card space-y-5 page-enter">
<div className="text-center mb-2">
<div className="text-body text-muted">Confirm transaction</div>
<div className="display-text text-display-sm text-ink mt-1">{amount} {token}</div>
</div>
<div className="bg-surface-soft rounded-xl p-4 space-y-3 text-body-sm">
<div className="flex justify-between"><span className="text-muted">To</span><span className="font-mono text-caption">{to.slice(0, 10)}…{to.slice(-6)}</span></div>
<div className="hairline-soft" />
<div className="flex justify-between"><span className="text-muted">Fee</span><span className="number-mono text-caption">~0.000005 SOL</span></div>
<div className="flex justify-between"><span className="text-muted">Network</span><span className="badge-pill text-[10px]">DEVNET</span></div>
</div>
{/* AI Risk Assessment */}
{risk && (
<div className={`rounded-xl p-4 text-body-sm ${risk.level === 'safe' ? 'bg-semantic-up/5' : risk.level === 'danger' ? 'bg-semantic-down/5' : 'bg-accent-yellow/5'}`}>
<div className="flex items-center gap-2 mb-1">
<span className={`badge-pill text-[10px] ${risk.level === 'safe' ? 'badge-pill-green' : 'badge-pill-red'}`}>AI RISK: {risk.level.toUpperCase()}</span>
<span className="number-mono text-caption text-muted">{risk.score}/100</span>
</div>
{risk.factors?.length > 0 && <ul className="text-caption text-body mt-1 space-y-0.5">{risk.factors.map((f: string, i: number) => <li key={i}>· {f}</li>)}</ul>}
{risk.recommendation && <div className="text-caption text-muted mt-1">{risk.recommendation}</div>}
<div className="text-caption text-muted-soft mt-1 flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-primary" />
Analyzed by @qvac/llm-llamacpp + @qvac/embed-llamacpp
</div>
</div>
)}
{error && <div className="text-body-sm text-semantic-down">{error}</div>}
<div className="flex gap-3">
<button onClick={() => setStep('form')} className="btn-secondary flex-1">Cancel</button>
<button onClick={send} disabled={loading} className="btn-primary flex-1 disabled:opacity-50">{loading ? 'Sending…' : `Send ${token}`}</button>
</div>
</div>
)}
{step === 'done' && success && (
<div className="card text-center space-y-5 page-enter">
<div className="w-14 h-14 mx-auto rounded-full bg-semantic-up/10 flex items-center justify-center">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#05b169" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div className="display-text text-display-sm text-ink">Sent</div>
<p className="text-body text-muted">{amount} {token} sent successfully</p>
<div className="bg-surface-soft rounded-xl p-3 font-mono text-caption text-muted break-all">{success.signature}</div>
<a href={success.explorer} target="_blank" rel="noopener noreferrer" className="btn-text text-body-sm">View on Solscan →</a>
<button onClick={reset} className="btn-primary w-full">Send another</button>
</div>
)}
</div>
);
}