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) => (
-
-
+
+
))}
- ) : (
-
Loading questions.
- )}
-
- {currentPage > 1 && (
-
- )}
- {currentPage * questionsPerPage < questions.length ? (
-
- ) : (
-
- )}
-
+ {/* Pagination / Action Buttons */}
+
+
+ {currentPage > 1 && (
+
+ )}
+ {currentPage < totalPages ? (
+
+ ) : (
+
+ )}
+
+
+
);
diff --git a/user_profile.db b/user_profile.db
index 974afec..2dbc6d0 100644
Binary files a/user_profile.db and b/user_profile.db differ