From 0738457a83e787c7f4acdf5b25f33faa6750fdfc Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 30 Apr 2025 16:22:33 +0000 Subject: [PATCH] Fixed Loading bar in Dashboard, updated InterestInventory UI. --- src/components/CareerSuggestions.js | 176 +++++++++--------- src/components/Dashboard.js | 28 ++- src/components/InterestInventory.js | 272 +++++++++++++++++----------- user_profile.db | Bin 106496 -> 106496 bytes 4 files changed, 278 insertions(+), 198 deletions(-) diff --git a/src/components/CareerSuggestions.js b/src/components/CareerSuggestions.js index 9a97e98..de40687 100644 --- a/src/components/CareerSuggestions.js +++ b/src/components/CareerSuggestions.js @@ -1,121 +1,121 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; -import './Dashboard.css'; +import './Dashboard.css'; // or replace with Tailwind classes if desired -const apiUrl = process.env.REACT_APP_API_URL || ''; // ✅ Load API URL directly +const apiUrl = process.env.REACT_APP_API_URL || ''; -export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle, onCareerClick }) { +export function CareerSuggestions({ + careerSuggestions = [], + userState, + areaTitle, + setLoading, + setProgress, + onCareerClick, +}) { const [updatedCareers, setUpdatedCareers] = useState([]); - const [loading, setLoading] = useState(true); - const [progress, setProgress] = useState(0); useEffect(() => { + // If no careers provided, stop any loading state if (!careerSuggestions || careerSuggestions.length === 0) { setLoading(false); return; - } + } - const token = localStorage.getItem('token'); // Get auth token + const token = localStorage.getItem('token'); const checkCareerDataAvailability = async () => { setLoading(true); setProgress(0); - const totalSteps = careerSuggestions.length * 4; // Each career has 4 API checks + + // Each career has 4 external calls + const totalSteps = careerSuggestions.length * 4; let completedSteps = 0; + // Helper function to increment the global progress const updateProgress = () => { completedSteps += 1; - setProgress((completedSteps / totalSteps) * 100); + const percent = Math.round((completedSteps / totalSteps) * 100); + setProgress(percent); }; + // Universal fetch helper + const fetchJSON = async (url, params) => { + try { + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + params: params || {}, + }); + updateProgress(); // increment if success + return response.data; + } catch (error) { + updateProgress(); // increment even on failure + return null; + } + }; + + // Map over careerSuggestions to fetch CIP, job details, economic, salary data in parallel const careerPromises = careerSuggestions.map(async (career) => { try { - const headers = { - Authorization: `Bearer ${token}`, - Accept: 'application/json', + // e.g. "15-1199.00" => "15-1199" + const strippedSoc = career.code.split('.')[0]; + + const [cipData, jobDetailsData, economicData, salaryData] = await Promise.all([ + fetchJSON(`${apiUrl}/cip/${career.code}`), + fetchJSON(`${apiUrl}/onet/career-description/${career.code}`), + fetchJSON(`${apiUrl}/projections/${strippedSoc}`), + fetchJSON(`${apiUrl}/salary`, { + socCode: strippedSoc, + area: areaTitle, + }), + ]); + + // Evaluate if any data is missing + const isCipMissing = !cipData || Object.keys(cipData).length === 0; + const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).length === 0; + const isEconomicMissing = + !economicData || + Object.values(economicData).every((val) => val === 'N/A' || val === '*'); + const isSalaryMissing = !salaryData; + + const isLimitedData = + isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing; + + return { + ...career, + limitedData: isLimitedData, }; + } catch (err) { + // If any errors occur mid-logic, mark it limited + return { ...career, limitedData: true }; + } + }); - const fetchJSON = async (url) => { - try { - const response = await axios.get(url, { headers }); - updateProgress(); // ✅ Update progress on success - return response.data; - } catch (error) { - updateProgress(); // ✅ Update progress even if failed - return null; - } - }; - - // Fetch Data in Parallel - const [cipData, jobDetailsData, economicData, salaryResponse] = await Promise.all([ - fetchJSON(`${apiUrl}/cip/${career.code}`), - fetchJSON(`${apiUrl}/onet/career-description/${career.code}`), - fetchJSON(`${apiUrl}/projections/${career.code.split('.')[0]}`), - axios.get(`${apiUrl}/salary`, { - params: { socCode: career.code.split('.')[0], area: areaTitle }, - headers, - }).then((res) => { - updateProgress(); - return res.data; - }).catch((error) => { - updateProgress(); - if (error.response?.status === 404) { - return null; - } - return error.response; - }), - ]); - - const isCipMissing = !cipData || Object.keys(cipData).length === 0; - const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).length === 0; - const isEconomicMissing = !economicData || Object.values(economicData).every(val => val === "N/A" || val === "*"); - const isSalaryMissing = salaryResponse === null || salaryResponse === undefined; - - const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing; - - return { ...career, limitedData: isLimitedData }; - - } catch (error) { - return { ...career, limitedData: true }; + try { + const updatedCareerList = await Promise.all(careerPromises); + setUpdatedCareers(updatedCareerList); + } finally { + setLoading(false); } - }); - - try { - const updatedCareerList = await Promise.all(careerPromises); - setUpdatedCareers(updatedCareerList); - } finally { - setLoading(false); - } - }; + }; checkCareerDataAvailability(); - }, [careerSuggestions, apiUrl, userState, areaTitle]); + }, [careerSuggestions, userState, areaTitle, setLoading, setProgress]); return ( -
- - {loading ? ( -
-
- {Math.round(progress)}% -
-

Loading Career Suggestions...

-
- ) : ( -
- {updatedCareers.map((career) => ( - - ))} -
- )} +
+ {updatedCareers.map((career) => ( + + ))}
); } diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index b0fc0a7..068a143 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -26,6 +26,7 @@ function Dashboard() { const [economicProjections, setEconomicProjections] = useState(null); const [tuitionData, setTuitionData] = useState(null); 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); @@ -359,6 +360,26 @@ function Dashboard() { ], }; + const renderLoadingOverlay = () => { + if (!loading) return null; + + return ( +
+
+
+
+
+

+ {progress}% — Loading Career Suggestions... +

+
+
+ ); + }; + return (
{showSessionExpiredModal && ( @@ -383,6 +404,8 @@ function Dashboard() {
)} + {renderLoadingOverlay()} +
@@ -427,7 +450,10 @@ function Dashboard() { + setLoading={setLoading} + setProgress={setProgress} + userState={userState} + areaTitle={areaTitle}/>
diff --git a/src/components/InterestInventory.js b/src/components/InterestInventory.js index 5e04892..1c9f5be 100644 --- a/src/components/InterestInventory.js +++ b/src/components/InterestInventory.js @@ -1,79 +1,69 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { ClipLoader } from 'react-spinners'; -import authFetch from '../utils/authFetch.js'; -import './InterestInventory.css'; +import authFetch from '../utils/authFetch.js'; const InterestInventory = () => { const [questions, setQuestions] = useState([]); const [responses, setResponses] = useState({}); const [currentPage, setCurrentPage] = useState(1); const [isSubmitting, setIsSubmitting] = useState(false); - const questionsPerPage = 6; - const navigate = useNavigate(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [userProfile, setUserProfile] = useState(null); - const userId = localStorage.getItem('userId'); - const apiUrl = process.env.REACT_APP_API_URL || ''; + const navigate = useNavigate(); - const fetchQuestions = async () => { - setLoading(true); // Start loading - setError(null); // Reset error state - - const url = '/api/onet/questions?start=1&end=60'; - try { - const response = await authFetch(url, { - method: 'GET', - headers: { Accept: 'application/json' }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch questions: ${response.statusText}`); - } - - const data = await response.json(); - - if (data && Array.isArray(data.questions)) { - setQuestions(data.questions); - console.log("Questions fetched:", data.questions); - } else { - throw new Error("Invalid question format."); - } - } catch (error) { - console.error("Error fetching questions:", error.message); - setError(error.message); // Set error message - } finally { - setLoading(false); // Stop loading - } - }; + const questionsPerPage = 6; + const totalPages = Math.ceil(questions.length / questionsPerPage) || 1; useEffect(() => { fetchQuestions(); fetchUserProfile(); }, []); + const fetchQuestions = async () => { + setLoading(true); + setError(null); + + try { + const response = await authFetch('/api/onet/questions?start=1&end=60', { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + if (!response.ok) { + throw new Error(`Failed to fetch questions: ${response.statusText}`); + } + + const data = await response.json(); + if (data && Array.isArray(data.questions)) { + setQuestions(data.questions); + } else { + throw new Error('Invalid question format.'); + } + } catch (err) { + setError(err.message); + console.error('Error fetching questions:', err.message); + } finally { + setLoading(false); + } + }; + const fetchUserProfile = async () => { try { - const res = await authFetch('/api/user-profile', { - method: 'GET', - }); - + const res = await authFetch('/api/user-profile', { method: 'GET' }); if (!res || !res.ok) throw new Error('Failed to fetch user profile'); - const data = await res.json(); setUserProfile(data); } catch (err) { console.error('Error fetching user profile:', err.message); } }; - - - + + // Restore previously saved answers if available useEffect(() => { const storedAnswers = userProfile?.interest_inventory_answers; - if (questions.length === 60 && storedAnswers && storedAnswers.length === 60) { + if (questions.length === 60 && storedAnswers?.length === 60) { const restored = {}; storedAnswers.split('').forEach((val, index) => { restored[index + 1] = val; @@ -81,23 +71,14 @@ const InterestInventory = () => { setResponses(restored); } }, [questions, userProfile]); - - + const handleResponseChange = (questionIndex, value) => { - setResponses((prevResponses) => ({ - ...prevResponses, + setResponses((prev) => ({ + ...prev, [questionIndex]: value, })); }; - const randomizeAnswers = () => { - const randomizedResponses = {}; - questions.forEach((question) => { - randomizedResponses[question.index] = Math.floor(Math.random() * 5) + 1; - }); - setResponses(randomizedResponses); - }; - const validateCurrentPage = () => { const start = (currentPage - 1) * questionsPerPage; const end = currentPage * questionsPerPage; @@ -106,9 +87,8 @@ const InterestInventory = () => { const unanswered = currentQuestions.filter( (q) => !responses[q.index] || responses[q.index] === '0' ); - if (unanswered.length > 0) { - alert(`Please answer all questions before proceeding.`); + alert('Please answer all questions before proceeding.'); return false; } return true; @@ -125,77 +105,124 @@ const InterestInventory = () => { } }; + const randomizeAnswers = () => { + const randomized = {}; + questions.forEach((question) => { + randomized[question.index] = Math.floor(Math.random() * 5) + 1; // 1–5 + }); + setResponses(randomized); + }; + const handleSubmit = async () => { if (!validateCurrentPage()) return; - + + // Combine answers into a 60-char string const answers = Array.from({ length: 60 }, (_, i) => responses[i + 1] || '0').join(''); - await authFetch(`${apiUrl}/user-profile`, { - method: 'POST', - body: JSON.stringify({ - firstName: userProfile?.firstname, - lastName: userProfile?.lastname, - email: userProfile?.email, - zipCode: userProfile?.zipcode, - state: userProfile?.state, - area: userProfile?.area, - careerSituation: userProfile?.career_situation || null, - interest_inventory_answers: answers, - }), - }); - - + // First save the answers to user profile + try { + await authFetch('/api/user-profile', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + firstName: userProfile?.firstname, + lastName: userProfile?.lastname, + email: userProfile?.email, + zipCode: userProfile?.zipcode, + state: userProfile?.state, + area: userProfile?.area, + careerSituation: userProfile?.career_situation || null, + interest_inventory_answers: answers, + }), + }); + } catch (err) { + console.error('Error saving answers to user profile:', err.message); + } + // Then submit to the O*Net logic try { setIsSubmitting(true); - setError(null); // Clear previous errors - const url = `${process.env.REACT_APP_API_URL}/onet/submit_answers`; - const response = await authFetch(url, { + setError(null); + const response = await authFetch(`${process.env.REACT_APP_API_URL}/onet/submit_answers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answers }), }); - + if (!response.ok) { throw new Error(`Failed to submit answers: ${response.statusText}`); } - const data = await response.json(); - console.log("Careers Response:", data); - const { careers: careerSuggestions, riaSecScores } = data; if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) { navigate('/dashboard', { state: { careerSuggestions, riaSecScores } }); } else { - console.error("Invalid data format:", data); - alert("Failed to process results. Please try again later."); + throw new Error('Invalid data format from the server.'); } } catch (error) { - console.error("Error submitting answers:", error.message); - alert("Failed to submit answers. Please try again later."); + console.error('Error submitting answers:', error.message); + alert('Failed to submit answers. Please try again later.'); } finally { setIsSubmitting(false); } }; - + // Compute which questions to show const start = (currentPage - 1) * questionsPerPage; const end = currentPage * questionsPerPage; const currentQuestions = questions.slice(start, end); - return ( -
-

Interest Inventory

- {loading && } - {error &&

{error}

} + // Calculate progress for the bar + const totalQuestions = 60; + const answeredCount = Object.keys(responses).filter((key) => responses[key] !== '0').length; + const progressPercent = Math.round((answeredCount / totalQuestions) * 100); - {questions.length > 0 ? ( -
+ return ( +
+ {/* Card Container */} +
+

+ Interest Inventory +

+ + {/* Loading & Error States */} + {loading && ( +
+ +
+ )} + {error && ( +

+ {error} +

+ )} + + {/* Progress Bar & Page Indicator */} +
+

+ Page {currentPage} of {totalPages} +

+
+
+
+

+ {answeredCount} / {totalQuestions} answered +

+
+ + {/* Questions */} +
{currentQuestions.map((question) => ( -
- +
+