Fixed Loading bar in Dashboard, updated InterestInventory UI.
This commit is contained in:
parent
c96cea59bd
commit
0738457a83
@ -1,121 +1,121 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import axios from 'axios';
|
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 [updatedCareers, setUpdatedCareers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// If no careers provided, stop any loading state
|
||||||
if (!careerSuggestions || careerSuggestions.length === 0) {
|
if (!careerSuggestions || careerSuggestions.length === 0) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token'); // Get auth token
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
const checkCareerDataAvailability = async () => {
|
const checkCareerDataAvailability = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setProgress(0);
|
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;
|
let completedSteps = 0;
|
||||||
|
|
||||||
|
// Helper function to increment the global progress
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
completedSteps += 1;
|
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) => {
|
const careerPromises = careerSuggestions.map(async (career) => {
|
||||||
try {
|
try {
|
||||||
const headers = {
|
// e.g. "15-1199.00" => "15-1199"
|
||||||
Authorization: `Bearer ${token}`,
|
const strippedSoc = career.code.split('.')[0];
|
||||||
Accept: 'application/json',
|
|
||||||
|
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 {
|
||||||
try {
|
const updatedCareerList = await Promise.all(careerPromises);
|
||||||
const response = await axios.get(url, { headers });
|
setUpdatedCareers(updatedCareerList);
|
||||||
updateProgress(); // ✅ Update progress on success
|
} finally {
|
||||||
return response.data;
|
setLoading(false);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkCareerDataAvailability();
|
checkCareerDataAvailability();
|
||||||
}, [careerSuggestions, apiUrl, userState, areaTitle]);
|
}, [careerSuggestions, userState, areaTitle, setLoading, setProgress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="career-suggestions-grid">
|
||||||
|
{updatedCareers.map((career) => (
|
||||||
{loading ? (
|
<button
|
||||||
<div className="progress-container">
|
key={career.code}
|
||||||
<div className="progress-bar" style={{
|
className={`career-button ${career.limitedData ? 'limited-data' : ''}`}
|
||||||
width: `${progress}%`,
|
onClick={() => onCareerClick(career)}
|
||||||
maxWidth: "100%", }}>
|
>
|
||||||
{Math.round(progress)}%
|
{career.title}
|
||||||
</div>
|
{career.limitedData && <span className="warning-icon"> ⚠️</span>}
|
||||||
<p>Loading Career Suggestions...</p>
|
</button>
|
||||||
</div>
|
))}
|
||||||
) : (
|
|
||||||
<div className="career-suggestions-grid">
|
|
||||||
{updatedCareers.map((career) => (
|
|
||||||
<button
|
|
||||||
key={career.code}
|
|
||||||
className={`career-button ${career.limitedData ? 'limited-data' : ''}`}
|
|
||||||
onClick={() => onCareerClick(career)}
|
|
||||||
>
|
|
||||||
{career.title} {career.limitedData && <span className="warning-icon">⚠️</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ function Dashboard() {
|
|||||||
const [economicProjections, setEconomicProjections] = useState(null);
|
const [economicProjections, setEconomicProjections] = useState(null);
|
||||||
const [tuitionData, setTuitionData] = useState(null);
|
const [tuitionData, setTuitionData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
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);
|
||||||
@ -359,6 +360,26 @@ function Dashboard() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderLoadingOverlay = () => {
|
||||||
|
if (!loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="mb-2 w-full max-w-md rounded bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded bg-blue-500 transition-all"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-center text-sm text-gray-600">
|
||||||
|
{progress}% — Loading Career Suggestions...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
{showSessionExpiredModal && (
|
{showSessionExpiredModal && (
|
||||||
@ -383,6 +404,8 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{renderLoadingOverlay()}
|
||||||
|
|
||||||
<div className="dashboard-content">
|
<div className="dashboard-content">
|
||||||
|
|
||||||
<div className="career-suggestions-container">
|
<div className="career-suggestions-container">
|
||||||
@ -427,7 +450,10 @@ function Dashboard() {
|
|||||||
<CareerSuggestions
|
<CareerSuggestions
|
||||||
careerSuggestions={memoizedCareerSuggestions}
|
careerSuggestions={memoizedCareerSuggestions}
|
||||||
onCareerClick={handleCareerClick}
|
onCareerClick={handleCareerClick}
|
||||||
/>
|
setLoading={setLoading}
|
||||||
|
setProgress={setProgress}
|
||||||
|
userState={userState}
|
||||||
|
areaTitle={areaTitle}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="riasec-container">
|
<div className="riasec-container">
|
||||||
|
@ -1,79 +1,69 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ClipLoader } from 'react-spinners';
|
import { ClipLoader } from 'react-spinners';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import './InterestInventory.css';
|
|
||||||
|
|
||||||
const InterestInventory = () => {
|
const InterestInventory = () => {
|
||||||
const [questions, setQuestions] = useState([]);
|
const [questions, setQuestions] = useState([]);
|
||||||
const [responses, setResponses] = useState({});
|
const [responses, setResponses] = useState({});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const questionsPerPage = 6;
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [userProfile, setUserProfile] = 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 () => {
|
const questionsPerPage = 6;
|
||||||
setLoading(true); // Start loading
|
const totalPages = Math.ceil(questions.length / questionsPerPage) || 1;
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchQuestions();
|
fetchQuestions();
|
||||||
fetchUserProfile();
|
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 () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/user-profile', {
|
const res = await authFetch('/api/user-profile', { method: 'GET' });
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res || !res.ok) throw new Error('Failed to fetch user profile');
|
if (!res || !res.ok) throw new Error('Failed to fetch user profile');
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setUserProfile(data);
|
setUserProfile(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching user profile:', err.message);
|
console.error('Error fetching user profile:', err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restore previously saved answers if available
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedAnswers = userProfile?.interest_inventory_answers;
|
const storedAnswers = userProfile?.interest_inventory_answers;
|
||||||
if (questions.length === 60 && storedAnswers && storedAnswers.length === 60) {
|
if (questions.length === 60 && storedAnswers?.length === 60) {
|
||||||
const restored = {};
|
const restored = {};
|
||||||
storedAnswers.split('').forEach((val, index) => {
|
storedAnswers.split('').forEach((val, index) => {
|
||||||
restored[index + 1] = val;
|
restored[index + 1] = val;
|
||||||
@ -81,23 +71,14 @@ const InterestInventory = () => {
|
|||||||
setResponses(restored);
|
setResponses(restored);
|
||||||
}
|
}
|
||||||
}, [questions, userProfile]);
|
}, [questions, userProfile]);
|
||||||
|
|
||||||
|
|
||||||
const handleResponseChange = (questionIndex, value) => {
|
const handleResponseChange = (questionIndex, value) => {
|
||||||
setResponses((prevResponses) => ({
|
setResponses((prev) => ({
|
||||||
...prevResponses,
|
...prev,
|
||||||
[questionIndex]: value,
|
[questionIndex]: value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const randomizeAnswers = () => {
|
|
||||||
const randomizedResponses = {};
|
|
||||||
questions.forEach((question) => {
|
|
||||||
randomizedResponses[question.index] = Math.floor(Math.random() * 5) + 1;
|
|
||||||
});
|
|
||||||
setResponses(randomizedResponses);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCurrentPage = () => {
|
const validateCurrentPage = () => {
|
||||||
const start = (currentPage - 1) * questionsPerPage;
|
const start = (currentPage - 1) * questionsPerPage;
|
||||||
const end = currentPage * questionsPerPage;
|
const end = currentPage * questionsPerPage;
|
||||||
@ -106,9 +87,8 @@ const InterestInventory = () => {
|
|||||||
const unanswered = currentQuestions.filter(
|
const unanswered = currentQuestions.filter(
|
||||||
(q) => !responses[q.index] || responses[q.index] === '0'
|
(q) => !responses[q.index] || responses[q.index] === '0'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unanswered.length > 0) {
|
if (unanswered.length > 0) {
|
||||||
alert(`Please answer all questions before proceeding.`);
|
alert('Please answer all questions before proceeding.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateCurrentPage()) return;
|
if (!validateCurrentPage()) return;
|
||||||
|
|
||||||
|
// Combine answers into a 60-char string
|
||||||
const answers = Array.from({ length: 60 }, (_, i) => responses[i + 1] || '0').join('');
|
const answers = Array.from({ length: 60 }, (_, i) => responses[i + 1] || '0').join('');
|
||||||
|
|
||||||
await authFetch(`${apiUrl}/user-profile`, {
|
// First save the answers to user profile
|
||||||
method: 'POST',
|
try {
|
||||||
body: JSON.stringify({
|
await authFetch('/api/user-profile', {
|
||||||
firstName: userProfile?.firstname,
|
method: 'POST',
|
||||||
lastName: userProfile?.lastname,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
email: userProfile?.email,
|
body: JSON.stringify({
|
||||||
zipCode: userProfile?.zipcode,
|
firstName: userProfile?.firstname,
|
||||||
state: userProfile?.state,
|
lastName: userProfile?.lastname,
|
||||||
area: userProfile?.area,
|
email: userProfile?.email,
|
||||||
careerSituation: userProfile?.career_situation || null,
|
zipCode: userProfile?.zipcode,
|
||||||
interest_inventory_answers: answers,
|
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 {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setError(null); // Clear previous errors
|
setError(null);
|
||||||
const url = `${process.env.REACT_APP_API_URL}/onet/submit_answers`;
|
const response = await authFetch(`${process.env.REACT_APP_API_URL}/onet/submit_answers`, {
|
||||||
const response = await authFetch(url, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ answers }),
|
body: JSON.stringify({ answers }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to submit answers: ${response.statusText}`);
|
throw new Error(`Failed to submit answers: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Careers Response:", data);
|
|
||||||
|
|
||||||
const { careers: careerSuggestions, riaSecScores } = data;
|
const { careers: careerSuggestions, riaSecScores } = data;
|
||||||
|
|
||||||
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
||||||
navigate('/dashboard', { state: { careerSuggestions, riaSecScores } });
|
navigate('/dashboard', { state: { careerSuggestions, riaSecScores } });
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid data format:", data);
|
throw new Error('Invalid data format from the server.');
|
||||||
alert("Failed to process results. Please try again later.");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting answers:", error.message);
|
console.error('Error submitting answers:', error.message);
|
||||||
alert("Failed to submit answers. Please try again later.");
|
alert('Failed to submit answers. Please try again later.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Compute which questions to show
|
||||||
const start = (currentPage - 1) * questionsPerPage;
|
const start = (currentPage - 1) * questionsPerPage;
|
||||||
const end = currentPage * questionsPerPage;
|
const end = currentPage * questionsPerPage;
|
||||||
const currentQuestions = questions.slice(start, end);
|
const currentQuestions = questions.slice(start, end);
|
||||||
|
|
||||||
return (
|
// Calculate progress for the bar
|
||||||
<div className="interest-inventory-container">
|
const totalQuestions = 60;
|
||||||
<h2>Interest Inventory</h2>
|
const answeredCount = Object.keys(responses).filter((key) => responses[key] !== '0').length;
|
||||||
{loading && <ClipLoader size={35} color="#4A90E2" />}
|
const progressPercent = Math.round((answeredCount / totalQuestions) * 100);
|
||||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
||||||
|
|
||||||
{questions.length > 0 ? (
|
return (
|
||||||
<div className="questions-container">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-4">
|
||||||
|
{/* Card Container */}
|
||||||
|
<div className="w-full max-w-xl rounded bg-white p-6 shadow-md">
|
||||||
|
<h2 className="mb-4 text-center text-2xl font-semibold">
|
||||||
|
Interest Inventory
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Loading & Error States */}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<ClipLoader size={35} color="#4A90E2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<p className="mb-4 rounded bg-red-50 p-2 text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Progress Bar & Page Indicator */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 h-2 w-full overflow-hidden rounded bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="h-full bg-blue-600 transition-all"
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-right text-xs text-gray-500">
|
||||||
|
{answeredCount} / {totalQuestions} answered
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Questions */}
|
||||||
|
<div className="space-y-4">
|
||||||
{currentQuestions.map((question) => (
|
{currentQuestions.map((question) => (
|
||||||
<div key={question.index} className="question">
|
<div key={question.index} className="flex flex-col">
|
||||||
<label>{question.text}</label>
|
<label className="mb-1 font-medium text-gray-700">
|
||||||
|
{question.text}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
className="rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none"
|
||||||
onChange={(e) => handleResponseChange(question.index, e.target.value)}
|
onChange={(e) => handleResponseChange(question.index, e.target.value)}
|
||||||
value={responses[question.index] || '0'}
|
value={responses[question.index] || '0'}
|
||||||
>
|
>
|
||||||
@ -209,20 +236,47 @@ const InterestInventory = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p>Loading questions.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="pagination-buttons">
|
{/* Pagination / Action Buttons */}
|
||||||
{currentPage > 1 && (
|
<div className="mt-6 flex flex-wrap items-center justify-between space-y-2 sm:space-y-0">
|
||||||
<button type="button" onClick={handlePreviousPage}>Previous</button>
|
<div className="flex space-x-2">
|
||||||
)}
|
{currentPage > 1 && (
|
||||||
{currentPage * questionsPerPage < questions.length ? (
|
<button
|
||||||
<button type="button" onClick={handleNextPage}>Next</button>
|
type="button"
|
||||||
) : (
|
onClick={handlePreviousPage}
|
||||||
<button type="button" onClick={handleSubmit} disabled={isSubmitting}>Submit</button>
|
className="rounded bg-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-400"
|
||||||
)}
|
>
|
||||||
<button type="button" onClick={randomizeAnswers} disabled={isSubmitting}>Randomize Answers</button>
|
Previous
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentPage < totalPages ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNextPage}
|
||||||
|
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700 disabled:bg-green-300"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={randomizeAnswers}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600 disabled:bg-orange-300"
|
||||||
|
>
|
||||||
|
Randomize Answers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user