import React, { useState, useEffect, useMemo, useCallback, useContext } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import ChatCtx from '../contexts/ChatCtx.js'; import CareerSuggestions from './CareerSuggestions.js'; import CareerPrioritiesModal from './CareerPrioritiesModal.js'; import CareerModal from './CareerModal.js'; import InterestMeaningModal from './InterestMeaningModal.js'; import CareerSearch from './CareerSearch.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' }, ]; // -------------- 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() { const navigate = useNavigate(); const location = useLocation(); const apiUrl = process.env.REACT_APP_API_URL || ''; // ---------- Component States ---------- const [userProfile, setUserProfile] = useState(null); const [masterCareerRatings, setMasterCareerRatings] = useState([]); const [careerList, setCareerList] = useState([]); const [careerDetails, setCareerDetails] = useState(null); const [showModal, setShowModal] = useState(false); const [userState, setUserState] = useState(null); const [areaTitle, setAreaTitle] = useState(null); const [userZipcode, setUserZipcode] = useState(null); const [error, setError] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const { setChatSnapshot } = useContext(ChatCtx); const [showInterestMeaningModal, setShowInterestMeaningModal] = useState(false); const [modalData, setModalData] = useState({ career: null, askForInterest: false, defaultInterest: 3, defaultMeaning: 3, }); // ... const fitRatingMap = { Best: 5, Great: 4, Good: 3, }; // This is where we'll hold ALL final suggestions (with job_zone merged) const [careerSuggestions, setCareerSuggestions] = useState([]); const [salaryData, setSalaryData] = useState([]); const [economicProjections, setEconomicProjections] = useState(null); const [selectedJobZone, setSelectedJobZone] = useState(''); const [selectedFit, setSelectedFit] = useState(''); const [selectedCareer, setSelectedCareer] = useState(null); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); // Weighted "match score" logic. (unchanged) const priorityWeight = (priority, response) => { const weightMap = { interests: { 'I know my interests (completed inventory)': 5, 'I’m not sure yet': 1, }, meaning: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, stability: { 'Very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, growth: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, balance: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, recognition: { 'Very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, }; return weightMap[priority][response] || 1; }; const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', '3': 'Medium Preparation Needed', '4': 'Considerable Preparation Needed', '5': 'Extensive Preparation Needed', }; const fitLabels = { Best: 'Best - Very Strong Match', Great: 'Great - Strong Match', Good: 'Good - Less Strong Match', }; // -------------------------------------------------- // fetchSuggestions - combined suggestions + job zone // -------------------------------------------------- const fetchSuggestions = async (answers, profileData) => { if (!answers) { setCareerSuggestions([]); localStorage.removeItem('careerSuggestionsCache'); // Reset loading & progress if userProfile has no answers setLoading(true); setProgress(0); setLoading(false); return; } try { setLoading(true); setProgress(0); // 1) O*NET answers -> initial career list const submitRes = await axios.post(`${apiUrl}/onet/submit_answers`, { answers, state: profileData.state, area: profileData.area, }); const { careers = [] } = submitRes.data || {}; const flattened = careers.flat(); // We'll do an extra single call for job zones + 4 calls for each career: // => total steps = 1 (jobZones) + (flattened.length * 4) let totalSteps = 1 + (flattened.length * 4); let completedSteps = 0; // Increments the global progress bar const increment = () => { completedSteps++; const pct = Math.round((completedSteps / totalSteps) * 100); setProgress(pct); }; // A helper that does a GET request, increments progress on success/fail const fetchWithProgress = async (url, params) => { try { const res = await axios.get(url, { params }); increment(); return res.data; } catch (err) { increment(); return {}; // or null } }; // 2) job zones (one call for all SOC codes) const socCodes = flattened.map((c) => c.code); const zonesRes = await axios.post(`${apiUrl}/job-zones`, { socCodes }).catch(() => null); // increment progress for this single request increment(); const jobZoneData = zonesRes?.data || {}; // 3) For each career, also fetch CIP, job details, projections, salary const enrichedPromiseArray = flattened.map(async (career) => { const strippedSoc = career.code.split('.')[0]; // build URLs const cipUrl = `${apiUrl}/cip/${career.code}`; const jobDetailsUrl = `${apiUrl}/onet/career-description/${career.code}`; const economicUrl = `${apiUrl}/projections/${strippedSoc}`; const salaryParams = { socCode: strippedSoc, area: profileData.area }; // We'll fetch them in parallel with our custom fetchWithProgress: const [cipRaw, jobRaw, ecoRaw, salRaw] = await Promise.all([ fetchWithProgress(cipUrl), fetchWithProgress(jobDetailsUrl), fetchWithProgress(economicUrl), fetchWithProgress(`${apiUrl}/salary`, salaryParams), ]); // parse data const cip = cipRaw || {}; const jobDetails = jobRaw || {}; const economic = ecoRaw || {}; const salary = salRaw || {}; // Check if data is missing const isCipMissing = !cip || Object.keys(cip).length === 0; const isJobDetailsMissing = !jobDetails || Object.keys(jobDetails).length === 0; const isEconomicMissing = !economic || Object.values(economic).every((val) => val === 'N/A' || val === '*'); const isSalaryMissing = !salary || Object.keys(salary).length === 0; const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing; return { ...career, job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, limitedData: isLimitedData, }; }); // Wait for everything to finish const finalEnrichedCareers = await Promise.all(enrichedPromiseArray); // Store final suggestions in local storage localStorage.setItem('careerSuggestionsCache', JSON.stringify(finalEnrichedCareers)); // Update React state setCareerSuggestions(finalEnrichedCareers); } catch (err) { console.error('[fetchSuggestions] Error:', err); setCareerSuggestions([]); localStorage.removeItem('careerSuggestionsCache'); } finally { // Hide spinner setLoading(false); } }; // -------------------------------------- // On mount, load suggestions from cache // -------------------------------------- useEffect(() => { const cached = localStorage.getItem('careerSuggestionsCache'); if (cached) { const parsed = JSON.parse(cached); if (parsed?.length) { setCareerSuggestions(parsed); } } }, []); // -------------------------------------- // 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; setUserProfile(profileData); setUserState(profileData.state); setAreaTitle(profileData.area); setUserZipcode(profileData.zipcode); if (profileData.career_list) { setCareerList(JSON.parse(profileData.career_list)); } if (!profileData.career_priorities) { setShowModal(true); } } else { setShowModal(true); } } catch (err) { console.error('Error fetching user profile:', err); setShowModal(true); } finally { setLoading(false); } }; fetchUserProfile(); }, [apiUrl]); // ------------------------------------------------------ // If user came from Interest Inventory => auto-fetch // ------------------------------------------------------ useEffect(() => { if ( location.state?.fromInterestInventory && userProfile?.interest_inventory_answers ) { fetchSuggestions(userProfile.interest_inventory_answers, userProfile); // remove that state so refresh doesn't re-fetch navigate('.', { replace: true }); } }, [location.state, userProfile, fetchSuggestions, navigate]); // ------------------------------------------------------ // handleCareerClick (detail fetch for CIP, Salary, etc.) // ------------------------------------------------------ const handleCareerClick = useCallback( async (career) => { console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2)); const socCode = career.code; setSelectedCareer(career); setError(null); setCareerDetails(null); setSalaryData([]); setEconomicProjections({}); setLoading(true); if (!socCode) { console.error('SOC Code is missing'); setError('SOC Code is missing'); setLoading(false); return; } try { // 1) 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); // 2) Job details (description + tasks) 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(); // 3) Salary data let salaryResponse; try { salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }, }); } catch (error) { salaryResponse = { data: {} }; } 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, }, ] : []; // 4) Economic Projections const fullStateName = getFullStateName(userState); // your helper let economicResponse = { data: {} }; try { economicResponse = await axios.get( `${apiUrl}/projections/${socCode.split('.')[0]}`, { params: { state: fullStateName }, } ); } catch (error) { economicResponse = { data: {} }; } // ---------------------------------------------------- // 5) AI RISK ANALYSIS LOGIC // Attempt to retrieve from server2 first; // if not found => call server3 => store in server2. // ---------------------------------------------------- let aiRisk = null; const strippedSocCode = socCode.split('.')[0]; try { // Check local DB first (SQLite -> server2) const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${socCode}`); aiRisk = localRiskRes.data; } catch (err) { // If 404, we call server3's ChatGPT route at the SAME base url if (err.response && err.response.status === 404) { try { const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, { socCode, careerName: career.title, jobDescription: description, tasks, }); const { riskLevel, reasoning } = aiRes.data; // store it back in server2 to avoid repeated GPT calls await axios.post(`${apiUrl}/ai-risk`, { socCode, careerName: aiRes.data.careerName, jobDescription: aiRes.data.jobDescription, tasks: aiRes.data.tasks, riskLevel: aiRes.data.riskLevel, reasoning: aiRes.data.reasoning, }); // build final object aiRisk = { socCode, careerName: career.title, jobDescription: description, tasks, riskLevel, reasoning, }; } catch (err2) { console.error('Error calling server3 or storing AI risk:', err2); // fallback } } else { console.error('Error fetching AI risk from server2:', err); } } // 6) Build final details object const updatedCareerDetails = { ...career, jobDescription: description, tasks, salaryData: salaryDataPoints, economicProjections: economicResponse.data || {}, aiRisk, // <--- Now we have it attached }; 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] ); // ------------------------------------------------------ // handleCareerFromSearch // ------------------------------------------------------ const handleCareerFromSearch = useCallback( (obj) => { const adapted = { code: obj.soc_code, title: obj.title, cipCode: obj.cip_code, fromManualSearch: true }; console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); handleCareerClick(adapted); }, [handleCareerClick] ); // ------------------------------------------------------ // pendingCareerForModal effect // ------------------------------------------------------ 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) => { if (!res.ok) throw new Error('Failed to fetch ratings JSON'); return res.json(); }) .then((data) => setMasterCareerRatings(data)) .catch((err) => console.error('Error fetching career ratings:', err)); }, []); // ------------------------------------------------------ // Derived data / Helpers // ------------------------------------------------------ const priorities = useMemo(() => { return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {}; }, [userProfile]); const priorityKeys = [ 'interests', 'meaning', 'stability', 'growth', 'balance', 'recognition', ]; /* ---------- core context: sent every turn ---------- */ const coreCtx = useMemo(() => { // 1) Riasec scores const riasecScores = userProfile?.riasec_scores ? JSON.parse(userProfile.riasec_scores) : null; // 2) priority weights normalised 0-1 const priorityWeights = priorities ? { stability : priorityWeight('stability' , priorities.stability) / 5, growth : priorityWeight('growth' , priorities.growth) / 5, balance : priorityWeight('balance' , priorities.balance) / 5, recognition : priorityWeight('recognition', priorities.recognition)/ 5, interests : priorityWeight('interests' , priorities.interests) / 5, mission : priorityWeight('meaning' , priorities.meaning) / 5, } : null; return { riasecScores, priorityWeights }; }, [userProfile, priorities]); /* ---------- modal context: exists only while a modal is open ---------- */ const modalCtx = useMemo(() => { if (!selectedCareer || !careerDetails) return null; const medianRow = careerDetails.salaryData ?.find(r => r.percentile === "Median"); return { socCode : selectedCareer.code, title : selectedCareer.title, aiRisk : careerDetails.aiRisk?.riskLevel ?? "n/a", salary : medianRow ? { regional : medianRow.regionalSalary, national : medianRow.nationalSalary } : null, projections : careerDetails.economicProjections ?? {}, description : careerDetails.jobDescription, tasks : careerDetails.tasks, }; }, [selectedCareer, careerDetails]); useEffect(() => { // send null when no modal is open → ChatDrawer simply omits it setChatSnapshot({ coreCtx, modalCtx }); }, [coreCtx, modalCtx, setChatSnapshot]); const getCareerRatingsBySocCode = (socCode) => { 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, 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); } }; // ------------------------------------------------------ // Add/Remove from comparison // ------------------------------------------------------ const addCareerToList = (career) => { // 1) get default (pre-calculated) ratings from your JSON const masterRatings = getCareerRatingsBySocCode(career.code); // 2) figure out interest const userHasInventory = !career.fromManualSearch && // ← skip the shortcut if manual priorities.interests && priorities.interests !== "I’m not sure yet"; const defaultInterestValue = userHasInventory ? (fitRatingMap[career.fit] || masterRatings.interests || 3) : 3; const defaultMeaningValue = 3; // 4) open the InterestMeaningModal instead of using prompt() setModalData({ career, masterRatings, askForInterest: !userHasInventory, defaultInterest: defaultInterestValue, defaultMeaning: defaultMeaningValue, }); setShowInterestMeaningModal(true); }; const handleModalSave = ({ interest, meaning }) => { const { career, masterRatings, askForInterest, defaultInterest } = modalData; if (!career) return; // If we asked for interest, use the user's input; otherwise keep the default const finalInterest = askForInterest && interest !== null ? interest : defaultInterest; const finalMeaning = meaning; 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: finalInterest, meaning: finalMeaning, 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); saveCareerListToBackend(newList); return newList; }); }; // ------------------------------------------------------ // "Select for Education" => navigate with CIP codes // ------------------------------------------------------ // CareerExplorer.js const handleSelectForEducation = (career) => { if (!career) return; // ─── 1. Ask first ───────────────────────────────────────────── const ok = window.confirm( `Are you sure you want to move on to Educational Programs for “${career.title}”?` ); if (!ok) return; // ─── 2. Make sure we have a full SOC code ───────────────────── const fullSoc = career.soc_code || career.code || ''; if (!fullSoc) { alert('Sorry – this career is missing a valid SOC code.'); return; } // ─── 3. Find & clean CIP codes (may be empty) ───────────────── const match = masterCareerRatings.find(r => r.soc_code === fullSoc); const rawCips = match?.cip_codes ?? []; // original array const cleanedCips = cleanCipCodes(rawCips); // “0402”, “1409”, … // ─── 4. Persist ONE tidy object for later pages ─────────────── const careerForStorage = { ...career, soc_code : fullSoc, cip_code : rawCips // keep the raw list; page cleans them again if needed }; localStorage.setItem('selectedCareer', JSON.stringify(careerForStorage)); // ─── 5. Off we go ───────────────────────────────────────────── navigate('/educational-programs', { state: { socCode : fullSoc, cipCodes : cleanedCips, // can be [], page handles it careerTitle : career.title, userZip : userZipcode, userState : userState } }); }; // ------------------------------------------------------ // Filter logic for jobZone, Fit // ------------------------------------------------------ const filteredCareers = useMemo(() => { return careerSuggestions.filter((career) => { // If user selected a jobZone, check if career.job_zone matches const jobZoneMatches = selectedJobZone ? career.job_zone !== null && career.job_zone !== undefined && Number(career.job_zone) === Number(selectedJobZone) : true; // If user selected a fit, check if career.fit matches const fitMatches = selectedFit ? career.fit === selectedFit : true; return jobZoneMatches && fitMatches; }); }, [careerSuggestions, selectedJobZone, selectedFit]); useEffect(() => { /* ---------- add-to-comparison ---------- */ const onAdd = (e) => { console.log('[onAdd] detail →', e.detail); const { socCode, careerName } = e.detail || {}; if (!socCode) { console.warn('[add-career] missing socCode – aborting'); return; } // 1. see if the career is already in the filtered list let career = filteredCareers.find((c) => c.code === socCode); // 2. if not, make a stub so the list can still save if (!career) { career = { code : socCode, title: careerName || '(name unavailable)', fit : 'Good', }; } // 3. push it into the comparison table addCareerToList(career); }; /* ---------- open-modal ---------- */ const onOpen = (e) => { const { socCode } = e.detail || {}; if (!socCode) return; const career = filteredCareers.find((c) => c.code === socCode); if (career) handleCareerClick(career); }; window.addEventListener('add-career', onAdd); window.addEventListener('open-career', onOpen); return () => { window.removeEventListener('add-career', onAdd); window.removeEventListener('open-career', onOpen); }; }, [filteredCareers, addCareerToList, handleCareerClick]); // ------------------------------------------------------ // Loading Overlay // ------------------------------------------------------ const renderLoadingOverlay = () => { if (!loading) return null; return (
{progress}% — Loading Career Suggestions...
Career | {priorityKeys.map((priority) => ({priority} | ))}Match | Actions | |||||
---|---|---|---|---|---|---|---|---|
{career.title} | {interestsRating} | {meaningRating} | {stabilityRating} | {growthRating} | {balanceRating} | {recognitionRating} | {matchScore.toFixed(1)}% |
No careers added to comparison.
)}