0
TOOLS#846edb3c
retirement 2
@GigaChad·deposited 3w ago·updated 2w ago·46 views
TOOLS#846edb3c
retirement 2
GI
@GigaChad
46Views
0Comments
0Forks
0Saves
SHARE · REMIX
retirement 2 — a JSX Tools widget by @GigaChad.
CONTROLS
No comments yet. Be the first!
✦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "retirement 2" by @GigaChad]
Source: https://itjustvibes.com/GigaChad/retirement-2
Type: React/JSX
--- SOURCE CODE ---
```jsx
import { useState, useMemo, useCallback } from "react";
const fmt = (n) => {
if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
if (n >= 1e3) return `$${(n / 1e3).toFixed(0)}K`;
return `$${Math.round(n).toLocaleString()}`;
};
const fmtFull = (n) => `$${Math.round(n).toLocaleString()}`;
const pct = (n) => `${n.toFixed(1)}%`;
const COLORS = {
bg: "#0a0f1a",
card: "#111827",
cardAlt: "#1a2235",
border: "#1e293b",
borderLight: "#334155",
accent: "#34d399",
accentDim: "#065f46",
warn: "#f59e0b",
danger: "#ef4444",
text: "#e2e8f0",
textDim: "#94a3b8",
textMuted: "#64748b",
track: "#1e293b",
positive: "#34d399",
negative: "#f87171",
chart1: "#34d399",
chart2: "#60a5fa",
chart3: "#a78bfa",
chart4: "#f472b6",
chart5: "#fbbf24",
};
function Slider({ label, value, onChange, min, max, step = 1, format = "number", suffix = "", prefix = "", info }) {
const display = format === "dollar" ? fmtFull(value) : format === "percent" ? `${value}%` : `${prefix}${value}${suffix}`;
return (
<div style={{ marginBottom: 18 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
<label style={{ fontSize: 13, color: COLORS.textDim, fontFamily: "'DM Sans', sans-serif", display: "flex", alignItems: "center", gap: 6 }}>
{label}
{info && <span title={info} style={{ cursor: "help", fontSize: 11, background: COLORS.border, borderRadius: "50%", width: 16, height: 16, display: "inline-flex", alignItems: "center", justifyContent: "center", color: COLORS.textMuted }}>?</span>}
</label>
<span style={{ fontSize: 14, color: COLORS.accent, fontFamily: "'JetBrains Mono', monospace", fontWeight: 600 }}>{display}</span>
</div>
<input type="range" min={min} max={max} step={step} value={value} onChange={(e) => onChange(Number(e.target.value))}
style={{ width: "100%", height: 6, appearance: "none", background: `linear-gradient(to right, ${COLORS.accent} 0%, ${COLORS.accent} ${((value - min) / (max - min)) * 100}%, ${COLORS.track} ${((value - min) / (max - min)) * 100}%, ${COLORS.track} 100%)`, borderRadius: 3, outline: "none", cursor: "pointer" }} />
</div>
);
}
function Toggle({ label, value, onChange, info }) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 0", borderBottom: `1px solid ${COLORS.border}` }}>
<span style={{ fontSize: 13, color: COLORS.textDim, fontFamily: "'DM Sans', sans-serif", display: "flex", alignItems: "center", gap: 6 }}>
{label}
{info && <span title={info} style={{ cursor: "help", fontSize: 11, background: COLORS.border, borderRadius: "50%", width: 16, height: 16, display: "inline-flex", alignItems: "center", justifyContent: "center", color: COLORS.textMuted }}>?</span>}
</span>
<div onClick={() => onChange(!value)} style={{ width: 42, height: 24, borderRadius: 12, background: value ? COLORS.accent : COLORS.track, cursor: "pointer", position: "relative", transition: "background 0.2s" }}>
<div style={{ width: 18, height: 18, borderRadius: "50%", background: "#fff", position: "absolute", top: 3, left: value ? 21 : 3, transition: "left 0.2s", boxShadow: "0 1px 3px rgba(0,0,0,0.3)" }} />
</div>
</div>
);
}
function Section({ title, children, icon }) {
const [open, setOpen] = useState(true);
return (
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, marginBottom: 16, overflow: "hidden" }}>
<div onClick={() => setOpen(!open)} style={{ padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "pointer", borderBottom: open ? `1px solid ${COLORS.border}` : "none" }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ fontSize: 18 }}>{icon}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: COLORS.text, fontFamily: "'DM Sans', sans-serif" }}>{title}</span>
</div>
<span style={{ color: COLORS.textMuted, fontSize: 18, transform: open ? "rotate(180deg)" : "none", transition: "transform 0.2s" }}>▾</span>
</div>
{open && <div style={{ padding: "16px 18px" }}>{children}</div>}
</div>
);
}
function MiniChart({ data, width = 300, height = 120, color = COLORS.chart1, label }) {
if (!data || data.length < 2) return null;
const maxVal = Math.max(...data.map((d) => d.value));
const minVal = Math.min(0, ...data.map((d) => d.value));
const range = maxVal - minVal || 1;
const pad = { top: 10, right: 10, bottom: 25, left: 10 };
const w = width - pad.left - pad.right;
const h = height - pad.top - pad.bottom;
const points = data.map((d, i) => {
const x = pad.left + (i / (data.length - 1)) * w;
const y = pad.top + h - ((d.value - minVal) / range) * h;
return `${x},${y}`;
});
const zeroY = pad.top + h - ((0 - minVal) / range) * h;
const areaPoints = [...points, `${pad.left + w},${zeroY}`, `${pad.left},${zeroY}`].join(" ");
return (
<svg viewBox={`0 0 ${width} ${height}`} style={{ width: "100%", height: "auto" }}>
<defs>
<linearGradient id={`grad-${label}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
<polygon points={areaPoints} fill={`url(#grad-${label})`} />
<polyline points={points.join(" ")} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" />
{data.filter((_, i) => i % Math.max(1, Math.floor(data.length / 5)) === 0 || i === data.length - 1).map((d, idx) => {
const i = data.indexOf(d);
const x = pad.left + (i / (data.length - 1)) * w;
return <text key={idx} x={x} y={height - 4} textAnchor="middle" fill={COLORS.textMuted} fontSize="9" fontFamily="'JetBrains Mono', monospace">{d.age}</text>;
})}
</svg>
);
}
function StatCard({ label, value, sub, color = COLORS.accent, large }) {
return (
<div style={{ background: COLORS.cardAlt, borderRadius: 10, padding: large ? "18px 16px" : "12px 14px", border: `1px solid ${COLORS.border}`, flex: 1, minWidth: 120 }}>
<div style={{ fontSize: 11, color: COLORS.textMuted, fontFamily: "'DM Sans', sans-serif", marginBottom: 4, textTransform: "uppercase", letterSpacing: 1 }}>{label}</div>
<div style={{ fontSize: large ? 26 : 20, fontWeight: 700, color, fontFamily: "'JetBrains Mono', monospace" }}>{value}</div>
{sub && <div style={{ fontSize: 11, color: COLORS.textDim, marginTop: 2 }}>{sub}</div>}
</div>
);
}
function ProgressBar({ value, max, color = COLORS.accent, label }) {
const p = Math.min(100, Math.max(0, (value / max) * 100));
return (
<div style={{ marginBottom: 10 }}>
{label && <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
<span style={{ fontSize: 11, color: COLORS.textDim }}>{label}</span>
<span style={{ fontSize: 11, color: COLORS.textMuted }}>{Math.round(p)}%</span>
</div>}
<div style={{ height: 6, background: COLORS.track, borderRadius: 3 }}>
<div style={{ height: "100%", width: `${p}%`, background: color, borderRadius: 3, transition: "width 0.4s ease" }} />
</div>
</div>
);
}
export default function RetirementPlanner() {
const [age, setAge] = useState(30);
const [retireAge, setRetireAge] = useState(65);
const [lifeExpectancy, setLifeExpectancy] = useState(90);
const [currentSavings, setCurrentSavings] = useState(50000);
const [annualIncome, setAnnualIncome] = useState(85000);
const [annualContribution, setAnnualContribution] = useState(15000);
const [contributionRate, setContributionRate] = useState(15);
const [returnRate, setReturnRate] = useState(7.0);
const [postRetireReturn, setPostRetireReturn] = useState(4.0);
const [inflationRate, setInflationRate] = useState(3.0);
const [desiredIncome, setDesiredIncome] = useState(60000);
const [taxRate, setTaxRate] = useState(22);
// Toggles
const [hasSocialSecurity, setHasSocialSecurity] = useState(true);
const [ssMonthly, setSsMonthly] = useState(2000);
const [hasPension, setHasPension] = useState(false);
const [pensionMonthly, setPensionMonthly] = useState(1500);
const [hasEmployerMatch, setHasEmployerMatch] = useState(true);
const [matchPercent, setMatchPercent] = useState(50);
const [matchLimit, setMatchLimit] = useState(6);
const [hasCatchUp, setHasCatchUp] = useState(true);
const [catchUpAmount, setCatchUpAmount] = useState(7500);
const [hasHSA, setHasHSA] = useState(false);
const [hsaBalance, setHsaBalance] = useState(10000);
const [hsaAnnual, setHsaAnnual] = useState(3850);
const [hasMortgage, setHasMortgage] = useState(true);
const [mortgageMonthly, setMortgageMonthly] = useState(1800);
const [mortgagePaidOffAge, setMortgagePaidOffAge] = useState(55);
const [hasPartTime, setHasPartTime] = useState(false);
const [partTimeIncome, setPartTimeIncome] = useState(20000);
const [partTimeYears, setPartTimeYears] = useState(5);
const [hasDebt, setHasDebt] = useState(false);
const [debtBalance, setDebtBalance] = useState(25000);
const [debtPayment, setDebtPayment] = useState(500);
const [hasLTC, setHasLTC] = useState(false);
const [ltcMonthly, setLtcMonthly] = useState(300);
const [includeHealthcare, setIncludeHealthcare] = useState(true);
const [healthcareInflation, setHealthcareInflation] = useState(5.5);
const [hasRentalIncome, setHasRentalIncome] = useState(false);
const [rentalMonthly, setRentalMonthly] = useState(1500);
const [activeTab, setActiveTab] = useState("overview");
const results = useMemo(() => {
const yearsToRetire = Math.max(0, retireAge - age);
const yearsInRetirement = Math.max(0, lifeExpectancy - retireAge);
const realReturn = (1 + returnRate / 100) / (1 + inflationRate / 100) - 1;
const realPostReturn = (1 + postRetireReturn / 100) / (1 + inflationRate / 100) - 1;
// Accumulation phase
let balance = currentSavings;
const yearlyData = [];
let totalContributed = currentSavings;
let totalEmployerMatch = 0;
for (let y = 0; y <= yearsToRetire + yearsInRetirement; y++) {
const currentAge = age + y;
const isRetired = currentAge >= retireAge;
if (!isRetired) {
let yearContrib = annualContribution;
// Catch-up contributions for 50+
if (hasCatchUp && currentAge >= 50) yearContrib += catchUpAmount;
// Employer match
let matchAmount = 0;
if (hasEmployerMatch) {
const employeeMatchable = Math.min(annualContribution, (matchLimit / 100) * annualIncome);
matchAmount = employeeMatchable * (matchPercent / 100);
totalEmployerMatch += matchAmount;
}
// Debt reduction from savings
let debtDrag = 0;
if (hasDebt && debtBalance > 0) {
debtDrag = debtPayment * 12;
}
balance = balance * (1 + returnRate / 100) + yearContrib + matchAmount;
totalContributed += yearContrib;
// HSA growth
let hsaBal = 0;
if (hasHSA) {
hsaBal = (hsaBalance + hsaAnnual * y) * Math.pow(1 + returnRate / 100, y);
}
yearlyData.push({ age: currentAge, value: balance, phase: "accumulation", hsaBal });
} else {
const retireYear = currentAge - retireAge;
// Income needed adjusted for inflation
let incomeNeeded = desiredIncome * Math.pow(1 + inflationRate / 100, y);
// Subtract income sources
let otherIncome = 0;
if (hasSocialSecurity) otherIncome += ssMonthly * 12 * Math.pow(1 + inflationRate / 100, retireYear);
if (hasPension) otherIncome += pensionMonthly * 12 * Math.pow(1 + inflationRate / 100, retireYear);
if (hasPartTime && retireYear < partTimeYears) otherIncome += partTimeIncome;
if (hasRentalIncome) otherIncome += rentalMonthly * 12;
// Mortgage
let housingCost = 0;
if (hasMortgage && currentAge < mortgagePaidOffAge) housingCost = mortgageMonthly * 12;
// Healthcare costs (grows faster than inflation)
let healthcareCost = 0;
if (includeHealthcare) {
const baseHealthcare = currentAge < 65 ? 8000 : 12000;
healthcareCost = baseHealthcare * Math.pow(1 + healthcareInflation / 100, retireYear);
}
// LTC insurance
let ltcCost = 0;
if (hasLTC) ltcCost = ltcMonthly * 12;
const grossNeed = incomeNeeded + housingCost + healthcareCost + ltcCost;
const taxAdjusted = grossNeed / (1 - taxRate / 100);
const withdrawal = Math.max(0, taxAdjusted - otherIncome);
balance = balance * (1 + postRetireReturn / 100) - withdrawal;
yearlyData.push({ age: currentAge, value: balance, phase: "retirement", withdrawal });
}
}
// Key metrics
const atRetirement = yearlyData.find((d) => d.age === retireAge)?.value || 0;
const atEnd = yearlyData[yearlyData.length - 1]?.value || 0;
const runsOutAge = yearlyData.find((d) => d.phase === "retirement" && d.value <= 0)?.age;
const fundedYears = runsOutAge ? runsOutAge - retireAge : yearsInRetirement;
// Annual SS
const annualSS = hasSocialSecurity ? ssMonthly * 12 : 0;
const annualPension = hasPension ? pensionMonthly * 12 : 0;
const annualRental = hasRentalIncome ? rentalMonthly * 12 : 0;
const totalGuaranteedIncome = annualSS + annualPension + annualRental;
// Safe withdrawal rate (4% rule)
const safeWithdrawal = atRetirement * 0.04;
const totalFirstYearIncome = safeWithdrawal + totalGuaranteedIncome;
// Income replacement ratio
const replacementRatio = (totalFirstYearIncome / annualIncome) * 100;
// Savings rate
const effectiveSavingsRate = (annualContribution / annualIncome) * 100;
// Gap
const incomeGap = desiredIncome - totalFirstYearIncome;
return {
yearlyData,
atRetirement,
atEnd,
runsOutAge,
fundedYears,
yearsToRetire,
yearsInRetirement,
totalContributed,
totalEmployerMatch,
totalGuaranteedIncome,
safeWithdrawal,
totalFirstYearIncome,
replacementRatio,
effectiveSavingsRate,
incomeGap,
realReturn,
};
}, [age, retireAge, lifeExpectancy, currentSavings, annualIncome, annualContribution, returnRate, postRetireReturn, inflationRate, desiredIncome, taxRate, hasSocialSecurity, ssMonthly, hasPension, pensionMonthly, hasEmployerMatch, matchPercent, matchLimit, hasCatchUp, catchUpAmount, hasHSA, hsaBalance, hsaAnnual, hasMortgage, mortgageMonthly, mortgagePaidOffAge, hasPartTime, partTimeIncome, partTimeYears, hasDebt, debtBalance, debtPayment, hasLTC, ltcMonthly, includeHealthcare, healthcareInflation, hasRentalIncome, rentalMonthly, contributionRate]);
const onTrack = results.atEnd > 0;
const score = results.runsOutAge ? Math.min(100, Math.round((results.fundedYears / results.yearsInRetirement) * 100)) : 100;
const scoreColor = score >= 90 ? COLORS.positive : score >= 60 ? COLORS.warn : COLORS.danger;
const tabs = [
{ id: "overview", label: "Overview" },
{ id: "inputs", label: "Adjustments" },
{ id: "income", label: "Income Sources" },
{ id: "expenses", label: "Expenses & Risks" },
{ id: "breakdown", label: "Projections" },
];
return (
<div style={{ fontFamily: "'DM Sans', sans-serif", background: COLORS.bg, color: COLORS.text, minHeight: "100vh", padding: "0" }}>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Playfair+Display:wght@700;800&display=swap" rel="stylesheet" />
<style>{`
input[type=range]::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; border-radius: 50%; background: ${COLORS.accent}; cursor: pointer; border: 2px solid ${COLORS.bg}; box-shadow: 0 0 8px ${COLORS.accent}44; }
input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: ${COLORS.accent}; cursor: pointer; border: 2px solid ${COLORS.bg}; }
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: ${COLORS.border} transparent; }
`}</style>
{/* Header */}
<div style={{ background: `linear-gradient(135deg, ${COLORS.card} 0%, ${COLORS.bg} 100%)`, borderBottom: `1px solid ${COLORS.border}`, padding: "28px 24px 20px" }}>
<div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: 3, color: COLORS.accent, fontWeight: 600, marginBottom: 6 }}>Retirement Planner</div>
<div style={{ fontFamily: "'Playfair Display', serif", fontSize: 28, fontWeight: 800, lineHeight: 1.1, color: COLORS.text }}>Your Financial<br />Future</div>
<div style={{ display: "flex", gap: 8, marginTop: 16, alignItems: "center" }}>
<div style={{ width: 52, height: 52, borderRadius: "50%", border: `3px solid ${scoreColor}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18, fontWeight: 700, color: scoreColor, fontFamily: "'JetBrains Mono', monospace", background: `${scoreColor}11` }}>{score}</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: onTrack ? COLORS.positive : COLORS.danger }}>{onTrack ? "On Track" : "Needs Attention"}</div>
<div style={{ fontSize: 11, color: COLORS.textMuted }}>
{results.runsOutAge ? `Funds last until age ${results.runsOutAge}` : `Funds last through age ${lifeExpectancy}`}
</div>
</div>
</div>
</div>
{/* Tabs */}
<div style={{ display: "flex", gap: 0, borderBottom: `1px solid ${COLORS.border}`, overflowX: "auto", background: COLORS.card }}>
{tabs.map((t) => (
<button key={t.id} onClick={() => setActiveTab(t.id)}
style={{ padding: "12px 16px", fontSize: 12, fontWeight: activeTab === t.id ? 600 : 400, color: activeTab === t.id ? COLORS.accent : COLORS.textMuted, background: "none", border: "none", borderBottom: activeTab === t.id ? `2px solid ${COLORS.accent}` : "2px solid transparent", cursor: "pointer", whiteSpace: "nowrap", fontFamily: "'DM Sans', sans-serif" }}>
{t.label}
</button>
))}
</div>
<div style={{ padding: "16px 16px 80px" }}>
{activeTab === "overview" && (
<>
{/* Key Stats */}
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginBottom: 16 }}>
<StatCard label="At Retirement" value={fmt(results.atRetirement)} sub={`Age ${retireAge}`} color={COLORS.chart1} large />
<StatCard label="At End" value={fmt(Math.max(0, results.atEnd))} sub={`Age ${lifeExpectancy}`} color={results.atEnd > 0 ? COLORS.chart2 : COLORS.danger} large />
</div>
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginBottom: 16 }}>
<StatCard label="Safe Withdrawal" value={fmt(results.safeWithdrawal)} sub="4% rule / year" />
<StatCard label="Income Replace" value={`${Math.round(results.replacementRatio)}%`} sub="of current income" color={results.replacementRatio >= 80 ? COLORS.positive : COLORS.warn} />
</div>
{results.incomeGap > 0 && (
<div style={{ background: `${COLORS.danger}15`, border: `1px solid ${COLORS.danger}33`, borderRadius: 10, padding: 14, marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: COLORS.danger, marginBottom: 4 }}>⚠ Annual Income Gap</div>
<div style={{ fontSize: 11, color: COLORS.textDim }}>
Your projected first-year retirement income ({fmtFull(Math.round(results.totalFirstYearIncome))}) is {fmtFull(Math.round(results.incomeGap))} short of your desired {fmtFull(desiredIncome)}/yr.
</div>
</div>
)}
{results.incomeGap <= 0 && (
<div style={{ background: `${COLORS.positive}12`, border: `1px solid ${COLORS.positive}33`, borderRadius: 10, padding: 14, marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: COLORS.positive, marginBottom: 4 }}>✓ Income Goal Met</div>
<div style={{ fontSize: 11, color: COLORS.textDim }}>
Your projected retirement income exceeds your desired {fmtFull(desiredIncome)}/yr by {fmtFull(Math.round(Math.abs(results.incomeGap)))}.
</div>
</div>
)}
{/* Chart */}
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 16, marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 10, color: COLORS.textDim }}>Portfolio Balance Over Time</div>
<MiniChart data={results.yearlyData} width={340} height={140} color={COLORS.chart1} label="main" />
</div>
{/* Quick metrics */}
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 12, color: COLORS.textDim }}>Key Metrics</div>
<ProgressBar value={results.effectiveSavingsRate} max={30} color={results.effectiveSavingsRate >= 15 ? COLORS.positive : COLORS.warn} label={`Savings Rate: ${results.effectiveSavingsRate.toFixed(1)}% (target: 15-20%)`} />
<ProgressBar value={results.fundedYears} max={results.yearsInRetirement} color={results.fundedYears >= results.yearsInRetirement ? COLORS.positive : COLORS.danger} label={`Funded Years: ${results.fundedYears} of ${results.yearsInRetirement}`} />
<ProgressBar value={results.replacementRatio} max={100} color={results.replacementRatio >= 80 ? COLORS.positive : COLORS.warn} label={`Income Replacement: ${Math.round(results.replacementRatio)}% (target: 80%+)`} />
<div style={{ marginTop: 12, fontSize: 11, color: COLORS.textMuted, lineHeight: 1.5 }}>
<div>• Years to retirement: <strong style={{ color: COLORS.text }}>{results.yearsToRetire}</strong></div>
<div>• Total contributed: <strong style={{ color: COLORS.text }}>{fmt(results.totalContributed)}</strong></div>
{results.totalEmployerMatch > 0 && <div>• Employer match total: <strong style={{ color: COLORS.accent }}>{fmt(results.totalEmployerMatch)}</strong></div>}
<div>• Real return (after inflation): <strong style={{ color: COLORS.text }}>{(results.realReturn * 100).toFixed(1)}%</strong></div>
</div>
</div>
</>
)}
{activeTab === "inputs" && (
<>
<Section title="Personal Details" icon="👤">
<Slider label="Current Age" value={age} onChange={setAge} min={18} max={75} suffix=" yrs" />
<Slider label="Retirement Age" value={retireAge} onChange={setRetireAge} min={Math.max(age + 1, 50)} max={80} suffix=" yrs" />
<Slider label="Life Expectancy" value={lifeExpectancy} onChange={setLifeExpectancy} min={Math.max(retireAge + 1, 70)} max={105} suffix=" yrs" info="Plan conservatively -- many people underestimate longevity." />
</Section>
<Section title="Savings & Contributions" icon="💰">
<Slider label="Current Retirement Savings" value={currentSavings} onChange={setCurrentSavings} min={0} max={3000000} step={5000} format="dollar" />
<Slider label="Annual Income" value={annualIncome} onChange={setAnnualIncome} min={20000} max={500000} step={5000} format="dollar" />
<Slider label="Annual 401k/IRA Contribution" value={annualContribution} onChange={setAnnualContribution} min={0} max={75000} step={500} format="dollar" info="2025 limit: $23,500 for 401k, $7,000 for IRA" />
<Slider label="Desired Retirement Income" value={desiredIncome} onChange={setDesiredIncome} min={20000} max={300000} step={5000} format="dollar" info="Typically 70-80% of pre-retirement income" />
</Section>
<Section title="Market Assumptions" icon="📈">
<Slider label="Pre-Retirement Return" value={returnRate} onChange={setReturnRate} min={1} max={12} step={0.5} format="percent" info="Historical S&P 500 avg: ~10%. Conservative: 6-7%." />
<Slider label="Post-Retirement Return" value={postRetireReturn} onChange={setPostRetireReturn} min={1} max={8} step={0.5} format="percent" info="Typically lower due to conservative allocation" />
<Slider label="Inflation Rate" value={inflationRate} onChange={setInflationRate} min={1} max={6} step={0.5} format="percent" info="Historical average: ~3%" />
<Slider label="Effective Tax Rate" value={taxRate} onChange={setTaxRate} min={0} max={40} step={1} format="percent" info="Combined federal + state on retirement withdrawals" />
</Section>
</>
)}
{activeTab === "income" && (
<>
<Section title="Social Security" icon="🏛️">
<Toggle label="Include Social Security" value={hasSocialSecurity} onChange={setHasSocialSecurity} info="Estimated monthly benefit at full retirement age" />
{hasSocialSecurity && (
<div style={{ marginTop: 12 }}>
<Slider label="Monthly SS Benefit" value={ssMonthly} onChange={setSsMonthly} min={500} max={5000} step={100} format="dollar" info="Check ssa.gov for your estimate. Avg 2025: ~$1,976" />
<div style={{ fontSize: 11, color: COLORS.textMuted, marginTop: 4 }}>Annual: {fmtFull(ssMonthly * 12)}</div>
</div>
)}
</Section>
<Section title="Pension" icon="🏦">
<Toggle label="Have a Pension" value={hasPension} onChange={setHasPension} />
{hasPension && (
<div style={{ marginTop: 12 }}>
<Slider label="Monthly Pension Benefit" value={pensionMonthly} onChange={setPensionMonthly} min={500} max={8000} step={100} format="dollar" />
<div style={{ fontSize: 11, color: COLORS.textMuted, marginTop: 4 }}>Annual: {fmtFull(pensionMonthly * 12)}</div>
</div>
)}
</Section>
<Section title="Employer Match" icon="🤝">
<Toggle label="Employer 401k Match" value={hasEmployerMatch} onChange={setHasEmployerMatch} info="Free money -- always maximize this" />
{hasEmployerMatch && (
<div style={{ marginTop: 12 }}>
<Slider label="Match Rate" value={matchPercent} onChange={setMatchPercent} min={25} max={100} step={25} suffix="%" info="e.g., 50% means employer matches 50¢ per $1" />
<Slider label="Match Up To % of Salary" value={matchLimit} onChange={setMatchLimit} min={1} max={10} step={1} suffix="%" info="e.g., 6% means match applies on first 6% of salary you contribute" />
<div style={{ fontSize: 11, color: COLORS.accent, marginTop: 4 }}>
Annual match value: {fmtFull(Math.round(Math.min(annualContribution, (matchLimit / 100) * annualIncome) * (matchPercent / 100)))}
</div>
</div>
)}
</Section>
<Section title="Catch-Up Contributions" icon="🚀">
<Toggle label="Catch-Up After 50" value={hasCatchUp} onChange={setHasCatchUp} info="Extra contributions allowed starting at age 50" />
{hasCatchUp && (
<div style={{ marginTop: 12 }}>
<Slider label="Annual Catch-Up Amount" value={catchUpAmount} onChange={setCatchUpAmount} min={1000} max={11250} step={500} format="dollar" info="2025 limits: $7,500 (401k), $1,000 (IRA). Ages 60-63: $11,250." />
</div>
)}
</Section>
<Section title="Part-Time Work in Retirement" icon="💼">
<Toggle label="Plan to Work Part-Time" value={hasPartTime} onChange={setHasPartTime} />
{hasPartTime && (
<div style={{ marginTop: 12 }}>
<Slider label="Annual Part-Time Income" value={partTimeIncome} onChange={setPartTimeIncome} min={5000} max={80000} step={5000} format="dollar" />
<Slider label="Years of Part-Time Work" value={partTimeYears} onChange={setPartTimeYears} min={1} max={15} suffix=" yrs" />
</div>
)}
</Section>
<Section title="Rental / Passive Income" icon="🏘️">
<Toggle label="Have Rental/Passive Income" value={hasRentalIncome} onChange={setHasRentalIncome} />
{hasRentalIncome && (
<div style={{ marginTop: 12 }}>
<Slider label="Monthly Rental Income" value={rentalMonthly} onChange={setRentalMonthly} min={500} max={10000} step={250} format="dollar" />
<div style={{ fontSize: 11, color: COLORS.textMuted, marginTop: 4 }}>Annual: {fmtFull(rentalMonthly * 12)}</div>
</div>
)}
</Section>
<Section title="HSA (Health Savings Account)" icon="🏥">
<Toggle label="Have an HSA" value={hasHSA} onChange={setHasHSA} info="Triple tax advantage -- great retirement supplement" />
{hasHSA && (
<div style={{ marginTop: 12 }}>
<Slider label="Current HSA Balance" value={hsaBalance} onChange={setHsaBalance} min={0} max={200000} step={1000} format="dollar" />
<Slider label="Annual HSA Contribution" value={hsaAnnual} onChange={setHsaAnnual} min={0} max={8550} step={50} format="dollar" info="2025 limits: $4,300 individual / $8,550 family" />
</div>
)}
</Section>
</>
)}
{activeTab === "expenses" && (
<>
<Section title="Healthcare Costs" icon="⚕️">
<Toggle label="Model Healthcare Inflation" value={includeHealthcare} onChange={setIncludeHealthcare} info="Healthcare costs typically rise 5-6% per year" />
{includeHealthcare && (
<div style={{ marginTop: 12 }}>
<Slider label="Healthcare Cost Inflation" value={healthcareInflation} onChange={setHealthcareInflation} min={3} max={8} step={0.5} format="percent" info="Historical avg ~5.5%. Pre-65 costs higher without Medicare." />
<div style={{ fontSize: 11, color: COLORS.textMuted, marginTop: 8, lineHeight: 1.5 }}>
Estimated annual healthcare costs:<br />
• Pre-65 (no Medicare): ~$8,000/yr starting<br />
• Post-65 (Medicare + supplement): ~$12,000/yr starting<br />
Costs compound at {healthcareInflation}% annually.
</div>
</div>
)}
</Section>
<Section title="Housing / Mortgage" icon="🏠">
<Toggle label="Have a Mortgage" value={hasMortgage} onChange={setHasMortgage} />
{hasMortgage && (
<div style={{ marginTop: 12 }}>
<Slider label="Monthly Mortgage Payment" value={mortgageMonthly} onChange={setMortgageMonthly} min={500} max={6000} step={100} format="dollar" />
<Slider label="Mortgage Paid Off By Age" value={mortgagePaidOffAge} onChange={setMortgagePaidOffAge} min={age} max={retireAge + 15} suffix=" yrs" info="Eliminating mortgage before retirement reduces draw-down" />
<div style={{ fontSize: 11, color: mortgagePaidOffAge <= retireAge ? COLORS.positive : COLORS.warn, marginTop: 4 }}>
{mortgagePaidOffAge <= retireAge ? "✓ Paid off before retirement" : `⚠ ${mortgagePaidOffAge - retireAge} years of payments in retirement`}
</div>
</div>
)}
</Section>
<Section title="Outstanding Debt" icon="💳">
<Toggle label="Have Non-Mortgage Debt" value={hasDebt} onChange={setHasDebt} info="Student loans, car loans, credit cards, etc." />
{hasDebt && (
<div style={{ marginTop: 12 }}>
<Slider label="Total Debt Balance" value={debtBalance} onChange={setDebtBalance} min={1000} max={200000} step={1000} format="dollar" />
<Slider label="Monthly Payment" value={debtPayment} onChange={setDebtPayment} min={100} max={3000} step={50} format="dollar" />
<div style={{ fontSize: 11, color: COLORS.warn, marginTop: 4 }}>
Annual drag on savings: {fmtFull(debtPayment * 12)}
</div>
</div>
)}
</Section>
<Section title="Long-Term Care Insurance" icon="🛡️">
<Toggle label="Plan for LTC Insurance" value={hasLTC} onChange={setHasLTC} info="~70% of people over 65 will need some form of long-term care" />
{hasLTC && (
<div style={{ marginTop: 12 }}>
<Slider label="Monthly LTC Premium" value={ltcMonthly} onChange={setLtcMonthly} min={100} max={800} step={25} format="dollar" info="Premiums vary widely by age of purchase and coverage" />
<div style={{ fontSize: 11, color: COLORS.textMuted, marginTop: 4 }}>Annual cost: {fmtFull(ltcMonthly * 12)}</div>
</div>
)}
</Section>
</>
)}
{activeTab === "breakdown" && (
<>
{/* Income breakdown */}
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 16, marginBottom: 16 }}>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 14, color: COLORS.text }}>First Year Retirement Income</div>
{[
{ label: "Portfolio (4% rule)", value: results.safeWithdrawal, color: COLORS.chart1 },
hasSocialSecurity && { label: "Social Security", value: ssMonthly * 12, color: COLORS.chart2 },
hasPension && { label: "Pension", value: pensionMonthly * 12, color: COLORS.chart3 },
hasPartTime && { label: "Part-Time Work", value: partTimeIncome, color: COLORS.chart4 },
hasRentalIncome && { label: "Rental Income", value: rentalMonthly * 12, color: COLORS.chart5 },
].filter(Boolean).map((item, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${COLORS.border}` }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ width: 10, height: 10, borderRadius: 3, background: item.color }} />
<span style={{ fontSize: 12, color: COLORS.textDim }}>{item.label}</span>
</div>
<span style={{ fontSize: 13, fontFamily: "'JetBrains Mono', monospace", color: COLORS.text }}>{fmtFull(Math.round(item.value))}</span>
</div>
))}
<div style={{ display: "flex", justifyContent: "space-between", padding: "10px 0 0", marginTop: 4 }}>
<span style={{ fontSize: 13, fontWeight: 600 }}>Total</span>
<span style={{ fontSize: 15, fontWeight: 700, color: COLORS.accent, fontFamily: "'JetBrains Mono', monospace" }}>{fmtFull(Math.round(results.totalFirstYearIncome))}</span>
</div>
</div>
{/* Year by year table */}
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, overflow: "hidden" }}>
<div style={{ padding: "14px 16px", borderBottom: `1px solid ${COLORS.border}`, fontSize: 13, fontWeight: 600 }}>Year-by-Year Projection</div>
<div style={{ maxHeight: 400, overflowY: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11, fontFamily: "'JetBrains Mono', monospace" }}>
<thead>
<tr style={{ position: "sticky", top: 0, background: COLORS.cardAlt }}>
{["Age", "Balance", "Phase"].map((h) => (
<th key={h} style={{ padding: "8px 10px", textAlign: "left", color: COLORS.textMuted, fontWeight: 500, borderBottom: `1px solid ${COLORS.border}`, fontSize: 10, textTransform: "uppercase", letterSpacing: 1 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{results.yearlyData.filter((_, i) => i % Math.max(1, Math.ceil(results.yearlyData.length / 30)) === 0 || _.age === retireAge).map((d, i) => (
<tr key={i} style={{ borderBottom: `1px solid ${COLORS.border}`, background: d.age === retireAge ? `${COLORS.accent}11` : "transparent" }}>
<td style={{ padding: "7px 10px", color: d.age === retireAge ? COLORS.accent : COLORS.text }}>{d.age}</td>
<td style={{ padding: "7px 10px", color: d.value < 0 ? COLORS.danger : COLORS.text }}>{fmt(Math.max(0, d.value))}</td>
<td style={{ padding: "7px 10px" }}>
<span style={{ fontSize: 9, padding: "2px 6px", borderRadius: 4, background: d.phase === "accumulation" ? `${COLORS.chart2}22` : `${COLORS.chart3}22`, color: d.phase === "accumulation" ? COLORS.chart2 : COLORS.chart3 }}>
{d.phase === "accumulation" ? "Saving" : "Retired"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Tips */}
<div style={{ background: COLORS.card, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 16, marginTop: 16 }}>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 10 }}>💡 Key Considerations</div>
<div style={{ fontSize: 11, color: COLORS.textDim, lineHeight: 1.7 }}>
<p style={{ margin: "0 0 8px" }}>• <strong style={{ color: COLORS.text }}>Sequence of Returns Risk:</strong> Poor market years early in retirement can devastate a portfolio far more than the same losses later. Consider a bond tent or bucket strategy.</p>
<p style={{ margin: "0 0 8px" }}>• <strong style={{ color: COLORS.text }}>RMDs:</strong> Required Minimum Distributions begin at age 73 (75 for those born after 1960). Plan Roth conversions before then to manage tax brackets.</p>
<p style={{ margin: "0 0 8px" }}>• <strong style={{ color: COLORS.text }}>Medicare Gap:</strong> If retiring before 65, budget for private health insurance (ACA marketplace). This can be $500-$1,500+/mo.</p>
<p style={{ margin: "0 0 8px" }}>• <strong style={{ color: COLORS.text }}>Social Security Timing:</strong> Delaying SS from 62 to 70 increases benefits ~8%/year. If you can bridge with savings, the guaranteed income boost is significant.</p>
<p style={{ margin: "0 0 8px" }}>• <strong style={{ color: COLORS.text }}>Roth vs Traditional:</strong> Roth withdrawals are tax-free. If your tax rate is lower now than expected in retirement, maximize Roth contributions.</p>
<p style={{ margin: "0 0 0" }}>• <strong style={{ color: COLORS.text }}>Estate Planning:</strong> Beneficiary designations, wills, trusts, and powers of attorney should be in place. Review every 3-5 years.</p>
</div>
</div>
</>
)}
</div>
</div>
);
}
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained JSX file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/GigaChad/retirement-2 */