UX issues, schools issues

This commit is contained in:
Josh 2024-12-27 19:03:32 +00:00
parent e07dc32b51
commit 9696943f9b
12 changed files with 394 additions and 215 deletions

View File

@ -171,7 +171,7 @@ app.post('/api/login', (req, res) => {
}
// 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 });
});
});

View File

@ -346,10 +346,16 @@ app.get('/api/cip/:socCode', (req, res) => {
res.status(404).json({ error: 'CIP code not found for this SOC code' });
});
app.get('/api/CIP_institution_mapping_fixed.json', (req, res) => {
const filePath = path.join(__dirname, 'CIP_institution_mapping_fixed.json'); // Adjust the path if needed
res.sendFile(filePath);
});
// Filtered schools endpoint
app.get('/api/schools', (req, res) => {
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' });
}

View File

@ -1,18 +1,25 @@
/* src/App.css */
/* Body styling for the entire application */
body {
background-color: #e9ecef; /* Light gray-blue background */
/* General body and root styling for full-page coverage */
body, #root {
background-color: #f4f7fb; /* Light gray-blue background */
font-family: Arial, sans-serif;
margin: 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 {
max-width: 1200px;
margin: 0 auto;
margin: 20px auto;
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 */
@ -110,3 +117,14 @@ input, select {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
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;
}
}

View File

@ -8,7 +8,7 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
return (
<div>
<h3>Career Suggestions</h3>
<h2>Career Suggestions</h2>
<ul>
{careerSuggestions.map((career, index) => {
return (

View File

@ -3,8 +3,8 @@
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Ensure responsive layout */
gap: 15px;
padding: 15px;
gap: 20px;
padding: 20px;
background-color: #f4f7fa;
}
@ -108,3 +108,26 @@ h2 {
.career-item:hover {
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;
}

View File

@ -16,6 +16,7 @@ function Dashboard() {
const navigate = useNavigate();
const [careerSuggestions, setCareerSuggestions] = useState([]);
const [careerDetails, setCareerDetails] = useState(null);
const [riaSecScores, setRiaSecScores] = useState([]);
const [selectedCareer, setSelectedCareer] = useState(null);
const [schools, setSchools] = useState([]);
@ -26,13 +27,18 @@ function Dashboard() {
const [error, setError] = useState(null);
const [userState, setUserState] = useState(null);
const [areaTitle, setAreaTitle] = useState(null);
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
const apiUrl = process.env.REACT_APP_API_URL;
useEffect(() => {
let descriptions = []; // Declare outside for scope accessibility
if (location.state) {
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
descriptions = scores.map((score) => score.description || "No description available.");
setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions); // Set descriptions
} else {
console.warn('No data found, redirecting to Interest Inventory');
navigate('/interest-inventory');
@ -68,7 +74,16 @@ function Dashboard() {
const handleCareerClick = useCallback(
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) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
@ -76,35 +91,25 @@ function Dashboard() {
}
try {
setLoading(true);
setError(null);
// Step 1: Fetch CIP Code
const cipResponse = await fetch(`${apiUrl}/api/cip/${socCode}`);
if (!cipResponse.ok) {
console.error('Failed to fetch CIP Code');
setError('Failed to fetch CIP Code');
return;
}
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
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}`, {
// Step 2: Fetch Data in Parallel
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
fetchSchools(cleanedCipCode, userState),
axios.get(`http://localhost:5001/api/projections/${socCode.split('.')[0]}`),
axios.get(`http://localhost:5001/api/tuition/${cleanedCipCode}`, {
params: { state: userState },
});
console.log('Salary Data Request Params:', { socCode: cleanedSocCode, area: areaTitle });
const salaryResponse = await axios.get(`http://localhost:5001/api/salary`, {
params: { socCode: cleanedSocCode, area: areaTitle },
});
}),
axios.get(`http://localhost:5001/api/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
}),
]);
// Step 3: Format Salary Data
const salaryDataPoints = [
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 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 },
];
setSelectedCareer(career);
setSchools(filteredSchools);
setEconomicProjections(economicResponse.data);
setTuitionData(tuitionResponse.data);
setSalaryData(salaryDataPoints);
// Step 4: Consolidate Career Details
setCareerDetails({
...career,
economicProjections: economicResponse.data,
salaryData: salaryDataPoints,
schools: filteredSchools,
tuitionData: tuitionResponse.data,
});
} catch (error) {
console.error('Error processing career click:', error.message);
setError('Failed to load data');
@ -144,25 +152,43 @@ function Dashboard() {
return (
<div className="dashboard">
<div className="career-suggestions-container">
<h2>Career Suggestions</h2>
<CareerSuggestions careerSuggestions={careerSuggestions} onCareerClick={handleCareerClick} />
</div>
{/* Right RIASEC Chart + Descriptions */}
<div className="riasec-container">
<div className="riasec-scores">
<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 && (
<PopoutPanel
career={selectedCareer}
schools={schools || []}
salaryData={salaryData || []}
economicProjections={economicProjections || {}}
tuitionData={tuitionData || {}}
data={careerDetails}
schools={schools}
salaryData={salaryData}
economicProjections={economicProjections}
tuitionData={tuitionData}
closePanel={() => setSelectedCareer(null)}
loading={loading}
error={error}
userState={userState}
/>
)}
</div>

View File

@ -1,78 +1,121 @@
/* src/components/InterestInventory.css */
/* Container Styling */
.interest-inventory-container {
max-width: 600px;
margin: 40px auto; /* Center the container */
padding: 20px;
background-color: #8d98f8; /* White background */
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #060c02;
background-color: #f0f8ff; /* Sky-white background */
border-radius: 12px; /* Rounded borders */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* Subtle shadow effect */
border: 4px solid #87ceeb; /* Sky-blue trim */
}
/* Header Styling */
.interest-inventory-container h2 {
color: #0e0202;
color: #0056b3; /* Dark blue header */
text-align: center;
font-size: 28px; /* Larger font size for emphasis */
margin-bottom: 20px;
font-weight: bold;
}
/* Question Container */
.question {
margin-bottom: 20px;
}
/* Label styling for each question */
/* Label Styling */
.question label {
display: block;
font-weight: bold;
font-size: 18px; /* Slightly larger text for questions */
font-weight: 600; /* Semi-bold text */
margin-bottom: 8px;
color: #0f0101;
color: #333; /* Darker text for readability */
}
/* Set fixed width and max-width for dropdowns within InterestInventory */
/* Dropdown Styling */
.question select {
width: auto; /* Allow it to adjust based on content */
max-width: 300px; /* Set a maximum width to prevent stretching */
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
width: 80%; /* Use percentage for responsiveness */
max-width: 400px; /* Limit maximum size */
padding: 10px;
border: 1px solid #87ceeb; /* Sky-blue border */
border-radius: 8px; /* Rounded edges */
font-size: 16px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */
transition: border-color 0.3s ease;
display: block;
margin: 0 auto; /* Center the dropdown */
}
/* Button styling for a uniform appearance */
/* Dropdown Hover Effect */
.question select:hover {
border-color: #007bff; /* Brighter blue on hover */
}
/* Dropdown Focus Effect */
.question select:focus {
border-color: #0056b3;
outline: none;
}
/* Buttons */
button {
margin-top: 20px;
margin: 10px 5px; /* Add spacing between buttons */
padding: 10px 20px;
background-color: #007bff;
background-color: #007bff; /* Blue button */
color: #fff;
border: none;
border-radius: 4px;
border-radius: 8px; /* Rounded edges */
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
/* Hover Effect for Buttons */
button:hover {
background-color: #0056b3;
background-color: #0056b3; /* Darker blue */
}
/* Submission message styling */
/* Submit Button Styling */
button:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
/* Submission Message Styling */
.submit-message {
color: #28a745;
color: #28a745; /* Green success text */
font-weight: bold;
text-align: center;
margin-top: 20px;
}
.validation-error-container {
margin-top: 10px; /* Position the error message below the questions */
}
/* Validation Error Message */
.validation-error {
color: red;
font-weight: bold;
}
/* Highlight Unanswered Questions */
.unanswered {
border: 2px solid #b22222; /* Darker red (Firebrick) */
background-color: #ee6161; /* Light red background remains unchanged */
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 */
}

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ClipLoader } from 'react-spinners';
import './InterestInventory.css';
const InterestInventory = () => {
@ -9,9 +10,14 @@ const InterestInventory = () => {
const [isSubmitting, setIsSubmitting] = useState(false);
const questionsPerPage = 6;
const navigate = useNavigate();
const [careerSuggestions, setCareerSuggestions] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
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 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();
// Adjusted to properly access the questions array
if (data && Array.isArray(data.questions)) {
setQuestions(data.questions);
console.log("Questions fetched:", data.questions);
} else {
console.error("Invalid question format:", data);
setQuestions([]);
throw new Error("Invalid question format.");
}
} catch (error) {
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 {
setIsSubmitting(true);
setError(null); // Clear previous errors
const baseUrl = process.env.NODE_ENV === 'production'
? '/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
@ -135,6 +142,9 @@ const InterestInventory = () => {
return (
<div className="interest-inventory-container">
<h2>Interest Inventory</h2>
{loading && <ClipLoader size={35} color="#4A90E2" />}
{error && <p style={{ color: 'red' }}>{error}</p>}
{questions.length > 0 ? (
<div className="questions-container">
{currentQuestions.map((question) => (

View File

@ -1,25 +1,17 @@
// PopoutPanel.js
import ClipLoader from 'react-spinners/ClipLoader.js';
import { ClipLoader } from 'react-spinners';
import LoanRepayment from './LoanRepayment.js';
import './PopoutPanel.css';
function PopoutPanel({
career = {},
schools = [],
salaryData = {},
economicProjections = {},
tuitionData = {},
data = {},
userState = 'N/A', // Passed explicitly from Dashboard
loading = false,
error = null,
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) {
return (
@ -41,43 +33,86 @@ function PopoutPanel({
);
}
if (!career) {
// Handle empty data gracefully
if (!data || Object.keys(data).length === 0) {
return (
<div className="popout-panel">
<button className="close-btn" onClick={closePanel}>X</button>
<h2>No Career Selected</h2>
<button onClick={closePanel}>Close</button>
<h2>No Career Data Available</h2>
</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(
(point) => point.percentile === '10th Percentile'
)?.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 (
<div className="popout-panel">
<button className="close-btn" onClick={closePanel}>X</button>
<h2>{career?.title || 'Career Details'}</h2>
<button onClick={closePanel}>Close</button>
<h2>{data.title || 'Career Details'}</h2>
<p>Description: {data.description || 'No description available.'}</p>
{/* Schools Offering Programs */}
<h3>Schools Offering Programs</h3>
{Array.isArray(schools) && schools.length > 0 ? (
<ul>
{schools.map((school, index) => (
{schools.map((school, index) => {
const matchingTuitionData = tuitionData.find(
(tuition) =>
tuition['school.name']?.toLowerCase().trim() ===
school['Institution Name']?.toLowerCase().trim()
);
return (
<li key={index}>
<strong>{school['Institution Name']}</strong> <br />
In-State Tuition: ${school.inStateTuition || 'N/A'} <br />
Out-of-State Tuition: ${school.outOfStateTuition || 'N/A'}
<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>
) : (
<p>No schools available.</p>
)}
{/* Economic Projections */}
<h3>Economic Projections</h3>
<h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? (
<ul>
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
@ -117,8 +152,24 @@ function PopoutPanel({
{tenthPercentileSalary > 0 && (
<LoanRepayment
tuitionCosts={{
inState: schools.map((s) => parseFloat(s.inStateTuition) || 0),
outOfState: schools.map((s) => parseFloat(s.outOfStateTuition) || 0),
inState: 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_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 }]}
earningHorizon={10}
@ -129,3 +180,4 @@ function PopoutPanel({
}
export default PopoutPanel;

View File

@ -8,29 +8,35 @@
}
.signin-form {
width: 300px;
width: 100%; /* Make the form responsive */
max-width: 350px; /* Set maximum width for the form */
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
text-align: center;
border-radius: 8px; /* Optional rounded corners */
}
.signin-input {
width: 100%;
width: 90%; /* Adjust input width to leave margin */
max-width: 280px; /* Optional max width */
padding: 10px;
margin: 10px 0;
margin: 10px auto; /* Center input fields with auto margin */
border: 1px solid #ddd;
border-radius: 5px;
display: block; /* Ensures inputs take full width of their container */
}
.signin-button {
width: 100%;
width: 90%; /* Match input width */
max-width: 280px; /* Optional max width */
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 10px; /* Add space above the button */
}
.signin-button:hover {

View File

@ -1,23 +1,23 @@
// components/SignIn.js
import React, { useState, useEffect } from 'react';
import React, { useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './SignIn.css';
function SignIn({ setIsAuthenticated }) {
console.log('SignIn component loaded');
console.log('Rendering SignIn component');
console.log('Authentication state in SignIn:', setIsAuthenticated);
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const usernameRef = useRef('');
const passwordRef = useRef('');
const [error, setError] = useState(''); // Still needs state for UI updates
const handleSignIn = async (event) => {
event.preventDefault();
setError('');
const username = usernameRef.current.value;
const password = passwordRef.current.value;
if (!username || !password) {
setError('Please enter both username and password');
return;
@ -48,7 +48,6 @@ function SignIn({ setIsAuthenticated }) {
// Call setIsAuthenticated(true) to update the state
setIsAuthenticated(true);
navigate('/getting-started'); // Redirect to GettingStarted after SignIn
} catch (error) {
console.error('Sign-In Error:', error.message);
@ -56,7 +55,6 @@ function SignIn({ setIsAuthenticated }) {
}
};
return (
<div className="signin-container">
<div className="signin-form">
@ -66,22 +64,20 @@ function SignIn({ setIsAuthenticated }) {
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
ref={usernameRef} // Use ref instead of state
className="signin-input"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
ref={passwordRef} // Use ref instead of state
className="signin-input"
/>
<button type="submit" className="signin-button">
Sign In
</button>
</form>
<p>Dont have an account? <Link to="/signup">Sign Up</Link></p> {/* Sign-Up Link */}
<p>Dont have an account? <Link to="/signup">Sign Up</Link></p>
</div>
</div>
);

View File

@ -19,22 +19,21 @@ export const fetchAreasByState = async (state) => {
};
//fetch schools
export const fetchSchools = async (cipCode, userState) => {
// Fetch schools
export const fetchSchools = async (cipCode, state = '', level = '', type = '') => {
try {
const response = await axios.get('http://127.0.0.1:5001/api/CIP_institution_mapping_fixed.json');
const schoolsData = response.data;
const response = await axios.get('http://127.0.0.1:5001/api/schools', {
params: {
cipCode,
state,
level,
type,
},
});
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
return schoolsData.filter(
(school) =>
typeof school['CIP Code'] === 'string' &&
school['CIP Code'].replace('.', '') === cleanedCipCode &&
school['State'] === userState
);
return response.data; // Return filtered data
} catch (error) {
console.error('Error fetching schools:', error);
return [];
}
};