gs-port / src /components /TransparencyModal.jsx
Scribbler310
feat: portfolio dashboard v1.0
dbc70ee
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Info, DollarSign, Shield } from 'lucide-react';
export default function TransparencyModal({ isOpen, onClose, data, currentPortfolio, totalValue, prices }) {
if (!isOpen || !data) return null;
return (
<AnimatePresence>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-gs-navy/60 backdrop-blur-sm"
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative bg-white rounded-3xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-hidden border border-gray-100 flex flex-col"
>
{/* Header (Fixed at top) */}
<div className="bg-gs-navy p-6 flex justify-between items-start text-white shrink-0">
<div>
<span className="text-gs-gold text-xs font-semibold uppercase tracking-wider mb-2 block">Recommendation</span>
<h2 className="text-2xl font-light">{data.title}</h2>
</div>
<button onClick={onClose} className="text-white/60 hover:text-white transition-colors">
<X size={24} />
</button>
</div>
<div className="p-8 space-y-6 overflow-y-auto scrollbar-hide">
{/* The Advice */}
<div className="bg-gs-light p-5 rounded-xl border-l-4 border-gs-gold">
<h3 className="font-medium text-gs-navy mb-1 flex items-center">
<Info size={18} className="mr-2 text-gs-gold" /> The Action Plan
</h3>
<p className="text-gs-slate font-light text-lg">
{(() => {
let advice = data.advice;
const hasBonds = currentPortfolio?.allocation?.some(a =>
a.ticker?.includes('BND') ||
a.name?.toLowerCase().includes('bond')
);
if (!hasBonds && advice.includes('Bonds')) {
advice = advice.replace('Bonds', 'Cash Reserves');
}
return advice;
})()}
</p>
</div>
{/* Visual Comparison Section */}
<div className="bg-gs-light/50 p-6 rounded-2xl border border-gray-100 min-h-[100px] flex flex-col justify-center">
<h3 className="text-xs uppercase tracking-widest text-gs-slate font-semibold mb-4">
Visual Rebalance Suggestion
</h3>
<div className="space-y-6">
{currentPortfolio?.allocation?.length > 0 ? (
currentPortfolio.allocation.slice(0, 3).map((asset, idx) => {
if (!asset) return null;
// Mock logic for "Target" based on scenario
let change = 0;
const trigger = data.trigger || "";
const assetName = asset.name || "";
if (trigger.includes("Market Drop")) {
if (assetName.includes("Bond") || assetName.includes("Cash")) change = -5;
else change = 5;
} else if (trigger.includes("Life Expense")) {
if (assetName.includes("Cash")) change = 20;
else change = -10;
} else if (trigger.includes("Inflation")) {
if (assetName.includes("Vanguard") || assetName.includes("Value")) change = 8;
else change = -4;
}
const val = Number(asset.value) || 0;
const targetValue = Math.max(0, Math.min(100, val + change));
// Calculate Dollar and Share Changes
const ticker = asset.ticker;
const priceObj = prices[ticker];
const price = priceObj?.price;
// Use a fallback total value if current calculation is still in flight
const activeTotal = totalValue > 0 ? totalValue : 50000;
const currentValue = (val / 100) * activeTotal;
const targetValueDollar = (targetValue / 100) * activeTotal;
const dollarDiff = targetValueDollar - currentValue;
const sharesDiff = price ? Math.abs(Math.round(dollarDiff / price)) : null;
return (
<div key={idx} className="space-y-2">
<div className="flex justify-between text-xs">
<span className="font-bold text-gs-navy">{ticker}</span>
<div className="text-right">
<span className="text-gs-slate">
{val.toFixed(2)}% <span className="mx-2"></span>
<span className={change > 0 ? 'text-green-600' : change < 0 ? 'text-red-500' : 'text-gs-navy'}>
{targetValue.toFixed(2)}%
</span>
</span>
<p className={`text-[10px] font-bold ${change > 0 ? 'text-green-600' : 'text-red-500'}`}>
{change > 0 ? 'Buy' : 'Sell'} ${Math.abs(Math.round(dollarDiff)).toLocaleString()}
{sharesDiff !== null ? ` (${sharesDiff} shares)` : ' (Calculating...)'}
</p>
</div>
</div>
<div className="h-3 w-full bg-gray-200 rounded-full overflow-hidden flex relative">
<div
className="h-full bg-gs-navy opacity-30"
style={{ width: `${val}%` }}
></div>
<div
className={`h-full absolute top-0 left-0 transition-all duration-1000 ${change >= 0 ? 'bg-green-500' : 'bg-red-500'}`}
style={{ width: `${targetValue}%` }}
></div>
</div>
</div>
);
})
) : (
<div className="text-center py-4">
<p className="text-xs text-gs-slate italic">Add at least one stock to see a visual rebalance simulation.</p>
</div>
)}
</div>
<p className="text-[10px] text-gs-slate mt-4 italic text-center">
*Simulated shift based on your {currentPortfolio?.riskLevel || 'selected'} risk profile.
</p>
</div>
{/* The Why */}
<div>
<h3 className="font-medium text-gs-navy mb-2 flex items-center">
<Shield size={18} className="mr-2 text-gs-slate" /> Why we recommend this
</h3>
<p className="text-gs-slate font-light text-sm leading-relaxed">
{data.explanation}
</p>
</div>
<hr className="border-gray-100" />
{/* Radical Transparency Section */}
<div>
<h3 className="text-xs uppercase tracking-widest text-gs-slate font-semibold mb-4">
Full Transparency
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 rounded-lg border border-gray-100 bg-white">
<span className="text-sm text-gs-slate font-light flex items-center">
<DollarSign size={14} className="mr-2 text-gray-400" /> Fee Impact
</span>
<span className="font-medium text-gs-navy text-sm">{data.feeImpactDollars}</span>
</div>
<div className="flex justify-between items-center p-3 rounded-lg border border-gray-100 bg-white">
<span className="text-sm text-gs-slate font-light flex items-center">
<Shield size={14} className="mr-2 text-gray-400" /> Tax Considerations
</span>
<span className="font-medium text-gs-navy text-sm">{data.taxImpact}</span>
</div>
</div>
</div>
<button
onClick={onClose}
className="w-full mt-4 bg-gs-navy text-white py-4 rounded-xl font-medium hover:bg-gs-navy/90 transition-colors shadow-md shrink-0"
>
I Understand
</button>
</div>
</motion.div>
</div>
</AnimatePresence>
);
}