← back to StudentLoanTracker

src/app/calculator/page.tsx

347 lines

'use client';

import { useState } from 'react';
import { Calculator, ChevronDown, ChevronUp, Download, Info } from 'lucide-react';
import {
  compareAllPlans,
  type LoanInputs,
  type ComparisonResult,
  type FilingStatus,
} from '@/lib/calculators';

const FILING_OPTIONS: { value: FilingStatus; label: string }[] = [
  { value: 'single', label: 'Single' },
  { value: 'married_filing_jointly', label: 'Married Filing Jointly' },
  { value: 'married_filing_separately', label: 'Married Filing Separately' },
  { value: 'head_of_household', label: 'Head of Household' },
];

const US_STATES = [
  'AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA',
  'KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ',
  'NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT',
  'VA','WA','WV','WI','WY','DC',
];

function formatCurrency(n: number): string {
  return n.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
}

function formatMonths(m: number): string {
  const years = Math.floor(m / 12);
  const months = m % 12;
  if (years === 0) return `${months}mo`;
  if (months === 0) return `${years}yr`;
  return `${years}yr ${months}mo`;
}

export default function CalculatorPage() {
  const [inputs, setInputs] = useState<LoanInputs>({
    totalBalance: 35000,
    weightedRate: 0.055,
    agi: 45000,
    familySize: 1,
    filingStatus: 'single',
    state: 'CA',
  });

  const [result, setResult] = useState<ComparisonResult | null>(null);
  const [showAdvanced, setShowAdvanced] = useState(false);
  const [expandedPlan, setExpandedPlan] = useState<string | null>(null);

  function handleCalculate() {
    const comparison = compareAllPlans(inputs);
    setResult(comparison);
  }

  function updateInput<K extends keyof LoanInputs>(key: K, value: LoanInputs[K]) {
    setInputs((prev) => ({ ...prev, [key]: value }));
    setResult(null); // Clear results when inputs change
  }

  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      {/* Header */}
      <div className="max-w-3xl mb-10">
        <div className="flex items-center gap-3 mb-4">
          <div className="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-teal-500/10 text-teal-600">
            <Calculator className="w-5 h-5" />
          </div>
          <h1 className="text-3xl font-bold">Repayment Plan Comparison</h1>
        </div>
        <p className="text-gray-600 text-lg leading-relaxed">
          Compare SAVE, IBR, PAYE, ICR, and Standard plans side-by-side.
          All calculations run in your browser &mdash; nothing is sent to a server.
        </p>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        {/* Input Form */}
        <div className="lg:col-span-1">
          <div className="bg-white rounded-xl border border-gray-200 p-6 sticky top-6">
            <h2 className="text-lg font-semibold mb-4">Your Information</h2>

            <div className="space-y-4">
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Total Loan Balance
                </label>
                <div className="relative">
                  <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
                  <input
                    type="number"
                    value={inputs.totalBalance}
                    onChange={(e) => updateInput('totalBalance', Number(e.target.value))}
                    className="w-full pl-7 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                  />
                </div>
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Interest Rate (%)
                </label>
                <input
                  type="number"
                  step="0.1"
                  value={(inputs.weightedRate * 100).toFixed(1)}
                  onChange={(e) => updateInput('weightedRate', Number(e.target.value) / 100)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Annual Income (AGI)
                </label>
                <div className="relative">
                  <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
                  <input
                    type="number"
                    value={inputs.agi}
                    onChange={(e) => updateInput('agi', Number(e.target.value))}
                    className="w-full pl-7 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                  />
                </div>
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Family Size
                </label>
                <input
                  type="number"
                  min="1"
                  max="20"
                  value={inputs.familySize}
                  onChange={(e) => updateInput('familySize', Number(e.target.value))}
                  className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Filing Status
                </label>
                <select
                  value={inputs.filingStatus}
                  onChange={(e) => updateInput('filingStatus', e.target.value as FilingStatus)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                >
                  {FILING_OPTIONS.map((o) => (
                    <option key={o.value} value={o.value}>{o.label}</option>
                  ))}
                </select>
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">State</label>
                <select
                  value={inputs.state}
                  onChange={(e) => updateInput('state', e.target.value)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                >
                  {US_STATES.map((s) => (
                    <option key={s} value={s}>{s}</option>
                  ))}
                </select>
              </div>

              {/* Advanced Toggle */}
              <button
                onClick={() => setShowAdvanced(!showAdvanced)}
                className="flex items-center gap-1 text-sm text-teal-600 hover:text-teal-700"
              >
                {showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
                Advanced Options
              </button>

              {showAdvanced && (
                <div className="space-y-4 pt-2 border-t border-gray-100">
                  <div>
                    <label className="block text-sm font-medium text-gray-700 mb-1">
                      Spouse Income (if MFJ)
                    </label>
                    <div className="relative">
                      <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
                      <input
                        type="number"
                        value={inputs.spouseIncome || 0}
                        onChange={(e) => updateInput('spouseIncome', Number(e.target.value))}
                        className="w-full pl-7 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
                      />
                    </div>
                  </div>
                </div>
              )}
            </div>

            <button
              onClick={handleCalculate}
              className="w-full mt-6 px-6 py-3 bg-teal-500 hover:bg-teal-600 text-white font-semibold rounded-lg transition-colors"
            >
              Compare Plans
            </button>
          </div>
        </div>

        {/* Results */}
        <div className="lg:col-span-2">
          {!result ? (
            <div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
              <Calculator className="w-12 h-12 text-gray-300 mx-auto mb-4" />
              <p className="text-gray-500 text-lg">
                Enter your loan details and click &ldquo;Compare Plans&rdquo; to see results.
              </p>
            </div>
          ) : (
            <div className="space-y-6">
              {/* Recommendations */}
              <div className="bg-teal-50 border border-teal-200 rounded-xl p-6">
                <h3 className="font-semibold text-teal-800 mb-3">Recommendations</h3>
                <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm">
                  <div>
                    <div className="text-teal-600 font-medium">Lowest Monthly</div>
                    <div className="text-teal-900 font-bold">{result.lowestMonthly}</div>
                  </div>
                  <div>
                    <div className="text-teal-600 font-medium">Lowest Total Cost</div>
                    <div className="text-teal-900 font-bold">{result.recommended}</div>
                  </div>
                  <div>
                    <div className="text-teal-600 font-medium">Fastest Payoff</div>
                    <div className="text-teal-900 font-bold">{result.fastestPayoff}</div>
                  </div>
                </div>
              </div>

              {/* Plan Cards */}
              {result.plans.map((plan) => {
                const isExpanded = expandedPlan === plan.planName;
                const isRecommended = plan.planName === result.recommended;

                return (
                  <div
                    key={plan.planName}
                    className={`bg-white rounded-xl border ${
                      isRecommended ? 'border-teal-300 ring-1 ring-teal-200' : 'border-gray-200'
                    } overflow-hidden`}
                  >
                    <div className="p-6">
                      <div className="flex items-start justify-between mb-4">
                        <div>
                          <div className="flex items-center gap-2">
                            <h3 className="text-lg font-semibold">{plan.planName}</h3>
                            {isRecommended && (
                              <span className="px-2 py-0.5 bg-teal-100 text-teal-700 text-xs font-medium rounded-full">
                                Recommended
                              </span>
                            )}
                          </div>
                        </div>
                        <div className="text-right">
                          <div className="text-2xl font-bold text-[#0f1b2d]">
                            {formatCurrency(plan.monthlyPayment)}
                          </div>
                          <div className="text-sm text-gray-500">per month</div>
                        </div>
                      </div>

                      <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
                        <div>
                          <div className="text-gray-500">Total Paid</div>
                          <div className="font-semibold">{formatCurrency(plan.totalPaid)}</div>
                        </div>
                        <div>
                          <div className="text-gray-500">Total Interest</div>
                          <div className="font-semibold">{formatCurrency(plan.totalInterest)}</div>
                        </div>
                        <div>
                          <div className="text-gray-500">Forgiven</div>
                          <div className="font-semibold">
                            {plan.forgivenessAmount > 0
                              ? formatCurrency(plan.forgivenessAmount)
                              : '—'}
                          </div>
                        </div>
                        <div>
                          <div className="text-gray-500">Timeline</div>
                          <div className="font-semibold">
                            {formatMonths(plan.repaymentMonths)}
                          </div>
                        </div>
                      </div>

                      {/* Expand/Collapse */}
                      <button
                        onClick={() => setExpandedPlan(isExpanded ? null : plan.planName)}
                        className="mt-4 flex items-center gap-1 text-sm text-teal-600 hover:text-teal-700"
                      >
                        <Info className="w-4 h-4" />
                        {isExpanded ? 'Hide details' : 'How we calculated this'}
                      </button>
                    </div>

                    {isExpanded && (
                      <div className="border-t border-gray-100 bg-gray-50 p-6 text-sm">
                        <div className="grid grid-cols-2 gap-4 mb-4">
                          <div>
                            <span className="text-gray-500">Poverty Guideline:</span>{' '}
                            <span className="font-medium">{formatCurrency(plan.povertyGuideline)}</span>
                          </div>
                          <div>
                            <span className="text-gray-500">Discretionary Income:</span>{' '}
                            <span className="font-medium">{formatCurrency(plan.discretionaryIncome)}</span>
                          </div>
                        </div>
                        {plan.forgivenessMonths > 0 && (
                          <p className="text-gray-600">
                            After {formatMonths(plan.forgivenessMonths)} of qualifying payments,
                            remaining balance of {formatCurrency(plan.forgivenessAmount)} would be
                            forgiven. Note: forgiven amounts may be taxable income (except PSLF).
                          </p>
                        )}
                      </div>
                    )}
                  </div>
                );
              })}

              {/* Disclaimer */}
              <div className="text-xs text-gray-400 text-center mt-8 space-y-1">
                <p>
                  Estimates are for informational purposes only. Actual payments depend on
                  your specific loan terms, servicer, and certification.
                </p>
                <p>
                  Not affiliated with the U.S. Department of Education. Not legal or financial advice.
                </p>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}