← back to StudentLoanTracker

src/app/pslf/page.tsx

351 lines

'use client';

import { useState } from 'react';
import { ShieldCheck, TrendingUp } from 'lucide-react';
import {
  calculatePSLF,
  type LoanInputs,
  type FilingStatus,
} from '@/lib/calculators';
import type { PSLFResult } from '@/lib/calculators';

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

export default function PSLFPage() {
  const [inputs, setInputs] = useState({
    totalBalance: 60000,
    weightedRate: 0.06,
    agi: 55000,
    familySize: 1,
    filingStatus: 'single' as FilingStatus,
    state: 'CA',
    qualifyingPayments: 48,
    selectedPlan: 'SAVE' as 'SAVE' | 'IBR' | 'PAYE' | 'ICR' | 'Standard',
    employerType: 'government' as 'government' | 'nonprofit' | 'for_profit',
  });

  const [result, setResult] = useState<PSLFResult | null>(null);

  function handleCalculate() {
    const r = calculatePSLF({
      totalBalance: inputs.totalBalance,
      weightedRate: inputs.weightedRate,
      agi: inputs.agi,
      familySize: inputs.familySize,
      filingStatus: inputs.filingStatus,
      state: inputs.state,
      qualifyingPayments: inputs.qualifyingPayments,
      selectedPlan: inputs.selectedPlan,
      employerType: inputs.employerType,
    });
    setResult(r);
  }

  const progressPct = Math.min(100, (inputs.qualifyingPayments / 120) * 100);

  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <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-emerald-500/10 text-emerald-600">
            <ShieldCheck className="w-5 h-5" />
          </div>
          <h1 className="text-3xl font-bold">PSLF Progress Tracker</h1>
        </div>
        <p className="text-gray-600 text-lg leading-relaxed">
          Track your progress toward 120 qualifying payments for Public Service Loan Forgiveness.
          See how much would be forgiven and when.
        </p>
      </div>

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

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">Qualifying Payments Made</label>
              <input type="number" min="0" max="120" value={inputs.qualifyingPayments}
                onChange={(e) => setInputs(p => ({...p, qualifyingPayments: Number(e.target.value)}))}
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
              />
            </div>

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">Employer Type</label>
              <select value={inputs.employerType}
                onChange={(e) => setInputs(p => ({...p, employerType: e.target.value as typeof inputs.employerType}))}
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
              >
                <option value="government">Government</option>
                <option value="nonprofit">501(c)(3) Nonprofit</option>
                <option value="for_profit">For-Profit (not eligible)</option>
              </select>
            </div>

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">Repayment Plan</label>
              <select value={inputs.selectedPlan}
                onChange={(e) => setInputs(p => ({...p, selectedPlan: e.target.value as typeof inputs.selectedPlan}))}
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
              >
                <option value="SAVE">SAVE</option>
                <option value="IBR">IBR</option>
                <option value="PAYE">PAYE</option>
                <option value="ICR">ICR</option>
                <option value="Standard">Standard (10-Year)</option>
              </select>
            </div>

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">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) => setInputs(p => ({...p, 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-emerald-500 focus:border-emerald-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) => setInputs(p => ({...p, weightedRate: Number(e.target.value) / 100}))}
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-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) => setInputs(p => ({...p, 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-emerald-500 focus:border-emerald-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) => setInputs(p => ({...p, familySize: Number(e.target.value)}))}
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
              />
            </div>

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

        {/* Results */}
        <div className="lg:col-span-2 space-y-6">
          {/* Thermometer Countdown */}
          <div className="bg-white rounded-xl border border-gray-200 p-6">
            <h3 className="font-semibold mb-6 text-center">Payment Progress</h3>
            <div className="flex items-center gap-8">
              {/* SVG Thermometer */}
              <div className="flex flex-col items-center shrink-0">
                <svg width="80" height="320" viewBox="0 0 80 320" className="drop-shadow-lg">
                  {/* Outer shell */}
                  <rect x="20" y="10" width="40" height="240" rx="20" ry="20"
                    fill="#f1f5f9" stroke="#e2e8f0" strokeWidth="2" />
                  {/* Fill (bottom-up) */}
                  <clipPath id="thermClip">
                    <rect x="22" y="12" width="36" height="236" rx="18" ry="18" />
                  </clipPath>
                  <rect
                    x="22"
                    y={12 + 236 * (1 - progressPct / 100)}
                    width="36"
                    height={236 * (progressPct / 100)}
                    clipPath="url(#thermClip)"
                    className="transition-all duration-700 ease-out"
                    fill="url(#thermoGrad)"
                  />
                  {/* Gradient */}
                  <defs>
                    <linearGradient id="thermoGrad" x1="0" y1="0" x2="0" y2="1">
                      <stop offset="0%" stopColor="#10b981" />
                      <stop offset="50%" stopColor="#34d399" />
                      <stop offset="100%" stopColor="#6ee7b7" />
                    </linearGradient>
                  </defs>
                  {/* Bulb at bottom */}
                  <circle cx="40" cy="280" r="30"
                    fill={progressPct >= 100 ? '#10b981' : '#f1f5f9'}
                    stroke="#e2e8f0" strokeWidth="2"
                    className="transition-all duration-700"
                  />
                  <circle cx="40" cy="280" r="24"
                    fill={progressPct > 0 ? 'url(#thermoGrad)' : '#e2e8f0'}
                    className="transition-all duration-700"
                  />
                  {/* Tick marks every 30 payments */}
                  {[0, 30, 60, 90, 120].map((tick) => {
                    const y = 248 - (tick / 120) * 236;
                    return (
                      <g key={tick}>
                        <line x1="60" y1={y} x2="70" y2={y} stroke="#94a3b8" strokeWidth="1.5" />
                        <text x="74" y={y + 4} fontSize="10" fill="#64748b" fontFamily="sans-serif">
                          {tick}
                        </text>
                      </g>
                    );
                  })}
                  {/* Current level indicator */}
                  {progressPct > 0 && progressPct < 100 && (
                    <g>
                      <line
                        x1="10" y1={248 - (inputs.qualifyingPayments / 120) * 236}
                        x2="20" y2={248 - (inputs.qualifyingPayments / 120) * 236}
                        stroke="#059669" strokeWidth="2"
                      />
                      <text
                        x="4" y={248 - (inputs.qualifyingPayments / 120) * 236 + 4}
                        fontSize="10" fill="#059669" fontWeight="bold" textAnchor="end"
                        fontFamily="sans-serif"
                      >
                      </text>
                    </g>
                  )}
                  {/* 120 goal star */}
                  {progressPct >= 100 && (
                    <text x="40" y="285" textAnchor="middle" fontSize="20">
                      &#11088;
                    </text>
                  )}
                </svg>
              </div>

              {/* Stats beside thermometer */}
              <div className="flex-1 space-y-4">
                <div>
                  <div className="text-5xl font-bold text-emerald-600 tracking-tight">
                    {inputs.qualifyingPayments}
                  </div>
                  <div className="text-gray-400 text-sm mt-1">of 120 qualifying payments</div>
                </div>

                <div className="h-px bg-gray-100" />

                <div className="grid grid-cols-2 gap-4">
                  <div>
                    <div className="text-2xl font-bold text-[#0f1b2d]">
                      {120 - inputs.qualifyingPayments}
                    </div>
                    <div className="text-xs text-gray-500">payments remaining</div>
                  </div>
                  <div>
                    <div className="text-2xl font-bold text-[#0f1b2d]">
                      {Math.ceil((120 - inputs.qualifyingPayments) / 12)}yr {(120 - inputs.qualifyingPayments) % 12}mo
                    </div>
                    <div className="text-xs text-gray-500">time remaining</div>
                  </div>
                </div>

                <div className="h-px bg-gray-100" />

                {/* Mini progress percentage */}
                <div className="flex items-center gap-3">
                  <div className="flex-1 bg-gray-100 rounded-full h-2 overflow-hidden">
                    <div
                      className="h-full bg-emerald-500 rounded-full transition-all duration-700"
                      style={{ width: `${progressPct}%` }}
                    />
                  </div>
                  <span className="text-sm font-semibold text-emerald-600 w-14 text-right">
                    {progressPct.toFixed(1)}%
                  </span>
                </div>

                {progressPct >= 100 && (
                  <div className="bg-emerald-50 border border-emerald-200 rounded-lg p-3 text-center">
                    <span className="text-emerald-700 font-semibold">
                      Congratulations! You&apos;ve reached 120 qualifying payments!
                    </span>
                  </div>
                )}
              </div>
            </div>
          </div>

          {inputs.employerType === 'for_profit' && (
            <div className="bg-red-50 border border-red-200 rounded-xl p-6">
              <p className="text-red-800 font-medium">
                For-profit employers do not qualify for PSLF. You must work full-time for a
                government organization or 501(c)(3) nonprofit.
              </p>
            </div>
          )}

          {result && inputs.employerType !== 'for_profit' && (
            <>
              {/* Key Metrics */}
              <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
                <div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
                  <div className="text-sm text-gray-500 mb-1">Monthly Payment</div>
                  <div className="text-2xl font-bold">{formatCurrency(result.monthlyPayment)}</div>
                  <div className="text-xs text-gray-400 mt-1">on {inputs.selectedPlan} plan</div>
                </div>
                <div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6 text-center">
                  <div className="text-sm text-emerald-600 mb-1">Amount Forgiven</div>
                  <div className="text-2xl font-bold text-emerald-700">{formatCurrency(result.forgivenessAmount)}</div>
                  <div className="text-xs text-emerald-500 mt-1">tax-free under PSLF</div>
                </div>
                <div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
                  <div className="text-sm text-gray-500 mb-1">Forgiveness Date</div>
                  <div className="text-2xl font-bold">{result.estimatedForgivenessDate}</div>
                  <div className="text-xs text-gray-400 mt-1">{result.paymentsRemaining} months away</div>
                </div>
              </div>

              {/* Plan Comparison */}
              <div className="bg-white rounded-xl border border-gray-200 p-6">
                <h3 className="font-semibold mb-4 flex items-center gap-2">
                  <TrendingUp className="w-5 h-5 text-emerald-500" />
                  Monthly Payment by Plan
                </h3>
                <div className="space-y-3">
                  {Object.entries(result.monthlyPaymentByPlan).map(([plan, payment]) => {
                    const maxPayment = Math.max(...Object.values(result.monthlyPaymentByPlan));
                    const widthPct = maxPayment > 0 ? (payment / maxPayment) * 100 : 0;
                    return (
                      <div key={plan}>
                        <div className="flex justify-between text-sm mb-1">
                          <span className={`font-medium ${plan === inputs.selectedPlan ? 'text-emerald-600' : 'text-gray-700'}`}>
                            {plan} {plan === inputs.selectedPlan && '(selected)'}
                          </span>
                          <span className="font-semibold">{formatCurrency(payment)}</span>
                        </div>
                        <div className="w-full bg-gray-100 rounded-full h-2">
                          <div
                            className={`h-full rounded-full ${plan === inputs.selectedPlan ? 'bg-emerald-500' : 'bg-gray-300'}`}
                            style={{ width: `${widthPct}%` }}
                          />
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>

              <div className="text-xs text-gray-400 text-center">
                PSLF forgiveness is tax-free. Estimates assume continuous qualifying employment and on-time payments.
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}