diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js index 0aba3b8..c7c3492 100644 --- a/src/components/CareerSearch.js +++ b/src/components/CareerSearch.js @@ -1,71 +1,89 @@ import React, { useEffect, useState } from 'react'; -import { Input } from './ui/input.js'; -const CareerSearch = ({ setPendingCareerForModal }) => { - const [careers, setCareers] = useState([]); +const CareerSearch = ({ onCareerSelected }) => { + const [careerObjects, setCareerObjects] = useState([]); const [searchInput, setSearchInput] = useState(''); useEffect(() => { - const fetchCareerTitles = async () => { + const fetchCareerData = async () => { try { const response = await fetch('/career_clusters.json'); const data = await response.json(); - const careerTitlesSet = new Set(); + // Create a Map keyed by title, storing one object per unique title + const uniqueByTitle = new Map(); - // Iterate using Object.keys at every level (no .forEach or .map) const clusters = Object.keys(data); for (let i = 0; i < clusters.length; i++) { - const cluster = clusters[i]; - const subdivisions = Object.keys(data[cluster]); + const clusterKey = clusters[i]; + const subdivisions = Object.keys(data[clusterKey]); for (let j = 0; j < subdivisions.length; j++) { - const subdivision = subdivisions[j]; - const careersArray = data[cluster][subdivision]; + const subKey = subdivisions[j]; + const careersList = data[clusterKey][subKey] || []; - for (let k = 0; k < careersArray.length; k++) { - const careerObj = careersArray[k]; - if (careerObj.title) { - careerTitlesSet.add(careerObj.title); + for (let k = 0; k < careersList.length; k++) { + const c = careersList[k]; + // If there's a title and soc_code, store the first we encounter for that title. + if (c.title && c.soc_code && c.cip_code !== undefined) { + if (!uniqueByTitle.has(c.title)) { + // Add it if we haven't seen this exact title yet + uniqueByTitle.set(c.title, { + title: c.title, + soc_code: c.soc_code, + cip_code: c.cip_code, + }); + } + // If you truly only want to keep the first occurrence, + // just do nothing if we see the same title again. } } } } - setCareers([...careerTitlesSet]); + // Convert Map to array + const dedupedArr = [...uniqueByTitle.values()]; + setCareerObjects(dedupedArr); } catch (error) { - console.error("Error fetching or processing career_clusters.json:", error); + console.error('Error loading or parsing career_clusters.json:', error); } }; - fetchCareerTitles(); + fetchCareerData(); }, []); + // Called when user clicks "Confirm New Career" const handleConfirmCareer = () => { - if (careers.includes(searchInput)) { - setPendingCareerForModal(searchInput); + // Find the full object by exact title match + const foundObj = careerObjects.find( + (obj) => obj.title.toLowerCase() === searchInput.toLowerCase() + ); + console.log('[CareerSearch] foundObj:', foundObj); + + if (foundObj) { + onCareerSelected(foundObj); } else { - alert("Please select a valid career from the suggestions."); + alert('Please select a valid career from the suggestions.'); } }; return ( -
+

Search for Career

- setSearchInput(e.target.value)} placeholder="Start typing a career..." list="career-titles" /> - {careers.map((career, index) => ( - -
diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index a21b283..481b400 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -1,13 +1,17 @@ // Dashboard.js + import axios from 'axios'; import React, { useMemo, useState, useCallback, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; + import { CareerSuggestions } from './CareerSuggestions.js'; import PopoutPanel from './PopoutPanel.js'; -import MilestoneTracker from './MilestoneTracker.js' -import './Dashboard.css'; +import MilestoneTracker from './MilestoneTracker.js'; +import CareerSearch from './CareerSearch.js'; // <--- Import your new search import Chatbot from "./Chatbot.js"; + +import './Dashboard.css'; import { Bar } from 'react-chartjs-2'; import { fetchSchools } from '../utils/apiUtils.js'; @@ -17,6 +21,7 @@ function Dashboard() { const location = useLocation(); const navigate = useNavigate(); + // ============= Existing States ============= const [careerSuggestions, setCareerSuggestions] = useState([]); const [careerDetails, setCareerDetails] = useState(null); const [riaSecScores, setRiaSecScores] = useState([]); @@ -25,8 +30,11 @@ function Dashboard() { const [salaryData, setSalaryData] = useState([]); const [economicProjections, setEconomicProjections] = useState(null); const [tuitionData, setTuitionData] = useState(null); + + // Overall Dashboard loading const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); + const [error, setError] = useState(null); const [userState, setUserState] = useState(null); const [areaTitle, setAreaTitle] = useState(null); @@ -37,51 +45,55 @@ function Dashboard() { const [selectedFit, setSelectedFit] = useState(''); const [results, setResults] = useState([]); const [chatbotContext, setChatbotContext] = useState({}); + + // Show session expired modal const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [sessionHandled, setSessionHandled] = useState(false); + // ============= NEW State ============= + // Holds the full career object { title, soc_code, cip_code } typed in CareerSearch + const [pendingCareerForModal, setPendingCareerForModal] = useState(null); + + // We'll treat "loading" as "loadingSuggestions" const loadingSuggestions = loading; + // We'll consider the popout panel visible if there's a selectedCareer const popoutVisible = !!selectedCareer; - // Function to handle the token check and fetch requests - const authFetch = async (url, options = {}, onUnauthorized) => { + // ============= Auth & URL Setup ============= + const apiUrl = process.env.REACT_APP_API_URL || ''; + + // AUTH fetch + const authFetch = async (url, options = {}, onUnauthorized) => { const token = localStorage.getItem("token"); - if (!token) { console.log("Token is missing, triggering session expired modal."); - if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } - const finalOptions = { ...options, headers: { ...(options.headers || {}), - Authorization: `Bearer ${token}`, // Attach the token to the request + Authorization: `Bearer ${token}`, }, }; - try { const res = await fetch(url, finalOptions); - - // Log the response status for debugging console.log("Response Status:", res.status); - if (res.status === 401 || res.status === 403) { console.log("Session expired, triggering session expired modal."); - if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } - return res; } catch (err) { console.error("Fetch error:", err); - if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } }; - // Fetch User Profile (with proper session handling) + // ============= User Profile Fetch ============= const fetchUserProfile = async () => { const res = await authFetch(`${apiUrl}/user-profile`); if (!res) return; @@ -95,8 +107,13 @@ function Dashboard() { console.error('Failed to fetch user profile'); } }; - + // ============= Lifecycle: Load Profile, Setup ============= + useEffect(() => { + fetchUserProfile(); + }, [apiUrl]); // load once + + // ============= jobZone & fit Setup ============= const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', @@ -111,49 +128,27 @@ function Dashboard() { 'Good': 'Good - Less Strong Match' }; - const apiUrl = process.env.REACT_APP_API_URL || ''; - - useEffect(() => { - const fetchUserProfile = async () => { - const res = await authFetch(`${apiUrl}/user-profile`); - if (!res) return; - - if (res.ok) { - const profileData = await res.json(); - setUserState(profileData.state); - setAreaTitle(profileData.area.trim() || ''); - setUserZipcode(profileData.zipcode); - } else { - console.error('Failed to fetch user profile'); - } - }; - - fetchUserProfile(); - }, [apiUrl]); - + // Fetch job zones for each career suggestion useEffect(() => { const fetchJobZones = async () => { if (careerSuggestions.length === 0) return; - const socCodes = careerSuggestions.map((career) => career.code); try { const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); const jobZoneData = response.data; - const updatedCareers = careerSuggestions.map((career) => ({ ...career, job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, })); - setCareersWithJobZone(updatedCareers); } catch (error) { console.error('Error fetching job zone information:', error); } }; - fetchJobZones(); }, [careerSuggestions, apiUrl]); + // Filter careers by job zone, fit const filteredCareers = useMemo(() => { return careersWithJobZone.filter((career) => { const jobZoneMatches = selectedJobZone @@ -164,11 +159,11 @@ function Dashboard() { : true; const fitMatches = selectedFit ? career.fit === selectedFit : true; - return jobZoneMatches && fitMatches; }); }, [careersWithJobZone, selectedJobZone, selectedFit]); + // Merge updated data into chatbot context const updateChatbotContext = (updatedData) => { setChatbotContext((prevContext) => { const mergedContext = { @@ -184,10 +179,11 @@ function Dashboard() { }); }; + // Our final array for CareerSuggestions const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]); + // ============= Popout Panel Setup ============= const memoizedPopoutPanel = useMemo(() => { - return ( ); - }, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]); + }, [ + selectedCareer, + careerDetails, + schools, + salaryData, + economicProjections, + tuitionData, + loading, + error, + userState, + results + ]); + // ============= On Page Load: get careerSuggestions from location.state, etc. ============= useEffect(() => { let descriptions = []; if (location.state) { const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {}; - descriptions = scores.map((score) => score.description || "No description available."); + descriptions = (scores || []).map((score) => score.description || "No description available."); setCareerSuggestions(suggestions || []); setRiaSecScores(scores || []); setRiaSecDescriptions(descriptions); @@ -220,8 +228,7 @@ function Dashboard() { } }, [location.state, navigate]); - - + // Once userState, areaTitle, userZipcode, etc. are set, update chatbot useEffect(() => { if ( careerSuggestions.length > 0 && @@ -230,7 +237,6 @@ function Dashboard() { areaTitle !== null && userZipcode !== null ) { - const newChatbotContext = { careerSuggestions: [...careersWithJobZone], riaSecScores: [...riaSecScores], @@ -238,15 +244,16 @@ function Dashboard() { areaTitle: areaTitle || "", userZipcode: userZipcode || "", }; - setChatbotContext(newChatbotContext); - } else { } }, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]); + // ============= handleCareerClick (for tile clicks) ============= const handleCareerClick = useCallback( async (career) => { + console.log('[handleCareerClick] career =>', career); const socCode = career.code; + console.log('[handleCareerClick] career.code =>', socCode); setSelectedCareer(career); setLoading(true); setError(null); @@ -263,15 +270,18 @@ function Dashboard() { } try { + // CIP fetch const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); 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) throw new Error('Failed to fetch job description'); const { description, tasks } = await jobDetailsResponse.json(); + // Salary let salaryResponse; try { salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } }); @@ -279,6 +289,7 @@ function Dashboard() { salaryResponse = { data: {} }; } + // Economic let economicResponse; try { economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`); @@ -286,6 +297,7 @@ function Dashboard() { economicResponse = { data: {} }; } + // Tuition let tuitionResponse; try { tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } }); @@ -293,8 +305,8 @@ function Dashboard() { tuitionResponse = { data: {} }; } + // Fetch schools const filteredSchools = await fetchSchools(cleanedCipCode, userState); - const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => { const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`; try { @@ -309,24 +321,47 @@ function Dashboard() { } })); - const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0 + // Build salary array + const sData = salaryResponse.data || {}; + const salaryDataPoints = sData && Object.keys(sData).length > 0 ? [ - { percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 }, - { percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 }, - { percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 }, - { percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 }, - { percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 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 + }, ] : []; + // Build final details const updatedCareerDetails = { ...career, jobDescription: description, tasks: tasks, - economicProjections: economicResponse.data || {}, + economicProjections: (economicResponse.data || {}), salaryData: salaryDataPoints, schools: schoolsWithDistance, - tuitionData: tuitionResponse.data || [], + tuitionData: (tuitionResponse.data || []), }; setCareerDetails(updatedCareerDetails); @@ -338,11 +373,33 @@ function Dashboard() { } finally { setLoading(false); } - }, - [userState, apiUrl, areaTitle, userZipcode] + [userState, apiUrl, areaTitle, userZipcode, updateChatbotContext] ); + // ============= Letting typed careers open PopoutPanel ============= + // Called if the user picks a career in "CareerSearch" => { title, soc_code, cip_code } + const handleCareerFromSearch = useCallback((obj) => { + // Convert to shape used by handleCareerClick => { code, title, cipCode } + const adapted = { + code: obj.soc_code, + title: obj.title, + cipCode: obj.cip_code + }; + console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); + handleCareerClick(adapted); + }, [handleCareerClick]); + + // If the user typed a career and clicked confirm + useEffect(() => { + if (pendingCareerForModal) { + console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal); + handleCareerFromSearch(pendingCareerForModal); + setPendingCareerForModal(null); + } + }, [pendingCareerForModal, handleCareerFromSearch]); + + // ============= RIASEC Chart Data ============= const chartData = { labels: riaSecScores.map((score) => score.area), datasets: [ @@ -356,10 +413,9 @@ function Dashboard() { ], }; + // ============= Hide the spinner if popout is open ============= const renderLoadingOverlay = () => { - // If we are NOT loading suggestions, or if the popout is visible, hide the overlay if (!loadingSuggestions || popoutVisible) return null; - return (
@@ -380,31 +436,37 @@ function Dashboard() { return (
{showSessionExpiredModal && ( -
-
-

Session Expired

-

Your session has expired or is invalid.

-
- - +
+
+

Session Expired

+

Your session has expired or is invalid.

+
+ + +
-
- )} - - {renderLoadingOverlay()} + )} + + {renderLoadingOverlay()}
+ {/* ====== 1) The new CareerSearch bar ====== */} + + {/* Existing filters + suggestions */}
+
+ { + console.log('[Dashboard] onCareerSelected =>', careerObj); + // Set the "pendingCareerForModal" so our useEffect fires below + setPendingCareerForModal(careerObj); + }} + /> +
+ areaTitle={areaTitle} + />
+ {/* RIASEC Container */}

RIASEC Scores

@@ -475,8 +548,10 @@ function Dashboard() {
+ {/* The PopoutPanel */} {memoizedPopoutPanel} + {/* Chatbot */}
{careerSuggestions.length > 0 ? ( @@ -484,9 +559,7 @@ function Dashboard() {

Loading Chatbot...

)}
- - - +
{
setPendingCareerForModal(careerName)} - setPendingCareerForModal={setPendingCareerForModal} - authFetch={authFetch} + onCareerSelected={(careerObj) => { + setPendingCareerForModal(careerObj.title); + }} /> { authFetch={authFetch} /> - {pendingCareerForModal && ( +{pendingCareerForModal && ( - )} + )}
); }; diff --git a/src/components/Paywall.js b/src/components/Paywall.js index 4a18038..2513c0c 100644 --- a/src/components/Paywall.js +++ b/src/components/Paywall.js @@ -6,8 +6,7 @@ const Paywall = () => { const navigate = useNavigate(); const handleSubscribe = () => { - // Implement subscription logic here (Stripe, etc.) - alert('Subscription logic placeholder!'); + navigate('/milestone-tracker'); }; return ( diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js index 3a902fa..9cf6d33 100644 --- a/src/components/PopoutPanel.js +++ b/src/components/PopoutPanel.js @@ -139,7 +139,7 @@ function PopoutPanel({ `Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.` ); if (decision) { - navigate("/financial-profile", { + navigate("/paywall", { state: { selectedCareer: { career_path_id: match.id, career_name: data.title }, }, diff --git a/src/components/UserProfile.js b/src/components/UserProfile.js index 8d90536..32ecc47 100644 --- a/src/components/UserProfile.js +++ b/src/components/UserProfile.js @@ -92,7 +92,7 @@ function UserProfile() { }; fetchProfileAndAreas(); - }, []); + }, []); // only runs once const handleFormSubmit = async (e) => { e.preventDefault(); @@ -125,12 +125,59 @@ function UserProfile() { } }; + // FULL list of states, including all 50 states (+ DC if desired) const states = [ { name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' }, - // ... (truncated for brevity, include all states) - { name: 'Wyoming', code: 'WY' } + { 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' }, ]; return ( @@ -195,7 +242,7 @@ function UserProfile() { />
- {/* State */} + {/* State Dropdown */}