import React, { useEffect, useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import CareerSearch from './CareerSearch.js'; import { ONET_DEFINITIONS } from './definitions.js'; import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js'; // Helper to combine IM and LV for each KSA function combineIMandLV(rows) { const map = new Map(); for (const row of rows) { const key = `${row.onetSocCode}::${row.elementName}::${row.ksa_type}`; if (!map.has(key)) { map.set(key, { onetSocCode: row.onetSocCode, elementName: row.elementName, ksa_type: row.ksa_type, importanceValue: null, levelValue: null, }); } const entry = map.get(key); if (row.scaleID === 'IM') { entry.importanceValue = row.dataValue; } else if (row.scaleID === 'LV') { entry.levelValue = row.dataValue; } map.set(key, entry); } return Array.from(map.values()); } function ensureHttp(urlString) { if (!urlString) return ''; // If it already starts with 'http://' or 'https://', just return as-is. if (/^https?:\/\//i.test(urlString)) { return urlString; } // Otherwise prepend 'https://' (or 'http://'). return `https://${urlString}`; } // Convert numeric importance (1–5) to star or emoji compact representation function renderImportance(val) { const max = 5; const rounded = Math.round(val); const stars = '★'.repeat(rounded) + '☆'.repeat(max - rounded); return `${stars}`; } // Convert numeric level (0–7) to bar or block representation function renderLevel(val) { const max = 7; const rounded = Math.round(val); const filled = '■'.repeat(rounded); const empty = '□'.repeat(max - rounded); return `${filled}${empty}`; } function EducationalProgramsPage() { const location = useLocation(); const navigate = useNavigate(); const [socCode, setsocCode] = useState(location.state?.socCode || ''); const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []); const [userState, setUserState] = useState(location.state?.userState || ''); const [userZip, setUserZip] = useState(location.state?.userZip || ''); const [allKsaData, setAllKsaData] = useState([]); const [ksaForCareer, setKsaForCareer] = useState([]); const [loadingKsa, setLoadingKsa] = useState(false); const [ksaError, setKsaError] = useState(null); const [schools, setSchools] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Additional filters const [sortBy, setSortBy] = useState('tuition'); const [maxTuition, setMaxTuition] = useState(20000); const [maxDistance, setMaxDistance] = useState(100); const [inStateOnly, setInStateOnly] = useState(false); const [careerTitle, setCareerTitle] = useState(location.state?.careerTitle || ''); // If user picks a new career from CareerSearch const handleCareerSelected = (foundObj) => { setCareerTitle(foundObj.title || ''); let rawCips = Array.isArray(foundObj.cip_code) ? foundObj.cip_code : [foundObj.cip_code]; const cleanedCips = rawCips.map((code) => { const codeStr = code.toString(); return codeStr.replace('.', '').slice(0, 4); }); setCipCodes(cleanedCips); setsocCode(foundObj.soc_code); }; // Fixed handleSelectSchool (removed extra brace) const handleSelectSchool = (school) => { const proceed = window.confirm( 'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?' ); if (proceed) { navigate('/milestone-tracker', { state: { selectedSchool: school } }); }; }; function getSearchLinks(ksaName, careerTitle) { const combinedQuery = `${careerTitle} ${ksaName}`.trim(); const encoded = encodeURIComponent(combinedQuery); const courseraUrl = `https://www.coursera.org/search?query=${encoded}`; const edxUrl = `https://www.edx.org/search?q=${encoded}`; return [ { title: 'Coursera', url: courseraUrl }, { title: 'edX', url: edxUrl }, ]; } // Load KSA data once useEffect(() => { async function loadKsaData() { setLoadingKsa(true); setKsaError(null); try { const resp = await fetch('/ksa_data.json'); if (!resp.ok) { throw new Error('Failed to fetch ksa_data.json'); } let data = await resp.json(); // skip possible header row data = data.filter((item) => item.onetSocCode !== 'O*NET-SOC Code'); setAllKsaData(data); } catch (err) { console.error('Error loading ksa_data.json:', err); setKsaError('Could not load KSA data'); } finally { setLoadingKsa(false); } } loadKsaData(); }, []); // Filter: only IM >=3, then combine IM+LV useEffect(() => { if (!socCode || !allKsaData.length) { setKsaForCareer([]); return; } let filtered = allKsaData.filter((r) => r.onetSocCode === socCode); filtered = filtered.filter((r) => r.recommendSuppress !== 'Y'); filtered = filtered.filter((r) => ['IM', 'LV'].includes(r.scaleID)); let combined = combineIMandLV(filtered); combined = combined.filter((item) => { return item.importanceValue !== null && item.importanceValue >= 3; }); combined.sort((a, b) => (b.importanceValue || 0) - (a.importanceValue || 0)); setKsaForCareer(combined); }, [socCode, allKsaData]); // Load user profile useEffect(() => { async function loadUserProfile() { try { const token = localStorage.getItem('token'); if (!token) { console.warn('No token found, cannot load user-profile.'); return; } const res = await fetch('/api/user-profile', { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { throw new Error('Failed to fetch user profile'); } const data = await res.json(); setUserZip(data.zipcode || ''); setUserState(data.state || ''); } catch (err) { console.error('Error loading user profile:', err); } } loadUserProfile(); }, []); // Fetch schools once CIP codes are set useEffect(() => { if (!cipCodes.length) return; const fetchData = async () => { setLoading(true); setError(null); try { const fetchedSchools = await fetchSchools(cipCodes); 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); } } 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) }; } return { ...sch, distance: null }; }); setSchools(schoolsWithDistance); } catch (err) { console.error('[EducationalProgramsPage] error:', err); setError('Failed to load schools.'); } finally { setLoading(false); } }; fetchData(); }, [cipCodes, userState, userZip]); // Sort schools in useMemo const filteredAndSortedSchools = useMemo(() => { if (!schools) return []; let result = [...schools]; // In-state if (inStateOnly && userState) { const userAbbr = userState.trim().toUpperCase(); result = result.filter((sch) => { const schoolAbbr = sch.State ? sch.State.trim().toUpperCase() : ''; return schoolAbbr === userAbbr; }); } // Max tuition result = result.filter((sch) => { const cost = sch['In_state cost'] ? parseFloat(sch['In_state cost']) : 999999; return cost <= maxTuition; }); // Max distance result = result.filter((sch) => { if (sch.distance === null) return true; return parseFloat(sch.distance) <= maxDistance; }); // Sort if (sortBy === 'distance') { result.sort((a, b) => { const distA = a.distance ? parseFloat(a.distance) : Infinity; const distB = b.distance ? parseFloat(b.distance) : Infinity; return distA - distB; }); } else { // Sort by in-state 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, inStateOnly, userState, maxTuition, maxDistance, sortBy]); // Render a single KSA row function renderKsaRow(k, idx, careerTitle) { const elementName = k.elementName; const impStars = renderImportance(k.importanceValue); const lvlBars = k.levelValue !== null ? renderLevel(k.levelValue) : 'n/a'; const isAbility = k.ksa_type === 'Ability'; const links = !isAbility ? getSearchLinks(elementName, careerTitle) : null; const definition = ONET_DEFINITIONS[elementName] || 'No definition available'; return (
Loading KSA data...
; if (ksaError) return{ksaError}
; if (!socCode) returnPlease select a career to see KSA data.
; if (!ksaForCareer.length) { returnNo Knowledge, Skills, and Abilities data found for {careerTitle}
; } const knowledge = ksaForCareer.filter((k) => k.ksa_type === 'Knowledge'); const skillRows = ksaForCareer.filter((k) => k.ksa_type === 'Skill'); const abilities = ksaForCareer.filter((k) => k.ksa_type === 'Ability'); return (Knowledge Domain | Importance | Level | Courses |
---|
No knowledge entries.
)}Skill | Importance | Level | Courses |
---|
No skill entries.
)}Ability | Importance | Level | {/* Info icon for "Why no courses?" */} Why no courses? i |
---|
No ability entries.
)}You have not selected a career yet. Please search for one below:
After you pick a career, we’ll display matching educational programs.
Error: {error}
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'}