Spaces:
Sleeping
Sleeping
| import React, { useState, useMemo } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { TrendingUp, Wallet, Landmark, ArrowRight, X, Info, Calculator, Percent, ShieldCheck } from 'lucide-react'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'; | |
| const CALCULATORS = [ | |
| { | |
| id: 'compound', | |
| tag: 'INVESTING', | |
| title: 'Compound Interest Calculator', | |
| description: 'Watch your money grow year by year with a live animated chart. Adjust return rate, contributions, and timeline.', | |
| icon: <TrendingUp className="text-blue-500" />, | |
| color: 'border-blue-400', | |
| features: [ | |
| 'Year-by-year growth chart', | |
| 'Contribution vs. gain breakdown', | |
| 'Rule of 72 insight built in', | |
| 'Compare fee drag scenarios' | |
| ] | |
| }, | |
| { | |
| id: 'retirement', | |
| tag: 'RETIREMENT', | |
| title: 'Retirement Savings Calculator', | |
| description: 'Find your magic number — how much you need to retire, based on your spending, Social Security, and withdrawal rate.', | |
| icon: <Landmark className="text-amber-500" />, | |
| color: 'border-amber-400', | |
| features: [ | |
| '4% rule & safe withdrawal modeling', | |
| 'Social Security income offset', | |
| 'On-track vs. gap analysis', | |
| 'Inflation-adjusted projections' | |
| ] | |
| }, | |
| { | |
| id: 'tax', | |
| tag: 'TAX STRATEGY', | |
| title: 'Roth vs. Traditional IRA', | |
| description: 'Side-by-side after-tax comparison at retirement. Select your current and expected future tax brackets.', | |
| icon: <Wallet className="text-purple-500" />, | |
| color: 'border-purple-400', | |
| features: [ | |
| 'Current vs. retirement bracket selector', | |
| 'After-tax value comparison', | |
| 'Break-even tax rate shown', | |
| 'RMD impact explained' | |
| ] | |
| } | |
| ]; | |
| export default function FinancialCalculators() { | |
| const [activeCalc, setActiveCalc] = useState(null); | |
| return ( | |
| <div className="mt-12 mb-16"> | |
| <div className="flex items-center justify-between mb-8"> | |
| <div> | |
| <h2 className="text-2xl font-light text-gs-navy">Institutional <span className="font-semibold">Planning Tools</span></h2> | |
| <p className="text-gs-slate text-sm">Advanced modeling used by our private wealth advisors.</p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| {CALCULATORS.map((calc) => ( | |
| <CalculatorCard | |
| key={calc.id} | |
| calc={calc} | |
| onClick={() => setActiveCalc(calc)} | |
| /> | |
| ))} | |
| </div> | |
| <AnimatePresence> | |
| {activeCalc && ( | |
| <CalculatorModal | |
| calc={activeCalc} | |
| onClose={() => setActiveCalc(null)} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| } | |
| function CalculatorCard({ calc, onClick }) { | |
| return ( | |
| <motion.div | |
| whileHover={{ y: -5 }} | |
| className={`bg-white rounded-xl shadow-sm border-t-4 ${calc.color} p-6 flex flex-col h-full border-x border-b border-gray-100 hover:shadow-md transition-shadow cursor-pointer group`} | |
| onClick={onClick} | |
| > | |
| <div className="flex justify-between items-start mb-6"> | |
| <div className="p-3 bg-gray-50 rounded-xl group-hover:bg-gs-light transition-colors"> | |
| {calc.icon} | |
| </div> | |
| <span className="text-[10px] font-bold tracking-widest text-gs-slate/60 bg-gray-100 px-2 py-1 rounded"> | |
| {calc.tag} | |
| </span> | |
| </div> | |
| <h3 className="text-xl font-semibold text-gs-navy mb-3 group-hover:text-gs-gold transition-colors"> | |
| {calc.title} | |
| </h3> | |
| <p className="text-gs-slate text-sm font-light leading-relaxed mb-6 flex-grow"> | |
| {calc.description} | |
| </p> | |
| <ul className="space-y-2 mb-8"> | |
| {calc.features.map((feature, i) => ( | |
| <li key={i} className="flex items-center text-xs text-gs-slate/80"> | |
| <ShieldCheck size={14} className="text-gs-gold mr-2 flex-shrink-0" /> | |
| {feature} | |
| </li> | |
| ))} | |
| </ul> | |
| <div className="flex items-center text-gs-navy font-semibold text-sm group-hover:translate-x-1 transition-transform"> | |
| Open Calculator <ArrowRight size={16} className="ml-2" /> | |
| </div> | |
| </motion.div> | |
| ); | |
| } | |
| function CalculatorModal({ calc, onClose }) { | |
| const [inputs, setInputs] = useState({ | |
| salary: 120000, | |
| contribution: 15, // percent | |
| has401k: true, | |
| employerMatch: 4, | |
| age: 30, | |
| retirementAge: 65, | |
| initialSavings: 50000, | |
| annualReturn: 7, | |
| taxBracket: 24, | |
| futureTaxBracket: 20 | |
| }); | |
| const handleInputChange = (e) => { | |
| const { name, value, type, checked } = e.target; | |
| setInputs(prev => ({ | |
| ...prev, | |
| [name]: type === 'checkbox' ? checked : parseFloat(value) | |
| })); | |
| }; | |
| const renderCalculatorContent = () => { | |
| switch (calc.id) { | |
| case 'compound': | |
| return <CompoundCalc inputs={inputs} onChange={handleInputChange} />; | |
| case 'retirement': | |
| return <RetirementCalc inputs={inputs} onChange={handleInputChange} />; | |
| case 'tax': | |
| return <TaxCalc inputs={inputs} onChange={handleInputChange} />; | |
| default: | |
| return null; | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gs-navy/40 backdrop-blur-sm"> | |
| <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="bg-white rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col md:flex-row" | |
| > | |
| {/* Sidebar / Inputs */} | |
| <div className="w-full md:w-80 bg-gs-light p-8 border-r border-gray-200 overflow-y-auto"> | |
| <div className="flex items-center mb-8"> | |
| <div className="p-2 bg-white rounded-lg shadow-sm mr-3"> | |
| {calc.icon} | |
| </div> | |
| <h3 className="font-bold text-gs-navy uppercase tracking-wider text-sm">{calc.tag}</h3> | |
| </div> | |
| <div className="space-y-6"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <InputGroup | |
| label="Current Age" | |
| name="age" | |
| value={inputs.age} | |
| onChange={handleInputChange} | |
| /> | |
| <InputGroup | |
| label="Retire Age" | |
| name="retirementAge" | |
| value={inputs.retirementAge} | |
| onChange={handleInputChange} | |
| /> | |
| </div> | |
| <InputGroup | |
| label="Annual Salary" | |
| name="salary" | |
| value={inputs.salary} | |
| onChange={handleInputChange} | |
| prefix="$" | |
| /> | |
| <InputGroup | |
| label="401k Contribution (%)" | |
| name="contribution" | |
| value={inputs.contribution} | |
| onChange={handleInputChange} | |
| suffix="%" | |
| type="range" | |
| min="0" | |
| max="50" | |
| /> | |
| <InputGroup | |
| label="Employer Match (%)" | |
| name="employerMatch" | |
| value={inputs.employerMatch} | |
| onChange={handleInputChange} | |
| suffix="%" | |
| /> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <InputGroup | |
| label="Current Tax" | |
| name="taxBracket" | |
| value={inputs.taxBracket} | |
| onChange={handleInputChange} | |
| suffix="%" | |
| /> | |
| <InputGroup | |
| label="Retire Tax" | |
| name="futureTaxBracket" | |
| value={inputs.futureTaxBracket} | |
| onChange={handleInputChange} | |
| suffix="%" | |
| /> | |
| </div> | |
| <InputGroup | |
| label="Initial Savings" | |
| name="initialSavings" | |
| value={inputs.initialSavings} | |
| onChange={handleInputChange} | |
| prefix="$" | |
| /> | |
| <InputGroup | |
| label="Expected Return" | |
| name="annualReturn" | |
| value={inputs.annualReturn} | |
| onChange={handleInputChange} | |
| suffix="%" | |
| /> | |
| </div> | |
| <div className="mt-12 p-4 bg-gs-gold/10 rounded-xl border border-gs-gold/20"> | |
| <h4 className="text-xs font-bold text-gs-gold mb-2 flex items-center"> | |
| <Info size={14} className="mr-1" /> GS ADVICE | |
| </h4> | |
| <InvestmentAdvice inputs={inputs} /> | |
| </div> | |
| </div> | |
| {/* Main Content / Results */} | |
| <div className="flex-grow flex flex-col h-full bg-white relative"> | |
| <button | |
| onClick={onClose} | |
| className="absolute top-6 right-6 p-2 hover:bg-gray-100 rounded-full transition-colors z-10" | |
| > | |
| <X size={20} className="text-gs-slate" /> | |
| </button> | |
| <div className="p-8 md:p-12 overflow-y-auto"> | |
| <div className="mb-10"> | |
| <h2 className="text-3xl font-light text-gs-navy mb-2">{calc.title}</h2> | |
| <p className="text-gs-slate font-light">Interactive projection based on institutional modeling.</p> | |
| </div> | |
| {renderCalculatorContent()} | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |
| function InputGroup({ label, name, value, onChange, prefix, suffix, type = "number", min, max }) { | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center"> | |
| <label className="text-xs font-semibold text-gs-slate uppercase tracking-tighter">{label}</label> | |
| {type === "range" && <span className="text-sm font-bold text-gs-navy">{value}{suffix}</span>} | |
| </div> | |
| <div className="relative"> | |
| {prefix && <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{prefix}</span>} | |
| <input | |
| type={type} | |
| name={name} | |
| value={value} | |
| onChange={onChange} | |
| min={min} | |
| max={max} | |
| className={`w-full bg-white border border-gray-200 rounded-lg p-2 text-sm focus:ring-2 focus:ring-gs-gold focus:border-transparent outline-none transition-all ${prefix ? 'pl-7' : ''} ${suffix && type !== 'range' ? 'pr-7' : ''}`} | |
| /> | |
| {suffix && type !== "range" && <span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{suffix}</span>} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function InvestmentAdvice({ inputs }) { | |
| const { salary, contribution, employerMatch } = inputs; | |
| const totalContribution = contribution + employerMatch; | |
| let advice = ""; | |
| if (totalContribution < 15) { | |
| advice = `Increase your total savings to at least 15% ($${(salary * 0.15 / 12).toLocaleString()} /mo) to remain on track for institutional-grade retirement.`; | |
| } else if (contribution > employerMatch + 10) { | |
| advice = "You are over-contributing to 401k. Consider diversifying into a brokerage account for liquidity or high-yield GS instruments."; | |
| } else { | |
| advice = "Excellent contribution level. Ensure your portfolio allocation matches your risk profile in the main dashboard."; | |
| } | |
| return <p className="text-xs text-gs-navy leading-relaxed">{advice}</p>; | |
| } | |
| // Calculator Logic Components | |
| function CompoundCalc({ inputs }) { | |
| const data = useMemo(() => { | |
| let current = inputs.initialSavings; | |
| let totalContributed = inputs.initialSavings; | |
| const yearlyReturn = inputs.annualReturn / 100; | |
| const yearlyContribution = (inputs.salary * (inputs.contribution / 100)); | |
| const results = []; | |
| for (let i = 0; i <= 30; i++) { | |
| results.push({ | |
| year: `Year ${i}`, | |
| balance: Math.round(current), | |
| contributions: Math.round(totalContributed) | |
| }); | |
| current = (current + yearlyContribution) * (1 + yearlyReturn); | |
| totalContributed += yearlyContribution; | |
| } | |
| return results; | |
| }, [inputs]); | |
| const finalBalance = data[data.length - 1].balance; | |
| return ( | |
| <div className="space-y-8"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="p-6 bg-gs-navy text-white rounded-2xl"> | |
| <p className="text-xs font-bold text-gs-gold uppercase tracking-widest mb-1">Estimated Value (30 Years)</p> | |
| <p className="text-4xl font-light">${finalBalance.toLocaleString()}</p> | |
| </div> | |
| <div className="p-6 bg-gs-light rounded-2xl border border-gray-100"> | |
| <p className="text-xs font-bold text-gs-slate uppercase tracking-widest mb-1">Total Interest Earned</p> | |
| <p className="text-4xl font-light text-gs-navy">${(finalBalance - data[data.length-1].contributions).toLocaleString()}</p> | |
| </div> | |
| </div> | |
| <div className="h-[350px] w-full"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <AreaChart data={data}> | |
| <defs> | |
| <linearGradient id="colorBalance" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="5%" stopColor="#C5A880" stopOpacity={0.3}/> | |
| <stop offset="95%" stopColor="#C5A880" stopOpacity={0}/> | |
| </linearGradient> | |
| </defs> | |
| <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#eee" /> | |
| <XAxis dataKey="year" hide /> | |
| <YAxis | |
| tickFormatter={(value) => `$${value / 1000}k`} | |
| axisLine={false} | |
| tickLine={false} | |
| tick={{fontSize: 12, fill: '#666'}} | |
| /> | |
| <Tooltip | |
| formatter={(value) => [`$${value.toLocaleString()}`, '']} | |
| contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }} | |
| /> | |
| <Area type="monotone" dataKey="balance" stroke="#C5A880" strokeWidth={3} fillOpacity={1} fill="url(#colorBalance)" /> | |
| <Area type="monotone" dataKey="contributions" stroke="#0B233F" strokeWidth={2} fillOpacity={0.1} fill="#0B233F" /> | |
| </AreaChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function RetirementCalc({ inputs }) { | |
| const { salary, contribution, employerMatch, initialSavings, annualReturn, age, retirementAge } = inputs; | |
| const yearsToRetirement = retirementAge - age; | |
| const annualContribution = salary * ((contribution + employerMatch) / 100); | |
| const rate = annualReturn / 100; | |
| // FV of current savings + FV of annual contributions | |
| const fvInitial = initialSavings * Math.pow(1 + rate, yearsToRetirement); | |
| const fvContributions = annualContribution * (Math.pow(1 + rate, yearsToRetirement) - 1) / rate; | |
| const projectedTotal = fvInitial + fvContributions; | |
| // Retirement Need: 80% of salary * 25 (4% rule) | |
| const annualNeed = salary * 0.8; | |
| const totalNeed = annualNeed * 25; | |
| const gap = totalNeed - projectedTotal; | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="p-8 bg-gs-light rounded-2xl border border-dashed border-gs-gold/50 flex flex-col items-center justify-center text-center"> | |
| <Landmark size={48} className="text-gs-gold mb-4" /> | |
| <h4 className="text-xl font-medium text-gs-navy mb-2">Retirement Readiness Analysis</h4> | |
| <p className="text-gs-slate max-w-md"> | |
| To maintain your lifestyle, you'll need approximately <span className="font-bold text-gs-navy">${(totalNeed / 1000000).toFixed(1)}M</span>. | |
| Your current path projects a fund of <span className="font-bold text-gs-navy">${(projectedTotal / 1000000).toFixed(1)}M</span> in {yearsToRetirement} years. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-3 gap-4"> | |
| <div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center"> | |
| <p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Current Plan</p> | |
| <p className="text-lg font-semibold text-gs-navy">${(projectedTotal / 1000000).toFixed(2)}M</p> | |
| </div> | |
| <div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center"> | |
| <p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Projected {gap > 0 ? 'Gap' : 'Surplus'}</p> | |
| <p className={`text-lg font-semibold ${gap > 0 ? 'text-red-500' : 'text-green-600'}`}> | |
| ${Math.abs(gap / 1000000).toFixed(2)}M | |
| </p> | |
| </div> | |
| <div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center"> | |
| <p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Savings Rate</p> | |
| <p className="text-lg font-semibold text-gs-gold">{contribution + employerMatch}%</p> | |
| </div> | |
| </div> | |
| <div className="mt-4 p-4 bg-gs-navy text-white rounded-xl text-xs font-light"> | |
| <p><span className="text-gs-gold font-bold">PROJECTION BASIS:</span> Assumes {annualReturn}% annual return, inflation-adjusted spending, and adherence to the 4% safe withdrawal rule. Modeling includes GS institutional market assumptions.</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function TaxCalc({ inputs }) { | |
| const { salary, contribution, initialSavings, annualReturn, age, retirementAge, taxBracket, futureTaxBracket } = inputs; | |
| const years = retirementAge - age; | |
| const rate = annualReturn / 100; | |
| const annualContribution = salary * (contribution / 100); | |
| // Traditional IRA: Invest pre-tax, pay tax at withdrawal | |
| const tradFV_Initial = initialSavings * Math.pow(1 + rate, years); | |
| const tradFV_Cont = annualContribution * (Math.pow(1 + rate, years) - 1) / rate; | |
| const tradNet = (tradFV_Initial + tradFV_Cont) * (1 - (futureTaxBracket / 100)); | |
| // Roth IRA: Invest after-tax, tax-free withdrawal | |
| const rothInitial = initialSavings * (1 - (taxBracket / 100)); | |
| const rothAnnual = annualContribution * (1 - (taxBracket / 100)); | |
| const rothFV_Initial = rothInitial * Math.pow(1 + rate, years); | |
| const rothFV_Cont = rothAnnual * (Math.pow(1 + rate, years) - 1) / rate; | |
| const rothNet = rothFV_Initial + rothFV_Cont; | |
| const winner = rothNet > tradNet ? "Roth" : "Traditional"; | |
| const diffPercent = Math.abs(((rothNet - tradNet) / Math.min(rothNet, tradNet)) * 100).toFixed(1); | |
| return ( | |
| <div className="space-y-8"> | |
| <div className="flex flex-col md:flex-row gap-4"> | |
| <div className={`flex-1 p-6 border rounded-2xl transition-all ${winner === 'Roth' ? 'border-purple-200 bg-purple-50/30' : 'border-gray-100 bg-gray-50/50 opacity-80'}`}> | |
| <h4 className="text-purple-700 font-bold text-xs uppercase mb-4 flex justify-between"> | |
| Roth Strategy {winner === 'Roth' && <span className="bg-purple-100 text-[8px] px-2 py-0.5 rounded-full text-purple-700">OPTIMAL</span>} | |
| </h4> | |
| <p className="text-gs-navy text-sm font-light leading-relaxed">Pay taxes now at <span className="font-bold">{taxBracket}%</span>. All future growth and withdrawals are <span className="text-purple-700 font-bold underline">100% Tax-Free</span>.</p> | |
| <div className="mt-6 pt-6 border-t border-purple-100"> | |
| <span className="text-3xl font-semibold text-purple-700">${(rothNet / 1000).toFixed(0)}k</span> | |
| <p className="text-xs text-purple-600 mt-1 uppercase tracking-wider font-bold">Estimated Net Wealth</p> | |
| </div> | |
| </div> | |
| <div className={`flex-1 p-6 border rounded-2xl transition-all ${winner === 'Traditional' ? 'border-gs-navy/20 bg-gs-light' : 'border-gray-100 bg-gray-50/50 opacity-80'}`}> | |
| <h4 className="text-gs-navy font-bold text-xs uppercase mb-4 flex justify-between"> | |
| Traditional {winner === 'Traditional' && <span className="bg-gs-navy text-white text-[8px] px-2 py-0.5 rounded-full">OPTIMAL</span>} | |
| </h4> | |
| <p className="text-gs-navy text-sm font-light leading-relaxed">Get a tax deduction now. Entire balance is taxed at <span className="font-bold">{futureTaxBracket}%</span> during retirement.</p> | |
| <div className="mt-6 pt-6 border-t border-gray-200"> | |
| <span className="text-3xl font-semibold text-gs-navy">${(tradNet / 1000).toFixed(0)}k</span> | |
| <p className="text-xs text-gs-slate mt-1 uppercase tracking-wider font-bold">Estimated Net Wealth</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-gs-navy text-white p-6 rounded-2xl flex flex-col md:flex-row justify-between items-center gap-4"> | |
| <div> | |
| <h4 className="text-gs-gold font-bold text-[10px] uppercase tracking-widest mb-1">Strategic Selection</h4> | |
| <p className="text-lg font-light">The <span className="font-bold text-gs-gold">{winner} Strategy</span> is projected to provide <span className="font-bold">{diffPercent}% more</span> spendable wealth.</p> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <div className="text-right hidden md:block"> | |
| <p className="text-[10px] text-gray-400 font-bold uppercase">Difference</p> | |
| <p className="text-gs-gold font-bold">${Math.abs((rothNet - tradNet) / 1000).toFixed(0)}k</p> | |
| </div> | |
| <div className="p-3 bg-gs-gold/20 rounded-full border border-gs-gold/30"> | |
| <Percent className="text-gs-gold" size={24} /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |