import { useState } from "react"; import { cn } from "~/lib/utils"; import { TrendingUp, TrendingDown, Wallet, Plus, BarChart3, ExternalLink, RefreshCw, Star, Bell, Eye, } from "lucide-react"; import { Button } from "@/routes/ui/button"; import { ChainIcon } from "../components/shared/ChainIcon"; export interface PortfolioHolding { tokenAddress: string; chain: string; symbol: string; name: string; amount: string; currentPrice: number; currentValue: number; change24h: number; change24hPercent: number; entryPrice?: number; pnl?: number; pnlPercent?: number; } export interface PortfolioAnalytics { bestPerformer: { symbol: string; change: number }; worstPerformer: { symbol: string; change: number }; winRate: number; avgHoldTime: number; totalTrades: number; } export interface PortfolioData { wallets: { address: string; chain: string; type: "metamask" | "phantom" | "coinbase"; }[]; totalValue: number; change24h: number; change24hPercent: number; holdings: PortfolioHolding[]; analytics: PortfolioAnalytics; } export interface PortfolioPanelProps { /** Portfolio data */ portfolio: PortfolioData; /** Callback when refresh is clicked */ onRefresh?: () => void; /** Callback when "Analyze" is clicked for a token */ onAnalyzeToken?: (holding: PortfolioHolding) => void; /** Callback when "Set Alert" is clicked for a token */ onSetAlert?: (holding: PortfolioHolding) => void; /** Callback when "View on DexScreener" is clicked */ onViewToken?: (holding: PortfolioHolding) => void; /** Callback when "Add Manual Position" is clicked */ onAddPosition?: () => void; /** Whether data is loading */ isLoading?: boolean; /** Additional class names */ className?: string; } /** * PortfolioPanel - Portfolio tracker with holdings and P&L * * Features: * - Total portfolio value and 24h change * - List of holdings with current value and P&L * - Performance analytics (best/worst performers, win rate) * - Quick actions per token (analyze, alert, view) * - Manual position entry */ export function PortfolioPanel({ portfolio, onRefresh, onAnalyzeToken, onSetAlert, onViewToken, onAddPosition, isLoading = false, className, }: PortfolioPanelProps) { const [isRefreshing, setIsRefreshing] = useState(false); const handleRefresh = async () => { setIsRefreshing(true); await onRefresh?.(); setTimeout(() => setIsRefreshing(false), 1000); }; const formatCurrency = (value: number) => { if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`; return `$${value.toFixed(2)}`; }; const formatPercent = (value: number) => { const sign = value >= 0 ? "+" : ""; return `${sign}${value.toFixed(2)}%`; }; return (
{/* Header */}

Portfolio

{portfolio.holdings.length} tokens

{/* Content */}
{/* Total Value */}
Total Value
{formatCurrency(portfolio.totalValue)}
= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" )}> {formatPercent(portfolio.change24hPercent)} ({portfolio.change24h >= 0 ? "+" : ""}{formatCurrency(portfolio.change24h)}) 24h {portfolio.change24hPercent >= 0 ? ( ) : ( )}
{/* Holdings List */}
{portfolio.holdings.map((holding) => (
{/* Token Info */}
{holding.symbol}
{holding.name}
{formatCurrency(holding.currentValue)}
= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" )}> {formatPercent(holding.change24hPercent)}
{/* Amount and Price */}
{holding.amount} tokens ${holding.currentPrice.toFixed(6)}
{/* P&L (if available) */} {holding.pnl !== undefined && holding.pnlPercent !== undefined && (
P&L
= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" )}> {holding.pnl >= 0 ? "+" : ""}{formatCurrency(holding.pnl)} = 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" )}> ({formatPercent(holding.pnlPercent)})
)} {/* Action Buttons */}
))}
{/* Add Position Button */}
{/* Performance Analytics */}

Performance

Best Performer {portfolio.analytics.bestPerformer.symbol} (+{portfolio.analytics.bestPerformer.change.toFixed(1)}%)
Worst Performer {portfolio.analytics.worstPerformer.symbol} ({portfolio.analytics.worstPerformer.change.toFixed(1)}%)
Win Rate {portfolio.analytics.winRate}%
); }