Branding/UI & Loan Repayment fixes
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

This commit is contained in:
Josh 2025-09-23 14:25:18 +00:00
parent 5a0c7efd25
commit e25662aae4
10 changed files with 463 additions and 273 deletions

View File

@ -1 +1 @@
77b19da169acfc9f13cfa14f2fb425fea6c03ef4-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b e520c3d4f21d892f230efe5f06e338b842191dd6-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b

View File

@ -182,6 +182,7 @@ services:
networks: [default, aptiva-shared] networks: [default, aptiva-shared]
environment: environment:
GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY} GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY}
ENV_NAME: ${ENV_NAME}
ports: ["80:80", "443:443"] ports: ["80:80", "443:443"]
volumes: volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx.conf:/etc/nginx/nginx.conf:ro

View File

@ -442,6 +442,12 @@ const cancelLogout = () => {
> >
Educational Programs Educational Programs
</Link> </Link>
<Link
to="/preparing?loan=1"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Education Repayment Calculator
</Link>
</div> </div>
</div> </div>
@ -689,6 +695,7 @@ const cancelLogout = () => {
Preparing Overview Preparing Overview
</Link> </Link>
<Link to="/educational-programs" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Educational Programs</Link> <Link to="/educational-programs" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Educational Programs</Link>
<Link to="/preparing?loan=1" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Education Repayment Calculator</Link>
</div> </div>
)} )}

View File

@ -1,64 +1,60 @@
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button } from "./ui/button.js"; import { Button } from "./ui/button.js";
import EconomicProjections from "./EconomicProjections.js";
/* simple pill-style label */
const Chip = ({ label }) => (
<span
className="
inline-flex items-center text-sm px-3 py-1
rounded-full bg-gray-100 text-gray-700
border border-gray-300
"
>
{label}
<span className="ml-1 text-xs text-green-600"> in Career Coach</span>
</span>
);
export default function EnhancingLanding({ userProfile }) { export default function EnhancingLanding({ userProfile }) {
const navigate = useNavigate(); const navigate = useNavigate();
const socCode = userProfile?.socCode;
const stateName = userProfile?.state;
return ( return (
<div className="min-h-screen bg-gray-50 py-8 px-4"> <div className="min-h-screen bg-gray-50 py-10 px-4">
<div className="max-w-4xl mx-auto space-y-10"> <div className="max-w-3xl mx-auto bg-white rounded-2xl shadow p-6 md:p-8">
{/* Title + intro (centered, blue headline) */}
{/* 📌 Current status */} <h1 className="text-3xl font-bold text-blue-600 text-center">
<section className="bg-white shadow rounded-lg p-6"> Enhancing Your Career
<h2 className="text-2xl font-semibold mb-4"> </h1>
📌 Your Current Status &amp; Next Steps <p className="text-center text-gray-600 mt-2">
</h2> Build momentum in your current role and prepare for your next step.
<p className="text-gray-600 mb-4"> Use Career Coach for guided milestones, and fine-tune your materials
Evaluate where you are in your career and discover upcoming milestones with our AI&nbsp;recommendations. with the Resume Optimizer.
</p> </p>
<EconomicProjections socCode={socCode} stateName={stateName} /> {/* Feature tiles (inside the main card) */}
<div className="grid gap-6 md:gap-8 md:grid-cols-2 mt-6">
{/* Career Coach */}
<div className="rounded-xl border border-gray-200 p-5">
<h3 className="text-lg font-medium">Career Coach</h3>
<p className="text-gray-600 mt-1">
Guided milestones for networking, interview prep, and job search.
</p>
<div className="mt-4"> <div className="mt-4">
<Button onClick={() => navigate("/career-roadmap")}> <Button size="lg" onClick={() => navigate("/career-roadmap")}>
Open Career Coach Open Career Coach
</Button> </Button>
</div> </div>
</section>
{/* 🚀 How do I get there */}
<section className="bg-white shadow rounded-lg p-6">
<h2 className="text-2xl font-semibold mb-4">🚀 How Do I Get There?</h2>
<div className="flex flex-wrap gap-3">
<Chip label="Resume Optimizer" />
<Chip label="Networking" />
<Chip label="Interview Help" />
<Chip label="Job Search" />
</div> </div>
<p className="text-sm text-gray-500 mt-3"> {/* Resume Optimizer (separate tool) */}
All of these tools live inside the <strong>Career Coach</strong>. Open it any time using the button above. <div className="rounded-xl border border-gray-200 p-5">
<h3 className="text-lg font-medium">Resume Optimizer</h3>
<p className="text-gray-600 mt-1">
Improve wording, keywords, and formatting to match target roles.
</p>
<div className="mt-4">
<Button size="lg" onClick={() => navigate("/resume-optmizer")}>
Open Resume Optimizer
</Button>
</div>
</div>
</div>
{/* Divider + lower subsection to mirror the secondary blocks on other landings */}
<hr className="my-6 border-gray-200" />
<h2 className="text-xl font-semibold">Your Current Status & Next Steps</h2>
<p className="text-gray-600 mt-1">
Evaluate where you are in your career and discover upcoming milestones
with our AI recommendations.
</p> </p>
</section>
</div> </div>
</div> </div>

View File

@ -6,6 +6,8 @@ import { Button } from './ui/button.js';
import { getNearbySchools } from '../utils/getNearbySchools.js'; import { getNearbySchools } from '../utils/getNearbySchools.js';
import UpsellSummary from './UpsellSummary.js'; import UpsellSummary from './UpsellSummary.js';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import api from '../auth/apiClient.js';
/* ───────────────────────── CONSTANTS ───────────────────────── */ /* ───────────────────────── CONSTANTS ───────────────────────── */
const DEGREE_OPTS = [ const DEGREE_OPTS = [
@ -32,9 +34,21 @@ export default function LoanRepaymentDrawer({
}) { }) {
/* Hooks must always run return null later if !open */ /* Hooks must always run return null later if !open */
/* ── Remote data for auto-suggest ─ */ /* ── Remote data for auto-suggest ─ */
const [cipData, setCipData] = useState([]);
const [schoolSearch, setSchoolSearch] = useState(''); const [schoolSearch, setSchoolSearch] = useState('');
const [icData, setIcData] = useState([]); const [selectedSchool, setSelectedSchool] = useState({ name: '', unitId: null });
const [schoolSug, setSchoolSug] = useState([]); // [{name, unitId}]
const [schoolSugOpen, setSchoolSugOpen] = useState(false);
const [schoolHi, setSchoolHi] = useState(-1);
// Program auto-suggest (depends on selectedSchool)
const [programSearch, setProgramSearch] = useState('');
const [selectedProgram, setSelectedProgram] = useState(''); // exact string
const [programSug, setProgramSug] = useState([]); // [{program}]
const [programSugOpen, setProgramSugOpen] = useState(false);
const [programHi, setProgramHi] = useState(-1);
// Degree types (filtered by school + program)
const [degreeTypes, setDegreeTypes] = useState([]); // ['Bachelor's Degree', ...]
/* ── Simple form fields ─ */ /* ── Simple form fields ─ */
const [degree, setDegree] = useState(''); const [degree, setDegree] = useState('');
@ -62,17 +76,9 @@ const getAnnualTuition = (schoolsArr, typed, isGrad, resid) =>
pickField(schoolsArr?.[0] ?? {}, isGrad, resid) || pickField(schoolsArr?.[0] ?? {}, isGrad, resid) ||
(typed ? Number(typed) : 0); (typed ? Number(typed) : 0);
// Degree menu: prefer backend-provided types (if any), else fall back
const degreeMenu = degreeTypes.length ? degreeTypes : DEGREE_OPTS;
/* ── memoed degree list for current school ── */
const schoolDegrees = useMemo(() => {
if (!schoolSearch.trim()) return [];
const list = cipData
.filter(r => r.INSTNM.toLowerCase() === schoolSearch.toLowerCase())
.map(r => r.CREDDESC);
return [...new Set(list)];
}, [schoolSearch, cipData]);
const degreeMenu = schoolDegrees.length ? schoolDegrees : DEGREE_OPTS;
const isGrad = /(Master|Doctoral|First Professional|Graduate|Certificate)/i.test(degree); const isGrad = /(Master|Doctoral|First Professional|Graduate|Certificate)/i.test(degree);
const annualTuition= getAnnualTuition(schools, tuition, isGrad, tuitionType); const annualTuition= getAnnualTuition(schools, tuition, isGrad, tuitionType);
@ -82,86 +88,102 @@ useEffect(() => {
if (!open) return; if (!open) return;
if (schools.length) return; if (schools.length) return;
if (!cipCodes.length) return; if (!cipCodes.length) return;
(async () => { (async () => {
try { try {
const seed = await getNearbySchools(cipCodes, userZip, userState); const seed = await getNearbySchools(cipCodes, userZip, userState);
if (seed.length) setSchools(seed); if (seed.length) setSchools(seed);
} catch (e) { } catch (e) { console.warn('auto-seed schools failed:', e); }
console.warn('auto-seed schools failed:', e);
}
})(); })();
}, [open, schools.length, cipCodes.join('-'), userZip, userState, setSchools]); }, [open, schools.length, cipCodes.join('-'), userZip, userState, setSchools]);
/* /* ───────── SCHOOL SUGGEST (server) ───────── */
FETCH CIP DATA (only once the drawer is ever opened)
*/
useEffect(() => {
if (!open || cipData.length) return;
fetch('/cip_institution_mapping_new.json')
.then(r => r.text())
.then(text =>
text
.split('\n')
.map(l => { try { return JSON.parse(l); } catch { return null; } })
.filter(Boolean)
)
.then(arr => setCipData(arr))
.catch(e => console.error('CIP fetch error', e));
}, [open, cipData.length]);
/*
SCHOOL AUTOCOMPLETE LIST (memoised)
*/
const suggestions = useMemo(() => {
if (!schoolSearch.trim()) return [];
const low = schoolSearch.toLowerCase();
const set = new Set(
cipData
.filter(r => r.INSTNM.toLowerCase().includes(low))
.map(r => r.INSTNM)
);
return [...set].slice(0, 10);
}, [schoolSearch, cipData]);
useEffect(() => { useEffect(() => {
if (!open || icData.length) return; if (!open) return;
fetch('/ic2023_ay.csv') const q = schoolSearch.trim();
.then(r => r.text()) if (q.length < 2) { setSchoolSug([]); return; }
.then(text => { let cancelled = false;
const [header, ...rows] = text.split('\n').map(l => l.split(',')); (async () => {
return rows.map(row => try {
Object.fromEntries(row.map((v, i) => [header[i], v])) const { data } = await api.get('/api/schools/suggest', {
); params: { query: q, limit: 10 },
}) withCredentials: true,
.then(setIcData) });
.catch(e => console.error('iPEDS load fail', e)); if (!cancelled) setSchoolSug(Array.isArray(data) ? data : []);
}, [open, icData.length]); } catch (e) {
if (!cancelled) setSchoolSug([]);
/* ───────── auto-tuition when schoolSearch settles ───────── */
useEffect(() => {
if (!schoolSearch.trim() || !icData.length) return;
const rec = cipData.find(r => r.INSTNM.toLowerCase() === schoolSearch.toLowerCase());
if (!rec) return;
const match = icData.find(r => r.UNITID === rec.UNITID);
if (!match) return;
const calc = () => {
const grad = /(Master|Doctoral|First Professional|Graduate|Certificate)/i.test(degree);
if (!grad) {
return tuitionType === 'inState'
? parseFloat(match.TUITION1 || match.TUITION2 || '')
: parseFloat(match.TUITION3 || '');
} }
return tuitionType === 'inState' })();
? parseFloat(match.TUITION5 || match.TUITION6 || '') return () => { cancelled = true; };
: parseFloat(match.TUITION7 || ''); }, [open, schoolSearch]);
};
const est = calc();
if (est && !tuitionManual) setTuition(String(est));
}, [schoolSearch, tuitionType, degree, cipData, icData, tuitionManual]);
/* ───────── PROGRAM SUGGEST (server; depends on school) ───────── */
useEffect(() => {
if (!open) return;
if (!selectedSchool?.name) { setProgramSug([]); return; }
const q = programSearch.trim();
if (q.length < 2) { setProgramSug([]); return; }
let cancelled = false;
(async () => {
try {
const { data } = await api.get('/api/programs/suggest', {
params: { school: selectedSchool.name, query: q, limit: 10 },
withCredentials: true,
});
if (!cancelled) setProgramSug(Array.isArray(data) ? data : []);
} catch (e) {
if (!cancelled) setProgramSug([]);
}
})();
return () => { cancelled = true; };
}, [open, selectedSchool?.name, programSearch]);
/* ───────── DEGREE TYPES (server; depends on school+program) ───────── */
useEffect(() => {
if (!open) return;
if (!selectedSchool?.name || !selectedProgram) { setDegreeTypes([]); return; }
let cancelled = false;
(async () => {
try {
const { data } = await api.get('/api/programs/types', {
params: { school: selectedSchool.name, program: selectedProgram },
withCredentials: true,
});
const types = Array.isArray(data?.types) ? data.types : [];
if (!cancelled) setDegreeTypes(types);
} catch (e) {
if (!cancelled) setDegreeTypes([]);
}
})();
return () => { cancelled = true; };
}, [open, selectedSchool?.name, selectedProgram]);
/* ───────── TUITION ESTIMATE (server) ───────── */
useEffect(() => {
if (!open) return;
if (!selectedSchool?.unitId) return;
if (!degree) return;
if (tuitionManual) return; // user overrode
(async () => {
try {
const { data } = await api.get('/api/tuition/estimate', {
params: {
unitId : String(selectedSchool.unitId),
programType: degree,
inState : tuitionType === 'inState' ? 1 : 0,
inDistrict: 0,
creditHoursPerYear: 0, // use school's full-time figure (server picks full when 0)
},
withCredentials: true,
});
if (data?.estimate) setTuition(String(data.estimate));
} catch (e) {
// keep existing value; user can enter manually
}
})();
},[open, selectedSchool?.unitId, degree, tuitionType, tuitionManual]);
/* /*
ESC --> close convenience ESC --> close convenience
@ -200,7 +222,7 @@ const handleContinue = () => {
} }
const stub = { const stub = {
name : schoolSearch || 'Unknown School', name : (selectedSchool?.name || schoolSearch || 'Unknown School'),
degreeType : degree || 'Unspecified', degreeType : degree || 'Unspecified',
programLength : 4, programLength : 4,
tuition : parseFloat(tuition), tuition : parseFloat(tuition),
@ -247,23 +269,89 @@ const handleContinue = () => {
className="space-y-4" className="space-y-4"
onSubmit={e => { e.preventDefault(); handleContinue(); }} onSubmit={e => { e.preventDefault(); handleContinue(); }}
> >
{/* School name (optional) */} {/* School name (server suggest) */}
<div> <div className="relative">
<label className="text-sm font-medium">School name</label> <label className="text-sm font-medium">School name</label>
<input <input
type="text" type="text"
value={schoolSearch} value={schoolSearch}
onChange={e => setSchoolSearch(e.target.value)} onChange={e => {
list="school-suggestions" setSchoolSearch(e.target.value);
setSelectedSchool({ name:'', unitId:null });
setSchoolSugOpen(true); setSchoolHi(-1);
// Clear program/degree when school changes
setProgramSearch(''); setSelectedProgram(''); setDegree(''); setDegreeTypes([]);
}}
onFocus={() => setSchoolSugOpen(true)}
onBlur={() => setTimeout(() => setSchoolSugOpen(false), 120)}
placeholder="Start typing…" placeholder="Start typing…"
className="mt-1 w-full rounded border px-3 py-2 text-sm" className="mt-1 w-full rounded border px-3 py-2 text-sm"
onBlur={e => setSchoolSearch(e.target.value.trim())} onKeyDown={e => {
if (!schoolSugOpen || schoolSug.length === 0) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setSchoolHi(h => Math.min(h + 1, schoolSug.length - 1)); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setSchoolHi(h => Math.max(h - 1, 0)); }
else if (e.key === 'Enter' && schoolHi >= 0) {
e.preventDefault();
const opt = schoolSug[schoolHi]; setSchoolSearch(opt.name); setSelectedSchool(opt); setSchoolSugOpen(false);
}
}}
/> />
<datalist id="school-suggestions"> {schoolSugOpen && schoolSug.length > 0 && (
{suggestions.map((s, i) => ( <div className="absolute z-50 mt-1 w-full max-h-56 overflow-auto rounded border bg-white shadow">
<option key={i} value={s} /> {schoolSug.map((s, i) => (
<button
key={`${s.name}-${s.unitId}-${i}`}
type="button"
className={`block w-full text-left px-3 py-2 text-sm hover:bg-gray-50 ${schoolHi===i ? 'bg-gray-100' : ''}`}
onMouseEnter={() => setSchoolHi(i)}
onMouseDown={e => e.preventDefault()}
onClick={() => { setSchoolSearch(s.name); setSelectedSchool(s); setSchoolSugOpen(false); }}
>
{s.name}
</button>
))} ))}
</datalist> </div>
)}
</div>
{/* Program (server suggest, requires selected school) */}
<div className="relative">
<label className="text-sm font-medium">Program / major</label>
<input
type="text"
value={programSearch}
onChange={e => { setProgramSearch(e.target.value); setSelectedProgram(''); setProgramSugOpen(true); setProgramHi(-1); }}
onFocus={() => setProgramSugOpen(true)}
onBlur={() => setTimeout(() => setProgramSugOpen(false), 120)}
placeholder={selectedSchool?.name ? 'Start typing…' : 'Pick a school first'}
disabled={!selectedSchool?.name}
className="mt-1 w-full rounded border px-3 py-2 text-sm disabled:bg-gray-100"
onKeyDown={e => {
if (!programSugOpen || programSug.length === 0) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setProgramHi(h => Math.min(h + 1, programSug.length - 1)); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setProgramHi(h => Math.max(h - 1, 0)); }
else if (e.key === 'Enter' && programHi >= 0) {
e.preventDefault();
const opt = programSug[programHi]; setProgramSearch(opt.program); setSelectedProgram(opt.program); setProgramSugOpen(false);
}
}}
/>
{programSugOpen && programSug.length > 0 && (
<div className="absolute z-50 mt-1 w-full max-h-56 overflow-auto rounded border bg-white shadow">
{programSug.map((s, i) => (
<button
key={`${s.program}-${i}`}
type="button"
className={`block w-full text-left px-3 py-2 text-sm hover:bg-gray-50 ${programHi===i ? 'bg-gray-100' : ''}`}
onMouseEnter={() => setProgramHi(i)}
onMouseDown={e => e.preventDefault()}
onClick={() => { setProgramSearch(s.program); setSelectedProgram(s.program); setProgramSugOpen(false); }}
>
{s.program}
</button>
))}
</div>
)}
</div> </div>
{/* Residency */} {/* Residency */}
@ -286,6 +374,7 @@ const handleContinue = () => {
value={degree} value={degree}
onChange={e => setDegree(e.target.value)} onChange={e => setDegree(e.target.value)}
className="mt-1 w-full rounded border px-3 py-2 text-sm" className="mt-1 w-full rounded border px-3 py-2 text-sm"
disabled={degreeTypes.length === 0 && !selectedProgram}
> >
<option value="">Select</option> <option value="">Select</option>
{degreeMenu.map((d, i) => ( {degreeMenu.map((d, i) => (

View File

@ -1,3 +1,4 @@
// src/components/PlanningLanding.jsx
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
@ -6,46 +7,52 @@ function PlanningLanding() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-aptiva-gray to-white p-6">
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8"> <div className="max-w-2xl w-full bg-white shadow-xl rounded-xl p-8 md:p-10">
<h1 className="text-3xl font-bold mb-4 text-center"> {/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl md:text-4xl font-bold text-aptiva mb-2">
Planning Your Career Planning Your Career
</h1> </h1>
<p className="text-gray-600 mb-6 text-center"> <p className="text-gray-600 max-w-xl mx-auto text-sm md:text-base leading-relaxed">
Discover career options that match your interests, skills, and potential. Discover career options that match your interests, skills, and
AptivaAI helps you find your ideal career path, provides insights into educational requirements, potential. AptivaAI helps you explore ideal paths, understand
expected salaries, job market trends, and more. educational requirements, compare salaries, and analyze job market
</p> trendsall in one place.
<div className="grid grid-cols-1 gap-6">
<div>
<Button className="w-full" onClick={() => navigate('/interest-inventory')}>
Take Interest Inventory
</Button>
<p className="mt-2 text-sm text-gray-500">
Identify your interests and discover careers aligned with your strengths.
</p> </p>
</div> </div>
<div> {/* Options grid */}
<Button className="w-full" onClick={() => navigate('/career-explorer')}> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
Explore Career Paths {/* Interest Inventory */}
</Button> <div className="flex flex-col border rounded-lg p-5 hover:shadow-md transition-colors">
<p className="mt-2 text-sm text-gray-500"> <h2 className="text-lg font-semibold mb-2">Interest Inventory</h2>
Research detailed career profiles, job descriptions, salaries, and employment outlooks. <p className="text-sm text-gray-600 mb-4 flex-1">
Identify your interests and discover careers aligned with your
strengths.
</p> </p>
<Button
className="w-full mt-auto bg-aptiva hover:bg-aptiva-dark text-white"
onClick={() => navigate('/interest-inventory')}
>
Take Inventory
</Button>
</div> </div>
<div> {/* Career Explorer */}
<Button className="w-full" onClick={() => navigate('/educational-programs')}> <div className="flex flex-col border rounded-lg p-5 hover:shadow-md transition-colors">
Discover Educational Programs <h2 className="text-lg font-semibold mb-2">Career Explorer</h2>
</Button> <p className="text-sm text-gray-600 mb-4 flex-1">
<p className="mt-2 text-sm text-gray-500"> Research career profiles, job descriptions, salaries, and
Find the right educational programs, degrees, and certifications needed to pursue your chosen career. employment outlooks.
</p> </p>
<Button
className="w-full mt-auto bg-aptiva hover:bg-aptiva-dark text-white"
onClick={() => navigate('/career-explorer')}
>
Explore Careers
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,13 @@
// src/components/PreparingLanding.js // src/components/PreparingLanding.js
import React, { useState, useCallback, useEffect, useContext } from 'react'; import React, { useState, useCallback, useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
import LoanRepaymentDrawer from './LoanRepaymentDrawer.js'; import LoanRepaymentDrawer from './LoanRepaymentDrawer.js';
import { ProfileCtx } from '../App.js'; import { ProfileCtx } from '../App.js';
function PreparingLanding() { function PreparingLanding() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const { user } = useContext(ProfileCtx); const { user } = useContext(ProfileCtx);
/* ─── Drawer visibility ─────────────────────────────── */ /* ─── Drawer visibility ─────────────────────────────── */
@ -19,9 +19,8 @@ function PreparingLanding() {
const [userZip] = useState(''); const [userZip] = useState('');
const [loanResults, setLoanResults] = useState([]); const [loanResults, setLoanResults] = useState([]);
/* Esc -to-close convenience */ /* Esc -to-close convenience */
const escHandler = useCallback(e => { const escHandler = useCallback((e) => {
if (e.key === 'Escape') setShowLoan(false); if (e.key === 'Escape') setShowLoan(false);
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -29,58 +28,83 @@ function PreparingLanding() {
return () => window.removeEventListener('keydown', escHandler); return () => window.removeEventListener('keydown', escHandler);
}, [showLoan, escHandler]); }, [showLoan, escHandler]);
/* ─── Auto-open drawer when routed from nav ───────── */
useEffect(() => {
// support either query ?loan=1 or navigation state { openLoan: true }
try {
const qs = new URLSearchParams(location.search);
const viaQuery = qs.get('loan') === '1';
const viaState = location.state && location.state.openLoan === true;
if ((viaQuery || viaState) && !showLoan) {
setShowLoan(true);
}
} catch {}
}, [location.search, location.state, showLoan]);
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-aptiva-gray to-white p-6">
<div className="max-w-3xl w-full bg-white shadow-lg rounded-lg p-8 space-y-6"> <div className="w-full max-w-3xl bg-white shadow-xl rounded-xl p-8 md:p-10 space-y-8">
{/* ───────────────── TITLE / INTRO ───────────────── */} {/* ───────────────── TITLE / INTRO ───────────────── */}
<h1 className="text-3xl font-bold text-center"> <div className="text-center">
<h1 className="text-3xl md:text-4xl font-bold text-aptiva mb-2">
Preparing for Your (Next) Career Preparing for Your (Next) Career
</h1> </h1>
<p className="text-gray-600 text-center"> <p className="text-gray-600 max-w-2xl mx-auto text-sm md:text-base leading-relaxed">
Build the right skills and plan your education so you can confidently Build the right skills and plan your education so you can confidently
enteror transition intoyour new career. enteror transition intoyour new career.
</p> </p>
</div>
{/* ──────────────── 1) PATH CHOICE ──────────────── */} {/* ──────────────── 1) PATH CHOICE ──────────────── */}
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-xl font-semibold">Which Path Fits You?</h2> <h2 className="text-xl font-semibold">Which Path Fits You?</h2>
<p className="text-gray-700"> <p className="text-gray-700">
We can help you identify whether a&nbsp; We can help you identify whether a <strong>skills-based program</strong> (certifications, bootcamps) or a{' '}
<strong>skills-based program</strong> (certifications, bootcamps) or a&nbsp; <strong>formal education route</strong> (two- or four-year college) is the best fit. Whichever path you choose,
<strong>formal education route</strong> (two- or four-year college) AptivaAI will help you map next stepsfrom applying to graduating.
is the best fit. Whichever path you choose, AptivaAI will help you map next stepsfrom applying to graduating.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={() => navigate('/educational-programs')}>
Plan My Education Path
</Button>
<Button onClick={() => setShowLoan(true)}> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Cost of Education & Loan Repayment {/* Education path card */}
<div className="flex flex-col border rounded-lg p-5 hover:shadow-md transition-colors">
<h3 className="text-lg font-semibold mb-2">Plan My Education Path</h3>
<p className="text-sm text-gray-600 mb-4 flex-1">
Compare degrees, certificates, and training paths for your target role.
</p>
<Button
className="w-full mt-auto bg-aptiva hover:bg-aptiva-dark text-white"
onClick={() => navigate('/educational-programs')}
>
Plan Education
</Button> </Button>
</div> </div>
{/* Loan/Cost card */}
<div className="flex flex-col border rounded-lg p-5 hover:shadow-md transition-colors">
<h3 className="text-lg font-semibold mb-2">Cost of Education &amp; Loan Repayment</h3>
<p className="text-sm text-gray-600 mb-4 flex-1">
Estimate total costs and monthly payments with realistic assumptions.
</p>
<Button
className="w-full mt-auto bg-aptiva hover:bg-aptiva-dark text-white"
onClick={() => setShowLoan(true)}
>
Open Loan Tool
</Button>
</div>
</div>
</section> </section>
{/* ──────────────── 2) LOAN BLURB ──────────────── */} {/* ──────────────── 2) STILL EXPLORING ──────────────── */}
<section className="space-y-4">
<h2 className="text-xl font-semibold">Financing Your Future</h2>
<p className="text-gray-700">
Already have an idea of where you want to enroll? Compare costs,
estimate student-loan repayments, and map out work-study or part-time
opportunities. Our integrated <strong>LoanRepayment</strong> tool shows
realistic monthly payments so you can make confident choices.
</p>
</section>
{/* ──────────────── 3) INTEREST INVENTORY ──────────────── */}
<section className="space-y-3"> <section className="space-y-3">
<h2 className="text-xl font-semibold">Still Exploring?</h2> <h2 className="text-xl font-semibold">Still Exploring?</h2>
<p className="text-gray-700"> <p className="text-gray-700">
Want to revisit career possibilities? Retake our Interest Inventory to Want to revisit career possibilities? Retake our Interest Inventory to see other matching paths.
see other matching paths.
</p> </p>
<Button onClick={() => navigate('/interest-inventory')}> <Button
className="bg-aptiva hover:bg-aptiva-dark text-white"
onClick={() => navigate('/interest-inventory')}
>
Retake Interest Inventory Retake Interest Inventory
</Button> </Button>
</section> </section>
@ -90,7 +114,11 @@ function PreparingLanding() {
{showLoan && ( {showLoan && (
<LoanRepaymentDrawer <LoanRepaymentDrawer
open={showLoan} open={showLoan}
onClose={() => setShowLoan(false)} onClose={() => {
setShowLoan(false);
// clear nav flag so it doesnt auto-reopen
navigate({ pathname: location.pathname }, { replace: true, state: {} });
}}
schools={schools} schools={schools}
setSchools={setSchools} setSchools={setSchools}
results={loanResults} results={loanResults}

View File

@ -1,25 +1,39 @@
import React from 'react'; import React from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { Button } from './ui/button.js'; import { Button } from "./ui/button.js";
function RetirementLanding() { export default function RetirementLanding() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6"> <div className="min-h-screen bg-gray-50 py-10 px-4">
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8"> <div className="max-w-3xl mx-auto bg-white rounded-2xl shadow p-6 md:p-8">
<h1 className="text-3xl font-bold mb-4 text-center"> {/* Title + intro (centered, blue headline) */}
<h1 className="text-3xl font-bold text-blue-600 text-center">
Retirement Planning Retirement Planning
</h1> </h1>
<p className="text-gray-600 mb-6 text-center"> <p className="text-center text-gray-600 mt-2">
Plan strategically and financially for retirement. AptivaAI provides you with clear financial projections, milestone tracking, and scenario analysis for a secure future. Plan strategically and financially for retirement. Model scenarios,
understand trade-offs, and track the milestones that keep you on course.
</p> </p>
<div className="grid grid-cols-1 gap-4">
<Button onClick={() => navigate('/retirement-planner')}>Compare different retirement scenarios and get AI help with planning</Button> {/* Feature tile(s) inside main card */}
<div className="grid gap-6 md:gap-8 md:grid-cols-2 mt-6">
{/* Retirement Planner */}
<div className="rounded-xl border border-gray-200 p-5 md:col-span-2">
<h3 className="text-lg font-medium">Retirement Planner</h3>
<p className="text-gray-600 mt-1">
Compare scenarios, project savings and income, and get AI guidance to
fine-tune your plan.
</p>
<div className="mt-4">
<Button size="lg" onClick={() => navigate("/retirement-planner")}>
Open Retirement Planner
</Button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export default RetirementLanding;

View File

@ -84,14 +84,17 @@ function SignIn({ setIsAuthenticated, setUser }) {
}; };
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-4"> <div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-aptiva-gray to-white p-4">
{showSessionExpiredMsg && ( {showSessionExpiredMsg && (
<div className="mb-4 p-2 bg-red-100 border border-red-300 text-red-700 rounded"> <div className="mb-4 p-2 bg-red-100 border border-red-300 text-red-700 rounded">
Your session has expired. Please sign in again. Your session has expired. Please sign in again.
</div> </div>
)} )}
<div className="w-full max-w-sm rounded-md bg-white p-6 shadow-md"> <div className="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg border border-gray-100">
<h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1> {/* Wordmark (text-only) */}
<div className="mb-1 text-center text-aptiva font-semibold tracking-tight">AptivaAI</div>
<h1 className="mb-1 text-center text-2xl font-semibold">Sign In</h1>
<p className="mb-6 text-center text-xs text-gray-500">Career guidance powered by data enhanced by AI</p>
{error && ( {error && (
<p className="mb-4 rounded bg-red-50 p-2 text-sm text-red-600"> <p className="mb-4 rounded bg-red-50 p-2 text-sm text-red-600">
@ -99,24 +102,24 @@ function SignIn({ setIsAuthenticated, setUser }) {
</p> </p>
)} )}
<form onSubmit={handleSignIn} className="flex flex-col space-y-4"> <form onSubmit={handleSignIn} className="flex flex-col space-y-3 md:space-y-4">
<input <input
type="text" type="text"
placeholder="Username" placeholder="Username"
ref={usernameRef} ref={usernameRef}
className="w-full rounded border border-gray-300 p-2 focus:border-blue-500 focus:outline-none" className="w-full rounded border border-gray-300 p-3 md:p-2 h-12 md:h-10 text-base md:text-sm focus:border-aptiva focus:outline-none"
autoComplete="username" autoComplete="username"
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="Password"
ref={passwordRef} ref={passwordRef}
className="w-full rounded border border-gray-300 p-2 focus:border-blue-500 focus:outline-none" className="w-full rounded border border-gray-300 p-3 md:p-2 h-12 md:h-10 text-base md:text-sm focus:border-aptiva focus:outline-none"
autoComplete="current-password" autoComplete="current-password"
/> />
<button <button
type="submit" type="submit"
className="mx-auto rounded bg-blue-600 px-6 py-2 text-center text-white transition-colors hover:bg-blue-700 focus:outline-none" className="w-full md:w-auto md:mx-auto rounded bg-aptiva px-6 h-12 md:h-10 text-center text-white transition-colors hover:bg-aptiva-dark focus:outline-none"
> >
Sign In Sign In
</button> </button>
@ -125,7 +128,7 @@ function SignIn({ setIsAuthenticated, setUser }) {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<p className="text-center text-sm text-gray-600"> <p className="text-center text-sm text-gray-600">
Dont have an account?{' '} Dont have an account?{' '}
<Link to="/signup" className="text-blue-600 hover:underline"> <Link to="/signup" className="text-aptiva hover:underline">
Sign Up Sign Up
</Link> </Link>
</p> </p>
@ -133,7 +136,7 @@ function SignIn({ setIsAuthenticated, setUser }) {
<div className="text-center"> <div className="text-center">
<Link <Link
to="/forgot-password" to="/forgot-password"
className="inline-block text-sm text-blue-600 hover:underline" className="inline-block text-sm text-aptiva hover:underline"
> >
Forgot your password? Forgot your password?
</Link> </Link>

View File

@ -1,44 +1,89 @@
// SignInLanding.jsx // src/components/SignInLanding.jsx
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
function SignInLanding({ user }) { function SignInLanding({ user }) {
return ( return (
<div className="max-w-2xl mx-auto p-4 bg-white shadow rounded"> <div className="max-w-2xl md:max-w-3xl mx-auto p-4 bg-white shadow rounded">
<h2 className="text-2xl font-semibold mb-4"> {/* Soft brand header band */}
Welcome to AptivaAI {user?.firstname}! <div className="rounded-md bg-aptiva-gray/70 px-4 py-3 mb-4">
<div className="text-aptiva text-sm font-semibold tracking-tight">
AptivaAI
</div>
<h2 className="text-xl md:text-2xl font-semibold">
Welcome{user?.firstname ? `, ${user.firstname}` : ''}!
</h2> </h2>
<p className="mb-4"> <p className="text-xs md:text-sm text-gray-600">
At AptivaAI, we aim to arm you with as much information as possible to make informed career decisions. Todays workplace is changing faster than ever, driven largely by AIbut our goal is to use that same technology to empower job seekers, not replace them. Career guidance powered by data enhanced by AI
We blend data-backed insights with human-centered design, enhanced -not driven by- AI. Giving you practical recommendations and real-world context so you stay in the drivers seat of your career. Whether youre planning your first step, advancing your current role, or ready to pivot entirely, our platform keeps you in controlhelping you adapt, grow, and thrive on your own terms.
</p> </p>
<ul className="list-disc ml-6 mb-4"> </div>
<li><strong>Planning:</strong> Just starting out? Looking for a different career that is a better fit? Explore options and figure out what careers match your interests and skills.</li> {/* Intro copy */}
<li><strong>Preparing:</strong> Know what you want but just not how to get there? Gain education, skills, or certifications required to start or transition.</li> <p className="mb-4 text-sm md:text-base leading-relaxed md:leading-7">
<li><strong>Enhancing:</strong> You've got some experience in your field but want to know how to get to the next level? Advance, seek promotions, or shift roles for an established professional.</li> At AptivaAI, we aim to arm you with as much information as possible to
<li><strong>Retirement:</strong> On your happy path and want to make sure you're financially ready when the time comes? Prepare financially and strategically for retirement.</li> make informed career decisions. Todays workplace is changing faster
</ul> than ever, driven largely by AIbut our goal is to use that same
<p className="mb-4"> technology to empower job seekers, not replace them.
Where would you like to go next? <br />
<br />
We blend data-backed insights with human-centered design, enhancednot
driven byAI. Giving you practical recommendations and real-world
context so you stay in the drivers seat of your career. Whether youre
planning your first step, advancing your current role, or ready to pivot
entirely, our platform keeps you in controlhelping you adapt, grow, and
thrive on your own terms.
</p>
<p className="mb-2 md:mb-3">Where would you like to go next?</p>
{/* Four clickable tiles */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
<Link
to="/planning"
className="block rounded-lg border px-4 py-3 transition-colors bg-white hover:bg-aptiva hover:text-white focus:outline-none focus:ring-2 focus:ring-aptiva"
>
<div className="text-sm font-semibold">Planning</div>
<p className="text-sm">
Just starting out or exploring a better fit? See careers that match
your interests and skills.
</p> </p>
{/* Mobile: stacked full-width; Desktop: original inline buttons with spacing */}
<div className="flex flex-col gap-2 md:block md:space-x-2">
<Link to="/planning" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-blue-600 text-white rounded hover:bg-blue-700">
Go to Exploring
</Link> </Link>
<Link to="/preparing" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-blue-600 text-white rounded hover:bg-blue-700">
Go to Preparing <Link
to="/preparing"
className="block rounded-lg border px-4 py-3 transition-colors bg-white hover:bg-aptiva hover:text-white focus:outline-none focus:ring-2 focus:ring-aptiva"
>
<div className="text-sm font-semibold">Preparing</div>
<p className="text-sm">
Know your target role but not the path? Find education, skills, or
certifications to start or transition.
</p>
</Link> </Link>
<Link to="/enhancing" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-green-600 text-white rounded hover:bg-green-700">
Go to Enhancing <Link
to="/enhancing"
className="block rounded-lg border px-4 py-3 transition-colors bg-white hover:bg-aptiva hover:text-white focus:outline-none focus:ring-2 focus:ring-aptiva"
>
<div className="text-sm font-semibold">Enhancing</div>
<p className="text-sm">
Already in-field? Chart your next steppromotions, role shifts, and
growth moves.
</p>
</Link> </Link>
<Link to="/retirement" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-green-600 text-white rounded hover:bg-green-700">
Go to Retirement <Link
to="/retirement"
className="block rounded-lg border px-4 py-3 transition-colors bg-white hover:bg-aptiva hover:text-white focus:outline-none focus:ring-2 focus:ring-aptiva"
>
<div className="text-sm font-semibold">Retirement</div>
<p className="text-sm">
On your happy path? Make sure the finances line up when the time
comes.
</p>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }