diff --git a/backend/server2.js b/backend/server2.js index bd66ead..4734679 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -392,9 +392,9 @@ app.get('/api/cip/:socCode', (req, res) => { **************************************************/ app.get('/api/schools', (req, res) => { const { cipCode, state } = req.query; - console.log('Query Params:', { cipCode, state }); + console.log('Query Params:', { cipCode }); if (!cipCode || !state) { - return res.status(400).json({ error: 'CIP Code and State are required.' }); + return res.status(400).json({ error: 'CIP Code is required' }); } try { const matchedCIP = cipCode.replace('.', '').slice(0, 4); diff --git a/src/App.js b/src/App.js index dd21f2c..c38cb52 100644 --- a/src/App.js +++ b/src/App.js @@ -19,6 +19,7 @@ import PlanningLanding from './components/PlanningLanding.js'; import CareerExplorer from './components/CareerExplorer.js'; import EducationalPrograms from './components/EducationalPrograms.js'; import PreparingLanding from './components/PreparingLanding.js'; +import EducationalProgramsPage from './components/EducationalProgramsPage.js'; import EnhancingLanding from './components/EnhancingLanding.js'; import RetirementLanding from './components/RetirementLanding.js'; import InterestInventory from './components/InterestInventory.js'; @@ -244,7 +245,7 @@ function App() { } /> } /> } /> - } /> + }/> } /> } /> } /> diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index 9fb614d..be992ea 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -404,7 +404,7 @@ function CareerExplorer({ }) { : fitRatingMap[career.fit] || masterRatings.interests || 3; const meaningRating = parseInt( - prompt("How meaningful is this career to you? (1-5):", "3"), + prompt("How important do you feel this job is to society or the world? (1-5):", "3"), 10 ); diff --git a/src/components/EducationalProgramsPage.js b/src/components/EducationalProgramsPage.js new file mode 100644 index 0000000..3b38d97 --- /dev/null +++ b/src/components/EducationalProgramsPage.js @@ -0,0 +1,284 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +// Your existing search component +import CareerSearch from './CareerSearch.js'; + +// The existing utility calls +import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js'; + +// A simple Button component (if you don’t already import from elsewhere) +function Button({ onClick, children, ...props }) { + return ( + + ); +} + +/** + * EducationalProgramsPage + * - If we have a CIP code (from location.state or otherwise), we fetch + display schools. + * - If no CIP code is provided, user sees a CareerSearch to pick a career => sets CIP code. + * - Then the user can filter & sort (tuition, distance, optional in-state only). + */ +function EducationalProgramsPage() { + // 1) Get CIP code from React Router location.state (if available) + // If no CIP code in route state, default to an empty string + const location = useLocation(); + const [cipCode, setCipCode] = useState(location.state?.cipCode || ''); + + // Optionally, you can also read userState / userZip from location.state or from user’s profile + const [userState, setUserState] = useState(location.state?.userState || ''); + const [userZip, setUserZip] = useState(location.state?.userZip || ''); + + // ============ Data + UI state ============ + const [schools, setSchools] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Filter states + const [sortBy, setSortBy] = useState('tuition'); // 'tuition' or 'distance' + const [maxTuition, setMaxTuition] = useState(99999); + const [maxDistance, setMaxDistance] = useState(99999); + // Optional “in-state only” toggle + const [inStateOnly, setInStateOnly] = useState(false); + + // ============ Handle Career Search -> CIP code ============ + const handleCareerSelected = (foundObj) => { + // foundObj = { title, soc_code, cip_code } from CareerSearch + if (foundObj?.cip_code) { + setCipCode(foundObj.cip_code); + } + }; + + // ============ Fetch + Compute Distance once we have a CIP code ============ + useEffect(() => { + // If no CIP code is set yet, do nothing. + if (!cipCode) return; + + const fetchData = async () => { + setLoading(true); + setError(null); + + try { + // 1) Fetch schools by CIP code (and userState if your API still uses it) + const fetchedSchools = await fetchSchools(cipCode, userState); + + // 2) Optionally geocode user ZIP to compute distances + let userLat = null; + let userLng = null; + if (userZip) { + try { + const geoResult = await clientGeocodeZip(userZip); + userLat = geoResult.lat; + userLng = geoResult.lng; + } catch (geoErr) { + console.warn('Unable to geocode user ZIP:', geoErr.message); + } + } + + // 3) Compute distance for each school (if lat/lng is available) + const schoolsWithDistance = fetchedSchools.map((sch) => { + const lat2 = sch.LATITUDE ? parseFloat(sch.LATITUDE) : null; + const lon2 = sch.LONGITUD ? parseFloat(sch.LONGITUD) : null; + + if (userLat && userLng && lat2 && lon2) { + const distMiles = haversineDistance(userLat, userLng, lat2, lon2); + return { ...sch, distance: distMiles.toFixed(1) }; + } else { + return { ...sch, distance: null }; + } + }); + + setSchools(schoolsWithDistance); + } catch (err) { + console.error('Error fetching/processing schools:', err); + setError('Failed to load schools.'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [cipCode, userState, userZip]); + + // ============ Filter + Sort ============ + const filteredAndSortedSchools = useMemo(() => { + if (!schools) return []; + let result = [...schools]; + + // 1) (Optional) In-state only + if (inStateOnly && userState) { + result = result.filter((sch) => sch.STABBR === userState); + } + + // 2) Filter by max tuition + // We’ll use “In_state cost” if your data references that, or you can adapt. + result = result.filter((sch) => { + const inStateCost = sch['In_state cost'] + ? parseFloat(sch['In_state cost']) + : 999999; + return inStateCost <= maxTuition; + }); + + // 3) Filter by max distance + result = result.filter((sch) => { + if (sch.distance === null) { + // If distance is unknown, decide if you want to include or exclude it + return true; // let’s include unknown + } + return parseFloat(sch.distance) <= maxDistance; + }); + + // 4) Sort + if (sortBy === 'distance') { + result.sort((a, b) => { + const distA = a.distance !== null ? parseFloat(a.distance) : Infinity; + const distB = b.distance !== null ? parseFloat(b.distance) : Infinity; + return distA - distB; + }); + } else { + // sort by tuition + result.sort((a, b) => { + const tA = a['In_state cost'] ? parseFloat(a['In_state cost']) : Infinity; + const tB = b['In_state cost'] ? parseFloat(b['In_state cost']) : Infinity; + return tA - tB; + }); + } + + return result; + }, [schools, sortBy, maxTuition, maxDistance, inStateOnly, userState]); + + // ============ Render UI ============ + + // 1) If we have NO CIP code yet, show the fallback “CareerSearch” + if (!cipCode) { + return ( +
+

Educational Programs

+

+ You have not selected a career yet. Please search for one below: +

+ +

+ After you pick a career, we’ll display matching educational programs. +

+
+ ); + } + + // 2) If we DO have a CIP code, show the filterable school list + if (loading) { + return
Loading schools...
; + } + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + return ( +
+

+ Schools Offering Programs for CIP: {cipCode} +

+ + {/* Filter Bar */} +
+ {/* Sort */} + + + {/* Tuition */} + + + {/* Distance */} + + + {/* Optional: In-State Only Toggle */} + {userState && ( + + )} +
+ + {/* School List */} + {filteredAndSortedSchools.length > 0 ? ( +
+ {filteredAndSortedSchools.map((school, idx) => ( +
+ {school['INSTNM'] || 'Unnamed School'} +

Degree Type: {school['CREDDESC'] || 'N/A'}

+

In-State Tuition: ${school['In_state cost'] || 'N/A'}

+

Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}

+

+ Distance:{' '} + {school.distance !== null ? `${school.distance} mi` : 'N/A'} +

+

+ Website:{' '} + {school['Website'] ? ( + + {school['Website']} + + ) : ( + 'N/A' + )} +

+
+ ))} +
+ ) : ( +

+ No schools matching your filters. +

+ )} +
+ ); +} + +export default EducationalProgramsPage; diff --git a/src/components/SignUp.js b/src/components/SignUp.js index a6f81e4..50ad145 100644 --- a/src/components/SignUp.js +++ b/src/components/SignUp.js @@ -252,7 +252,7 @@ function SignUp() { ) : ( <> -

Choose Your Career Stage

+

Where are you in your career journey right now?

{careerSituations.map((situation) => ( { const token = localStorage.getItem('token'); if (!token) { @@ -48,10 +49,6 @@ function UserProfile() { const res = await authFetch('/api/user-profile', { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, }); if (!res || !res.ok) return; @@ -70,6 +67,7 @@ function UserProfile() { setIsPremiumUser(true); } + // If we have a state, load its areas if (data.state) { setLoadingAreas(true); try { @@ -92,8 +90,10 @@ function UserProfile() { }; fetchProfileAndAreas(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // only runs once + // Whenever user changes "selectedState", re-fetch areas useEffect(() => { const fetchAreasByState = async () => { if (!selectedState) { @@ -144,9 +144,6 @@ function UserProfile() { try { const response = await authFetch('/api/user-profile', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, body: JSON.stringify(profileData), }); @@ -159,59 +156,45 @@ function UserProfile() { } }; - // FULL list of states, including all 50 states (+ DC if desired) + // FULL list of states for your dropdown const states = [ - { name: 'Alabama', code: 'AL' }, - { name: 'Alaska', code: 'AK' }, - { name: 'Arizona', code: 'AZ' }, - { name: 'Arkansas', code: 'AR' }, - { name: 'California', code: 'CA' }, - { name: 'Colorado', code: 'CO' }, - { name: 'Connecticut', code: 'CT' }, - { name: 'Delaware', code: 'DE' }, - { name: 'District of Columbia', code: 'DC' }, - { name: 'Florida', code: 'FL' }, - { name: 'Georgia', code: 'GA' }, - { name: 'Hawaii', code: 'HI' }, - { name: 'Idaho', code: 'ID' }, - { name: 'Illinois', code: 'IL' }, - { name: 'Indiana', code: 'IN' }, - { name: 'Iowa', code: 'IA' }, - { name: 'Kansas', code: 'KS' }, - { name: 'Kentucky', code: 'KY' }, - { name: 'Louisiana', code: 'LA' }, - { name: 'Maine', code: 'ME' }, - { name: 'Maryland', code: 'MD' }, - { name: 'Massachusetts', code: 'MA' }, - { name: 'Michigan', code: 'MI' }, - { name: 'Minnesota', code: 'MN' }, - { name: 'Mississippi', code: 'MS' }, - { name: 'Missouri', code: 'MO' }, - { name: 'Montana', code: 'MT' }, - { name: 'Nebraska', code: 'NE' }, - { name: 'Nevada', code: 'NV' }, - { name: 'New Hampshire', code: 'NH' }, - { name: 'New Jersey', code: 'NJ' }, - { name: 'New Mexico', code: 'NM' }, - { name: 'New York', code: 'NY' }, - { name: 'North Carolina', code: 'NC' }, - { name: 'North Dakota', code: 'ND' }, - { name: 'Ohio', code: 'OH' }, - { name: 'Oklahoma', code: 'OK' }, - { name: 'Oregon', code: 'OR' }, - { name: 'Pennsylvania', code: 'PA' }, - { name: 'Rhode Island', code: 'RI' }, - { name: 'South Carolina', code: 'SC' }, - { name: 'South Dakota', code: 'SD' }, - { name: 'Tennessee', code: 'TN' }, - { name: 'Texas', code: 'TX' }, - { name: 'Utah', code: 'UT' }, - { name: 'Vermont', code: 'VT' }, - { name: 'Virginia', code: 'VA' }, - { name: 'Washington', code: 'WA' }, - { name: 'West Virginia', code: 'WV' }, - { name: 'Wisconsin', code: 'WI' }, - { name: 'Wyoming', code: 'WY' }, + { name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' }, + { name: 'Arkansas', code: 'AR' }, { name: 'California', code: 'CA' }, { name: 'Colorado', code: 'CO' }, + { name: 'Connecticut', code: 'CT' }, { name: 'Delaware', code: 'DE' }, { name: 'District of Columbia', code: 'DC' }, + { name: 'Florida', code: 'FL' }, { name: 'Georgia', code: 'GA' }, { name: 'Hawaii', code: 'HI' }, + { name: 'Idaho', code: 'ID' }, { name: 'Illinois', code: 'IL' }, { name: 'Indiana', code: 'IN' }, + { name: 'Iowa', code: 'IA' }, { name: 'Kansas', code: 'KS' }, { name: 'Kentucky', code: 'KY' }, + { name: 'Louisiana', code: 'LA' }, { name: 'Maine', code: 'ME' }, { name: 'Maryland', code: 'MD' }, + { name: 'Massachusetts', code: 'MA' }, { name: 'Michigan', code: 'MI' }, { name: 'Minnesota', code: 'MN' }, + { name: 'Mississippi', code: 'MS' }, { name: 'Missouri', code: 'MO' }, { name: 'Montana', code: 'MT' }, + { name: 'Nebraska', code: 'NE' }, { name: 'Nevada', code: 'NV' }, { name: 'New Hampshire', code: 'NH' }, + { name: 'New Jersey', code: 'NJ' }, { name: 'New Mexico', code: 'NM' }, { name: 'New York', code: 'NY' }, + { name: 'North Carolina', code: 'NC' }, { name: 'North Dakota', code: 'ND' }, { name: 'Ohio', code: 'OH' }, + { name: 'Oklahoma', code: 'OK' }, { name: 'Oregon', code: 'OR' }, { name: 'Pennsylvania', code: 'PA' }, + { name: 'Rhode Island', code: 'RI' }, { name: 'South Carolina', code: 'SC' }, { name: 'South Dakota', code: 'SD' }, + { name: 'Tennessee', code: 'TN' }, { name: 'Texas', code: 'TX' }, { name: 'Utah', code: 'UT' }, + { name: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' }, + { name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' }, + ]; + + // The updated career situations (same as in SignUp.js) + const careerSituations = [ + { + id: 'planning', + title: 'Planning Your Career', + }, + { + id: 'preparing', + title: 'Preparing for Your (Next) Career', + }, + { + id: 'enhancing', + title: 'Enhancing Your Career', + }, + { + id: 'retirement', + title: 'Retirement Planning', + }, ]; return ( @@ -296,54 +279,52 @@ function UserProfile() {
- {loadingAreas &&

Loading areas...

} - - {/* Areas Dropdown */} - {loadingAreas ? ( + {/* Loading indicator for areas */} + {loadingAreas && (

Loading areas...

- ) : ( - areas.length > 0 && ( -
- - -
- ) )} - {/* Premium-Only Field */} - {isPremiumUser && ( + {/* Areas Dropdown */} + {!loadingAreas && areas.length > 0 && (
)} + {/* Career Situation */} +
+ + +
+ {/* Form Buttons */}