- {newMilestone.impacts.map((imp,idx)=>(
-
+ {newMilestone.impacts.map((imp, idx) => (
+
+ {/* Type */}
- {imp.impact_type!=='salary' && (
+
+ {/* Direction (spacer when salary) */}
+ {imp.impact_type !== 'salary' ? (
+ ) : (
+
)}
-
+
+ {/* Amount (fixed width) */}
+
updateNewImpact(idx,'amount',e.target.value)}
+ onChange={(e) => updateNewImpact(idx, 'amount', e.target.value)}
/>
-
-
updateNewImpact(idx,'start_date',e.target.value)}
- />
- {imp.impact_type==='MONTHLY' && (
+
+ {/* Dates (flex) */}
+
+
+ {/* Remove */}
))}
+
{/* save row */}
diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js
index 880a40c..ce8460a 100644
--- a/src/components/PremiumOnboarding/CareerOnboarding.js
+++ b/src/components/PremiumOnboarding/CareerOnboarding.js
@@ -79,9 +79,7 @@ function handleSubmit() {
}
}
- const nextLabel = skipFin
- ? inCollege ? 'College β' : 'Finish β'
- : inCollege ? 'College β' : 'Financial β';
+const nextLabel = !skipFin ? 'Financial β' : (inCollege ? 'College β' : 'Finish β');
return (
@@ -185,9 +183,9 @@ function handleSubmit() {
- {/* Career Search */}
-
-
-
- {careerMatches.length > 0 && (
-
- {careerMatches.map((c, idx) => (
- - handleSelectCareer(c)}
- >
- {c}
-
- ))}
-
- )}
-
- Current Career: {formData.career_name || '(none)'}
-
-
+ {/* Career Search (shared component) */}
+
+
+
{
+ if (!found) return;
+ setFormData(prev => ({ ...prev, career_name: found.title || prev.career_name || '' }));
+ }}/>
+
+ Selected Career: {formData.career_name || '(none)'}
+
+
{/* Status */}
@@ -937,13 +827,10 @@ if (formData.retirement_start_date) {
max-h-48 overflow-auto mt-1
"
>
- {schoolSuggestions.map((sch, i) => (
-
handleSchoolSelect(sch)}
- >
- {sch}
+ {schoolSuggestions.map((sch, i) => (
+ handleSchoolSelect(sch)}>
+ {sch.name}
))}
@@ -1216,11 +1103,6 @@ if (formData.retirement_start_date) {
- {!formData.retirement_start_date && (
-
- Pick a Planned Retirement Date to run the simulation.
-
- )}
{/* Show a preview if we have simulation data */}
diff --git a/src/components/SignUp.js b/src/components/SignUp.js
index 964d461..37b5df6 100644
--- a/src/components/SignUp.js
+++ b/src/components/SignUp.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from './ui/button.js';
import SituationCard from './ui/SituationCard.js';
@@ -54,11 +54,16 @@ function SignUp() {
const [zipcode, setZipcode] = useState('');
const [state, setState] = useState('');
const [area, setArea] = useState('');
- const [areas, setAreas] = useState([]);
const [error, setError] = useState('');
const [loadingAreas, setLoadingAreas] = useState(false);
const [phone, setPhone] = useState('+1');
- const [optIn, setOptIn] = useState(false);
+ const [optIn, setOptIn] = useState(false);
+ const [areas, setAreas] = useState([]);
+ const [areasErr, setAreasErr] = useState('');
+ const areasCacheRef = useRef(new Map()); // cache: stateCode -> areas[]
+ const debounceRef = useRef(null); // debounce timer
+ const inflightRef = useRef(null); // AbortController for in-flight
+
const [showCareerSituations, setShowCareerSituations] = useState(false);
const [selectedSituation, setSelectedSituation] = useState(null);
@@ -235,6 +240,60 @@ const handleSituationConfirm = async () => {
}
};
+useEffect(() => {
+ // reset UI
+ setAreasErr('');
+ if (!state) { setAreas([]); return; }
+
+ // cached? instant
+ if (areasCacheRef.current.has(state)) {
+ setAreas(areasCacheRef.current.get(state));
+ return;
+ }
+
+ // debounce to avoid rapid refetch on quick clicks
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(async () => {
+ // cancel previous request if any
+ if (inflightRef.current) inflightRef.current.abort();
+ const controller = new AbortController();
+ inflightRef.current = controller;
+
+ setLoadingAreas(true);
+ try {
+ // client-side timeout race (6s)
+ const timeout = new Promise((_, rej) =>
+ setTimeout(() => rej(new Error('timeout')), 6000)
+ );
+
+ const res = await Promise.race([
+ fetch(`/api/areas?state=${encodeURIComponent(state)}`, {
+ signal: controller.signal,
+ }),
+ timeout,
+ ]);
+
+ if (!res || !res.ok) throw new Error('bad_response');
+ const data = await res.json();
+
+ // normalize, uniq, sort for UX
+ const list = Array.from(new Set((data.areas || []).filter(Boolean))).sort();
+ areasCacheRef.current.set(state, list); // cache it
+ setAreas(list);
+ } catch (err) {
+ if (err.name === 'AbortError') return; // superseded by a newer request
+ setAreas([]);
+ setAreasErr('Could not load Areas. You can proceed without selecting one.');
+ } finally {
+ if (inflightRef.current === controller) inflightRef.current = null;
+ setLoadingAreas(false);
+ }
+ }, 250); // 250ms debounce
+
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ };
+ }, [state]);
return (
@@ -341,35 +400,37 @@ return (
-
+
+
+ {loadingAreas && (
+
+ Loading...
+
+ )}
+ {areasErr && (
+
{areasErr}
+ )}