Able to pass selected career from Explorer to EducationalProgramsPage.
This commit is contained in:
parent
77cd3b6845
commit
3b4c44c088
@ -17,7 +17,6 @@ import SignIn from './components/SignIn.js';
|
|||||||
import SignUp from './components/SignUp.js';
|
import SignUp from './components/SignUp.js';
|
||||||
import PlanningLanding from './components/PlanningLanding.js';
|
import PlanningLanding from './components/PlanningLanding.js';
|
||||||
import CareerExplorer from './components/CareerExplorer.js';
|
import CareerExplorer from './components/CareerExplorer.js';
|
||||||
import EducationalPrograms from './components/EducationalPrograms.js';
|
|
||||||
import PreparingLanding from './components/PreparingLanding.js';
|
import PreparingLanding from './components/PreparingLanding.js';
|
||||||
import EducationalProgramsPage from './components/EducationalProgramsPage.js';
|
import EducationalProgramsPage from './components/EducationalProgramsPage.js';
|
||||||
import EnhancingLanding from './components/EnhancingLanding.js';
|
import EnhancingLanding from './components/EnhancingLanding.js';
|
||||||
@ -126,6 +125,11 @@ function App() {
|
|||||||
Career Explorer
|
Career Explorer
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link className="text-blue-600 hover:text-blue-800" to="/educational-programs">
|
||||||
|
Skills/Educational Planner
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link className="text-blue-600 hover:text-blue-800" to="/interest-inventory">
|
<Link className="text-blue-600 hover:text-blue-800" to="/interest-inventory">
|
||||||
Interest Inventory
|
Interest Inventory
|
||||||
|
@ -1,73 +1,57 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
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 CareerSuggestions from './CareerSuggestions.js';
|
||||||
import CareerPrioritiesModal from './CareerPrioritiesModal.js';
|
import CareerPrioritiesModal from './CareerPrioritiesModal.js';
|
||||||
import CareerModal from './CareerModal.js';
|
import CareerModal from './CareerModal.js';
|
||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
import {Button} from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const STATES = [
|
const STATES = [
|
||||||
{ name: 'Alabama', code: 'AL' },
|
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
||||||
{ name: 'Alaska', code: 'AK' },
|
{ name: 'Arkansas', code: 'AR' }, { name: 'California', code: 'CA' }, { name: 'Colorado', code: 'CO' },
|
||||||
{ name: 'Arizona', code: 'AZ' },
|
{ name: 'Connecticut', code: 'CT' }, { name: 'Delaware', code: 'DE' }, { name: 'District of Columbia', code: 'DC' },
|
||||||
{ name: 'Arkansas', code: 'AR' },
|
{ name: 'Florida', code: 'FL' }, { name: 'Georgia', code: 'GA' }, { name: 'Hawaii', code: 'HI' },
|
||||||
{ name: 'California', code: 'CA' },
|
{ name: 'Idaho', code: 'ID' }, { name: 'Illinois', code: 'IL' }, { name: 'Indiana', code: 'IN' },
|
||||||
{ name: 'Colorado', code: 'CO' },
|
{ name: 'Iowa', code: 'IA' }, { name: 'Kansas', code: 'KS' }, { name: 'Kentucky', code: 'KY' },
|
||||||
{ name: 'Connecticut', code: 'CT' },
|
{ name: 'Louisiana', code: 'LA' }, { name: 'Maine', code: 'ME' }, { name: 'Maryland', code: 'MD' },
|
||||||
{ name: 'Delaware', code: 'DE' },
|
{ name: 'Massachusetts', code: 'MA' }, { name: 'Michigan', code: 'MI' }, { name: 'Minnesota', code: 'MN' },
|
||||||
{ name: 'District of Columbia', code: 'DC' },
|
{ name: 'Mississippi', code: 'MS' }, { name: 'Missouri', code: 'MO' }, { name: 'Montana', code: 'MT' },
|
||||||
{ name: 'Florida', code: 'FL' },
|
{ name: 'Nebraska', code: 'NE' }, { name: 'Nevada', code: 'NV' }, { name: 'New Hampshire', code: 'NH' },
|
||||||
{ name: 'Georgia', code: 'GA' },
|
{ name: 'New Jersey', code: 'NJ' }, { name: 'New Mexico', code: 'NM' }, { name: 'New York', code: 'NY' },
|
||||||
{ name: 'Hawaii', code: 'HI' },
|
{ name: 'North Carolina', code: 'NC' }, { name: 'North Dakota', code: 'ND' }, { name: 'Ohio', code: 'OH' },
|
||||||
{ name: 'Idaho', code: 'ID' },
|
{ name: 'Oklahoma', code: 'OK' }, { name: 'Oregon', code: 'OR' }, { name: 'Pennsylvania', code: 'PA' },
|
||||||
{ name: 'Illinois', code: 'IL' },
|
{ name: 'Rhode Island', code: 'RI' }, { name: 'South Carolina', code: 'SC' }, { name: 'South Dakota', code: 'SD' },
|
||||||
{ name: 'Indiana', code: 'IN' },
|
{ name: 'Tennessee', code: 'TN' }, { name: 'Texas', code: 'TX' }, { name: 'Utah', code: 'UT' },
|
||||||
{ name: 'Iowa', code: 'IA' },
|
{ name: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' },
|
||||||
{ name: 'Kansas', code: 'KS' },
|
{ name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' },
|
||||||
{ name: 'Kentucky', code: 'KY' },
|
];
|
||||||
{ name: 'Louisiana', code: 'LA' },
|
|
||||||
{ name: 'Maine', code: 'ME' },
|
// -------------- CIP HELPER FUNCTIONS --------------
|
||||||
{ name: 'Maryland', code: 'MD' },
|
|
||||||
{ name: 'Massachusetts', code: 'MA' },
|
// 1) Insert leading zero if there's only 1 digit before the decimal
|
||||||
{ name: 'Michigan', code: 'MI' },
|
function ensureTwoDigitsBeforeDecimal(cipStr) {
|
||||||
{ name: 'Minnesota', code: 'MN' },
|
// e.g. "4.0201" => "04.0201"
|
||||||
{ name: 'Mississippi', code: 'MS' },
|
return cipStr.replace(/^(\d)\./, '0$1.');
|
||||||
{ name: 'Missouri', code: 'MO' },
|
}
|
||||||
{ name: 'Montana', code: 'MT' },
|
|
||||||
{ name: 'Nebraska', code: 'NE' },
|
// 2) Clean an array of CIP codes, e.g. ["4.0201", "14.0901"] => ["0402", "1409"]
|
||||||
{ name: 'Nevada', code: 'NV' },
|
function cleanCipCodes(cipArray) {
|
||||||
{ name: 'New Hampshire', code: 'NH' },
|
return cipArray.map((code) => {
|
||||||
{ name: 'New Jersey', code: 'NJ' },
|
let codeStr = code.toString();
|
||||||
{ name: 'New Mexico', code: 'NM' },
|
codeStr = ensureTwoDigitsBeforeDecimal(codeStr); // ensure "04.0201"
|
||||||
{ name: 'New York', code: 'NY' },
|
return codeStr.replace('.', '').slice(0, 4); // => "040201" => "0402"
|
||||||
{ 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' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function getFullStateName(code) {
|
function getFullStateName(code) {
|
||||||
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
||||||
return found ? found.name : '';
|
return found ? found.name : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function CareerExplorer({ }) {
|
function CareerExplorer() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
@ -105,242 +89,243 @@ function CareerExplorer({ }) {
|
|||||||
Good: 'Good - Less Strong Match',
|
Good: 'Good - Less Strong Match',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===================== Load user profile =====================
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
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 fetchUserProfile = async () => {
|
||||||
const profileData = res.data;
|
try {
|
||||||
console.log('[fetchUserProfile] loaded profileData =>', profileData);
|
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`:
|
if (res.status === 200) {
|
||||||
setUserProfile(profileData);
|
const profileData = res.data;
|
||||||
setUserState(profileData.state);
|
console.log('[fetchUserProfile] loaded profileData =>', profileData);
|
||||||
setAreaTitle(profileData.area);
|
|
||||||
setUserZipcode(profileData.zipcode);
|
|
||||||
|
|
||||||
// 2) Load saved career list if it exists
|
setUserProfile(profileData);
|
||||||
if (profileData.career_list) {
|
setUserState(profileData.state);
|
||||||
setCareerList(JSON.parse(profileData.career_list));
|
setAreaTitle(profileData.area);
|
||||||
}
|
setUserZipcode(profileData.zipcode);
|
||||||
|
|
||||||
// 3) If user has interest inventory answers, fetch suggestions
|
// If they have a saved career list
|
||||||
if (profileData.interest_inventory_answers) {
|
if (profileData.career_list) {
|
||||||
const answers = profileData.interest_inventory_answers;
|
setCareerList(JSON.parse(profileData.career_list));
|
||||||
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
|
}
|
||||||
answers,
|
|
||||||
state: profileData.state,
|
|
||||||
area: profileData.area,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { careers = [] } = careerSuggestionsRes.data || {};
|
// If they have interest inventory, fetch suggestions
|
||||||
setCareerSuggestions(careers.flat());
|
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 {
|
} 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);
|
setShowModal(true);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
// Not a 200 response => fallback
|
console.error('Error fetching user profile:', err);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
};
|
||||||
console.error('Error fetching user profile:', err);
|
|
||||||
setShowModal(true); // fallback if error
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
}, [apiUrl]);
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
// ===================== If location.state has careerSuggestions =====================
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state?.careerSuggestions) {
|
if (location.state?.careerSuggestions) {
|
||||||
setCareerSuggestions(location.state.careerSuggestions);
|
setCareerSuggestions(location.state.careerSuggestions);
|
||||||
}
|
}
|
||||||
}, [location.state]);
|
}, [location.state]);
|
||||||
|
|
||||||
// Fetch Job Zones if suggestions are provided
|
// ===================== Fetch job zones for suggestions =====================
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchJobZones = async () => {
|
const fetchJobZones = async () => {
|
||||||
if (!careerSuggestions.length) return;
|
if (!careerSuggestions.length) return;
|
||||||
|
|
||||||
const flatSuggestions = careerSuggestions.flat();
|
const flatSuggestions = careerSuggestions.flat();
|
||||||
const socCodes = flatSuggestions.map((career) => career.code);
|
const socCodes = flatSuggestions.map((career) => career.code);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
||||||
const jobZoneData = response.data;
|
const jobZoneData = response.data;
|
||||||
const updatedCareers = flatSuggestions.map((career) => ({
|
const updatedCareers = flatSuggestions.map((career) => ({
|
||||||
...career,
|
...career,
|
||||||
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// IMPORTANT: Ensure this actually sets a new array
|
setCareersWithJobZone([...updatedCareers]);
|
||||||
setCareersWithJobZone([...updatedCareers]);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('Error fetching job zone information:', error);
|
||||||
console.error('Error fetching job zone information:', error);
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
fetchJobZones();
|
fetchJobZones();
|
||||||
}, [careerSuggestions, apiUrl]);
|
}, [careerSuggestions, apiUrl]);
|
||||||
|
|
||||||
const handleCareerClick = useCallback(
|
// ===================== handleCareerClick (detail fetch) =====================
|
||||||
async (career) => {
|
const handleCareerClick = useCallback(
|
||||||
console.log('[handleCareerClick] career =>', career);
|
async (career) => {
|
||||||
const socCode = career.code;
|
console.log('[handleCareerClick] career =>', career);
|
||||||
setSelectedCareer(career);
|
const socCode = career.code;
|
||||||
|
setSelectedCareer(career);
|
||||||
setError(null);
|
setError(null);
|
||||||
setCareerDetails(null);
|
setCareerDetails(null);
|
||||||
setSalaryData([]);
|
setSalaryData([]);
|
||||||
setEconomicProjections({});
|
setEconomicProjections({});
|
||||||
|
|
||||||
// We can set selectedCareer immediately so that our Modal condition is met.
|
setSelectedCareer(career);
|
||||||
setSelectedCareer(career);
|
|
||||||
|
|
||||||
if (!socCode) {
|
|
||||||
console.error('SOC Code is missing');
|
|
||||||
setError('SOC Code is missing');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (!socCode) {
|
||||||
// CIP fetch
|
console.error('SOC Code is missing');
|
||||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
setError('SOC Code is missing');
|
||||||
if (!cipResponse.ok) {setError(`We're sorry, but specific details for "${career.title}" are not available at this time.`);
|
setLoading(false);
|
||||||
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
|
return;
|
||||||
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: {} };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
try {
|
||||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
// CIP fetch
|
||||||
params: { state: fullStateName },
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
});
|
if (!cipResponse.ok) {
|
||||||
} catch (error) {
|
setError(
|
||||||
economicResponse = { data: {} };
|
`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
|
// Job details
|
||||||
const updatedCareerDetails = {
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
...career,
|
if (!jobDetailsResponse.ok){
|
||||||
jobDescription: description,
|
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
|
||||||
tasks,
|
setLoading(false);
|
||||||
salaryData: salaryDataPoints,
|
return;
|
||||||
economicProjections: economicResponse.data || {},
|
}
|
||||||
};
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
// Now set the fully fetched data
|
// Salary
|
||||||
setCareerDetails(updatedCareerDetails);
|
let salaryResponse;
|
||||||
|
try {
|
||||||
|
salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
||||||
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
salaryResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
// Build salary array
|
||||||
console.error('Error processing career click:', error.message);
|
const sData = salaryResponse.data || {};
|
||||||
setCareerDetails({
|
const salaryDataPoints =
|
||||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`
|
sData && Object.keys(sData).length > 0
|
||||||
});
|
? [
|
||||||
} finally {
|
{
|
||||||
setLoading(false);
|
percentile: '10th Percentile',
|
||||||
}
|
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
|
||||||
},
|
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
|
||||||
[userState, apiUrl, areaTitle, userZipcode]
|
},
|
||||||
);
|
{
|
||||||
|
percentile: '25th Percentile',
|
||||||
// ============= Let typed careers open PopoutPanel =============
|
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
|
||||||
const handleCareerFromSearch = useCallback(
|
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
|
||||||
(obj) => {
|
},
|
||||||
const adapted = {
|
{
|
||||||
code: obj.soc_code,
|
percentile: 'Median',
|
||||||
title: obj.title,
|
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
|
||||||
cipCode: obj.cip_code,
|
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(() => {
|
useEffect(() => {
|
||||||
fetch('/careers_with_ratings.json')
|
fetch('/careers_with_ratings.json')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -350,7 +335,7 @@ function CareerExplorer({ }) {
|
|||||||
.then((data) => setMasterCareerRatings(data))
|
.then((data) => setMasterCareerRatings(data))
|
||||||
.catch((err) => console.error('Error fetching career ratings:', err));
|
.catch((err) => console.error('Error fetching career ratings:', err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const priorities = useMemo(() => {
|
const priorities = useMemo(() => {
|
||||||
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
|
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
@ -358,103 +343,129 @@ function CareerExplorer({ }) {
|
|||||||
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
|
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
|
||||||
|
|
||||||
const getCareerRatingsBySocCode = (socCode) => {
|
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) => {
|
const saveCareerListToBackend = async (newCareerList) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
await axios.post(`${apiUrl}/user-profile`, {
|
await axios.post(
|
||||||
|
`${apiUrl}/user-profile`,
|
||||||
firstName: userProfile?.firstname,
|
{
|
||||||
lastName: userProfile?.lastname,
|
firstName: userProfile?.firstname,
|
||||||
email: userProfile?.email,
|
lastName: userProfile?.lastname,
|
||||||
zipCode: userProfile?.zipcode,
|
email: userProfile?.email,
|
||||||
state: userProfile?.state,
|
zipCode: userProfile?.zipcode,
|
||||||
area: userProfile?.area,
|
state: userProfile?.state,
|
||||||
careerSituation: userProfile?.career_situation,
|
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,
|
||||||
interest_inventory_answers: userProfile?.interest_inventory_answers,
|
career_priorities: userProfile?.career_priorities,
|
||||||
career_priorities: userProfile?.career_priorities,
|
career_list: JSON.stringify(newCareerList),
|
||||||
|
},
|
||||||
// IMPORTANT: We convert the newCareerList to JSON if your DB expects text
|
{
|
||||||
career_list: JSON.stringify(newCareerList),
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
}, {
|
}
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
);
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {
|
console.error('Error saving career_list:', 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 === "I’m 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];
|
// ===================== Add/Remove from comparison =====================
|
||||||
// Call the API to save
|
const addCareerToList = (career) => {
|
||||||
saveCareerListToBackend(newList);
|
const masterRatings = getCareerRatingsBySocCode(career.code);
|
||||||
return newList;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const fitRatingMap = {
|
||||||
|
Best: 5,
|
||||||
|
Great: 4,
|
||||||
|
Good: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const interestsRating =
|
||||||
|
priorities.interests === "I’m 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) => {
|
const removeCareerFromList = (careerCode) => {
|
||||||
setCareerList((prevList) => {
|
setCareerList((prevList) => {
|
||||||
const newList = prevList.filter((c) => c.code !== careerCode);
|
const newList = prevList.filter((c) => c.code !== careerCode);
|
||||||
// Call the API to save
|
saveCareerListToBackend(newList);
|
||||||
saveCareerListToBackend(newList);
|
return 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(() => {
|
const filteredCareers = useMemo(() => {
|
||||||
return careersWithJobZone.filter((career) => {
|
return careersWithJobZone.filter((career) => {
|
||||||
const jobZoneMatches = selectedJobZone
|
const jobZoneMatches = selectedJobZone
|
||||||
@ -468,6 +479,7 @@ function CareerExplorer({ }) {
|
|||||||
});
|
});
|
||||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||||
|
|
||||||
|
// Weighted “match score” logic. (unchanged)
|
||||||
const priorityWeight = (priority, response) => {
|
const priorityWeight = (priority, response) => {
|
||||||
const weightMap = {
|
const weightMap = {
|
||||||
interests: {
|
interests: {
|
||||||
@ -502,28 +514,27 @@ function CareerExplorer({ }) {
|
|||||||
};
|
};
|
||||||
return weightMap[priority][response] || 1;
|
return weightMap[priority][response] || 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLoadingOverlay = () => {
|
const renderLoadingOverlay = () => {
|
||||||
if (!loading) return null;
|
if (!loading) return null;
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
<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="rounded bg-white p-6 shadow-lg">
|
||||||
<div className="mb-2 w-full max-w-md rounded bg-gray-200">
|
<div className="mb-2 w-full max-w-md rounded bg-gray-200">
|
||||||
<div
|
<div
|
||||||
className="h-2 rounded bg-blue-500 transition-all"
|
className="h-2 rounded bg-blue-500 transition-all"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-center text-sm text-gray-600">
|
<p className="mt-1 text-center text-sm text-gray-600">
|
||||||
{progress}% — Loading Career Suggestions...
|
{progress}% — Loading Career Suggestions...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
||||||
{renderLoadingOverlay()}
|
{renderLoadingOverlay()}
|
||||||
{showModal && (
|
{showModal && (
|
||||||
@ -533,14 +544,15 @@ function CareerExplorer({ }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between items-center mb-4">
|
<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
|
<CareerSearch
|
||||||
onCareerSelected={(careerObj) => {
|
onCareerSelected={(careerObj) => {
|
||||||
console.log('[Dashboard] onCareerSelected =>', careerObj);
|
console.log('[Dashboard] onCareerSelected =>', careerObj);
|
||||||
// Set the "pendingCareerForModal" so our useEffect fires
|
setPendingCareerForModal(careerObj);
|
||||||
setPendingCareerForModal(careerObj);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Career Comparison</h2>
|
<h2 className="text-xl font-semibold mb-4">Career Comparison</h2>
|
||||||
@ -550,7 +562,9 @@ function CareerExplorer({ }) {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="border p-2">Career</th>
|
<th className="border p-2">Career</th>
|
||||||
{priorityKeys.map((priority) => (
|
{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">Match</th>
|
||||||
<th className="border p-2">Actions</th>
|
<th className="border p-2">Actions</th>
|
||||||
@ -559,7 +573,7 @@ function CareerExplorer({ }) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{careerList.map((career) => {
|
{careerList.map((career) => {
|
||||||
const ratings = career.ratings || {};
|
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 meaningRating = ratings.meaning || 3;
|
||||||
const stabilityRating = ratings.stability || 3;
|
const stabilityRating = ratings.stability || 3;
|
||||||
const growthRating = ratings.growth || 3;
|
const growthRating = ratings.growth || 3;
|
||||||
@ -601,17 +615,30 @@ function CareerExplorer({ }) {
|
|||||||
<td className="border p-2">{balanceRating}</td>
|
<td className="border p-2">{balanceRating}</td>
|
||||||
<td className="border p-2">{recognitionRating}</td>
|
<td className="border p-2">{recognitionRating}</td>
|
||||||
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
||||||
<td className="border p-2">
|
<td className="border p-2 space-x-2">
|
||||||
<Button className="bg-red-600 text-black-500" onClick={() => removeCareerFromList(career.code)}>Remove</Button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
) : <p>No careers added to comparison.</p>}
|
) : (
|
||||||
|
<p>No careers added to comparison.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4 mb-4">
|
<div className="flex gap-4 mb-4">
|
||||||
<select
|
<select
|
||||||
@ -621,7 +648,9 @@ function CareerExplorer({ }) {
|
|||||||
>
|
>
|
||||||
<option value="">All Preparation Levels</option>
|
<option value="">All Preparation Levels</option>
|
||||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||||
<option key={zone} value={zone}>{label}</option>
|
<option key={zone} value={zone}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@ -632,7 +661,9 @@ function CareerExplorer({ }) {
|
|||||||
>
|
>
|
||||||
<option value="">All Fit Levels</option>
|
<option value="">All Fit Levels</option>
|
||||||
{Object.entries(fitLabels).map(([key, label]) => (
|
{Object.entries(fitLabels).map(([key, label]) => (
|
||||||
<option key={key} value={key}>{label}</option>
|
<option key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -649,25 +680,35 @@ function CareerExplorer({ }) {
|
|||||||
areaTitle={areaTitle}
|
areaTitle={areaTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
<CareerModal
|
<CareerModal
|
||||||
career={selectedCareer}
|
career={selectedCareer}
|
||||||
careerDetails={careerDetails}
|
careerDetails={careerDetails}
|
||||||
closeModal={() => {
|
closeModal={() => {
|
||||||
setSelectedCareer(null);
|
setSelectedCareer(null);
|
||||||
setCareerDetails(null);
|
setCareerDetails(null);
|
||||||
}}
|
}}
|
||||||
addCareerToList={addCareerToList}
|
addCareerToList={addCareerToList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
||||||
Career results and details provided by
|
Career results and details provided by
|
||||||
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>,
|
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer">
|
||||||
in partnership with
|
{' '}
|
||||||
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>
|
O*Net
|
||||||
|
</a>
|
||||||
|
, in partnership with
|
||||||
|
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer">
|
||||||
|
{' '}
|
||||||
|
Bureau of Labor Statistics
|
||||||
|
</a>
|
||||||
and
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user