Able to pass selected career from Explorer to EducationalProgramsPage.

This commit is contained in:
Josh 2025-05-16 17:00:15 +00:00
parent 46553c002c
commit 36375775c2
2 changed files with 437 additions and 392 deletions

View File

@ -17,7 +17,6 @@ import SignIn from './components/SignIn.js';
import SignUp from './components/SignUp.js';
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';
@ -126,6 +125,11 @@ function App() {
Career Explorer
</Link>
</li>
<li>
<Link className="text-blue-600 hover:text-blue-800" to="/educational-programs">
Skills/Educational Planner
</Link>
</li>
<li>
<Link className="text-blue-600 hover:text-blue-800" to="/interest-inventory">
Interest Inventory

View File

@ -1,73 +1,57 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import CareerSuggestions from './CareerSuggestions.js';
import CareerPrioritiesModal from './CareerPrioritiesModal.js';
import CareerModal from './CareerModal.js';
import CareerSearch from './CareerSearch.js';
import {Button} from './ui/button.js';
import { Button } from './ui/button.js';
import axios from 'axios';
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' },
];
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' },
];
// -------------- CIP HELPER FUNCTIONS --------------
// 1) Insert leading zero if there's only 1 digit before the decimal
function ensureTwoDigitsBeforeDecimal(cipStr) {
// e.g. "4.0201" => "04.0201"
return cipStr.replace(/^(\d)\./, '0$1.');
}
// 2) Clean an array of CIP codes, e.g. ["4.0201", "14.0901"] => ["0402", "1409"]
function cleanCipCodes(cipArray) {
return cipArray.map((code) => {
let codeStr = code.toString();
codeStr = ensureTwoDigitsBeforeDecimal(codeStr); // ensure "04.0201"
return codeStr.replace('.', '').slice(0, 4); // => "040201" => "0402"
});
}
function getFullStateName(code) {
const found = STATES.find((s) => s.code === code?.toUpperCase());
return found ? found.name : '';
}
function CareerExplorer({ }) {
function CareerExplorer() {
const navigate = useNavigate();
const location = useLocation();
const apiUrl = process.env.REACT_APP_API_URL || '';
@ -105,242 +89,243 @@ function CareerExplorer({ }) {
Good: 'Good - Less Strong Match',
};
// ===================== Load user profile =====================
useEffect(() => {
setLoading(true);
const fetchUserProfile = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.get(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 200) {
const profileData = res.data;
console.log('[fetchUserProfile] loaded profileData =>', profileData);
const fetchUserProfile = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.get(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
// 1) Set userProfile and all relevant states using `profileData`:
setUserProfile(profileData);
setUserState(profileData.state);
setAreaTitle(profileData.area);
setUserZipcode(profileData.zipcode);
if (res.status === 200) {
const profileData = res.data;
console.log('[fetchUserProfile] loaded profileData =>', profileData);
// 2) Load saved career list if it exists
if (profileData.career_list) {
setCareerList(JSON.parse(profileData.career_list));
}
setUserProfile(profileData);
setUserState(profileData.state);
setAreaTitle(profileData.area);
setUserZipcode(profileData.zipcode);
// 3) If user has interest inventory answers, fetch suggestions
if (profileData.interest_inventory_answers) {
const answers = profileData.interest_inventory_answers;
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
answers,
state: profileData.state,
area: profileData.area,
});
// If they have a saved career list
if (profileData.career_list) {
setCareerList(JSON.parse(profileData.career_list));
}
const { careers = [] } = careerSuggestionsRes.data || {};
setCareerSuggestions(careers.flat());
// If they have interest inventory, fetch suggestions
if (profileData.interest_inventory_answers) {
const answers = profileData.interest_inventory_answers;
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
answers,
state: profileData.state,
area: profileData.area,
});
const { careers = [] } = careerSuggestionsRes.data || {};
setCareerSuggestions(careers.flat());
} else {
setCareerSuggestions([]);
}
// Check if priorities answered
const priorities = profileData.career_priorities
? JSON.parse(profileData.career_priorities)
: {};
const allAnswered = ['interests','meaning','stability','growth','balance','recognition']
.every((key) => priorities[key]);
if (!allAnswered) {
setShowModal(true);
}
} else {
// No inventory => no suggestions (or do something else here)
setCareerSuggestions([]);
}
// 4) Check if all priorities are answered
const priorities = profileData.career_priorities
? JSON.parse(profileData.career_priorities)
: {};
const allAnswered = ['interests','meaning','stability','growth','balance','recognition']
.every((key) => priorities[key]);
if (!allAnswered) {
// If user hasn't answered them all, show the priorities modal
setShowModal(true);
}
} else {
// Not a 200 response => fallback
} catch (err) {
console.error('Error fetching user profile:', err);
setShowModal(true);
setLoading(false);
}
} catch (err) {
console.error('Error fetching user profile:', err);
setShowModal(true); // fallback if error
setLoading(false);
}
};
};
fetchUserProfile();
}, [apiUrl]);
fetchUserProfile();
}, [apiUrl]);
// ===================== If location.state has careerSuggestions =====================
useEffect(() => {
if (location.state?.careerSuggestions) {
setCareerSuggestions(location.state.careerSuggestions);
}
}, [location.state]);
// Fetch Job Zones if suggestions are provided
// ===================== Fetch job zones for suggestions =====================
useEffect(() => {
const fetchJobZones = async () => {
if (!careerSuggestions.length) return;
const fetchJobZones = async () => {
if (!careerSuggestions.length) return;
const flatSuggestions = careerSuggestions.flat();
const socCodes = flatSuggestions.map((career) => career.code);
const flatSuggestions = careerSuggestions.flat();
const socCodes = flatSuggestions.map((career) => career.code);
try {
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
const jobZoneData = response.data;
const updatedCareers = flatSuggestions.map((career) => ({
...career,
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
}));
try {
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
const jobZoneData = response.data;
const updatedCareers = flatSuggestions.map((career) => ({
...career,
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
}));
// IMPORTANT: Ensure this actually sets a new array
setCareersWithJobZone([...updatedCareers]);
} catch (error) {
console.error('Error fetching job zone information:', error);
}
};
setCareersWithJobZone([...updatedCareers]);
} catch (error) {
console.error('Error fetching job zone information:', error);
}
};
fetchJobZones();
}, [careerSuggestions, apiUrl]);
fetchJobZones();
}, [careerSuggestions, apiUrl]);
const handleCareerClick = useCallback(
async (career) => {
console.log('[handleCareerClick] career =>', career);
const socCode = career.code;
setSelectedCareer(career);
// ===================== handleCareerClick (detail fetch) =====================
const handleCareerClick = useCallback(
async (career) => {
console.log('[handleCareerClick] career =>', career);
const socCode = career.code;
setSelectedCareer(career);
setError(null);
setCareerDetails(null);
setSalaryData([]);
setEconomicProjections({});
// We can set selectedCareer immediately so that our Modal condition is met.
setSelectedCareer(career);
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
setSelectedCareer(career);
try {
// CIP fetch
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
if (!cipResponse.ok) {setError(`We're sorry, but specific details for "${career.title}" are not available at this time.`);
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
setLoading(false);
return;
}
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// Job details
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
if (!jobDetailsResponse.ok){setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
setLoading(false);
return;
}
const { description, tasks } = await jobDetailsResponse.json();
// Salary
let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { data: {} };
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
// Build salary array
const sData = salaryResponse.data || {};
const salaryDataPoints = sData && Object.keys(sData).length > 0
? [
{
percentile: '10th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
},
{
percentile: '25th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
},
{
percentile: 'Median',
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0,
},
{
percentile: '75th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0,
},
{
percentile: '90th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
},
]
: [];
// Economic
const fullStateName = getFullStateName(userState);
let economicResponse = { data: {} };
try {
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
params: { state: fullStateName },
});
} catch (error) {
economicResponse = { data: {} };
}
// CIP fetch
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
if (!cipResponse.ok) {
setError(
`We're sorry, but specific details for "${career.title}" are not available at this time.`
);
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
setLoading(false);
return;
}
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// Build final details
const updatedCareerDetails = {
...career,
jobDescription: description,
tasks,
salaryData: salaryDataPoints,
economicProjections: economicResponse.data || {},
};
// Job details
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
if (!jobDetailsResponse.ok){
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
setLoading(false);
return;
}
const { description, tasks } = await jobDetailsResponse.json();
// Now set the fully fetched data
setCareerDetails(updatedCareerDetails);
// Salary
let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { data: {} };
}
} catch (error) {
console.error('Error processing career click:', error.message);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`
});
} finally {
setLoading(false);
}
},
[userState, apiUrl, areaTitle, userZipcode]
);
// ============= Let typed careers open PopoutPanel =============
const handleCareerFromSearch = useCallback(
(obj) => {
const adapted = {
code: obj.soc_code,
title: obj.title,
cipCode: obj.cip_code,
// Build salary array
const sData = salaryResponse.data || {};
const salaryDataPoints =
sData && Object.keys(sData).length > 0
? [
{
percentile: '10th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
},
{
percentile: '25th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
},
{
percentile: 'Median',
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0,
},
{
percentile: '75th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0,
},
{
percentile: '90th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
},
]
: [];
// Economic
const fullStateName = getFullStateName(userState);
let economicResponse = { data: {} };
try {
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
params: { state: fullStateName },
});
} catch (error) {
economicResponse = { data: {} };
}
// Build final details
const updatedCareerDetails = {
...career,
jobDescription: description,
tasks,
salaryData: salaryDataPoints,
economicProjections: economicResponse.data || {},
};
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
handleCareerClick(adapted);
},
[handleCareerClick]
);
useEffect(() => {
if (pendingCareerForModal) {
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
handleCareerFromSearch(pendingCareerForModal);
setPendingCareerForModal(null);
}
}, [pendingCareerForModal, handleCareerFromSearch]);
setCareerDetails(updatedCareerDetails);
} catch (error) {
console.error('Error processing career click:', error.message);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
} finally {
setLoading(false);
}
},
[userState, apiUrl, areaTitle, userZipcode]
);
// ===================== handleCareerFromSearch =====================
const handleCareerFromSearch = useCallback(
(obj) => {
const adapted = {
code: obj.soc_code,
title: obj.title,
cipCode: obj.cip_code,
};
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
handleCareerClick(adapted);
},
[handleCareerClick]
);
useEffect(() => {
if (pendingCareerForModal) {
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
handleCareerFromSearch(pendingCareerForModal);
setPendingCareerForModal(null);
}
}, [pendingCareerForModal, handleCareerFromSearch]);
// ===================== Load careers_with_ratings for CIP arrays =====================
useEffect(() => {
fetch('/careers_with_ratings.json')
.then((res) => {
@ -350,7 +335,7 @@ function CareerExplorer({ }) {
.then((data) => setMasterCareerRatings(data))
.catch((err) => console.error('Error fetching career ratings:', err));
}, []);
const priorities = useMemo(() => {
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
}, [userProfile]);
@ -358,103 +343,129 @@ function CareerExplorer({ }) {
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
const getCareerRatingsBySocCode = (socCode) => {
return masterCareerRatings.find(c => c.soc_code === socCode)?.ratings || {};
return masterCareerRatings.find((c) => c.soc_code === socCode)?.ratings || {};
};
// ===================== Save comparison list to backend =====================
const saveCareerListToBackend = async (newCareerList) => {
try {
const token = localStorage.getItem('token');
await axios.post(`${apiUrl}/user-profile`, {
firstName: userProfile?.firstname,
lastName: userProfile?.lastname,
email: userProfile?.email,
zipCode: userProfile?.zipcode,
state: userProfile?.state,
area: userProfile?.area,
careerSituation: userProfile?.career_situation,
// For the rest, ensure we're not overwriting if not needed
interest_inventory_answers: userProfile?.interest_inventory_answers,
career_priorities: userProfile?.career_priorities,
// IMPORTANT: We convert the newCareerList to JSON if your DB expects text
career_list: JSON.stringify(newCareerList),
}, {
headers: { Authorization: `Bearer ${token}` },
});
} catch (err) {
console.error('Error saving career_list:', err);
// optional: show a user-friendly error
}
};
const addCareerToList = (career) => {
const masterRatings = getCareerRatingsBySocCode(career.code);
const fitRatingMap = {
Best: 5,
Great: 4,
Good: 3,
};
const interestsRating =
priorities.interests === "Im not sure yet"
? parseInt(prompt("Rate your interest in this career (1-5):", "3"), 10)
: fitRatingMap[career.fit] || masterRatings.interests || 3;
const meaningRating = parseInt(
prompt("How important do you feel this job is to society or the world? (1-5):", "3"),
10
);
const stabilityRating =
career.ratings && career.ratings.stability !== undefined
? career.ratings.stability
: masterRatings.stability || 3;
const growthRating = masterRatings.growth || 3;
const balanceRating = masterRatings.balance || 3;
const recognitionRating = masterRatings.recognition || 3;
const careerWithUserRatings = {
...career,
ratings: {
interests: interestsRating,
meaning: meaningRating,
stability: stabilityRating,
growth: growthRating,
balance: balanceRating,
recognition: recognitionRating,
},
};
setCareerList((prevList) => {
if (prevList.some((c) => c.code === career.code)) {
alert("Career already in comparison list.");
return prevList;
try {
const token = localStorage.getItem('token');
await axios.post(
`${apiUrl}/user-profile`,
{
firstName: userProfile?.firstname,
lastName: userProfile?.lastname,
email: userProfile?.email,
zipCode: userProfile?.zipcode,
state: userProfile?.state,
area: userProfile?.area,
careerSituation: userProfile?.career_situation,
interest_inventory_answers: userProfile?.interest_inventory_answers,
career_priorities: userProfile?.career_priorities,
career_list: JSON.stringify(newCareerList),
},
{
headers: { Authorization: `Bearer ${token}` },
}
);
} catch (err) {
console.error('Error saving career_list:', err);
}
};
const newList = [...prevList, careerWithUserRatings];
// Call the API to save
saveCareerListToBackend(newList);
return newList;
});
};
// ===================== Add/Remove from comparison =====================
const addCareerToList = (career) => {
const masterRatings = getCareerRatingsBySocCode(career.code);
const fitRatingMap = {
Best: 5,
Great: 4,
Good: 3,
};
const interestsRating =
priorities.interests === "Im not sure yet"
? parseInt(prompt("Rate your interest in this career (1-5):", "3"), 10)
: fitRatingMap[career.fit] || masterRatings.interests || 3;
const meaningRating = parseInt(
prompt("How important do you feel this job is to society or the world? (1-5):", "3"),
10
);
const stabilityRating =
career.ratings && career.ratings.stability !== undefined
? career.ratings.stability
: masterRatings.stability || 3;
const growthRating = masterRatings.growth || 3;
const balanceRating = masterRatings.balance || 3;
const recognitionRating = masterRatings.recognition || 3;
const careerWithUserRatings = {
...career,
ratings: {
interests: interestsRating,
meaning: meaningRating,
stability: stabilityRating,
growth: growthRating,
balance: balanceRating,
recognition: recognitionRating,
},
};
setCareerList((prevList) => {
if (prevList.some((c) => c.code === career.code)) {
alert("Career already in comparison list.");
return prevList;
}
const newList = [...prevList, careerWithUserRatings];
saveCareerListToBackend(newList);
return newList;
});
};
const removeCareerFromList = (careerCode) => {
setCareerList((prevList) => {
const newList = prevList.filter((c) => c.code !== careerCode);
// Call the API to save
saveCareerListToBackend(newList);
return newList;
});
};
setCareerList((prevList) => {
const newList = prevList.filter((c) => c.code !== careerCode);
saveCareerListToBackend(newList);
return newList;
});
};
// ===================== Let user pick a career from comparison => "Select for Education" =====================
const handleSelectForEducation = (career) => {
// 1) Confirm
const confirmed = window.confirm(
`Are you sure you want to move on to Educational Programs for ${career.title}?`
);
if (!confirmed) return;
// Filtering logic (Job Zone and Fit)
// 2) Look up CIP codes from masterCareerRatings by SOC code
const matching = masterCareerRatings.find((r) => r.soc_code === career.code);
if (!matching) {
alert(`No CIP codes found for ${career.title}.`);
return;
}
// 3) Clean CIP codes
const rawCips = matching.cip_codes || [];
const cleanedCips = cleanCipCodes(rawCips); // from top-level function
console.log('cleanedCips =>', cleanedCips);
// 4) Navigate
navigate('/educational-programs', {
state: {
cipCodes: cleanedCips,
careerTitle: career.title,
userZip: userZipcode,
userState: userState,
},
});
};
// ===================== Filter logic for jobZone, Fit =====================
const filteredCareers = useMemo(() => {
return careersWithJobZone.filter((career) => {
const jobZoneMatches = selectedJobZone
@ -468,6 +479,7 @@ function CareerExplorer({ }) {
});
}, [careersWithJobZone, selectedJobZone, selectedFit]);
// Weighted “match score” logic. (unchanged)
const priorityWeight = (priority, response) => {
const weightMap = {
interests: {
@ -502,28 +514,27 @@ function CareerExplorer({ }) {
};
return weightMap[priority][response] || 1;
};
const renderLoadingOverlay = () => {
if (!loading) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
<div className="rounded bg-white p-6 shadow-lg">
<div className="mb-2 w-full max-w-md rounded bg-gray-200">
<div
className="h-2 rounded bg-blue-500 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-1 text-center text-sm text-gray-600">
{progress}% Loading Career Suggestions...
</p>
</div>
</div>
);
};
if (!loading) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
<div className="rounded bg-white p-6 shadow-lg">
<div className="mb-2 w-full max-w-md rounded bg-gray-200">
<div
className="h-2 rounded bg-blue-500 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-1 text-center text-sm text-gray-600">
{progress}% Loading Career Suggestions...
</p>
</div>
</div>
);
};
return (
<div className="career-explorer-container bg-white p-6 rounded shadow">
{renderLoadingOverlay()}
{showModal && (
@ -533,14 +544,15 @@ function CareerExplorer({ }) {
/>
)}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Explore Careers - use the tools in this area to find your perfect career</h2>
<h2 className="text-xl font-semibold">
Explore Careers - use the tools below to find your perfect career
</h2>
<CareerSearch
onCareerSelected={(careerObj) => {
console.log('[Dashboard] onCareerSelected =>', careerObj);
// Set the "pendingCareerForModal" so our useEffect fires
setPendingCareerForModal(careerObj);
}}
/>
onCareerSelected={(careerObj) => {
console.log('[Dashboard] onCareerSelected =>', careerObj);
setPendingCareerForModal(careerObj);
}}
/>
</div>
<h2 className="text-xl font-semibold mb-4">Career Comparison</h2>
@ -550,7 +562,9 @@ function CareerExplorer({ }) {
<tr>
<th className="border p-2">Career</th>
{priorityKeys.map((priority) => (
<th key={priority} className="border p-2 capitalize">{priority}</th>
<th key={priority} className="border p-2 capitalize">
{priority}
</th>
))}
<th className="border p-2">Match</th>
<th className="border p-2">Actions</th>
@ -559,7 +573,7 @@ function CareerExplorer({ }) {
<tbody>
{careerList.map((career) => {
const ratings = career.ratings || {};
const interestsRating = ratings.interests || 3; // default to 3 if not rated via InterestInventory
const interestsRating = ratings.interests || 3;
const meaningRating = ratings.meaning || 3;
const stabilityRating = ratings.stability || 3;
const growthRating = ratings.growth || 3;
@ -601,17 +615,30 @@ function CareerExplorer({ }) {
<td className="border p-2">{balanceRating}</td>
<td className="border p-2">{recognitionRating}</td>
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
<td className="border p-2">
<Button className="bg-red-600 text-black-500" onClick={() => removeCareerFromList(career.code)}>Remove</Button>
<td className="border p-2 space-x-2">
<Button
className="bg-red-600 text-black-500"
onClick={() => removeCareerFromList(career.code)}
>
Remove
</Button>
{/* New Button -> "Select for Education" */}
<Button
className="bg-green-600 text-white"
onClick={() => handleSelectForEducation(career)}
>
Search for Education
</Button>
</td>
</tr>
);
})}
</tbody>
</table>
) : <p>No careers added to comparison.</p>}
) : (
<p>No careers added to comparison.</p>
)}
<div className="flex gap-4 mb-4">
<select
@ -621,7 +648,9 @@ function CareerExplorer({ }) {
>
<option value="">All Preparation Levels</option>
{Object.entries(jobZoneLabels).map(([zone, label]) => (
<option key={zone} value={zone}>{label}</option>
<option key={zone} value={zone}>
{label}
</option>
))}
</select>
@ -632,7 +661,9 @@ function CareerExplorer({ }) {
>
<option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
@ -649,25 +680,35 @@ function CareerExplorer({ }) {
areaTitle={areaTitle}
/>
{selectedCareer && (
<CareerModal
career={selectedCareer}
careerDetails={careerDetails}
closeModal={() => {
setSelectedCareer(null);
setCareerDetails(null);
}}
addCareerToList={addCareerToList}
/>
)}
{selectedCareer && (
<CareerModal
career={selectedCareer}
careerDetails={careerDetails}
closeModal={() => {
setSelectedCareer(null);
setCareerDetails(null);
}}
addCareerToList={addCareerToList}
/>
)}
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
Career results and details provided by
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>,
in partnership with
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer">
{' '}
O*Net
</a>
, in partnership with
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer">
{' '}
Bureau of Labor Statistics
</a>
and
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> NCES</a>.
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer">
{' '}
NCES
</a>
.
</div>
</div>
);