UX issues, schools issues
This commit is contained in:
parent
e07dc32b51
commit
9696943f9b
@ -171,7 +171,7 @@ app.post('/api/login', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT
|
// Generate JWT
|
||||||
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '1h' });
|
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' });
|
||||||
res.status(200).json({ token });
|
res.status(200).json({ token });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -346,10 +346,16 @@ app.get('/api/cip/:socCode', (req, res) => {
|
|||||||
res.status(404).json({ error: 'CIP code not found for this SOC code' });
|
res.status(404).json({ error: 'CIP code not found for this SOC code' });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/CIP_institution_mapping_fixed.json', (req, res) => {
|
// Filtered schools endpoint
|
||||||
const filePath = path.join(__dirname, 'CIP_institution_mapping_fixed.json'); // Adjust the path if needed
|
app.get('/api/schools', (req, res) => {
|
||||||
res.sendFile(filePath);
|
const { cipCode, state, level, type } = req.query;
|
||||||
});
|
const filePath = path.join(__dirname, 'CIP_institution_mapping_fixed.json');
|
||||||
|
|
||||||
|
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error reading file:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to load data' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
26
src/App.css
26
src/App.css
@ -1,18 +1,25 @@
|
|||||||
/* src/App.css */
|
/* src/App.css */
|
||||||
|
|
||||||
/* Body styling for the entire application */
|
/* Body styling for the entire application */
|
||||||
body {
|
/* General body and root styling for full-page coverage */
|
||||||
background-color: #e9ecef; /* Light gray-blue background */
|
body, #root {
|
||||||
|
background-color: #f4f7fb; /* Light gray-blue background */
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
min-height: 100vh; /* Full viewport height */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main container to center and constrain content */
|
/* Main App container for consistent centering */
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 20px auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background-color: #ffffff; /* White background for sections */
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main App styling */
|
/* Main App styling */
|
||||||
@ -110,3 +117,14 @@ input, select {
|
|||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard {
|
||||||
|
grid-template-columns: 1fr; /* Stack vertically on smaller screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
.riasec-scores, .riasec-descriptions {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Career Suggestions</h3>
|
<h2>Career Suggestions</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{careerSuggestions.map((career, index) => {
|
{careerSuggestions.map((career, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
.dashboard {
|
.dashboard {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Ensure responsive layout */
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Ensure responsive layout */
|
||||||
gap: 15px;
|
gap: 20px;
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
background-color: #f4f7fa;
|
background-color: #f4f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,3 +108,26 @@ h2 {
|
|||||||
.career-item:hover {
|
.career-item:hover {
|
||||||
background-color: #e6f7ff; /* Lighter blue when hovering */
|
background-color: #e6f7ff; /* Lighter blue when hovering */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.riasec-descriptions {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f1f8ff;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.riasec-descriptions ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.riasec-descriptions li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.riasec-descriptions strong {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ function Dashboard() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
|
const [careerDetails, setCareerDetails] = useState(null);
|
||||||
const [riaSecScores, setRiaSecScores] = useState([]);
|
const [riaSecScores, setRiaSecScores] = useState([]);
|
||||||
const [selectedCareer, setSelectedCareer] = useState(null);
|
const [selectedCareer, setSelectedCareer] = useState(null);
|
||||||
const [schools, setSchools] = useState([]);
|
const [schools, setSchools] = useState([]);
|
||||||
@ -26,14 +27,19 @@ function Dashboard() {
|
|||||||
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);
|
||||||
|
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let descriptions = []; // Declare outside for scope accessibility
|
||||||
if (location.state) {
|
if (location.state) {
|
||||||
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
||||||
setCareerSuggestions(suggestions || []);
|
descriptions = scores.map((score) => score.description || "No description available.");
|
||||||
|
setCareerSuggestions(suggestions || []);
|
||||||
setRiaSecScores(scores || []);
|
setRiaSecScores(scores || []);
|
||||||
} else {
|
setRiaSecDescriptions(descriptions); // Set descriptions
|
||||||
|
} else {
|
||||||
console.warn('No data found, redirecting to Interest Inventory');
|
console.warn('No data found, redirecting to Interest Inventory');
|
||||||
navigate('/interest-inventory');
|
navigate('/interest-inventory');
|
||||||
}
|
}
|
||||||
@ -68,7 +74,16 @@ function Dashboard() {
|
|||||||
|
|
||||||
const handleCareerClick = useCallback(
|
const handleCareerClick = useCallback(
|
||||||
async (career) => {
|
async (career) => {
|
||||||
const socCode = career.code;
|
const socCode = career.code; // Extract SOC code from career object
|
||||||
|
setSelectedCareer(career); // Set career first to trigger loading panel
|
||||||
|
setLoading(true); // Enable loading state only when career is clicked
|
||||||
|
setError(null); // Clear previous errors
|
||||||
|
setCareerDetails({}); // Reset career details to avoid undefined errors
|
||||||
|
setSchools([]);
|
||||||
|
setSalaryData([]);
|
||||||
|
setEconomicProjections({});
|
||||||
|
setTuitionData([]);
|
||||||
|
|
||||||
if (!socCode) {
|
if (!socCode) {
|
||||||
console.error('SOC Code is missing');
|
console.error('SOC Code is missing');
|
||||||
setError('SOC Code is missing');
|
setError('SOC Code is missing');
|
||||||
@ -76,35 +91,25 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
// Step 1: Fetch CIP Code
|
||||||
setError(null);
|
const cipResponse = await fetch(`${apiUrl}/api/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);
|
||||||
|
|
||||||
const cipResponse = await fetch(`${apiUrl}/api/cip/${socCode}`);
|
// Step 2: Fetch Data in Parallel
|
||||||
|
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
|
||||||
if (!cipResponse.ok) {
|
fetchSchools(cleanedCipCode, userState),
|
||||||
console.error('Failed to fetch CIP Code');
|
axios.get(`http://localhost:5001/api/projections/${socCode.split('.')[0]}`),
|
||||||
setError('Failed to fetch CIP Code');
|
axios.get(`http://localhost:5001/api/tuition/${cleanedCipCode}`, {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { cipCode } = await cipResponse.json();
|
|
||||||
if (!cipCode) throw new Error('Failed to fetch CIP Code');
|
|
||||||
|
|
||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
|
||||||
|
|
||||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
|
||||||
const cleanedSocCode = socCode.split('.')[0];
|
|
||||||
|
|
||||||
const economicResponse = await axios.get(`http://localhost:5001/api/projections/${cleanedSocCode}`);
|
|
||||||
const tuitionResponse = await axios.get(`http://localhost:5001/api/tuition/${cleanedCipCode}`, {
|
|
||||||
params: { state: userState },
|
params: { state: userState },
|
||||||
});
|
}),
|
||||||
|
axios.get(`http://localhost:5001/api/salary`, {
|
||||||
console.log('Salary Data Request Params:', { socCode: cleanedSocCode, area: areaTitle });
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
const salaryResponse = await axios.get(`http://localhost:5001/api/salary`, {
|
}),
|
||||||
params: { socCode: cleanedSocCode, area: areaTitle },
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
|
// Step 3: Format Salary Data
|
||||||
const salaryDataPoints = [
|
const salaryDataPoints = [
|
||||||
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
|
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
|
||||||
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
|
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
|
||||||
@ -113,11 +118,14 @@ function Dashboard() {
|
|||||||
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
setSelectedCareer(career);
|
// Step 4: Consolidate Career Details
|
||||||
setSchools(filteredSchools);
|
setCareerDetails({
|
||||||
setEconomicProjections(economicResponse.data);
|
...career,
|
||||||
setTuitionData(tuitionResponse.data);
|
economicProjections: economicResponse.data,
|
||||||
setSalaryData(salaryDataPoints);
|
salaryData: salaryDataPoints,
|
||||||
|
schools: filteredSchools,
|
||||||
|
tuitionData: tuitionResponse.data,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing career click:', error.message);
|
console.error('Error processing career click:', error.message);
|
||||||
setError('Failed to load data');
|
setError('Failed to load data');
|
||||||
@ -144,25 +152,43 @@ function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<div className="career-suggestions-container">
|
<div className="career-suggestions-container">
|
||||||
<h2>Career Suggestions</h2>
|
|
||||||
<CareerSuggestions careerSuggestions={careerSuggestions} onCareerClick={handleCareerClick} />
|
<CareerSuggestions careerSuggestions={careerSuggestions} onCareerClick={handleCareerClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="riasec-scores">
|
{/* Right RIASEC Chart + Descriptions */}
|
||||||
<h2>RIASEC Scores</h2>
|
<div className="riasec-container">
|
||||||
<Bar data={chartData} />
|
<div className="riasec-scores">
|
||||||
</div>
|
<h2>RIASEC Scores</h2>
|
||||||
|
<Bar data={chartData} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="riasec-descriptions">
|
||||||
|
<h3>RIASEC Personality Descriptions</h3>
|
||||||
|
{riaSecDescriptions.length > 0 ? (
|
||||||
|
<ul>
|
||||||
|
{riaSecDescriptions.map((desc, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<strong>{riaSecScores[index]?.area}:</strong> {desc}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>Loading descriptions...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
<PopoutPanel
|
<PopoutPanel
|
||||||
career={selectedCareer}
|
data={careerDetails}
|
||||||
schools={schools || []}
|
schools={schools}
|
||||||
salaryData={salaryData || []}
|
salaryData={salaryData}
|
||||||
economicProjections={economicProjections || {}}
|
economicProjections={economicProjections}
|
||||||
tuitionData={tuitionData || {}}
|
tuitionData={tuitionData}
|
||||||
closePanel={() => setSelectedCareer(null)}
|
closePanel={() => setSelectedCareer(null)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
|
userState={userState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,78 +1,121 @@
|
|||||||
/* src/components/InterestInventory.css */
|
/* src/components/InterestInventory.css */
|
||||||
|
|
||||||
|
/* Container Styling */
|
||||||
.interest-inventory-container {
|
.interest-inventory-container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 40px auto; /* Center the container */
|
margin: 40px auto; /* Center the container */
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #8d98f8; /* White background */
|
background-color: #f0f8ff; /* Sky-white background */
|
||||||
border-radius: 8px;
|
border-radius: 12px; /* Rounded borders */
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* Subtle shadow effect */
|
||||||
border: 1px solid #060c02;
|
border: 4px solid #87ceeb; /* Sky-blue trim */
|
||||||
}
|
}
|
||||||
|
|
||||||
.interest-inventory-container h2 {
|
/* Header Styling */
|
||||||
color: #0e0202;
|
.interest-inventory-container h2 {
|
||||||
text-align: center;
|
color: #0056b3; /* Dark blue header */
|
||||||
margin-bottom: 20px;
|
text-align: center;
|
||||||
}
|
font-size: 28px; /* Larger font size for emphasis */
|
||||||
|
margin-bottom: 20px;
|
||||||
.question {
|
font-weight: bold;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
|
||||||
|
/* Question Container */
|
||||||
/* Label styling for each question */
|
.question {
|
||||||
.question label {
|
margin-bottom: 20px;
|
||||||
display: block;
|
}
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 8px;
|
/* Label Styling */
|
||||||
color: #0f0101;
|
.question label {
|
||||||
}
|
display: block;
|
||||||
|
font-size: 18px; /* Slightly larger text for questions */
|
||||||
/* Set fixed width and max-width for dropdowns within InterestInventory */
|
font-weight: 600; /* Semi-bold text */
|
||||||
.question select {
|
margin-bottom: 8px;
|
||||||
width: auto; /* Allow it to adjust based on content */
|
color: #333; /* Darker text for readability */
|
||||||
max-width: 300px; /* Set a maximum width to prevent stretching */
|
}
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #ccc;
|
/* Dropdown Styling */
|
||||||
border-radius: 4px;
|
.question select {
|
||||||
font-size: 16px;
|
width: 80%; /* Use percentage for responsiveness */
|
||||||
}
|
max-width: 400px; /* Limit maximum size */
|
||||||
|
padding: 10px;
|
||||||
/* Button styling for a uniform appearance */
|
border: 1px solid #87ceeb; /* Sky-blue border */
|
||||||
button {
|
border-radius: 8px; /* Rounded edges */
|
||||||
margin-top: 20px;
|
font-size: 16px;
|
||||||
padding: 10px 20px;
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */
|
||||||
background-color: #007bff;
|
transition: border-color 0.3s ease;
|
||||||
color: #fff;
|
display: block;
|
||||||
border: none;
|
margin: 0 auto; /* Center the dropdown */
|
||||||
border-radius: 4px;
|
}
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
/* Dropdown Hover Effect */
|
||||||
}
|
.question select:hover {
|
||||||
|
border-color: #007bff; /* Brighter blue on hover */
|
||||||
button:hover {
|
}
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
/* Dropdown Focus Effect */
|
||||||
|
.question select:focus {
|
||||||
/* Submission message styling */
|
border-color: #0056b3;
|
||||||
.submit-message {
|
outline: none;
|
||||||
color: #28a745;
|
}
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
/* Buttons */
|
||||||
margin-top: 20px;
|
button {
|
||||||
}
|
margin: 10px 5px; /* Add spacing between buttons */
|
||||||
|
padding: 10px 20px;
|
||||||
.validation-error-container {
|
background-color: #007bff; /* Blue button */
|
||||||
margin-top: 10px; /* Position the error message below the questions */
|
color: #fff;
|
||||||
}
|
border: none;
|
||||||
|
border-radius: 8px; /* Rounded edges */
|
||||||
.validation-error {
|
cursor: pointer;
|
||||||
color: red;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unanswered {
|
/* Hover Effect for Buttons */
|
||||||
border: 2px solid #b22222; /* Darker red (Firebrick) */
|
button:hover {
|
||||||
background-color: #ee6161; /* Light red background remains unchanged */
|
background-color: #0056b3; /* Darker blue */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Submit Button Styling */
|
||||||
|
button:disabled {
|
||||||
|
background-color: #a0aec0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submission Message Styling */
|
||||||
|
.submit-message {
|
||||||
|
color: #28a745; /* Green success text */
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation Error Message */
|
||||||
|
.validation-error {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight Unanswered Questions */
|
||||||
|
.unanswered {
|
||||||
|
border: 2px solid #b22222; /* Darker red (Firebrick) */
|
||||||
|
background-color: #ffe6e6; /* Light red background */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Buttons Styling */
|
||||||
|
.pagination-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Randomize Answers Button */
|
||||||
|
.randomize-button {
|
||||||
|
background-color: #17a2b8; /* Teal button */
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.randomize-button:hover {
|
||||||
|
background-color: #138496; /* Darker teal */
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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 './InterestInventory.css';
|
import './InterestInventory.css';
|
||||||
|
|
||||||
const InterestInventory = () => {
|
const InterestInventory = () => {
|
||||||
@ -9,9 +10,14 @@ const InterestInventory = () => {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const questionsPerPage = 6;
|
const questionsPerPage = 6;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
const fetchQuestions = async () => {
|
const fetchQuestions = async () => {
|
||||||
|
setLoading(true); // Start loading
|
||||||
|
setError(null); // Reset error state
|
||||||
|
|
||||||
const baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5001'; // Default to localhost:5001 for development
|
const baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5001'; // Default to localhost:5001 for development
|
||||||
const url = `${baseUrl}/api/onet/questions?start=1&end=60`; // Make sure the endpoint is correctly appended to the base URL
|
const url = `${baseUrl}/api/onet/questions?start=1&end=60`; // Make sure the endpoint is correctly appended to the base URL
|
||||||
|
|
||||||
@ -27,17 +33,17 @@ const InterestInventory = () => {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Adjusted to properly access the questions array
|
|
||||||
if (data && Array.isArray(data.questions)) {
|
if (data && Array.isArray(data.questions)) {
|
||||||
setQuestions(data.questions);
|
setQuestions(data.questions);
|
||||||
console.log("Questions fetched:", data.questions);
|
console.log("Questions fetched:", data.questions);
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid question format:", data);
|
throw new Error("Invalid question format.");
|
||||||
setQuestions([]);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching questions:", error.message);
|
console.error("Error fetching questions:", error.message);
|
||||||
alert("Failed to load questions. Please try again later.");
|
setError(error.message); // Set error message
|
||||||
|
} finally {
|
||||||
|
setLoading(false); // Stop loading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,6 +100,7 @@ const InterestInventory = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
setError(null); // Clear previous errors
|
||||||
const baseUrl = process.env.NODE_ENV === 'production'
|
const baseUrl = process.env.NODE_ENV === 'production'
|
||||||
? '/api/onet/submit_answers' // In production, this is proxied by Nginx to server2 (port 5001)
|
? '/api/onet/submit_answers' // In production, this is proxied by Nginx to server2 (port 5001)
|
||||||
: 'http://localhost:5001/api/onet/submit_answers'; // In development, server2 runs on port 5001
|
: 'http://localhost:5001/api/onet/submit_answers'; // In development, server2 runs on port 5001
|
||||||
@ -135,6 +142,9 @@ const InterestInventory = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="interest-inventory-container">
|
<div className="interest-inventory-container">
|
||||||
<h2>Interest Inventory</h2>
|
<h2>Interest Inventory</h2>
|
||||||
|
{loading && <ClipLoader size={35} color="#4A90E2" />}
|
||||||
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||||
|
|
||||||
{questions.length > 0 ? (
|
{questions.length > 0 ? (
|
||||||
<div className="questions-container">
|
<div className="questions-container">
|
||||||
{currentQuestions.map((question) => (
|
{currentQuestions.map((question) => (
|
||||||
|
@ -1,26 +1,18 @@
|
|||||||
// PopoutPanel.js
|
// PopoutPanel.js
|
||||||
import ClipLoader from 'react-spinners/ClipLoader.js';
|
import { ClipLoader } from 'react-spinners';
|
||||||
import LoanRepayment from './LoanRepayment.js';
|
import LoanRepayment from './LoanRepayment.js';
|
||||||
import './PopoutPanel.css';
|
import './PopoutPanel.css';
|
||||||
|
|
||||||
function PopoutPanel({
|
function PopoutPanel({
|
||||||
career = {},
|
data = {},
|
||||||
schools = [],
|
userState = 'N/A', // Passed explicitly from Dashboard
|
||||||
salaryData = {},
|
|
||||||
economicProjections = {},
|
|
||||||
tuitionData = {},
|
|
||||||
loading = false,
|
loading = false,
|
||||||
error = null,
|
error = null,
|
||||||
closePanel
|
closePanel
|
||||||
}) {
|
}) {
|
||||||
console.log('PopoutPanel Props:', { career, schools, salaryData, economicProjections, tuitionData, loading, error });
|
|
||||||
|
|
||||||
// Validation Checks
|
|
||||||
const isValidCareer = career && career.title && career.code;
|
|
||||||
const isValidSchools = Array.isArray(schools) && schools.length > 0;
|
|
||||||
const isValidSalaryData = salaryData && Object.keys(salaryData).length > 0;
|
|
||||||
const isValidProjections = economicProjections && Object.keys(economicProjections).length > 0;
|
|
||||||
|
|
||||||
|
console.log('PopoutPanel Props:', { data, loading, error, userState });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="popout-panel">
|
<div className="popout-panel">
|
||||||
@ -41,43 +33,86 @@ function PopoutPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!career) {
|
// Handle empty data gracefully
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="popout-panel">
|
<div className="popout-panel">
|
||||||
<button className="close-btn" onClick={closePanel}>X</button>
|
<button onClick={closePanel}>Close</button>
|
||||||
<h2>No Career Selected</h2>
|
<h2>No Career Data Available</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle empty data gracefully
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="popout-panel">
|
||||||
|
<button onClick={closePanel}>Close</button>
|
||||||
|
<h2>No Career Data Available</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely access nested data with fallbacks
|
||||||
|
const {
|
||||||
|
title = 'Career Details',
|
||||||
|
description = 'No description available.',
|
||||||
|
economicProjections = {},
|
||||||
|
salaryData = [],
|
||||||
|
schools = [],
|
||||||
|
tuitionData = []
|
||||||
|
} = data;
|
||||||
|
|
||||||
const tenthPercentileSalary = salaryData?.find(
|
const tenthPercentileSalary = salaryData?.find(
|
||||||
(point) => point.percentile === '10th Percentile'
|
(point) => point.percentile === '10th Percentile'
|
||||||
)?.value || 0;
|
)?.value || 0;
|
||||||
|
|
||||||
|
const getProgramLength = (degreeType) => {
|
||||||
|
if (degreeType?.includes("Associate")) return 2;
|
||||||
|
if (degreeType?.includes("Bachelor")) return 4;
|
||||||
|
if (degreeType?.includes("Master")) return 6;
|
||||||
|
if (degreeType?.includes("Doctoral") || degreeType?.includes("First Professional")) return 8;
|
||||||
|
if (degreeType?.includes("Certificate")) return 1;
|
||||||
|
return 4; // Default to 4 years if unspecified
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('PopoutPanel Props:', { data, schools, salaryData, economicProjections, tuitionData, loading, error, userState });
|
||||||
return (
|
return (
|
||||||
<div className="popout-panel">
|
<div className="popout-panel">
|
||||||
<button className="close-btn" onClick={closePanel}>X</button>
|
<button onClick={closePanel}>Close</button>
|
||||||
<h2>{career?.title || 'Career Details'}</h2>
|
<h2>{data.title || 'Career Details'}</h2>
|
||||||
|
<p>Description: {data.description || 'No description available.'}</p>
|
||||||
|
|
||||||
{/* Schools Offering Programs */}
|
{/* Schools Offering Programs */}
|
||||||
<h3>Schools Offering Programs</h3>
|
<h3>Schools Offering Programs</h3>
|
||||||
{Array.isArray(schools) && schools.length > 0 ? (
|
{Array.isArray(schools) && schools.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{schools.map((school, index) => (
|
{schools.map((school, index) => {
|
||||||
<li key={index}>
|
const matchingTuitionData = tuitionData.find(
|
||||||
<strong>{school['Institution Name']}</strong> <br />
|
(tuition) =>
|
||||||
In-State Tuition: ${school.inStateTuition || 'N/A'} <br />
|
tuition['school.name']?.toLowerCase().trim() ===
|
||||||
Out-of-State Tuition: ${school.outOfStateTuition || 'N/A'}
|
school['Institution Name']?.toLowerCase().trim()
|
||||||
</li>
|
);
|
||||||
))}
|
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<strong>{school['Institution Name']}</strong>
|
||||||
|
<br />
|
||||||
|
Degree Type: {school['CREDDESC'] || 'N/A'}
|
||||||
|
<br />
|
||||||
|
In-State Tuition: ${matchingTuitionData?.['latest.cost.tuition.in_state'] || 'N/A'}
|
||||||
|
<br />
|
||||||
|
Out-of-State Tuition: ${matchingTuitionData?.['latest.cost.tuition.out_of_state'] || 'N/A'}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p>No schools available.</p>
|
<p>No schools available.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Economic Projections */}
|
{/* Economic Projections */}
|
||||||
<h3>Economic Projections</h3>
|
<h3>Economic Projections for {userState}</h3>
|
||||||
{economicProjections && typeof economicProjections === 'object' ? (
|
{economicProjections && typeof economicProjections === 'object' ? (
|
||||||
<ul>
|
<ul>
|
||||||
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
|
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
|
||||||
@ -117,8 +152,24 @@ function PopoutPanel({
|
|||||||
{tenthPercentileSalary > 0 && (
|
{tenthPercentileSalary > 0 && (
|
||||||
<LoanRepayment
|
<LoanRepayment
|
||||||
tuitionCosts={{
|
tuitionCosts={{
|
||||||
inState: schools.map((s) => parseFloat(s.inStateTuition) || 0),
|
inState: schools.map((school) => {
|
||||||
outOfState: schools.map((s) => parseFloat(s.outOfStateTuition) || 0),
|
const matchingTuitionData = tuitionData.find(
|
||||||
|
(tuition) =>
|
||||||
|
tuition['school.name']?.toLowerCase().trim() ===
|
||||||
|
school['Institution Name']?.toLowerCase().trim()
|
||||||
|
);
|
||||||
|
const years = getProgramLength(school['CREDDESC']);
|
||||||
|
return parseFloat(matchingTuitionData?.['latest.cost.tuition_in_state'] * years) || 0;
|
||||||
|
}),
|
||||||
|
outOfState: schools.map((school) => {
|
||||||
|
const matchingTuitionData = tuitionData.find(
|
||||||
|
(tuition) =>
|
||||||
|
tuition['school.name']?.toLowerCase().trim() ===
|
||||||
|
school['Institution Name']?.toLowerCase().trim()
|
||||||
|
);
|
||||||
|
const years = getProgramLength(school['CREDDESC']);
|
||||||
|
return parseFloat(matchingTuitionData?.['latest.cost.tuition_out_of_state'] * years) || 0;
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
salaryData={[{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]}
|
salaryData={[{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]}
|
||||||
earningHorizon={10}
|
earningHorizon={10}
|
||||||
@ -129,3 +180,4 @@ function PopoutPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default PopoutPanel;
|
export default PopoutPanel;
|
||||||
|
|
||||||
|
@ -8,30 +8,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signin-form {
|
.signin-form {
|
||||||
width: 300px;
|
width: 100%; /* Make the form responsive */
|
||||||
|
max-width: 350px; /* Set maximum width for the form */
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
border-radius: 8px; /* Optional rounded corners */
|
||||||
|
}
|
||||||
|
|
||||||
.signin-input {
|
.signin-input {
|
||||||
width: 100%;
|
width: 90%; /* Adjust input width to leave margin */
|
||||||
padding: 10px;
|
max-width: 280px; /* Optional max width */
|
||||||
margin: 10px 0;
|
padding: 10px;
|
||||||
border: 1px solid #ddd;
|
margin: 10px auto; /* Center input fields with auto margin */
|
||||||
border-radius: 5px;
|
border: 1px solid #ddd;
|
||||||
}
|
border-radius: 5px;
|
||||||
|
display: block; /* Ensures inputs take full width of their container */
|
||||||
|
}
|
||||||
|
|
||||||
.signin-button {
|
.signin-button {
|
||||||
width: 100%;
|
width: 90%; /* Match input width */
|
||||||
padding: 10px;
|
max-width: 280px; /* Optional max width */
|
||||||
background-color: #4CAF50;
|
padding: 10px;
|
||||||
color: #fff;
|
background-color: #4CAF50;
|
||||||
border: none;
|
color: #fff;
|
||||||
border-radius: 5px;
|
border: none;
|
||||||
cursor: pointer;
|
border-radius: 5px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
margin-top: 10px; /* Add space above the button */
|
||||||
|
}
|
||||||
|
|
||||||
.signin-button:hover {
|
.signin-button:hover {
|
||||||
background-color: #45a049;
|
background-color: #45a049;
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
// components/SignIn.js
|
import React, { useRef, useState } from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import './SignIn.css';
|
import './SignIn.css';
|
||||||
|
|
||||||
function SignIn({ setIsAuthenticated }) {
|
function SignIn({ setIsAuthenticated }) {
|
||||||
console.log('SignIn component loaded');
|
console.log('SignIn component loaded');
|
||||||
console.log('Rendering SignIn component');
|
|
||||||
console.log('Authentication state in SignIn:', setIsAuthenticated);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState('');
|
const usernameRef = useRef('');
|
||||||
const [password, setPassword] = useState('');
|
const passwordRef = useRef('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState(''); // Still needs state for UI updates
|
||||||
|
|
||||||
const handleSignIn = async (event) => {
|
const handleSignIn = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
const username = usernameRef.current.value;
|
||||||
|
const password = passwordRef.current.value;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
setError('Please enter both username and password');
|
setError('Please enter both username and password');
|
||||||
return;
|
return;
|
||||||
@ -38,24 +38,22 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('SignIn response data:', data);
|
console.log('SignIn response data:', data);
|
||||||
|
|
||||||
const { token, userId } = data;
|
const { token, userId } = data;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
localStorage.setItem('userId', userId);
|
localStorage.setItem('userId', userId);
|
||||||
|
|
||||||
console.log('Token and userId saved in localStorage');
|
console.log('Token and userId saved in localStorage');
|
||||||
|
|
||||||
// Call setIsAuthenticated(true) to update the state
|
// Call setIsAuthenticated(true) to update the state
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
|
||||||
navigate('/getting-started'); // Redirect to GettingStarted after SignIn
|
navigate('/getting-started'); // Redirect to GettingStarted after SignIn
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Sign-In Error:', error.message);
|
console.error('Sign-In Error:', error.message);
|
||||||
setError(error.message); // Display error message to the user
|
setError(error.message); // Display error message to the user
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="signin-container">
|
<div className="signin-container">
|
||||||
@ -66,22 +64,20 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={username}
|
ref={usernameRef} // Use ref instead of state
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
className="signin-input"
|
className="signin-input"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
ref={passwordRef} // Use ref instead of state
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="signin-input"
|
className="signin-input"
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="signin-button">
|
<button type="submit" className="signin-button">
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<p>Don’t have an account? <Link to="/signup">Sign Up</Link></p> {/* Sign-Up Link */}
|
<p>Don’t have an account? <Link to="/signup">Sign Up</Link></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -19,22 +19,21 @@ export const fetchAreasByState = async (state) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
//fetch schools
|
// Fetch schools
|
||||||
export const fetchSchools = async (cipCode, userState) => {
|
export const fetchSchools = async (cipCode, state = '', level = '', type = '') => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('http://127.0.0.1:5001/api/CIP_institution_mapping_fixed.json');
|
const response = await axios.get('http://127.0.0.1:5001/api/schools', {
|
||||||
const schoolsData = response.data;
|
params: {
|
||||||
|
cipCode,
|
||||||
|
state,
|
||||||
|
level,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
return response.data; // Return filtered data
|
||||||
return schoolsData.filter(
|
|
||||||
(school) =>
|
|
||||||
typeof school['CIP Code'] === 'string' &&
|
|
||||||
school['CIP Code'].replace('.', '') === cleanedCipCode &&
|
|
||||||
school['State'] === userState
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching schools:', error);
|
console.error('Error fetching schools:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user