Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitattributes +0 -16
- src/components/FeeTransparencyModule.jsx +132 -41
- src/components/HoldingsList.jsx +304 -0
- src/components/Indicators.jsx +11 -5
- src/components/InvestmentCommittee.jsx +27 -31
- src/components/StockPopup.jsx +75 -18
- src/data/mockData.js +17 -17
- src/pages/Dashboard.jsx +149 -119
.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 |
-
<
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
<
|
| 28 |
-
<
|
|
|
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
-
</header>
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
<
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
<
|
| 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 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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"
|
| 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=
|
| 156 |
-
<header className=
|
| 157 |
<div>
|
| 158 |
-
<h2 className=
|
| 159 |
-
<Users className=
|
| 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=
|
| 170 |
>
|
| 171 |
-
{loading ? <Loader2 size={16} className="animate-spin mr-
|
| 172 |
-
|
| 173 |
</button>
|
| 174 |
)}
|
| 175 |
</header>
|
| 176 |
|
| 177 |
-
<div className="flex-1 overflow-y-auto space-y-
|
| 178 |
{!ticker && (
|
| 179 |
-
<div className="h-full flex items-center justify-center text-gray-400 text-
|
| 180 |
-
Waiting for
|
| 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-
|
| 191 |
>
|
| 192 |
-
<div className={`mt-
|
| 193 |
{agents[msg.agent].icon}
|
| 194 |
</div>
|
| 195 |
-
<div className="bg-gray-50 rounded-xl p-
|
| 196 |
-
<span className={`text-
|
| 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-
|
| 207 |
-
<Loader2 size={
|
| 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=
|
| 217 |
>
|
| 218 |
-
<div className="bg-gs-light p-
|
| 219 |
<div className="flex justify-between items-center">
|
| 220 |
-
<span className="font-
|
| 221 |
<div className="flex items-center">
|
| 222 |
-
<span className={`text-
|
| 223 |
{convictionScore}
|
| 224 |
</span>
|
| 225 |
-
<span className="text-gray-400 ml-
|
| 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-
|
| 81 |
style={{ backgroundColor: bgColor }}
|
| 82 |
>
|
| 83 |
{/* Header */}
|
| 84 |
-
<div className="p-6 pb-
|
| 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-
|
| 106 |
{/* Main Chart Area */}
|
| 107 |
-
<div className="lg:col-span-
|
| 108 |
-
<div className="h-
|
| 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-
|
| 139 |
{['1W', '1M', '3M', '1Y', 'ALL'].map(t => (
|
| 140 |
<button
|
| 141 |
key={t}
|
| 142 |
onClick={() => setTimeframe(t)}
|
| 143 |
-
className={`text-sm font-
|
| 144 |
t === timeframe
|
| 145 |
? (viewData.isPositive ? 'text-[#00C805] border-[#00C805]' : 'text-[#FF5000] border-[#FF5000]')
|
| 146 |
-
: 'text-gray-
|
| 147 |
}`}
|
| 148 |
>
|
| 149 |
{t}
|
|
@@ -151,15 +165,7 @@ export default function StockPopup({ ticker, assetName, onClose }) {
|
|
| 151 |
))}
|
| 152 |
</div>
|
| 153 |
|
| 154 |
-
<div className="mt-
|
| 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}`))]);
|
| 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 |
-
{/*
|
| 240 |
-
<div className="
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
<button
|
| 271 |
-
onClick={() =>
|
| 272 |
-
className="bg-gs-navy text-white
|
| 273 |
-
title="Add Asset"
|
| 274 |
>
|
| 275 |
-
<
|
|
|
|
| 276 |
</button>
|
| 277 |
<button
|
| 278 |
onClick={() => setShowHeatmap(true)}
|
| 279 |
-
className="text-gs-
|
| 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 |
-
|
| 306 |
-
<
|
| 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
|
| 357 |
-
{/*
|
| 358 |
-
<div className="
|
| 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
|