gs-port / src /components /HoldingsList.jsx
Scribbler310's picture
Upload folder using huggingface_hub
0c5252a verified
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
ChevronDown,
ChevronUp,
Plus,
Search,
TrendingUp,
TrendingDown,
Trash2,
PieChart as PieIcon,
BarChart3,
Info
} from 'lucide-react';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import InvestmentCommittee from './InvestmentCommittee';
export default function HoldingsList({
allocation,
prices,
onAddClick,
onAssetClick,
onRemoveAsset
}) {
const [expandedId, setExpandedId] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const filteredAllocation = useMemo(() => {
return allocation.filter(asset =>
asset.ticker.toLowerCase().includes(searchTerm.toLowerCase()) ||
asset.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [allocation, searchTerm]);
const toggleExpand = (ticker) => {
setExpandedId(expandedId === ticker ? null : ticker);
};
return (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden">
{/* Header section matching the user's requested layout */}
<div className="p-6 border-b border-gray-50 flex flex-col md:flex-row justify-between items-center gap-4 bg-gs-light/30">
<div className="flex items-center gap-3">
<div className="p-2 bg-gs-navy rounded-xl text-white shadow-lg shadow-gs-navy/20">
<BarChart3 size={20} />
</div>
<h2 className="text-xl font-bold text-gs-navy tracking-tight">Portfolio Holdings</h2>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gs-slate/40" size={16} />
<input
type="text"
placeholder="Search holdings..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-gs-gold/30 focus:border-gs-gold transition-all w-full md:w-64 shadow-sm"
/>
</div>
<button
onClick={onAddClick}
className="flex items-center gap-2 px-4 py-2 bg-gs-navy text-white rounded-xl hover:bg-gs-gold hover:text-gs-navy transition-all shadow-md font-bold text-sm whitespace-nowrap group"
>
<Plus size={16} className="group-hover:rotate-90 transition-transform duration-300" />
Add Holding
</button>
</div>
</div>
{/* Table Header */}
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-[10px] font-bold text-gs-slate/50 uppercase tracking-[0.1em] border-b border-gray-50">
<th className="px-6 py-4 font-bold">Asset</th>
<th className="px-6 py-4 font-bold text-right">Price</th>
<th className="px-6 py-4 font-bold text-right hidden sm:table-cell">Shares</th>
<th className="px-6 py-4 font-bold text-right">Value</th>
<th className="px-6 py-4 font-bold text-right hidden lg:table-cell">Avg Cost</th>
<th className="px-6 py-4 font-bold text-right hidden md:table-cell">Gain/Loss</th>
<th className="px-6 py-4 font-bold text-right">Return</th>
<th className="px-6 py-4 font-bold text-right">Alloc</th>
<th className="px-4 py-4 w-10"></th>
</tr>
</thead>
<tbody>
{filteredAllocation.length > 0 ? (
filteredAllocation.map((asset) => (
<HoldingRow
key={asset.ticker}
asset={asset}
priceData={prices[asset.ticker]}
isExpanded={expandedId === asset.ticker}
onToggle={() => toggleExpand(asset.ticker)}
onRemove={() => onRemoveAsset(asset.ticker)}
/>
))
) : (
<tr>
<td colSpan="9" className="px-6 py-20 text-center">
<div className="flex flex-col items-center gap-3">
<div className="p-4 bg-gs-light rounded-full text-gs-slate/20">
<Search size={40} />
</div>
<p className="text-gs-slate font-light text-lg">No holdings found matching your search.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}
function HoldingRow({ asset, priceData, isExpanded, onToggle, onRemove }) {
const currentPrice = priceData?.price || 0;
const marketValue = asset.shares * currentPrice;
const totalCost = asset.shares * (asset.avgCost || 0);
const gainLoss = marketValue - totalCost;
const returnPct = (totalCost > 0 && !isNaN(gainLoss)) ? (gainLoss / totalCost) * 100 : 0;
const isPositive = gainLoss >= 0;
return (
<>
<tr
onClick={onToggle}
className={`group cursor-pointer border-b border-gray-50 transition-all ${isExpanded ? 'bg-gs-gold/5' : 'hover:bg-gs-light/50'}`}
>
{/* Asset Column */}
<td className="px-6 py-4">
<div className="flex items-center gap-4">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-xs shadow-inner"
style={{ backgroundColor: asset.color }}
>
{asset.ticker?.substring(0, 2) || '??'}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-gs-navy uppercase tracking-wide">{asset.ticker}</span>
<span className="text-[10px] px-1.5 py-0.5 bg-gs-light text-gs-slate font-bold rounded uppercase">
{asset.type === 'mf' ? 'ETF' : 'STOCK'}
</span>
</div>
<p className="text-xs text-gs-slate/60 font-medium truncate max-w-[120px]">{asset.name}</p>
{priceData?.percent !== undefined && !isNaN(priceData.percent) && (
<div className={`flex items-center gap-1 text-[10px] font-bold mt-0.5 ${priceData.percent >= 0 ? 'text-green-600' : 'text-red-500'}`}>
{priceData.percent >= 0 ? '▲' : '▼'} {Math.abs(priceData.percent).toFixed(2)}% today
</div>
)}
</div>
</div>
</td>
{/* Price Column */}
<td className="px-6 py-4 text-right">
<p className="font-bold text-gs-navy">${currentPrice.toFixed(2)}</p>
</td>
{/* Shares Column */}
<td className="px-6 py-4 text-right hidden sm:table-cell">
<p className="text-gs-slate font-medium">{asset.shares}</p>
</td>
{/* Value Column */}
<td className="px-6 py-4 text-right">
<p className="font-bold text-gs-navy">${marketValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
</td>
{/* Avg Cost Column */}
<td className="px-6 py-4 text-right hidden lg:table-cell">
<p className="text-gs-slate/60 font-medium">${(asset.avgCost || 0).toFixed(2)}</p>
</td>
{/* Gain/Loss Column */}
<td className="px-6 py-4 text-right hidden md:table-cell">
<p className={`font-bold ${isPositive ? 'text-green-600' : 'text-red-500'}`}>
{isPositive ? '+' : ''}${Math.abs(gainLoss).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</td>
{/* Return Column */}
<td className="px-6 py-4 text-right">
<div className={`inline-flex items-center px-2 py-1 rounded-lg text-xs font-bold ${isPositive ? 'bg-green-50 text-green-600' : 'bg-red-50 text-red-500'}`}>
{isPositive ? '+' : ''}{returnPct.toFixed(2)}%
</div>
</td>
{/* Alloc Column */}
<td className="px-6 py-4 text-right">
<div className="flex flex-col items-end gap-1.5">
<span className="font-bold text-gs-navy text-xs">{asset.value.toFixed(1)}%</span>
<div className="w-16 h-1.5 bg-gs-light rounded-full overflow-hidden shadow-inner">
<div
className="h-full rounded-full transition-all duration-1000"
style={{ width: `${asset.value}%`, backgroundColor: asset.color }}
></div>
</div>
</div>
</td>
{/* Actions/Expand Column */}
<td className="px-4 py-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2">
<button
onClick={() => onRemove()}
className="p-1.5 text-gs-slate/20 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all opacity-0 group-hover:opacity-100"
title="Remove Holding"
>
<Trash2 size={14} />
</button>
<div className={`text-gs-slate/30 group-hover:text-gs-gold transition-colors ${isExpanded ? 'rotate-180' : ''}`}>
<ChevronDown size={18} />
</div>
</div>
</td>
</tr>
{/* Expanded Section */}
<AnimatePresence>
{isExpanded && (
<motion.tr
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="bg-gs-light/30 border-b border-gray-50 overflow-hidden"
>
<td colSpan="9" className="p-0">
<div className="p-6 grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Visual Analysis */}
<div className="xl:col-span-2 space-y-4">
<div className="flex justify-between items-end">
<div>
<h4 className="text-sm font-bold text-gs-navy uppercase tracking-wider mb-1">Price Performance</h4>
<p className="text-xs text-gs-slate/60">Real-time market tracking for {asset.ticker}</p>
</div>
<div className="flex gap-2">
<span className="text-[10px] font-bold px-2 py-1 bg-white border border-gray-100 rounded-lg shadow-sm">1 Month</span>
</div>
</div>
<div className="h-48 bg-white p-4 rounded-2xl border border-gray-100 shadow-sm">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={priceData?.history || []}>
<defs>
<linearGradient id={`color-${asset.ticker}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={asset.color} stopOpacity={0.1}/>
<stop offset="95%" stopColor={asset.color} stopOpacity={0}/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }}
/>
<Area
type="monotone"
dataKey="price"
stroke={asset.color}
strokeWidth={2}
fill={`url(#color-${asset.ticker})`}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
<p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Volatility</p>
<p className="text-sm font-bold text-gs-navy">Moderate</p>
</div>
<div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
<p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Buy Price</p>
<p className="text-sm font-bold text-gs-navy">${(asset.avgCost || 0).toFixed(2)}</p>
</div>
<div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
<p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Hold Since</p>
<p className="text-sm font-bold text-gs-navy">{new Date(asset.dateBought).toLocaleDateString()}</p>
</div>
</div>
</div>
{/* AI Analysis Sidebar */}
<div className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-6 h-6 rounded-lg bg-gs-gold/20 flex items-center justify-center text-gs-gold">
<Info size={14} />
</div>
<h4 className="text-sm font-bold text-gs-navy uppercase tracking-wider">AI Investment Thesis</h4>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<InvestmentCommittee ticker={asset.ticker} inline={true} />
</div>
</div>
</div>
</td>
</motion.tr>
)}
</AnimatePresence>
</>
);
}