Scribbler310 commited on
Commit
0c5252a
·
verified ·
1 Parent(s): c2b7eb3

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,19 +1,3 @@
1
  *.png filter=lfs diff=lfs merge=lfs -text
2
  *.jpg filter=lfs diff=lfs merge=lfs -text
3
  *.svg filter=lfs diff=lfs merge=lfs -text
4
- node_modules/@esbuild/win32-x64/esbuild.exe filter=lfs diff=lfs merge=lfs -text
5
- node_modules/@rollup/rollup-win32-x64-gnu/rollup.win32-x64-gnu.node filter=lfs diff=lfs merge=lfs -text
6
- node_modules/@rollup/rollup-win32-x64-msvc/rollup.win32-x64-msvc.node filter=lfs diff=lfs merge=lfs -text
7
- node_modules/bare-fs/prebuilds/ios-arm64/bare-fs.bare filter=lfs diff=lfs merge=lfs -text
8
- node_modules/bare-fs/prebuilds/win32-arm64/bare-fs.bare filter=lfs diff=lfs merge=lfs -text
9
- node_modules/bare-fs/prebuilds/win32-x64/bare-fs.bare filter=lfs diff=lfs merge=lfs -text
10
- node_modules/bare-os/prebuilds/win32-arm64/bare-os.bare filter=lfs diff=lfs merge=lfs -text
11
- node_modules/bare-os/prebuilds/win32-x64/bare-os.bare filter=lfs diff=lfs merge=lfs -text
12
- node_modules/bare-url/prebuilds/android-arm64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
13
- node_modules/bare-url/prebuilds/android-x64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
14
- node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
15
- node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
16
- node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare filter=lfs diff=lfs merge=lfs -text
17
- node_modules/bare-url/prebuilds/linux-x64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
18
- node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
19
- node_modules/bare-url/prebuilds/win32-x64/bare-url.bare filter=lfs diff=lfs merge=lfs -text
 
1
  *.png filter=lfs diff=lfs merge=lfs -text
2
  *.jpg filter=lfs diff=lfs merge=lfs -text
3
  *.svg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/FeeTransparencyModule.jsx CHANGED
@@ -1,7 +1,9 @@
1
- import React from 'react';
2
- import { Search } from 'lucide-react';
 
3
 
4
  export default function FeeTransparencyModule({ portfolio }) {
 
5
  const { hiddenFees } = portfolio;
6
 
7
  // Convert percentages to an illustrative dollar amount based on a $10,000 investment over 1 year
@@ -11,48 +13,137 @@ export default function FeeTransparencyModule({ portfolio }) {
11
  const totalCost = (parseFloat(expenseRatioCost) + parseFloat(advisoryCost) + hiddenFees.tradingCosts).toFixed(2);
12
 
13
  return (
14
- <div className="bg-gs-navy text-white rounded-2xl p-8 shadow-lg relative overflow-hidden">
15
- {/* Background Accent */}
16
- <div className="absolute -right-10 -top-10 w-40 h-40 bg-white/5 rounded-full blur-2xl"></div>
17
-
18
- <div className="relative z-10">
19
- <header className="mb-6 flex justify-between items-end border-b border-white/10 pb-4">
20
- <div>
21
- <h2 className="text-2xl font-light mb-1 flex items-center">
22
- <Search className="mr-2 text-gs-gold" size={24} /> Radical Transparency
23
- </h2>
24
- <p className="text-white/60 text-sm font-light">What you actually pay per $10,000 invested yearly</p>
25
- </div>
26
- <div className="text-right">
27
- <span className="text-xs uppercase tracking-widest text-gs-gold font-semibold block mb-1">Fee Rating</span>
28
- <span className="text-lg font-medium">{portfolio.feeImpact}</span>
 
 
 
29
  </div>
30
- </header>
31
 
32
- <div className="space-y-4 font-light text-sm">
33
- <div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
34
- <span className="text-white/80">Fund Expense Ratios ({hiddenFees.expenseRatio}%)</span>
35
- <span className="font-medium text-white">${expenseRatioCost}</span>
36
- </div>
37
- <div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
38
- <span className="text-white/80">Platform/Advisory Fee ({hiddenFees.advisoryFee}%)</span>
39
- <span className="font-medium text-white">${advisoryCost}</span>
40
- </div>
41
- <div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
42
- <span className="text-white/80">Estimated Trading Costs</span>
43
- <span className="font-medium text-white">${hiddenFees.tradingCosts.toFixed(2)}</span>
44
- </div>
45
-
46
- <div className="flex justify-between items-center pt-4 border-t border-white/10">
47
- <span className="font-medium">Total Yearly Cost</span>
48
- <span className="text-xl font-medium text-gs-gold">${totalCost}</span>
49
  </div>
50
  </div>
51
-
52
- <p className="text-xs text-white/40 mt-6 italic">
53
- *Many traditional brokerages hide these numbers in dense prospectuses. We show them to you upfront so there are no surprises.
54
- </p>
55
- </div>
56
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  );
58
  }
 
1
+ import React, { useState } from 'react';
2
+ import { Search, ChevronRight, ShieldCheck, X, DollarSign, Info, Activity } from 'lucide-react';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
 
5
  export default function FeeTransparencyModule({ portfolio }) {
6
+ const [isModalOpen, setIsModalOpen] = useState(false);
7
  const { hiddenFees } = portfolio;
8
 
9
  // Convert percentages to an illustrative dollar amount based on a $10,000 investment over 1 year
 
13
  const totalCost = (parseFloat(expenseRatioCost) + parseFloat(advisoryCost) + hiddenFees.tradingCosts).toFixed(2);
14
 
15
  return (
16
+ <>
17
+ <motion.div
18
+ whileHover={{ scale: 1.02 }}
19
+ whileTap={{ scale: 0.98 }}
20
+ onClick={() => setIsModalOpen(true)}
21
+ className="h-full flex items-center justify-center"
22
+ >
23
+ <div className="w-full bg-gs-navy text-white rounded-[4rem] p-4 pl-6 pr-8 shadow-xl flex items-center justify-between cursor-pointer group hover:bg-gs-navy/95 transition-all border border-white/10 relative overflow-hidden">
24
+ <div className="absolute -right-4 -top-4 w-24 h-24 bg-gs-gold/5 rounded-full blur-2xl"></div>
25
+
26
+ <div className="flex items-center gap-4">
27
+ <div className="p-3 rounded-full bg-gs-gold/20 text-gs-gold group-hover:bg-gs-gold group-hover:text-gs-navy transition-all">
28
+ <ShieldCheck size={20} />
29
+ </div>
30
+ <div>
31
+ <h2 className="text-sm font-bold tracking-tight">Radical Transparency</h2>
32
+ <p className="text-white/40 text-[9px] uppercase font-bold tracking-[0.1em]">Fee Report</p>
33
+ </div>
34
  </div>
 
35
 
36
+ <div className="flex items-center gap-5">
37
+ <div className="text-right">
38
+ <span className="block text-xl font-bold text-gs-gold">${Math.round(totalCost)}</span>
39
+ </div>
40
+ <div className="p-2 rounded-full bg-white/5 text-white/40 group-hover:bg-white/10 group-hover:text-white transition-all">
41
+ <ChevronRight size={16} />
42
+ </div>
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
  </div>
45
+ </motion.div>
46
+
47
+ <AnimatePresence>
48
+ {isModalOpen && (
49
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-6">
50
+ <motion.div
51
+ initial={{ opacity: 0 }}
52
+ animate={{ opacity: 1 }}
53
+ exit={{ opacity: 0 }}
54
+ onClick={() => setIsModalOpen(false)}
55
+ className="absolute inset-0 bg-gs-navy/60 backdrop-blur-md"
56
+ />
57
+
58
+ <motion.div
59
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
60
+ animate={{ opacity: 1, scale: 1, y: 0 }}
61
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
62
+ className="relative bg-white rounded-[2.5rem] shadow-2xl w-full max-w-lg overflow-hidden border border-white/20"
63
+ >
64
+ {/* Modal Header */}
65
+ <div className="bg-gs-navy p-8 text-white relative">
66
+ <div className="absolute -right-10 -top-10 w-40 h-40 bg-gs-gold/10 rounded-full blur-3xl"></div>
67
+ <div className="flex justify-between items-start relative z-10">
68
+ <div>
69
+ <div className="flex items-center gap-3 mb-2">
70
+ <div className="p-2 bg-gs-gold rounded-lg text-gs-navy">
71
+ <Search size={20} />
72
+ </div>
73
+ <span className="text-gs-gold text-[10px] font-black uppercase tracking-[0.2em]">Transparency Report</span>
74
+ </div>
75
+ <h2 className="text-3xl font-light">Radical Transparency</h2>
76
+ <p className="text-white/40 text-xs mt-1">Cost breakdown per $10,000 invested annually</p>
77
+ </div>
78
+ <button
79
+ onClick={() => setIsModalOpen(false)}
80
+ className="p-2 bg-white/5 hover:bg-white/10 text-white/60 hover:text-white rounded-full transition-all"
81
+ >
82
+ <X size={20} />
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <div className="p-8 space-y-6">
88
+ <div className="space-y-4">
89
+ <div className="flex justify-between items-center bg-gs-light/50 p-5 rounded-2xl border border-gray-100">
90
+ <div>
91
+ <span className="block text-xs font-bold text-gs-navy uppercase tracking-wider">Fund Expense Ratios</span>
92
+ <span className="text-[10px] text-gs-slate/60">{hiddenFees.expenseRatio}% blended average</span>
93
+ </div>
94
+ <span className="font-bold text-xl text-gs-navy">${expenseRatioCost}</span>
95
+ </div>
96
+
97
+ <div className="flex justify-between items-center bg-gs-light/50 p-5 rounded-2xl border border-gray-100">
98
+ <div>
99
+ <span className="block text-xs font-bold text-gs-navy uppercase tracking-wider">Platform & Advisory</span>
100
+ <span className="text-[10px] text-gs-slate/60">{hiddenFees.advisoryFee}% management fee</span>
101
+ </div>
102
+ <span className="font-bold text-xl text-gs-navy">${advisoryCost}</span>
103
+ </div>
104
+
105
+ <div className="flex justify-between items-center bg-gs-light/50 p-5 rounded-2xl border border-gray-100">
106
+ <div>
107
+ <span className="block text-xs font-bold text-gs-navy uppercase tracking-wider">Estimated Trading Costs</span>
108
+ <span className="text-[10px] text-gs-slate/60">Bid/ask spreads & execution</span>
109
+ </div>
110
+ <span className="font-bold text-xl text-gs-navy">${hiddenFees.tradingCosts.toFixed(2)}</span>
111
+ </div>
112
+ </div>
113
+
114
+ <div className="bg-gs-navy rounded-2xl p-6 text-white flex justify-between items-center shadow-lg">
115
+ <div className="flex items-center gap-4">
116
+ <div className="w-12 h-12 bg-white/5 rounded-xl flex items-center justify-center text-gs-gold">
117
+ <Activity size={24} />
118
+ </div>
119
+ <div>
120
+ <p className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Total Yearly Impact</p>
121
+ <p className="text-2xl font-bold text-gs-gold">${totalCost}</p>
122
+ </div>
123
+ </div>
124
+ <div className="text-right">
125
+ <p className="text-[9px] font-bold text-white/40 uppercase tracking-widest mb-1">Fee Rating</p>
126
+ <span className="px-3 py-1 bg-gs-gold text-gs-navy text-[10px] font-black uppercase rounded-lg">
127
+ {portfolio.feeImpact}
128
+ </span>
129
+ </div>
130
+ </div>
131
+
132
+ <p className="text-[11px] text-gs-slate/60 leading-relaxed italic text-center px-4">
133
+ "The most expensive investment is the one with hidden costs. We provide this report to ensure your capital works for you, not the middleman."
134
+ </p>
135
+
136
+ <button
137
+ onClick={() => setIsModalOpen(false)}
138
+ className="w-full bg-gs-navy text-white py-4 rounded-2xl font-bold text-sm hover:bg-gs-navy/95 transition-all shadow-xl shadow-gs-navy/20 active:scale-[0.98]"
139
+ >
140
+ Close Report
141
+ </button>
142
+ </div>
143
+ </motion.div>
144
+ </div>
145
+ )}
146
+ </AnimatePresence>
147
+ </>
148
  );
149
  }
src/components/HoldingsList.jsx ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ ChevronDown,
5
+ ChevronUp,
6
+ Plus,
7
+ Search,
8
+ TrendingUp,
9
+ TrendingDown,
10
+ Trash2,
11
+ PieChart as PieIcon,
12
+ BarChart3,
13
+ Info
14
+ } from 'lucide-react';
15
+ import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
16
+ import InvestmentCommittee from './InvestmentCommittee';
17
+
18
+ export default function HoldingsList({
19
+ allocation,
20
+ prices,
21
+ onAddClick,
22
+ onAssetClick,
23
+ onRemoveAsset
24
+ }) {
25
+ const [expandedId, setExpandedId] = useState(null);
26
+ const [searchTerm, setSearchTerm] = useState('');
27
+
28
+ const filteredAllocation = useMemo(() => {
29
+ return allocation.filter(asset =>
30
+ asset.ticker.toLowerCase().includes(searchTerm.toLowerCase()) ||
31
+ asset.name.toLowerCase().includes(searchTerm.toLowerCase())
32
+ );
33
+ }, [allocation, searchTerm]);
34
+
35
+ const toggleExpand = (ticker) => {
36
+ setExpandedId(expandedId === ticker ? null : ticker);
37
+ };
38
+
39
+ return (
40
+ <div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden">
41
+ {/* Header section matching the user's requested layout */}
42
+ <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">
43
+ <div className="flex items-center gap-3">
44
+ <div className="p-2 bg-gs-navy rounded-xl text-white shadow-lg shadow-gs-navy/20">
45
+ <BarChart3 size={20} />
46
+ </div>
47
+ <h2 className="text-xl font-bold text-gs-navy tracking-tight">Portfolio Holdings</h2>
48
+ </div>
49
+
50
+ <div className="flex items-center gap-3 w-full md:w-auto">
51
+ <div className="relative flex-1 md:flex-none">
52
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gs-slate/40" size={16} />
53
+ <input
54
+ type="text"
55
+ placeholder="Search holdings..."
56
+ value={searchTerm}
57
+ onChange={(e) => setSearchTerm(e.target.value)}
58
+ 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"
59
+ />
60
+ </div>
61
+ <button
62
+ onClick={onAddClick}
63
+ 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"
64
+ >
65
+ <Plus size={16} className="group-hover:rotate-90 transition-transform duration-300" />
66
+ Add Holding
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ {/* Table Header */}
72
+ <div className="overflow-x-auto">
73
+ <table className="w-full text-left border-collapse">
74
+ <thead>
75
+ <tr className="text-[10px] font-bold text-gs-slate/50 uppercase tracking-[0.1em] border-b border-gray-50">
76
+ <th className="px-6 py-4 font-bold">Asset</th>
77
+ <th className="px-6 py-4 font-bold text-right">Price</th>
78
+ <th className="px-6 py-4 font-bold text-right hidden sm:table-cell">Shares</th>
79
+ <th className="px-6 py-4 font-bold text-right">Value</th>
80
+ <th className="px-6 py-4 font-bold text-right hidden lg:table-cell">Avg Cost</th>
81
+ <th className="px-6 py-4 font-bold text-right hidden md:table-cell">Gain/Loss</th>
82
+ <th className="px-6 py-4 font-bold text-right">Return</th>
83
+ <th className="px-6 py-4 font-bold text-right">Alloc</th>
84
+ <th className="px-4 py-4 w-10"></th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ {filteredAllocation.length > 0 ? (
89
+ filteredAllocation.map((asset) => (
90
+ <HoldingRow
91
+ key={asset.ticker}
92
+ asset={asset}
93
+ priceData={prices[asset.ticker]}
94
+ isExpanded={expandedId === asset.ticker}
95
+ onToggle={() => toggleExpand(asset.ticker)}
96
+ onRemove={() => onRemoveAsset(asset.ticker)}
97
+ />
98
+ ))
99
+ ) : (
100
+ <tr>
101
+ <td colSpan="9" className="px-6 py-20 text-center">
102
+ <div className="flex flex-col items-center gap-3">
103
+ <div className="p-4 bg-gs-light rounded-full text-gs-slate/20">
104
+ <Search size={40} />
105
+ </div>
106
+ <p className="text-gs-slate font-light text-lg">No holdings found matching your search.</p>
107
+ </div>
108
+ </td>
109
+ </tr>
110
+ )}
111
+ </tbody>
112
+ </table>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ function HoldingRow({ asset, priceData, isExpanded, onToggle, onRemove }) {
119
+ const currentPrice = priceData?.price || 0;
120
+ const marketValue = asset.shares * currentPrice;
121
+ const totalCost = asset.shares * (asset.avgCost || 0);
122
+ const gainLoss = marketValue - totalCost;
123
+ const returnPct = (totalCost > 0 && !isNaN(gainLoss)) ? (gainLoss / totalCost) * 100 : 0;
124
+
125
+ const isPositive = gainLoss >= 0;
126
+
127
+ return (
128
+ <>
129
+ <tr
130
+ onClick={onToggle}
131
+ className={`group cursor-pointer border-b border-gray-50 transition-all ${isExpanded ? 'bg-gs-gold/5' : 'hover:bg-gs-light/50'}`}
132
+ >
133
+ {/* Asset Column */}
134
+ <td className="px-6 py-4">
135
+ <div className="flex items-center gap-4">
136
+ <div
137
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-xs shadow-inner"
138
+ style={{ backgroundColor: asset.color }}
139
+ >
140
+ {asset.ticker?.substring(0, 2) || '??'}
141
+ </div>
142
+ <div>
143
+ <div className="flex items-center gap-2">
144
+ <span className="font-bold text-gs-navy uppercase tracking-wide">{asset.ticker}</span>
145
+ <span className="text-[10px] px-1.5 py-0.5 bg-gs-light text-gs-slate font-bold rounded uppercase">
146
+ {asset.type === 'mf' ? 'ETF' : 'STOCK'}
147
+ </span>
148
+ </div>
149
+ <p className="text-xs text-gs-slate/60 font-medium truncate max-w-[120px]">{asset.name}</p>
150
+ {priceData?.percent !== undefined && !isNaN(priceData.percent) && (
151
+ <div className={`flex items-center gap-1 text-[10px] font-bold mt-0.5 ${priceData.percent >= 0 ? 'text-green-600' : 'text-red-500'}`}>
152
+ {priceData.percent >= 0 ? '▲' : '▼'} {Math.abs(priceData.percent).toFixed(2)}% today
153
+ </div>
154
+ )}
155
+ </div>
156
+ </div>
157
+ </td>
158
+
159
+ {/* Price Column */}
160
+ <td className="px-6 py-4 text-right">
161
+ <p className="font-bold text-gs-navy">${currentPrice.toFixed(2)}</p>
162
+ </td>
163
+
164
+ {/* Shares Column */}
165
+ <td className="px-6 py-4 text-right hidden sm:table-cell">
166
+ <p className="text-gs-slate font-medium">{asset.shares}</p>
167
+ </td>
168
+
169
+ {/* Value Column */}
170
+ <td className="px-6 py-4 text-right">
171
+ <p className="font-bold text-gs-navy">${marketValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
172
+ </td>
173
+
174
+ {/* Avg Cost Column */}
175
+ <td className="px-6 py-4 text-right hidden lg:table-cell">
176
+ <p className="text-gs-slate/60 font-medium">${(asset.avgCost || 0).toFixed(2)}</p>
177
+ </td>
178
+
179
+ {/* Gain/Loss Column */}
180
+ <td className="px-6 py-4 text-right hidden md:table-cell">
181
+ <p className={`font-bold ${isPositive ? 'text-green-600' : 'text-red-500'}`}>
182
+ {isPositive ? '+' : ''}${Math.abs(gainLoss).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
183
+ </p>
184
+ </td>
185
+
186
+ {/* Return Column */}
187
+ <td className="px-6 py-4 text-right">
188
+ <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'}`}>
189
+ {isPositive ? '+' : ''}{returnPct.toFixed(2)}%
190
+ </div>
191
+ </td>
192
+
193
+ {/* Alloc Column */}
194
+ <td className="px-6 py-4 text-right">
195
+ <div className="flex flex-col items-end gap-1.5">
196
+ <span className="font-bold text-gs-navy text-xs">{asset.value.toFixed(1)}%</span>
197
+ <div className="w-16 h-1.5 bg-gs-light rounded-full overflow-hidden shadow-inner">
198
+ <div
199
+ className="h-full rounded-full transition-all duration-1000"
200
+ style={{ width: `${asset.value}%`, backgroundColor: asset.color }}
201
+ ></div>
202
+ </div>
203
+ </div>
204
+ </td>
205
+
206
+ {/* Actions/Expand Column */}
207
+ <td className="px-4 py-4" onClick={(e) => e.stopPropagation()}>
208
+ <div className="flex items-center gap-2">
209
+ <button
210
+ onClick={() => onRemove()}
211
+ 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"
212
+ title="Remove Holding"
213
+ >
214
+ <Trash2 size={14} />
215
+ </button>
216
+ <div className={`text-gs-slate/30 group-hover:text-gs-gold transition-colors ${isExpanded ? 'rotate-180' : ''}`}>
217
+ <ChevronDown size={18} />
218
+ </div>
219
+ </div>
220
+ </td>
221
+ </tr>
222
+
223
+ {/* Expanded Section */}
224
+ <AnimatePresence>
225
+ {isExpanded && (
226
+ <motion.tr
227
+ initial={{ opacity: 0, height: 0 }}
228
+ animate={{ opacity: 1, height: 'auto' }}
229
+ exit={{ opacity: 0, height: 0 }}
230
+ className="bg-gs-light/30 border-b border-gray-50 overflow-hidden"
231
+ >
232
+ <td colSpan="9" className="p-0">
233
+ <div className="p-6 grid grid-cols-1 xl:grid-cols-3 gap-8">
234
+ {/* Visual Analysis */}
235
+ <div className="xl:col-span-2 space-y-4">
236
+ <div className="flex justify-between items-end">
237
+ <div>
238
+ <h4 className="text-sm font-bold text-gs-navy uppercase tracking-wider mb-1">Price Performance</h4>
239
+ <p className="text-xs text-gs-slate/60">Real-time market tracking for {asset.ticker}</p>
240
+ </div>
241
+ <div className="flex gap-2">
242
+ <span className="text-[10px] font-bold px-2 py-1 bg-white border border-gray-100 rounded-lg shadow-sm">1 Month</span>
243
+ </div>
244
+ </div>
245
+
246
+ <div className="h-48 bg-white p-4 rounded-2xl border border-gray-100 shadow-sm">
247
+ <ResponsiveContainer width="100%" height="100%">
248
+ <AreaChart data={priceData?.history || []}>
249
+ <defs>
250
+ <linearGradient id={`color-${asset.ticker}`} x1="0" y1="0" x2="0" y2="1">
251
+ <stop offset="5%" stopColor={asset.color} stopOpacity={0.1}/>
252
+ <stop offset="95%" stopColor={asset.color} stopOpacity={0}/>
253
+ </linearGradient>
254
+ </defs>
255
+ <Tooltip
256
+ contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }}
257
+ />
258
+ <Area
259
+ type="monotone"
260
+ dataKey="price"
261
+ stroke={asset.color}
262
+ strokeWidth={2}
263
+ fill={`url(#color-${asset.ticker})`}
264
+ />
265
+ </AreaChart>
266
+ </ResponsiveContainer>
267
+ </div>
268
+
269
+ <div className="grid grid-cols-3 gap-4">
270
+ <div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
271
+ <p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Volatility</p>
272
+ <p className="text-sm font-bold text-gs-navy">Moderate</p>
273
+ </div>
274
+ <div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
275
+ <p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Buy Price</p>
276
+ <p className="text-sm font-bold text-gs-navy">${(asset.avgCost || 0).toFixed(2)}</p>
277
+ </div>
278
+ <div className="bg-white p-3 rounded-xl border border-gray-100 shadow-sm">
279
+ <p className="text-[10px] font-bold text-gs-slate/40 uppercase mb-1">Hold Since</p>
280
+ <p className="text-sm font-bold text-gs-navy">{new Date(asset.dateBought).toLocaleDateString()}</p>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ {/* AI Analysis Sidebar */}
286
+ <div className="space-y-4">
287
+ <div className="flex items-center gap-2 mb-2">
288
+ <div className="w-6 h-6 rounded-lg bg-gs-gold/20 flex items-center justify-center text-gs-gold">
289
+ <Info size={14} />
290
+ </div>
291
+ <h4 className="text-sm font-bold text-gs-navy uppercase tracking-wider">AI Investment Thesis</h4>
292
+ </div>
293
+ <div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
294
+ <InvestmentCommittee ticker={asset.ticker} inline={true} />
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </td>
299
+ </motion.tr>
300
+ )}
301
+ </AnimatePresence>
302
+ </>
303
+ );
304
+ }
src/components/Indicators.jsx CHANGED
@@ -2,19 +2,19 @@ import React, { useState } from 'react';
2
  import { Activity, ShieldAlert, Info, X, PieChart, DollarSign, Target } from 'lucide-react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
 
5
- export default function Indicators({ portfolio }) {
6
  const [activeInfo, setActiveInfo] = useState(null); // 'health' or 'risk'
7
 
8
  // A simple gauge visualization using SVG
9
  const dashArray = 283; // 2 * pi * r (r=45)
10
  const dashOffset = dashArray - (dashArray * portfolio.healthScore) / 100;
11
 
12
- return (
13
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative">
14
  {/* Portfolio Health Score */}
15
  <div
16
  onClick={() => setActiveInfo('health')}
17
- className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex items-center justify-between cursor-pointer hover:border-gs-gold transition-all group"
18
  >
19
  <div>
20
  <h3 className="text-sm text-gs-slate uppercase tracking-wider mb-1 font-medium flex items-center">
@@ -44,7 +44,7 @@ export default function Indicators({ portfolio }) {
44
  {/* Risk Meter */}
45
  <div
46
  onClick={() => setActiveInfo('risk')}
47
- className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col justify-center cursor-pointer hover:border-gs-gold transition-all group"
48
  >
49
  <h3 className="text-sm text-gs-slate uppercase tracking-wider mb-4 font-medium flex items-center">
50
  <ShieldAlert size={16} className="mr-2 text-gs-gold" /> Risk Level
@@ -163,6 +163,12 @@ export default function Indicators({ portfolio }) {
163
  </div>
164
  )}
165
  </AnimatePresence>
 
 
 
 
 
 
166
  </div>
167
  );
168
  }
 
2
  import { Activity, ShieldAlert, Info, X, PieChart, DollarSign, Target } from 'lucide-react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
 
5
+ export default function Indicators({ portfolio, isFlattened = false }) {
6
  const [activeInfo, setActiveInfo] = useState(null); // 'health' or 'risk'
7
 
8
  // A simple gauge visualization using SVG
9
  const dashArray = 283; // 2 * pi * r (r=45)
10
  const dashOffset = dashArray - (dashArray * portfolio.healthScore) / 100;
11
 
12
+ const content = (
13
+ <>
14
  {/* Portfolio Health Score */}
15
  <div
16
  onClick={() => setActiveInfo('health')}
17
+ className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex items-center justify-between cursor-pointer hover:border-gs-gold transition-all group h-full"
18
  >
19
  <div>
20
  <h3 className="text-sm text-gs-slate uppercase tracking-wider mb-1 font-medium flex items-center">
 
44
  {/* Risk Meter */}
45
  <div
46
  onClick={() => setActiveInfo('risk')}
47
+ className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col justify-center cursor-pointer hover:border-gs-gold transition-all group h-full"
48
  >
49
  <h3 className="text-sm text-gs-slate uppercase tracking-wider mb-4 font-medium flex items-center">
50
  <ShieldAlert size={16} className="mr-2 text-gs-gold" /> Risk Level
 
163
  </div>
164
  )}
165
  </AnimatePresence>
166
+ </>
167
+ );
168
+
169
+ return isFlattened ? content : (
170
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative">
171
+ {content}
172
  </div>
173
  );
174
  }
src/components/InvestmentCommittee.jsx CHANGED
@@ -44,12 +44,16 @@ const mockDebates = {
44
  ]
45
  };
46
 
47
- export default function InvestmentCommittee({ ticker, isDebating, setIsDebating }) {
48
  const [messages, setMessages] = useState([]);
49
  const [convictionScore, setConvictionScore] = useState(null);
50
  const [loading, setLoading] = useState(false);
 
51
  const scrollRef = useRef(null);
52
 
 
 
 
53
  useEffect(() => {
54
  setMessages([]);
55
  setConvictionScore(null);
@@ -147,37 +151,35 @@ export default function InvestmentCommittee({ ticker, isDebating, setIsDebating
147
  } else {
148
  clearInterval(interval);
149
  setLoading(false);
 
150
  }
151
  }, 1200);
152
  };
153
 
154
  return (
155
- <div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col h-[500px]">
156
- <header className="mb-4 flex justify-between items-center border-b pb-4">
157
  <div>
158
- <h2 className="text-xl font-medium text-gs-navy flex items-center">
159
- <Users className="mr-2 text-gs-gold" size={20} /> AI Investment Committee
160
  </h2>
161
- <p className="text-sm text-gs-slate font-light mt-1">
162
- {ticker ? `Analyzing: ${ticker}` : 'Select an asset from your portfolio chart to debate.'}
163
- </p>
164
  </div>
165
  {ticker && !isDebating && !convictionScore && (
166
  <button
167
  onClick={runDebate}
168
  disabled={loading}
169
- className="flex items-center px-4 py-2 bg-gs-navy text-white text-sm rounded-lg hover:bg-gs-navy/90 transition-colors"
170
  >
171
- {loading ? <Loader2 size={16} className="animate-spin mr-2" /> : <PlayCircle size={16} className="mr-2" />}
172
- Start Debate
173
  </button>
174
  )}
175
  </header>
176
 
177
- <div className="flex-1 overflow-y-auto space-y-4 pr-2 custom-scrollbar" ref={scrollRef}>
178
  {!ticker && (
179
- <div className="h-full flex items-center justify-center text-gray-400 text-sm italic">
180
- Waiting for asset selection...
181
  </div>
182
  )}
183
 
@@ -187,13 +189,13 @@ export default function InvestmentCommittee({ ticker, isDebating, setIsDebating
187
  key={idx}
188
  initial={{ opacity: 0, y: 10 }}
189
  animate={{ opacity: 1, y: 0 }}
190
- className="flex gap-3"
191
  >
192
- <div className={`mt-1 flex-shrink-0 w-8 h-8 rounded-full ${agents[msg.agent].bg} ${agents[msg.agent].color} flex items-center justify-center`}>
193
  {agents[msg.agent].icon}
194
  </div>
195
- <div className="bg-gray-50 rounded-xl p-3 text-sm text-gs-slate border border-gray-100 w-full">
196
- <span className={`text-xs font-semibold uppercase tracking-wider block mb-1 ${agents[msg.agent].color}`}>
197
  {agents[msg.agent].name}
198
  </span>
199
  {msg.text}
@@ -203,8 +205,8 @@ export default function InvestmentCommittee({ ticker, isDebating, setIsDebating
203
  </AnimatePresence>
204
 
205
  {loading && messages.length > 0 && messages.length < 4 && (
206
- <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex items-center text-gray-400 text-sm ml-11">
207
- <Loader2 size={14} className="animate-spin mr-2" /> Typing...
208
  </motion.div>
209
  )}
210
  </div>
@@ -213,24 +215,18 @@ export default function InvestmentCommittee({ ticker, isDebating, setIsDebating
213
  <motion.div
214
  initial={{ opacity: 0, scale: 0.95 }}
215
  animate={{ opacity: 1, scale: 1 }}
216
- className="mt-4 pt-4 border-t"
217
  >
218
- <div className="bg-gs-light p-4 rounded-xl">
219
  <div className="flex justify-between items-center">
220
- <span className="font-medium text-gs-navy">Consensus Conviction Score</span>
221
  <div className="flex items-center">
222
- <span className={`text-2xl font-bold ${convictionScore >= 70 ? 'text-green-600' : convictionScore >= 40 ? 'text-gs-gold' : 'text-red-500'}`}>
223
  {convictionScore}
224
  </span>
225
- <span className="text-gray-400 ml-1">/ 100</span>
226
  </div>
227
  </div>
228
- <p className="text-[10px] text-gs-slate mt-2 italic leading-tight border-t border-gray-200 pt-2">
229
- The committee's confidence in this asset's current risk-to-reward.
230
- <span className="font-bold ml-1">70+ Overweight</span>,
231
- <span className="font-bold ml-1">40-69 Hold</span>,
232
- <span className="font-bold ml-1">Below 40 Underweight</span>.
233
- </p>
234
  </div>
235
  </motion.div>
236
  )}
 
44
  ]
45
  };
46
 
47
+ export default function InvestmentCommittee({ ticker, isDebating: externalIsDebating, setIsDebating: externalSetIsDebating, inline = false }) {
48
  const [messages, setMessages] = useState([]);
49
  const [convictionScore, setConvictionScore] = useState(null);
50
  const [loading, setLoading] = useState(false);
51
+ const [internalIsDebating, setInternalIsDebating] = useState(false);
52
  const scrollRef = useRef(null);
53
 
54
+ const isDebating = externalIsDebating !== undefined ? externalIsDebating : internalIsDebating;
55
+ const setIsDebating = externalSetIsDebating !== undefined ? externalSetIsDebating : setInternalIsDebating;
56
+
57
  useEffect(() => {
58
  setMessages([]);
59
  setConvictionScore(null);
 
151
  } else {
152
  clearInterval(interval);
153
  setLoading(false);
154
+ setIsDebating(false);
155
  }
156
  }, 1200);
157
  };
158
 
159
  return (
160
+ <div className={`bg-white rounded-2xl shadow-sm border border-gray-100 flex flex-col ${inline ? 'h-[350px] p-4' : 'h-[500px] p-6'}`}>
161
+ <header className={`flex justify-between items-center border-b ${inline ? 'mb-2 pb-2' : 'mb-4 pb-4'}`}>
162
  <div>
163
+ <h2 className={`${inline ? 'text-sm' : 'text-xl'} font-medium text-gs-navy flex items-center`}>
164
+ <Users className={`${inline ? 'mr-1.5' : 'mr-2'} text-gs-gold`} size={inline ? 16 : 20} /> AI Committee
165
  </h2>
 
 
 
166
  </div>
167
  {ticker && !isDebating && !convictionScore && (
168
  <button
169
  onClick={runDebate}
170
  disabled={loading}
171
+ className={`flex items-center bg-gs-navy text-white rounded-lg hover:bg-gs-navy/90 transition-colors font-bold ${inline ? 'px-2 py-1 text-[10px]' : 'px-4 py-2 text-sm'}`}
172
  >
173
+ {loading ? <Loader2 size={inline ? 12 : 16} className="animate-spin mr-1.5" /> : <PlayCircle size={inline ? 12 : 16} className="mr-1.5" />}
174
+ Debate
175
  </button>
176
  )}
177
  </header>
178
 
179
+ <div className="flex-1 overflow-y-auto space-y-3 pr-1 custom-scrollbar" ref={scrollRef}>
180
  {!ticker && (
181
+ <div className="h-full flex items-center justify-center text-gray-400 text-xs italic">
182
+ Waiting for selection...
183
  </div>
184
  )}
185
 
 
189
  key={idx}
190
  initial={{ opacity: 0, y: 10 }}
191
  animate={{ opacity: 1, y: 0 }}
192
+ className="flex gap-2"
193
  >
194
+ <div className={`mt-0.5 flex-shrink-0 w-6 h-6 rounded-lg ${agents[msg.agent].bg} ${agents[msg.agent].color} flex items-center justify-center`}>
195
  {agents[msg.agent].icon}
196
  </div>
197
+ <div className="bg-gray-50 rounded-xl p-2.5 text-[11px] text-gs-slate border border-gray-100 w-full leading-relaxed">
198
+ <span className={`text-[9px] font-bold uppercase tracking-wider block mb-0.5 ${agents[msg.agent].color}`}>
199
  {agents[msg.agent].name}
200
  </span>
201
  {msg.text}
 
205
  </AnimatePresence>
206
 
207
  {loading && messages.length > 0 && messages.length < 4 && (
208
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex items-center text-gray-400 text-[10px] ml-8">
209
+ <Loader2 size={10} className="animate-spin mr-1.5" /> Typing...
210
  </motion.div>
211
  )}
212
  </div>
 
215
  <motion.div
216
  initial={{ opacity: 0, scale: 0.95 }}
217
  animate={{ opacity: 1, scale: 1 }}
218
+ className={`mt-2 pt-2 border-t`}
219
  >
220
+ <div className="bg-gs-light p-3 rounded-xl border border-gs-gold/10">
221
  <div className="flex justify-between items-center">
222
+ <span className="text-[10px] font-bold text-gs-navy uppercase">Conviction Score</span>
223
  <div className="flex items-center">
224
+ <span className={`text-lg font-bold ${convictionScore >= 70 ? 'text-green-600' : convictionScore >= 40 ? 'text-gs-gold' : 'text-red-500'}`}>
225
  {convictionScore}
226
  </span>
227
+ <span className="text-[10px] text-gray-400 ml-0.5">/ 100</span>
228
  </div>
229
  </div>
 
 
 
 
 
 
230
  </div>
231
  </motion.div>
232
  )}
src/components/StockPopup.jsx CHANGED
@@ -13,7 +13,7 @@ const TIMEFRAME_DAYS = {
13
  'ALL': 365
14
  };
15
 
16
- export default function StockPopup({ ticker, assetName, onClose }) {
17
  const [data, setData] = useState(null);
18
  const [loading, setLoading] = useState(false);
19
  const [isDebating, setIsDebating] = useState(false);
@@ -32,6 +32,20 @@ export default function StockPopup({ ticker, assetName, onClose }) {
32
  loadData();
33
  }, [ticker]);
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const viewData = useMemo(() => {
36
  if (!data) return null;
37
 
@@ -77,11 +91,11 @@ export default function StockPopup({ ticker, assetName, onClose }) {
77
  return (
78
  <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
79
  <div
80
- className="w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-xl shadow-2xl flex flex-col animate-in zoom-in-95 duration-200"
81
  style={{ backgroundColor: bgColor }}
82
  >
83
  {/* Header */}
84
- <div className="p-6 pb-2 flex justify-between items-start sticky top-0 bg-[#111111] z-10 border-b border-gray-800">
85
  <div>
86
  <h2 className="text-2xl font-bold text-white flex items-center">
87
  {assetName} <Star size={18} className="ml-3 text-gray-500 hover:text-yellow-400 cursor-pointer" />
@@ -102,10 +116,10 @@ export default function StockPopup({ ticker, assetName, onClose }) {
102
  </button>
103
  </div>
104
 
105
- <div className="p-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
106
  {/* Main Chart Area */}
107
- <div className="lg:col-span-2">
108
- <div className="h-72 w-full mt-4">
109
  <ResponsiveContainer width="100%" height="100%">
110
  <AreaChart data={viewData.history} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}>
111
  <defs>
@@ -135,15 +149,15 @@ export default function StockPopup({ ticker, assetName, onClose }) {
135
  </div>
136
 
137
  {/* Time Controls */}
138
- <div className="flex space-x-4 mt-6 border-b border-gray-800 pb-2">
139
  {['1W', '1M', '3M', '1Y', 'ALL'].map(t => (
140
  <button
141
  key={t}
142
  onClick={() => setTimeframe(t)}
143
- className={`text-sm font-medium pb-2 border-b-2 transition-colors ${
144
  t === timeframe
145
  ? (viewData.isPositive ? 'text-[#00C805] border-[#00C805]' : 'text-[#FF5000] border-[#FF5000]')
146
- : 'text-gray-500 border-transparent hover:text-gray-300'
147
  }`}
148
  >
149
  {t}
@@ -151,15 +165,7 @@ export default function StockPopup({ ticker, assetName, onClose }) {
151
  ))}
152
  </div>
153
 
154
- <div className="mt-6 text-gray-300 text-sm leading-relaxed">
155
- <p>Historical charting for {assetName} over the selected period. Notice the volatility patterns represented by the area chart.</p>
156
- </div>
157
- </div>
158
-
159
- {/* Right Sidebar: AI Committee */}
160
- <div className="lg:col-span-1">
161
- {/* We render the Investment Committee inside a styled container so it fits the dark theme or stands out as a module */}
162
- <div className="bg-white rounded-xl shadow-inner overflow-hidden border border-gray-200">
163
  <InvestmentCommittee
164
  ticker={ticker}
165
  isDebating={isDebating}
@@ -167,6 +173,57 @@ export default function StockPopup({ ticker, assetName, onClose }) {
167
  />
168
  </div>
169
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  </div>
171
  </div>
172
  </div>
 
13
  'ALL': 365
14
  };
15
 
16
+ export default function StockPopup({ ticker, assetName, onClose, allHoldings = [], prices = {} }) {
17
  const [data, setData] = useState(null);
18
  const [loading, setLoading] = useState(false);
19
  const [isDebating, setIsDebating] = useState(false);
 
32
  loadData();
33
  }, [ticker]);
34
 
35
+ // Calculate Top Performers
36
+ const topPerformers = useMemo(() => {
37
+ if (!allHoldings.length || !prices) return [];
38
+
39
+ return allHoldings
40
+ .map(asset => ({
41
+ ...asset,
42
+ perf: prices[asset.ticker]?.percent || 0,
43
+ currentPrice: prices[asset.ticker]?.price || 0
44
+ }))
45
+ .sort((a, b) => b.perf - a.perf)
46
+ .slice(0, 5);
47
+ }, [allHoldings, prices]);
48
+
49
  const viewData = useMemo(() => {
50
  if (!data) return null;
51
 
 
91
  return (
92
  <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
93
  <div
94
+ className="w-full max-w-5xl max-h-[90vh] overflow-y-auto rounded-xl shadow-2xl flex flex-col animate-in zoom-in-95 duration-200"
95
  style={{ backgroundColor: bgColor }}
96
  >
97
  {/* Header */}
98
+ <div className="p-6 pb-4 flex justify-between items-start sticky top-0 bg-[#111111] z-10 border-b border-gray-800">
99
  <div>
100
  <h2 className="text-2xl font-bold text-white flex items-center">
101
  {assetName} <Star size={18} className="ml-3 text-gray-500 hover:text-yellow-400 cursor-pointer" />
 
116
  </button>
117
  </div>
118
 
119
+ <div className="p-8 grid grid-cols-1 lg:grid-cols-12 gap-10">
120
  {/* Main Chart Area */}
121
+ <div className="lg:col-span-8">
122
+ <div className="h-80 w-full mt-4">
123
  <ResponsiveContainer width="100%" height="100%">
124
  <AreaChart data={viewData.history} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}>
125
  <defs>
 
149
  </div>
150
 
151
  {/* Time Controls */}
152
+ <div className="flex space-x-6 mt-6 border-b border-gray-800 pb-2">
153
  {['1W', '1M', '3M', '1Y', 'ALL'].map(t => (
154
  <button
155
  key={t}
156
  onClick={() => setTimeframe(t)}
157
+ className={`text-sm font-bold pb-2 border-b-2 transition-colors ${
158
  t === timeframe
159
  ? (viewData.isPositive ? 'text-[#00C805] border-[#00C805]' : 'text-[#FF5000] border-[#FF5000]')
160
+ : 'text-gray-600 border-transparent hover:text-gray-300'
161
  }`}
162
  >
163
  {t}
 
165
  ))}
166
  </div>
167
 
168
+ <div className="mt-8">
 
 
 
 
 
 
 
 
169
  <InvestmentCommittee
170
  ticker={ticker}
171
  isDebating={isDebating}
 
173
  />
174
  </div>
175
  </div>
176
+
177
+ {/* Right Sidebar: Top Performers */}
178
+ <div className="lg:col-span-4 space-y-8">
179
+ <div>
180
+ <h3 className="text-white text-xs font-black uppercase tracking-[0.2em] mb-8 flex items-center gap-3">
181
+ <span className="text-lg">🏆</span> Top Performers
182
+ </h3>
183
+
184
+ <div className="space-y-6">
185
+ {topPerformers.map((asset, idx) => (
186
+ <div key={asset.ticker} className="flex items-center justify-between group">
187
+ <div className="flex items-center gap-4">
188
+ <div className="w-6 flex justify-center">
189
+ {idx === 0 ? <span className="text-lg">🥇</span> :
190
+ idx === 1 ? <span className="text-lg">🥈</span> :
191
+ idx === 2 ? <span className="text-lg">🥉</span> :
192
+ <div className="p-1.5 bg-gs-navy rounded-md text-gs-gold/50"><TrendingUp size={12} /></div>}
193
+ </div>
194
+
195
+ <div
196
+ className="w-12 h-12 rounded-xl flex items-center justify-center text-[10px] font-black text-gs-navy"
197
+ style={{ backgroundColor: asset.color }}
198
+ >
199
+ {asset.ticker}
200
+ </div>
201
+
202
+ <div>
203
+ <p className="text-sm font-bold text-white tracking-tight">{asset.ticker}</p>
204
+ <p className="text-[10px] text-gray-500 font-medium tracking-wide">${asset.currentPrice.toLocaleString()}</p>
205
+ </div>
206
+ </div>
207
+
208
+ <div className="text-right">
209
+ <span className={`text-sm font-black ${asset.perf >= 0 ? 'text-[#00C805]' : 'text-[#FF5000]'}`}>
210
+ {asset.perf >= 0 ? '+' : ''}{asset.perf.toFixed(2)}%
211
+ </span>
212
+ </div>
213
+ </div>
214
+ ))}
215
+ </div>
216
+ </div>
217
+
218
+ <div className="pt-8 border-t border-gray-800">
219
+ <div className="bg-gs-navy/30 rounded-2xl p-5 border border-white/5">
220
+ <h4 className="text-[10px] font-bold text-gs-gold uppercase tracking-widest mb-3">Portfolio Insight</h4>
221
+ <p className="text-xs text-gray-400 leading-relaxed">
222
+ {assetName} currently ranks #{topPerformers.findIndex(a => a.ticker === ticker) + 1} among your top-tier performers.
223
+ </p>
224
+ </div>
225
+ </div>
226
+ </div>
227
  </div>
228
  </div>
229
  </div>
src/data/mockData.js CHANGED
@@ -1,11 +1,11 @@
1
  export const mockPortfolios = {
2
  Cautious: {
3
  allocation: [
4
- { name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 40, color: '#1E293B', shares: 145, dateBought: '2023-01-15', type: 'mf' },
5
- { name: 'Vanguard Intl Bond (BNDX)', ticker: 'BNDX', value: 20, color: '#334155', shares: 85, dateBought: '2023-03-22', type: 'mf' },
6
- { name: 'SPDR S&P 500 (SPY)', ticker: 'SPY', value: 15, color: '#C5A880', shares: 12, dateBought: '2022-11-10', type: 'mf' },
7
- { name: 'Johnson & Johnson (JNJ)', ticker: 'JNJ', value: 15, color: '#64748B', shares: 35, dateBought: '2023-05-05', type: 'stock' },
8
- { name: 'Cash Equivalents', ticker: 'CASH', value: 10, color: '#E5E7EB', shares: 2500, dateBought: '2024-01-01', type: 'stock' }
9
  ],
10
  riskLevel: 'Low',
11
  expectedReturn: '4-5%',
@@ -14,12 +14,12 @@ export const mockPortfolios = {
14
  },
15
  Balanced: {
16
  allocation: [
17
- { name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 30, color: '#0B233F', shares: 45, dateBought: '2022-08-14', type: 'mf' },
18
- { name: 'Microsoft Corp (MSFT)', ticker: 'MSFT', value: 15, color: '#C5A880', shares: 25, dateBought: '2021-12-01', type: 'stock' },
19
- { name: 'Apple Inc. (AAPL)', ticker: 'AAPL', value: 15, color: '#1E293B', shares: 60, dateBought: '2022-02-18', type: 'stock' },
20
- { name: 'Vanguard Total Intl (VXUS)', ticker: 'VXUS', value: 15, color: '#64748B', shares: 90, dateBought: '2023-06-30', type: 'mf' },
21
- { name: 'Berkshire Hathaway (BRK-B)', ticker: 'BRK-B', value: 10, color: '#334155', shares: 18, dateBought: '2022-05-12', type: 'stock' },
22
- { name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 15, color: '#94A3B8', shares: 70, dateBought: '2023-11-20', type: 'mf' }
23
  ],
24
  riskLevel: 'Medium',
25
  expectedReturn: '7-9%',
@@ -28,12 +28,12 @@ export const mockPortfolios = {
28
  },
29
  Bold: {
30
  allocation: [
31
- { name: 'Invesco QQQ Trust (QQQ)', ticker: 'QQQ', value: 25, color: '#0B233F', shares: 35, dateBought: '2021-09-10', type: 'mf' },
32
- { name: 'Tesla, Inc. (TSLA)', ticker: 'TSLA', value: 20, color: '#C5A880', shares: 40, dateBought: '2022-10-05', type: 'stock' },
33
- { name: 'Amazon.com (AMZN)', ticker: 'AMZN', value: 15, color: '#1E293B', shares: 55, dateBought: '2023-02-28', type: 'stock' },
34
- { name: 'Google (GOOGL)', ticker: 'GOOGL', value: 15, color: '#64748B', shares: 65, dateBought: '2023-04-14', type: 'stock' },
35
- { name: 'JPMorgan Chase (JPM)', ticker: 'JPM', value: 15, color: '#334155', shares: 42, dateBought: '2022-07-22', type: 'stock' },
36
- { name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 10, color: '#94A3B8', shares: 15, dateBought: '2024-01-10', type: 'mf' }
37
  ],
38
  riskLevel: 'High',
39
  expectedReturn: '12-15%',
 
1
  export const mockPortfolios = {
2
  Cautious: {
3
  allocation: [
4
+ { name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 40, color: '#1E293B', shares: 145, avgCost: 74.50, dateBought: '2023-01-15', type: 'mf' },
5
+ { name: 'Vanguard Intl Bond (BNDX)', ticker: 'BNDX', value: 20, color: '#334155', shares: 85, avgCost: 48.20, dateBought: '2023-03-22', type: 'mf' },
6
+ { name: 'SPDR S&P 500 (SPY)', ticker: 'SPY', value: 15, color: '#C5A880', shares: 12, avgCost: 410.00, dateBought: '2022-11-10', type: 'mf' },
7
+ { name: 'Johnson & Johnson (JNJ)', ticker: 'JNJ', value: 15, color: '#64748B', shares: 35, avgCost: 155.00, dateBought: '2023-05-05', type: 'stock' },
8
+ { name: 'Cash Equivalents', ticker: 'CASH', value: 10, color: '#E5E7EB', shares: 2500, avgCost: 1.00, dateBought: '2024-01-01', type: 'stock' }
9
  ],
10
  riskLevel: 'Low',
11
  expectedReturn: '4-5%',
 
14
  },
15
  Balanced: {
16
  allocation: [
17
+ { name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 30, color: '#0B233F', shares: 45, avgCost: 380.00, dateBought: '2022-08-14', type: 'mf' },
18
+ { name: 'Microsoft Corp (MSFT)', ticker: 'MSFT', value: 15, color: '#C5A880', shares: 25, avgCost: 290.00, dateBought: '2021-12-01', type: 'stock' },
19
+ { name: 'Apple Inc. (AAPL)', ticker: 'AAPL', value: 15, color: '#1E293B', shares: 60, avgCost: 145.00, dateBought: '2022-02-18', type: 'stock' },
20
+ { name: 'Vanguard Total Intl (VXUS)', ticker: 'VXUS', value: 15, color: '#64748B', shares: 90, avgCost: 52.00, dateBought: '2023-06-30', type: 'mf' },
21
+ { name: 'Berkshire Hathaway (BRK-B)', ticker: 'BRK-B', value: 10, color: '#334155', shares: 18, avgCost: 310.00, dateBought: '2022-05-12', type: 'stock' },
22
+ { name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 15, color: '#94A3B8', shares: 70, avgCost: 72.00, dateBought: '2023-11-20', type: 'mf' }
23
  ],
24
  riskLevel: 'Medium',
25
  expectedReturn: '7-9%',
 
28
  },
29
  Bold: {
30
  allocation: [
31
+ { name: 'Invesco QQQ Trust (QQQ)', ticker: 'QQQ', value: 25, color: '#0B233F', shares: 35, avgCost: 320.00, dateBought: '2021-09-10', type: 'mf' },
32
+ { name: 'Tesla, Inc. (TSLA)', ticker: 'TSLA', value: 20, color: '#C5A880', shares: 40, avgCost: 180.00, dateBought: '2022-10-05', type: 'stock' },
33
+ { name: 'Amazon.com (AMZN)', ticker: 'AMZN', value: 15, color: '#1E293B', shares: 55, avgCost: 120.00, dateBought: '2023-02-28', type: 'stock' },
34
+ { name: 'Google (GOOGL)', ticker: 'GOOGL', value: 15, color: '#64748B', shares: 65, avgCost: 110.00, dateBought: '2023-04-14', type: 'stock' },
35
+ { name: 'JPMorgan Chase (JPM)', ticker: 'JPM', value: 15, color: '#334155', shares: 42, avgCost: 135.00, dateBought: '2022-07-22', type: 'stock' },
36
+ { name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 10, color: '#94A3B8', shares: 15, avgCost: 400.00, dateBought: '2024-01-10', type: 'mf' }
37
  ],
38
  riskLevel: 'High',
39
  expectedReturn: '12-15%',
src/pages/Dashboard.jsx CHANGED
@@ -11,9 +11,10 @@ import MacroTracker from '../components/MacroTracker';
11
  import StockPopup from '../components/StockPopup';
12
  import PortfolioHeatmap from '../components/PortfolioHeatmap';
13
  import FinancialCalculators from '../components/FinancialCalculators';
14
- import { LayoutGrid, Plus } from 'lucide-react';
15
- import { AnimatePresence } from 'framer-motion';
16
  import StockSearch from '../components/StockSearch';
 
17
 
18
  export default function Dashboard({ riskProfile }) {
19
  // Initialize with a cached portfolio if available, otherwise an empty one
@@ -26,10 +27,7 @@ export default function Dashboard({ riskProfile }) {
26
  console.error("Error loading saved portfolio:", e);
27
  }
28
  }
29
- return {
30
- ...mockPortfolios[riskProfile],
31
- allocation: []
32
- };
33
  });
34
 
35
  // Save to cache whenever portfolio changes
@@ -42,6 +40,7 @@ export default function Dashboard({ riskProfile }) {
42
  const [activePopupAsset, setActivePopupAsset] = useState(null);
43
  const [showHeatmap, setShowHeatmap] = useState(false);
44
  const [isSearchOpen, setIsSearchOpen] = useState(false);
 
45
  const [totals, setTotals] = useState({ value: 0, change: 0, percent: 0, rawValue: 0, loading: true });
46
  const [prices, setPrices] = useState({});
47
 
@@ -63,6 +62,10 @@ export default function Dashboard({ riskProfile }) {
63
  const marketVal = asset.shares * priceData.price;
64
  const weight = marketTotal > 0 ? (marketVal / marketTotal) * 100 : 0;
65
 
 
 
 
 
66
  const metrics = tickerMetrics[asset.ticker] || { beta: 1, expenseRatio: 0.1 };
67
  totalBeta += (metrics.beta || 1) * (weight / 100);
68
  totalExpense += (metrics.expenseRatio || 0.1) * (weight / 100);
@@ -71,7 +74,8 @@ export default function Dashboard({ riskProfile }) {
71
  ...asset,
72
  value: Number(weight.toFixed(2)),
73
  dayChange: priceData.percent,
74
- dollarChange: priceData.change
 
75
  };
76
  });
77
 
@@ -155,6 +159,32 @@ export default function Dashboard({ riskProfile }) {
155
  });
156
  };
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  useEffect(() => {
159
  async function calculateTotals() {
160
  if (currentPortfolio.allocation.length === 0) {
@@ -176,7 +206,8 @@ export default function Dashboard({ riskProfile }) {
176
  priceMap[ticker] = {
177
  price: data.currentPrice,
178
  change: data.change,
179
- percent: (data.change / (data.currentPrice - data.change)) * 100
 
180
  };
181
  newTotalValue += currentPortfolio.allocation[index].shares * data.currentPrice;
182
  newTotalChange += (currentPortfolio.allocation[index].shares * data.change);
@@ -197,7 +228,7 @@ export default function Dashboard({ riskProfile }) {
197
  });
198
  }
199
  calculateTotals();
200
- }, [JSON.stringify(currentPortfolio.allocation.map(a => `${a.ticker}-${a.shares}`))]); // Only watch ticker/shares, not the derived weights changes
201
 
202
  return (
203
  <div className="min-h-screen bg-gs-light p-6 md:p-12 relative">
@@ -236,132 +267,101 @@ export default function Dashboard({ riskProfile }) {
236
  </div>
237
  </header>
238
 
239
- {/* Top Section: Allocation */}
240
- <div className="bg-white rounded-2xl p-8 shadow-sm border border-gray-100 flex flex-col md:flex-row items-center mb-8">
241
- <div className="w-full md:w-1/2 h-64">
242
- <ResponsiveContainer width="100%" height="100%">
243
- <PieChart>
244
- <Pie
245
- data={displayPortfolio.allocation}
246
- cx="50%"
247
- cy="50%"
248
- innerRadius={80}
249
- outerRadius={110}
250
- paddingAngle={2}
251
- dataKey="value"
252
- nameKey="name"
253
- stroke="none"
254
- >
255
- {displayPortfolio.allocation.map((entry, index) => (
256
- <Cell key={`cell-${index}`} fill={entry.color} />
257
- ))}
258
- </Pie>
259
- <Tooltip
260
- formatter={(value, name) => [`${value}%`, name]}
261
- contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }}
262
- />
263
- </PieChart>
264
- </ResponsiveContainer>
265
- </div>
266
- <div className="w-full md:w-1/2 space-y-4">
267
- <div className="flex justify-between items-center mb-4">
268
- <h3 className="text-xl font-medium text-gs-navy">Current Allocation</h3>
269
- <div className="flex gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  <button
271
- onClick={() => setIsSearchOpen(true)}
272
- className="bg-gs-navy text-white p-1.5 rounded-lg hover:bg-gs-gold hover:text-gs-navy transition-all shadow-sm"
273
- title="Add Asset"
274
  >
275
- <Plus size={18} />
 
276
  </button>
277
  <button
278
  onClick={() => setShowHeatmap(true)}
279
- className="text-gs-slate hover:text-gs-gold transition-colors p-1"
280
- title="View Heatmap"
281
  >
282
- <LayoutGrid size={18} />
 
283
  </button>
284
  </div>
285
  </div>
286
-
287
- {/* Asset Type Split Indicator */}
288
- <div className="mb-6 bg-gs-light/30 p-4 rounded-xl border border-gray-100">
289
- <div className="flex justify-between text-[10px] font-bold text-gs-slate uppercase tracking-widest mb-2">
290
- <span>Stocks ({displayPortfolio.stockSplit.toFixed(2)}%)</span>
291
- <span>Mutual Funds ({displayPortfolio.mfSplit.toFixed(2)}%)</span>
292
- </div>
293
- <div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden flex">
294
- <div
295
- className="h-full bg-gs-navy transition-all duration-1000"
296
- style={{ width: `${displayPortfolio.stockSplit}%` }}
297
- ></div>
298
- <div
299
- className="h-full bg-gs-gold transition-all duration-1000"
300
- style={{ width: `${displayPortfolio.mfSplit}%` }}
301
- ></div>
302
- </div>
303
- </div>
304
 
305
- <p className="text-xs text-gs-slate mb-3 italic">Click an asset to view historical performance and AI analysis.</p>
306
- <div className="max-h-60 overflow-y-auto pr-2">
307
- {displayPortfolio.allocation.length > 0 ? (
308
- displayPortfolio.allocation.map((asset, idx) => (
309
- <button
310
- key={idx}
311
- onClick={() => {
312
- if (asset.ticker) {
313
- setActivePopupAsset({ ticker: asset.ticker, name: asset.name });
314
- }
315
- }}
316
- className="w-full text-left flex justify-between items-center p-3 rounded-lg transition-colors mb-2 bg-gs-light/50 hover:bg-gray-100 hover:shadow-sm border border-transparent hover:border-gray-200 group"
317
- >
318
- <div className="flex items-center justify-between w-full">
319
- <div className="flex items-center">
320
- <div className="w-4 h-4 rounded-full mr-3 shadow-sm" style={{ backgroundColor: asset.color }}></div>
321
- <div className="flex flex-col items-start">
322
- <span className="text-gs-slate font-medium text-sm group-hover:text-gs-navy transition-colors">{asset.name}</span>
323
- <div className="flex items-center gap-2 mt-0.5">
324
- <span className="text-[10px] text-gray-400 font-medium">{asset.shares} shares</span>
325
- {asset.dayChange !== undefined && (
326
- <span className={`text-[10px] font-bold ${asset.dayChange >= 0 ? 'text-green-600' : 'text-red-500'}`}>
327
- {asset.dayChange >= 0 ? '+' : ''}${Math.abs(asset.dollarChange || 0).toFixed(2)} ({asset.dayChange >= 0 ? '▲' : '▼'} {Math.abs(asset.dayChange).toFixed(2)}%)
328
- </span>
329
- )}
330
- </div>
331
- </div>
332
- </div>
333
- <span className="font-bold text-gs-navy text-sm ml-4">{asset.value.toFixed(2)}%</span>
334
- </div>
335
- </button>
336
- ))
337
- ) : (
338
- <div className="text-center py-12 bg-gs-light/20 rounded-2xl border-2 border-dashed border-gray-200">
339
- <p className="text-gs-slate text-sm font-light mb-6 italic">Your portfolio is currently empty.</p>
340
- <button
341
- onClick={() => setIsSearchOpen(true)}
342
- className="inline-flex items-center gap-2 px-8 py-3 bg-gs-navy text-white rounded-xl hover:bg-gs-gold hover:text-gs-navy transition-all shadow-lg font-bold"
343
- >
344
- <Plus size={18} />
345
- Build Your Portfolio
346
- </button>
347
- </div>
348
- )}
349
- </div>
350
  </div>
351
  </div>
352
 
353
  {/* Investment & Retirement Planning Section */}
354
  <FinancialCalculators />
355
 
356
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
357
- {/* Left Column: Indicators */}
358
- <div className="lg:col-span-2 space-y-8">
359
- <Indicators portfolio={displayPortfolio} />
360
- <FeeTransparencyModule portfolio={displayPortfolio} />
361
- </div>
362
-
363
- {/* Right Column: Rebalancing Engine */}
364
- <div className="space-y-8">
365
  <RebalancingEngine onScenarioSelect={handleRebalance} />
366
  </div>
367
  </div>
@@ -383,8 +383,38 @@ export default function Dashboard({ riskProfile }) {
383
  ticker={activePopupAsset?.ticker}
384
  assetName={activePopupAsset?.name}
385
  onClose={closeStockPopup}
 
 
386
  />
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  <AnimatePresence>
389
  {showHeatmap && (
390
  <PortfolioHeatmap
 
11
  import StockPopup from '../components/StockPopup';
12
  import PortfolioHeatmap from '../components/PortfolioHeatmap';
13
  import FinancialCalculators from '../components/FinancialCalculators';
14
+ import { LayoutGrid, Plus, PieChart as PieIcon, BarChart3, X, ChevronRight } from 'lucide-react';
15
+ import { motion, AnimatePresence } from 'framer-motion';
16
  import StockSearch from '../components/StockSearch';
17
+ import HoldingsList from '../components/HoldingsList';
18
 
19
  export default function Dashboard({ riskProfile }) {
20
  // Initialize with a cached portfolio if available, otherwise an empty one
 
27
  console.error("Error loading saved portfolio:", e);
28
  }
29
  }
30
+ return mockPortfolios[riskProfile] || { allocation: [] };
 
 
 
31
  });
32
 
33
  // Save to cache whenever portfolio changes
 
40
  const [activePopupAsset, setActivePopupAsset] = useState(null);
41
  const [showHeatmap, setShowHeatmap] = useState(false);
42
  const [isSearchOpen, setIsSearchOpen] = useState(false);
43
+ const [isHoldingsOpen, setIsHoldingsOpen] = useState(false);
44
  const [totals, setTotals] = useState({ value: 0, change: 0, percent: 0, rawValue: 0, loading: true });
45
  const [prices, setPrices] = useState({});
46
 
 
62
  const marketVal = asset.shares * priceData.price;
63
  const weight = marketTotal > 0 ? (marketVal / marketTotal) * 100 : 0;
64
 
65
+ const totalCost = asset.shares * (asset.buyPrice || priceData.price);
66
+ const gainLoss = marketVal - totalCost;
67
+ const returnPct = (totalCost > 0 && !isNaN(gainLoss) && isFinite(gainLoss)) ? (gainLoss / totalCost) * 100 : 0;
68
+
69
  const metrics = tickerMetrics[asset.ticker] || { beta: 1, expenseRatio: 0.1 };
70
  totalBeta += (metrics.beta || 1) * (weight / 100);
71
  totalExpense += (metrics.expenseRatio || 0.1) * (weight / 100);
 
74
  ...asset,
75
  value: Number(weight.toFixed(2)),
76
  dayChange: priceData.percent,
77
+ dollarChange: priceData.change,
78
+ returnPct
79
  };
80
  });
81
 
 
159
  });
160
  };
161
 
162
+ const handleRemoveAsset = (ticker) => {
163
+ setCurrentPortfolio(prev => {
164
+ const updatedAllocation = prev.allocation.filter(a => a.ticker !== ticker);
165
+
166
+ // Redistribute value to maintain 100%
167
+ if (updatedAllocation.length > 0) {
168
+ const currentSum = updatedAllocation.reduce((acc, a) => acc + a.value, 0);
169
+ const scale = 100 / currentSum;
170
+ updatedAllocation.forEach(a => {
171
+ a.value = Number((a.value * scale).toFixed(2));
172
+ });
173
+
174
+ // Final adjust for rounding
175
+ const finalSum = updatedAllocation.reduce((acc, a) => acc + a.value, 0);
176
+ if (finalSum !== 100) {
177
+ updatedAllocation[0].value += Number((100 - finalSum).toFixed(2));
178
+ }
179
+ }
180
+
181
+ return {
182
+ ...prev,
183
+ allocation: updatedAllocation
184
+ };
185
+ });
186
+ };
187
+
188
  useEffect(() => {
189
  async function calculateTotals() {
190
  if (currentPortfolio.allocation.length === 0) {
 
206
  priceMap[ticker] = {
207
  price: data.currentPrice,
208
  change: data.change,
209
+ percent: (data.change / (data.currentPrice - data.change)) * 100,
210
+ history: data.history
211
  };
212
  newTotalValue += currentPortfolio.allocation[index].shares * data.currentPrice;
213
  newTotalChange += (currentPortfolio.allocation[index].shares * data.change);
 
228
  });
229
  }
230
  calculateTotals();
231
+ }, [JSON.stringify(currentPortfolio.allocation.map(a => `${a.ticker}-${a.shares}`))]);
232
 
233
  return (
234
  <div className="min-h-screen bg-gs-light p-6 md:p-12 relative">
 
267
  </div>
268
  </header>
269
 
270
+ {/* Portfolio Overview & Holdings Section */}
271
+ <div className="space-y-8 mb-10">
272
+ {/* Top Landscape "Tablet": Allocation & Summary */}
273
+ <div className="bg-white rounded-[2.5rem] p-10 shadow-xl border border-gray-100 flex flex-col xl:flex-row items-center gap-12 relative overflow-hidden">
274
+ <div className="absolute top-0 right-0 w-64 h-64 bg-gs-gold/5 rounded-full -mr-32 -mt-32 blur-3xl"></div>
275
+
276
+ {/* Left Column: Chart & Composition */}
277
+ <div className="flex flex-col items-center xl:items-start flex-1 min-w-[320px]">
278
+ <h3 className="text-2xl font-bold text-gs-navy mb-6 flex items-center gap-3">
279
+ <PieIcon className="text-gs-gold" size={24} /> Allocation
280
+ </h3>
281
+ <div className="w-full h-64 mb-8">
282
+ <ResponsiveContainer width="100%" height="100%">
283
+ <PieChart>
284
+ <Pie
285
+ data={displayPortfolio.allocation}
286
+ cx="50%"
287
+ cy="50%"
288
+ innerRadius={85}
289
+ outerRadius={115}
290
+ paddingAngle={4}
291
+ dataKey="value"
292
+ nameKey="name"
293
+ stroke="none"
294
+ >
295
+ {displayPortfolio.allocation.map((entry, index) => (
296
+ <Cell key={`cell-${index}`} fill={entry.color} />
297
+ ))}
298
+ </Pie>
299
+ <Tooltip
300
+ contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)' }}
301
+ formatter={(value) => [`${value}%`, 'Weight']}
302
+ />
303
+ </PieChart>
304
+ </ResponsiveContainer>
305
+ </div>
306
+
307
+ {/* Portfolio Composition moved here under the chart */}
308
+ <div className="w-full max-w-[300px] mt-2">
309
+ <div className="flex justify-between text-[10px] font-bold text-gs-navy uppercase tracking-widest mb-2.5">
310
+ <span>Equities ({displayPortfolio.stockSplit.toFixed(0)}%)</span>
311
+ <span>Fixed Income ({displayPortfolio.mfSplit.toFixed(0)}%)</span>
312
+ </div>
313
+ <div className="h-1.5 w-full bg-gray-100 rounded-full overflow-hidden flex shadow-inner">
314
+ <div className="h-full bg-gs-navy" style={{ width: `${displayPortfolio.stockSplit}%` }}></div>
315
+ <div className="h-full bg-gs-gold" style={{ width: `${displayPortfolio.mfSplit}%` }}></div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ {/* Right: Strategy, Transparency & Side-by-Side Actions */}
321
+ <div className="flex-[1.5] w-full flex flex-col justify-between space-y-6">
322
+ <div className="space-y-4">
323
+ <div className="bg-gs-navy rounded-[2rem] p-6 text-white relative overflow-hidden shadow-lg border border-white/5">
324
+ <div className="absolute -right-6 -top-6 w-24 h-24 bg-gs-gold/10 rounded-full blur-2xl"></div>
325
+ <h4 className="text-[9px] font-bold text-gs-gold uppercase tracking-[0.2em] mb-2.5">Strategy Target</h4>
326
+ <p className="text-sm font-light leading-relaxed">
327
+ Portfolio optimized for a <span className="font-bold text-gs-gold">{riskProfile}</span> objective.
328
+ </p>
329
+ </div>
330
+
331
+ <FeeTransparencyModule portfolio={displayPortfolio} />
332
+ </div>
333
+
334
+ {/* Side-by-Side Action Buttons at the bottom */}
335
+ <div className="grid grid-cols-2 gap-4">
336
  <button
337
+ onClick={() => setIsHoldingsOpen(true)}
338
+ className="flex items-center justify-center gap-3 py-5 bg-gs-navy text-white rounded-2xl hover:bg-gs-gold hover:text-gs-navy transition-all group shadow-xl shadow-gs-navy/20 active:scale-95 border border-white/5"
 
339
  >
340
+ <BarChart3 size={18} className="group-hover:scale-110 transition-transform" />
341
+ <span className="font-bold text-xs tracking-tight uppercase">Holdings</span>
342
  </button>
343
  <button
344
  onClick={() => setShowHeatmap(true)}
345
+ className="flex items-center justify-center gap-3 py-5 bg-gs-light text-gs-navy rounded-2xl hover:bg-gs-navy hover:text-white transition-all group active:scale-95 border border-gs-navy/5 shadow-sm"
 
346
  >
347
+ <LayoutGrid size={18} className="group-hover:scale-110 transition-transform" />
348
+ <span className="font-bold text-xs tracking-tight uppercase">Heatmap</span>
349
  </button>
350
  </div>
351
  </div>
352
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
355
+ <Indicators portfolio={displayPortfolio} isFlattened={true} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  </div>
357
  </div>
358
 
359
  {/* Investment & Retirement Planning Section */}
360
  <FinancialCalculators />
361
 
362
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-8 mt-8">
363
+ {/* Rebalancing Engine - Spanning more width since Indicators are above */}
364
+ <div className="xl:col-span-3">
 
 
 
 
 
 
365
  <RebalancingEngine onScenarioSelect={handleRebalance} />
366
  </div>
367
  </div>
 
383
  ticker={activePopupAsset?.ticker}
384
  assetName={activePopupAsset?.name}
385
  onClose={closeStockPopup}
386
+ allHoldings={displayPortfolio.allocation}
387
+ prices={prices}
388
  />
389
 
390
+ <AnimatePresence>
391
+ {isHoldingsOpen && (
392
+ <div className="fixed inset-0 z-[60] flex items-center justify-center p-4 md:p-10 bg-gs-navy/40 backdrop-blur-md">
393
+ <motion.div
394
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
395
+ animate={{ opacity: 1, scale: 1, y: 0 }}
396
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
397
+ className="w-full max-w-7xl max-h-full overflow-hidden flex flex-col relative"
398
+ >
399
+ <button
400
+ onClick={() => setIsHoldingsOpen(false)}
401
+ className="absolute top-4 right-6 z-10 p-2 bg-white/10 hover:bg-white/20 text-white rounded-full transition-all backdrop-blur-md border border-white/10"
402
+ >
403
+ <X size={20} />
404
+ </button>
405
+ <div className="overflow-y-auto rounded-[2rem] shadow-2xl">
406
+ <HoldingsList
407
+ allocation={displayPortfolio.allocation}
408
+ prices={prices}
409
+ onAddClick={() => setIsSearchOpen(true)}
410
+ onRemoveAsset={handleRemoveAsset}
411
+ />
412
+ </div>
413
+ </motion.div>
414
+ </div>
415
+ )}
416
+ </AnimatePresence>
417
+
418
  <AnimatePresence>
419
  {showHeatmap && (
420
  <PortfolioHeatmap