Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions components/bags/bag-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,34 @@ import { useState } from 'react';
import { DepositModal } from './deposit-modal';
import { ArrowRight, RotateCw } from 'lucide-react';
import { allocationLabel, type SmartBagTemplate } from '@/lib/smart-bags/session-engine';
import { useWalletBalances } from '@/hooks/use-wallet-balances';
import { useWallet } from '@solana/wallet-adapter-react';

export function BagCard(bag: SmartBagTemplate) {
const [isDepositOpen, setIsDepositOpen] = useState(false);
const { connected } = useWallet();
const { balances } = useWalletBalances();
const { title, description, metricLabel, metricValue, risk, assets, strategy, maxSlippageBps } = bag;

// Compute user position for this bag's target mints
const position = connected && balances.length > 0 ? (() => {
const held = assets
.map((asset) => {
const match = balances.find((b) => b.mint === asset.mint);
return {
symbol: asset.symbol,
targetBps: asset.allocationBps,
valueUsd: match?.valueUsd ?? 0,
};
})
.filter((item) => item.valueUsd > 0);

const totalValue = held.reduce((sum, h) => sum + h.valueUsd, 0);
if (totalValue < 0.01) return null;

return { held, totalValue };
})() : null;

return (
<>
<div className="glass-card p-6 flex flex-col h-full hover:border-accentPrimary/40 transition-colors">
Expand Down Expand Up @@ -47,6 +70,36 @@ export function BagCard(bag: SmartBagTemplate) {
</div>
</div>

{/* User position section */}
{position && (
<div className="bg-accentPrimary/5 border border-accentPrimary/15 rounded-xl p-4 mb-6">
<div className="flex justify-between items-center mb-3">
<span className="text-xs text-accentPrimary/80 uppercase tracking-wider font-bold">Your Position</span>
<span className="text-sm font-bold text-accentPrimary">
${position.totalValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
<div className="space-y-2">
{position.held.map((item) => {
const actualPct = position.totalValue > 0 ? (item.valueUsd / position.totalValue) * 100 : 0;
const targetPct = item.targetBps / 100;
const drift = actualPct - targetPct;
return (
<div key={item.symbol} className="flex items-center justify-between text-xs">
<span className="text-white/70">{item.symbol}</span>
<div className="flex items-center gap-2">
<span className="text-white/50">{actualPct.toFixed(1)}%</span>
<span className={`font-mono ${Math.abs(drift) > bag.rebalanceThresholdBps / 100 ? 'text-amber-400' : 'text-white/30'}`}>
{drift >= 0 ? '+' : ''}{drift.toFixed(1)}%
</span>
</div>
</div>
);
})}
</div>
</div>
)}

<div className="flex items-center justify-between mt-auto">
<div>
<div className="text-xs text-white/50">{strategy}</div>
Expand Down
5 changes: 5 additions & 0 deletions components/bags/deposit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AlertTriangle, CheckCircle, ClipboardList, Loader2, Send, Shield, Walle
import { useWallet } from '@solana/wallet-adapter-react';
import { Connection, VersionedTransaction } from '@solana/web3.js';
import { DEPOSIT_TOKENS } from '@/lib/smart-bags/catalog';
import { useBalanceStore } from '@/lib/stores/balance-store';
import {
allocationLabel,
attachQuoteSnapshots,
Expand Down Expand Up @@ -87,6 +88,7 @@ function receiptId(snapshotId: string) {

export function DepositModal({ isOpen, onClose, bag }: DepositModalProps) {
const { connected, publicKey, signTransaction } = useWallet();
const triggerRefresh = useBalanceStore((state) => state.triggerRefresh);
const walletAddress = publicKey?.toBase58();
const [amount, setAmount] = useState('');
const [inputTokenSymbol, setInputTokenSymbol] = useState(DEPOSIT_TOKENS[0].symbol);
Expand Down Expand Up @@ -281,6 +283,9 @@ export function DepositModal({ isOpen, onClose, bag }: DepositModalProps) {
throw error;
}
}

// Trigger balance refresh across all dashboard components
triggerRefresh();
} catch (error) {
setSessionError(error instanceof Error ? error.message : 'Failed to execute deposit session.');
} finally {
Expand Down
69 changes: 51 additions & 18 deletions components/dashboard/asset-allocation.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
'use client';

import { useWalletBalances } from '@/hooks/use-wallet-balances';
import { useWallet } from '@solana/wallet-adapter-react';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import { useMemo } from 'react';
import { Loader2 } from 'lucide-react';

ChartJS.register(ArcElement, Tooltip, Legend);

const CHART_COLORS = [
'#48CAE4', // Primary Accent
'#0077B6', // Tertiary Accent
'#00B4D8', // Secondary
'#90E0EF', // Light blue
'#023E8A', // Deep blue
'#1C2541', // Surface
];

export function AssetAllocation() {
const { connected } = useWallet();
const { balances, totalValueUsd, isLoading } = useWalletBalances();

const topAsset = useMemo(() => {
if (balances.length === 0) return null;
const top = balances[0]; // already sorted by valueUsd descending
const pct = totalValueUsd > 0 ? Math.round((top.valueUsd / totalValueUsd) * 100) : 0;
return { symbol: top.symbol, pct };
}, [balances, totalValueUsd]);

const data = useMemo(() => {
return {
labels: ['SOL', 'USDC', 'JUP', 'BONK'],
labels: balances.map((b) => b.symbol),
datasets: [
{
data: [6500, 4200, 1100, 650.75],
backgroundColor: [
'#48CAE4', // Primary Accent
'#0077B6', // Tertiary Accent
'#00B4D8', // Secondary
'#1C2541', // Surface
],
borderColor: '#0B132B', // Deep Navy border
data: balances.map((b) => b.valueUsd),
backgroundColor: balances.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]),
borderColor: '#0B132B',
borderWidth: 2,
hoverOffset: 4,
},
],
};
}, []);
}, [balances]);

const options = {
responsive: true,
Expand All @@ -55,12 +69,12 @@ export function AssetAllocation() {
borderColor: '#3A506B',
borderWidth: 1,
callbacks: {
label: function(context: any) {
label: function(context: { label?: string; parsed?: number }) {
let label = context.label || '';
if (label) {
label += ': ';
}
if (context.parsed !== null) {
if (context.parsed !== null && context.parsed !== undefined) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed);
}
return label;
Expand All @@ -78,6 +92,23 @@ export function AssetAllocation() {
);
}

if (isLoading && balances.length === 0) {
return (
<div className="glass-card p-6 min-h-[300px] flex flex-col items-center justify-center gap-3">
<Loader2 className="h-6 w-6 animate-spin text-accentPrimary" />
<span className="text-white/40 text-sm">Loading allocation…</span>
</div>
);
}

if (balances.length === 0) {
return (
<div className="glass-card p-6 min-h-[300px] flex items-center justify-center">
<p className="text-white/40 text-sm">No assets to display</p>
</div>
);
}

return (
<div className="glass-card p-6 h-full min-h-[300px] flex flex-col">
<h3 className="font-display text-lg font-semibold mb-6">Asset Allocation</h3>
Expand All @@ -86,13 +117,15 @@ export function AssetAllocation() {
<Doughnut data={data} options={options} />
</div>
{/* Center text */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none pr-24">
<div className="text-center">
<span className="block text-white/50 text-xs font-medium">Top Asset</span>
<span className="block font-display text-xl font-bold">SOL</span>
<span className="block text-accentPrimary text-sm">52%</span>
{topAsset && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none pr-24">
<div className="text-center">
<span className="block text-white/50 text-xs font-medium">Top Asset</span>
<span className="block font-display text-xl font-bold">{topAsset.symbol}</span>
<span className="block text-accentPrimary text-sm">{topAsset.pct}%</span>
</div>
</div>
</div>
)}
</div>
</div>
);
Expand Down
118 changes: 66 additions & 52 deletions components/dashboard/holdings-table.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
'use client';

import { useWalletBalances } from '@/hooks/use-wallet-balances';
import { useWallet } from '@solana/wallet-adapter-react';
import { ArrowUpDown } from 'lucide-react';

const MOCK_HOLDINGS = [
{ id: 1, symbol: 'SOL', name: 'Solana', balance: '25.5', valueUSD: 6500, chain: 'Solana', chainColor: 'bg-gradient-to-br from-purple-500 to-blue-500' },
{ id: 2, symbol: 'USDC', name: 'USD Coin', balance: '4200.00', valueUSD: 4200, chain: 'Solana', chainColor: 'bg-gradient-to-br from-purple-500 to-blue-500' },
{ id: 3, symbol: 'JUP', name: 'Jupiter', balance: '1250', valueUSD: 1100, chain: 'Solana', chainColor: 'bg-gradient-to-br from-purple-500 to-blue-500' },
{ id: 4, symbol: 'BONK', name: 'Bonk', balance: '5000000', valueUSD: 650.75, chain: 'Solana', chainColor: 'bg-gradient-to-br from-purple-500 to-blue-500' },
];
import { ArrowUpDown, Loader2 } from 'lucide-react';

export function HoldingsTable() {
const { connected } = useWallet();
const { balances, isLoading } = useWalletBalances();

if (!connected) {
return null;
Expand All @@ -22,52 +17,71 @@ export function HoldingsTable() {
<div className="p-6 border-b border-surfaceCardBorder/50">
<h3 className="font-display text-lg font-semibold">Detailed Holdings</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-white/50 uppercase bg-surfaceCard/30">
<tr>
<th scope="col" className="px-6 py-4 font-medium">Asset</th>
<th scope="col" className="px-6 py-4 font-medium">Balance</th>
<th scope="col" className="px-6 py-4 font-medium cursor-pointer hover:text-white transition-colors group">
<div className="flex items-center gap-1">
Value (USD)
<ArrowUpDown className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</th>
<th scope="col" className="px-6 py-4 font-medium">Network</th>
</tr>
</thead>
<tbody className="divide-y divide-surfaceCardBorder/50">
{MOCK_HOLDINGS.map((asset) => (
<tr key={asset.id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-accentPrimary/20 flex items-center justify-center text-xs font-bold text-accentPrimary">
{asset.symbol[0]}
</div>
<div>
<div className="font-medium text-white">{asset.symbol}</div>
<div className="text-white/50 text-xs">{asset.name}</div>
</div>
</div>
</td>
<td className="px-6 py-4 text-white/80 font-medium">
{asset.balance}
</td>
<td className="px-6 py-4 text-white font-medium">
${asset.valueUSD.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${asset.chainColor}`}></div>
<span className="text-white/70">{asset.chain}</span>

{isLoading && balances.length === 0 ? (
<div className="p-12 flex flex-col items-center justify-center gap-3 text-white/50">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm">Loading on-chain balances…</span>
</div>
) : balances.length === 0 ? (
<div className="p-12 text-center text-white/40 text-sm">
No holdings found for this wallet.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-white/50 uppercase bg-surfaceCard/30">
<tr>
<th scope="col" className="px-6 py-4 font-medium">Asset</th>
<th scope="col" className="px-6 py-4 font-medium">Balance</th>
<th scope="col" className="px-6 py-4 font-medium cursor-pointer hover:text-white transition-colors group">
<div className="flex items-center gap-1">
Value (USD)
<ArrowUpDown className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</td>
</th>
<th scope="col" className="px-6 py-4 font-medium">Network</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody className="divide-y divide-surfaceCardBorder/50">
{balances.map((asset) => (
<tr key={asset.mint} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-accentPrimary/20 flex items-center justify-center text-xs font-bold text-accentPrimary">
{asset.icon}
</div>
<div>
<div className="font-medium text-white">{asset.symbol}</div>
<div className="text-white/50 text-xs">{asset.name}</div>
</div>
</div>
</td>
<td className="px-6 py-4 text-white/80 font-medium">
{asset.balanceUi.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 })}
</td>
<td className="px-6 py-4 text-white font-medium">
${asset.valueUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-gradient-to-br from-purple-500 to-blue-500"></div>
<span className="text-white/70">Solana</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}

{isLoading && balances.length > 0 && (
<div className="px-6 py-3 border-t border-surfaceCardBorder/50 flex items-center gap-2 text-white/40 text-xs">
<Loader2 className="h-3 w-3 animate-spin" />
Refreshing…
</div>
)}
</div>
);
}
Loading
Loading