Added CareerSearch to Dashboard

This commit is contained in:
Josh 2025-05-01 13:53:57 +00:00
parent 1cbaa7c171
commit 2dd508c38a
10 changed files with 267 additions and 127 deletions

View File

@ -1,71 +1,89 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Input } from './ui/input.js';
const CareerSearch = ({ setPendingCareerForModal }) => { const CareerSearch = ({ onCareerSelected }) => {
const [careers, setCareers] = useState([]); const [careerObjects, setCareerObjects] = useState([]);
const [searchInput, setSearchInput] = useState(''); const [searchInput, setSearchInput] = useState('');
useEffect(() => { useEffect(() => {
const fetchCareerTitles = async () => { const fetchCareerData = async () => {
try { try {
const response = await fetch('/career_clusters.json'); const response = await fetch('/career_clusters.json');
const data = await response.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); const clusters = Object.keys(data);
for (let i = 0; i < clusters.length; i++) { for (let i = 0; i < clusters.length; i++) {
const cluster = clusters[i]; const clusterKey = clusters[i];
const subdivisions = Object.keys(data[cluster]); const subdivisions = Object.keys(data[clusterKey]);
for (let j = 0; j < subdivisions.length; j++) { for (let j = 0; j < subdivisions.length; j++) {
const subdivision = subdivisions[j]; const subKey = subdivisions[j];
const careersArray = data[cluster][subdivision]; const careersList = data[clusterKey][subKey] || [];
for (let k = 0; k < careersArray.length; k++) { for (let k = 0; k < careersList.length; k++) {
const careerObj = careersArray[k]; const c = careersList[k];
if (careerObj.title) { // If there's a title and soc_code, store the first we encounter for that title.
careerTitlesSet.add(careerObj.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) { } 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 = () => { const handleConfirmCareer = () => {
if (careers.includes(searchInput)) { // Find the full object by exact title match
setPendingCareerForModal(searchInput); const foundObj = careerObjects.find(
(obj) => obj.title.toLowerCase() === searchInput.toLowerCase()
);
console.log('[CareerSearch] foundObj:', foundObj);
if (foundObj) {
onCareerSelected(foundObj);
} else { } else {
alert("Please select a valid career from the suggestions."); alert('Please select a valid career from the suggestions.');
} }
}; };
return ( return (
<div> <div style={{ marginBottom: '1rem' }}>
<h3>Search for Career</h3> <h3>Search for Career</h3>
<Input <input
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
placeholder="Start typing a career..." placeholder="Start typing a career..."
list="career-titles" list="career-titles"
/> />
<datalist id="career-titles"> <datalist id="career-titles">
{careers.map((career, index) => ( {careerObjects.map((obj, index) => (
<option key={index} value={career} /> <option key={index} value={obj.title} />
))} ))}
</datalist> </datalist>
<button onClick={handleConfirmCareer}> <button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
Confirm New Career Confirm New Career
</button> </button>
</div> </div>

View File

@ -1,13 +1,17 @@
// Dashboard.js // Dashboard.js
import axios from 'axios'; import axios from 'axios';
import React, { useMemo, useState, useCallback, useEffect } from 'react'; import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
import { CareerSuggestions } from './CareerSuggestions.js'; import { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js'; import PopoutPanel from './PopoutPanel.js';
import MilestoneTracker from './MilestoneTracker.js' import MilestoneTracker from './MilestoneTracker.js';
import './Dashboard.css'; import CareerSearch from './CareerSearch.js'; // <--- Import your new search
import Chatbot from "./Chatbot.js"; import Chatbot from "./Chatbot.js";
import './Dashboard.css';
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
import { fetchSchools } from '../utils/apiUtils.js'; import { fetchSchools } from '../utils/apiUtils.js';
@ -17,6 +21,7 @@ function Dashboard() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
// ============= Existing States =============
const [careerSuggestions, setCareerSuggestions] = useState([]); const [careerSuggestions, setCareerSuggestions] = useState([]);
const [careerDetails, setCareerDetails] = useState(null); const [careerDetails, setCareerDetails] = useState(null);
const [riaSecScores, setRiaSecScores] = useState([]); const [riaSecScores, setRiaSecScores] = useState([]);
@ -25,8 +30,11 @@ function Dashboard() {
const [salaryData, setSalaryData] = useState([]); const [salaryData, setSalaryData] = useState([]);
const [economicProjections, setEconomicProjections] = useState(null); const [economicProjections, setEconomicProjections] = useState(null);
const [tuitionData, setTuitionData] = useState(null); const [tuitionData, setTuitionData] = useState(null);
// Overall Dashboard loading
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [userState, setUserState] = useState(null); const [userState, setUserState] = useState(null);
const [areaTitle, setAreaTitle] = useState(null); const [areaTitle, setAreaTitle] = useState(null);
@ -37,51 +45,55 @@ function Dashboard() {
const [selectedFit, setSelectedFit] = useState(''); const [selectedFit, setSelectedFit] = useState('');
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [chatbotContext, setChatbotContext] = useState({}); const [chatbotContext, setChatbotContext] = useState({});
// Show session expired modal
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [sessionHandled, setSessionHandled] = 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; const loadingSuggestions = loading;
// We'll consider the popout panel visible if there's a selectedCareer
const popoutVisible = !!selectedCareer; const popoutVisible = !!selectedCareer;
// Function to handle the token check and fetch requests // ============= Auth & URL Setup =============
const authFetch = async (url, options = {}, onUnauthorized) => { const apiUrl = process.env.REACT_APP_API_URL || '';
// AUTH fetch
const authFetch = async (url, options = {}, onUnauthorized) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
console.log("Token is missing, triggering session expired modal."); 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; return null;
} }
const finalOptions = { const finalOptions = {
...options, ...options,
headers: { headers: {
...(options.headers || {}), ...(options.headers || {}),
Authorization: `Bearer ${token}`, // Attach the token to the request Authorization: `Bearer ${token}`,
}, },
}; };
try { try {
const res = await fetch(url, finalOptions); const res = await fetch(url, finalOptions);
// Log the response status for debugging
console.log("Response Status:", res.status); console.log("Response Status:", res.status);
if (res.status === 401 || res.status === 403) { if (res.status === 401 || res.status === 403) {
console.log("Session expired, triggering session expired modal."); 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 null;
} }
return res; return res;
} catch (err) { } catch (err) {
console.error("Fetch error:", err); console.error("Fetch error:", err);
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal if (typeof onUnauthorized === 'function') onUnauthorized();
return null; return null;
} }
}; };
// Fetch User Profile (with proper session handling) // ============= User Profile Fetch =============
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
const res = await authFetch(`${apiUrl}/user-profile`); const res = await authFetch(`${apiUrl}/user-profile`);
if (!res) return; if (!res) return;
@ -95,8 +107,13 @@ function Dashboard() {
console.error('Failed to fetch user profile'); console.error('Failed to fetch user profile');
} }
}; };
// ============= Lifecycle: Load Profile, Setup =============
useEffect(() => {
fetchUserProfile();
}, [apiUrl]); // load once
// ============= jobZone & fit Setup =============
const jobZoneLabels = { const jobZoneLabels = {
'1': 'Little or No Preparation', '1': 'Little or No Preparation',
'2': 'Some Preparation Needed', '2': 'Some Preparation Needed',
@ -111,49 +128,27 @@ function Dashboard() {
'Good': 'Good - Less Strong Match' 'Good': 'Good - Less Strong Match'
}; };
const apiUrl = process.env.REACT_APP_API_URL || ''; // Fetch job zones for each career suggestion
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]);
useEffect(() => { useEffect(() => {
const fetchJobZones = async () => { const fetchJobZones = async () => {
if (careerSuggestions.length === 0) return; if (careerSuggestions.length === 0) return;
const socCodes = careerSuggestions.map((career) => career.code); const socCodes = careerSuggestions.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 = careerSuggestions.map((career) => ({ const updatedCareers = careerSuggestions.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,
})); }));
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]);
// Filter careers by job zone, fit
const filteredCareers = useMemo(() => { const filteredCareers = useMemo(() => {
return careersWithJobZone.filter((career) => { return careersWithJobZone.filter((career) => {
const jobZoneMatches = selectedJobZone const jobZoneMatches = selectedJobZone
@ -164,11 +159,11 @@ function Dashboard() {
: true; : true;
const fitMatches = selectedFit ? career.fit === selectedFit : true; const fitMatches = selectedFit ? career.fit === selectedFit : true;
return jobZoneMatches && fitMatches; return jobZoneMatches && fitMatches;
}); });
}, [careersWithJobZone, selectedJobZone, selectedFit]); }, [careersWithJobZone, selectedJobZone, selectedFit]);
// Merge updated data into chatbot context
const updateChatbotContext = (updatedData) => { const updateChatbotContext = (updatedData) => {
setChatbotContext((prevContext) => { setChatbotContext((prevContext) => {
const mergedContext = { const mergedContext = {
@ -184,10 +179,11 @@ function Dashboard() {
}); });
}; };
// Our final array for CareerSuggestions
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]); const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
// ============= Popout Panel Setup =============
const memoizedPopoutPanel = useMemo(() => { const memoizedPopoutPanel = useMemo(() => {
return ( return (
<PopoutPanel <PopoutPanel
isVisible={!!selectedCareer} isVisible={!!selectedCareer}
@ -204,13 +200,25 @@ function Dashboard() {
updateChatbotContext={updateChatbotContext} updateChatbotContext={updateChatbotContext}
/> />
); );
}, [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(() => { useEffect(() => {
let descriptions = []; let descriptions = [];
if (location.state) { if (location.state) {
const { careerSuggestions: suggestions, riaSecScores: scores } = 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 || []); setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []); setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions); setRiaSecDescriptions(descriptions);
@ -220,8 +228,7 @@ function Dashboard() {
} }
}, [location.state, navigate]); }, [location.state, navigate]);
// Once userState, areaTitle, userZipcode, etc. are set, update chatbot
useEffect(() => { useEffect(() => {
if ( if (
careerSuggestions.length > 0 && careerSuggestions.length > 0 &&
@ -230,7 +237,6 @@ function Dashboard() {
areaTitle !== null && areaTitle !== null &&
userZipcode !== null userZipcode !== null
) { ) {
const newChatbotContext = { const newChatbotContext = {
careerSuggestions: [...careersWithJobZone], careerSuggestions: [...careersWithJobZone],
riaSecScores: [...riaSecScores], riaSecScores: [...riaSecScores],
@ -238,15 +244,16 @@ function Dashboard() {
areaTitle: areaTitle || "", areaTitle: areaTitle || "",
userZipcode: userZipcode || "", userZipcode: userZipcode || "",
}; };
setChatbotContext(newChatbotContext); setChatbotContext(newChatbotContext);
} else {
} }
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]); }, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
// ============= handleCareerClick (for tile clicks) =============
const handleCareerClick = useCallback( const handleCareerClick = useCallback(
async (career) => { async (career) => {
console.log('[handleCareerClick] career =>', career);
const socCode = career.code; const socCode = career.code;
console.log('[handleCareerClick] career.code =>', socCode);
setSelectedCareer(career); setSelectedCareer(career);
setLoading(true); setLoading(true);
setError(null); setError(null);
@ -263,15 +270,18 @@ function Dashboard() {
} }
try { try {
// CIP fetch
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
const { cipCode } = await cipResponse.json(); const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// Job details
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description'); if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
const { description, tasks } = await jobDetailsResponse.json(); const { description, tasks } = await jobDetailsResponse.json();
// Salary
let salaryResponse; let salaryResponse;
try { try {
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } }); salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
@ -279,6 +289,7 @@ function Dashboard() {
salaryResponse = { data: {} }; salaryResponse = { data: {} };
} }
// Economic
let economicResponse; let economicResponse;
try { try {
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`); economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
@ -286,6 +297,7 @@ function Dashboard() {
economicResponse = { data: {} }; economicResponse = { data: {} };
} }
// Tuition
let tuitionResponse; let tuitionResponse;
try { try {
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } }); tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
@ -293,8 +305,8 @@ function Dashboard() {
tuitionResponse = { data: {} }; tuitionResponse = { data: {} };
} }
// Fetch schools
const filteredSchools = await fetchSchools(cleanedCipCode, userState); const filteredSchools = await fetchSchools(cleanedCipCode, userState);
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => { const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`; const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
try { 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: "10th Percentile",
{ percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 }, regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
{ percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 }, nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0
{ percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 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 = { const updatedCareerDetails = {
...career, ...career,
jobDescription: description, jobDescription: description,
tasks: tasks, tasks: tasks,
economicProjections: economicResponse.data || {}, economicProjections: (economicResponse.data || {}),
salaryData: salaryDataPoints, salaryData: salaryDataPoints,
schools: schoolsWithDistance, schools: schoolsWithDistance,
tuitionData: tuitionResponse.data || [], tuitionData: (tuitionResponse.data || []),
}; };
setCareerDetails(updatedCareerDetails); setCareerDetails(updatedCareerDetails);
@ -338,11 +373,33 @@ function Dashboard() {
} finally { } finally {
setLoading(false); 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 = { const chartData = {
labels: riaSecScores.map((score) => score.area), labels: riaSecScores.map((score) => score.area),
datasets: [ datasets: [
@ -356,10 +413,9 @@ function Dashboard() {
], ],
}; };
// ============= Hide the spinner if popout is open =============
const renderLoadingOverlay = () => { const renderLoadingOverlay = () => {
// If we are NOT loading suggestions, or if the popout is visible, hide the overlay
if (!loadingSuggestions || popoutVisible) return null; if (!loadingSuggestions || popoutVisible) 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">
@ -380,31 +436,37 @@ function Dashboard() {
return ( return (
<div className="dashboard"> <div className="dashboard">
{showSessionExpiredModal && ( {showSessionExpiredModal && (
<div className="modal-overlay"> <div className="modal-overlay">
<div className="modal"> <div className="modal">
<h3>Session Expired</h3> <h3>Session Expired</h3>
<p>Your session has expired or is invalid.</p> <p>Your session has expired or is invalid.</p>
<div className="modal-actions"> <div className="modal-actions">
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}> <button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
Stay Signed In Stay Signed In
</button> </button>
<button className="confirm-btn" onClick={() => { <button
localStorage.removeItem("token"); className="confirm-btn"
localStorage.removeItem("UserId"); onClick={() => {
setShowSessionExpiredModal(false); localStorage.removeItem("token");
navigate("/signin"); localStorage.removeItem("UserId");
}}> setShowSessionExpiredModal(false);
Sign In Again navigate("/signin");
</button> }}
>
Sign In Again
</button>
</div>
</div> </div>
</div> </div>
</div> )}
)}
{renderLoadingOverlay()}
{renderLoadingOverlay()}
<div className="dashboard-content"> <div className="dashboard-content">
{/* ====== 1) The new CareerSearch bar ====== */}
{/* Existing filters + suggestions */}
<div className="career-suggestions-container"> <div className="career-suggestions-container">
<div <div
className="career-suggestions-header" className="career-suggestions-header"
@ -443,6 +505,15 @@ function Dashboard() {
))} ))}
</select> </select>
</label> </label>
<div style={{ marginLeft: 'auto' }}>
<CareerSearch
onCareerSelected={(careerObj) => {
console.log('[Dashboard] onCareerSelected =>', careerObj);
// Set the "pendingCareerForModal" so our useEffect fires below
setPendingCareerForModal(careerObj);
}}
/>
</div>
</div> </div>
<CareerSuggestions <CareerSuggestions
careerSuggestions={memoizedCareerSuggestions} careerSuggestions={memoizedCareerSuggestions}
@ -450,9 +521,11 @@ function Dashboard() {
setLoading={setLoading} setLoading={setLoading}
setProgress={setProgress} setProgress={setProgress}
userState={userState} userState={userState}
areaTitle={areaTitle}/> areaTitle={areaTitle}
/>
</div> </div>
{/* RIASEC Container */}
<div className="riasec-container"> <div className="riasec-container">
<div className="riasec-scores"> <div className="riasec-scores">
<h2>RIASEC Scores</h2> <h2>RIASEC Scores</h2>
@ -475,8 +548,10 @@ function Dashboard() {
</div> </div>
</div> </div>
{/* The PopoutPanel */}
{memoizedPopoutPanel} {memoizedPopoutPanel}
{/* Chatbot */}
<div className="chatbot-widget"> <div className="chatbot-widget">
{careerSuggestions.length > 0 ? ( {careerSuggestions.length > 0 ? (
<Chatbot context={chatbotContext} /> <Chatbot context={chatbotContext} />
@ -484,9 +559,7 @@ function Dashboard() {
<p>Loading Chatbot...</p> <p>Loading Chatbot...</p>
)} )}
</div> </div>
<div <div
className="data-source-acknowledgment" className="data-source-acknowledgment"
style={{ style={{
@ -509,4 +582,4 @@ function Dashboard() {
); );
} }
export default Dashboard; export default Dashboard;

View File

@ -496,9 +496,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
</div> </div>
<CareerSearch <CareerSearch
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)} onCareerSelected={(careerObj) => {
setPendingCareerForModal={setPendingCareerForModal} setPendingCareerForModal(careerObj.title);
authFetch={authFetch} }}
/> />
<ScenarioEditModal <ScenarioEditModal
@ -512,16 +512,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
authFetch={authFetch} authFetch={authFetch}
/> />
{pendingCareerForModal && ( {pendingCareerForModal && (
<button <button
onClick={() => { onClick={() => {
// Example Confirm // Example: Actually adopt this career as a new scenario or update the DB
console.log('TODO: handleConfirmCareerSelection => new scenario?'); console.log('User confirmed new career path:', pendingCareerForModal);
// Perhaps you open another modal or POST to your API
// Then reset pendingCareerForModal:
setPendingCareerForModal(null);
}} }}
> >
Confirm Career Change to {pendingCareerForModal} Confirm Career Change to {pendingCareerForModal}
</button> </button>
)} )}
</div> </div>
); );
}; };

View File

@ -6,8 +6,7 @@ const Paywall = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleSubscribe = () => { const handleSubscribe = () => {
// Implement subscription logic here (Stripe, etc.) navigate('/milestone-tracker');
alert('Subscription logic placeholder!');
}; };
return ( return (

View File

@ -139,7 +139,7 @@ function PopoutPanel({
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.` `Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
); );
if (decision) { if (decision) {
navigate("/financial-profile", { navigate("/paywall", {
state: { state: {
selectedCareer: { career_path_id: match.id, career_name: data.title }, selectedCareer: { career_path_id: match.id, career_name: data.title },
}, },

View File

@ -92,7 +92,7 @@ function UserProfile() {
}; };
fetchProfileAndAreas(); fetchProfileAndAreas();
}, []); }, []); // only runs once
const handleFormSubmit = async (e) => { const handleFormSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -125,12 +125,59 @@ function UserProfile() {
} }
}; };
// FULL list of states, including all 50 states (+ DC if desired)
const states = [ const states = [
{ name: 'Alabama', code: 'AL' }, { name: 'Alabama', code: 'AL' },
{ name: 'Alaska', code: 'AK' }, { name: 'Alaska', code: 'AK' },
{ name: 'Arizona', code: 'AZ' }, { name: 'Arizona', code: 'AZ' },
// ... (truncated for brevity, include all states) { name: 'Arkansas', code: 'AR' },
{ name: 'Wyoming', code: 'WY' } { 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 ( return (
@ -195,7 +242,7 @@ function UserProfile() {
/> />
</div> </div>
{/* State */} {/* State Dropdown */}
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-700"> <label className="mb-1 block text-sm font-medium text-gray-700">
State: State:

Binary file not shown.

View File

Binary file not shown.

View File