diff --git a/.env.development b/.env.development
index 47226a6..9ca6be9 100755
--- a/.env.development
+++ b/.env.development
@@ -2,7 +2,7 @@ ONET_USERNAME=aptivaai
ONET_PASSWORD=2296ahq
REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231
-GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
+REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
REACT_APP_API_URL=http://localhost:5001
diff --git a/.gitignore b/.gitignore
index 05d6bcd..5cb4377 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,10 +13,6 @@
# misc
.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
npm-debug.log*
yarn-debug.log*
diff --git a/backend/server2.js b/backend/server2.js
index 325af58..62118f6 100755
--- a/backend/server2.js
+++ b/backend/server2.js
@@ -23,6 +23,8 @@ dotenv.config({ path: envPath });
console.log(`Loaded environment variables from: ${envPath}`);
console.log('ONET_USERNAME:', process.env.ONET_USERNAME);
console.log('ONET_PASSWORD:', process.env.ONET_PASSWORD);
+console.log('Google Maps API Key:', process.env.GOOGLE_MAPS_API_KEY)
+
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev.aptivaai.com'];
const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.xlsx';
@@ -49,6 +51,22 @@ const initDB = async () => {
// Initialize database before starting the server
initDB();
+let userProfileDb;
+const initUserProfileDb = async () => {
+ try {
+ userProfileDb = await open({
+ filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', // Path to user_profile.db
+ driver: sqlite3.Database
+ });
+ console.log('Connected to user_profile.db.');
+ } catch (error) {
+ console.error('Error connecting to user_profile.db:', error);
+ }
+};
+
+// Initialize user_profile.db before starting the server
+initUserProfileDb();
+
// Add security headers using helmet
app.use(
helmet({
@@ -158,34 +176,88 @@ app.get('/api/onet/questions', async (req, res) => {
}
});
-// New route to handle Google Maps geocoding
-app.get('/api/maps/distance', async (req, res) => {
- const { origins, destinations } = req.query;
- console.log('Query parameters received:', req.query); // Log the entire query object
+// Helper function to geocode an address or zip code
+// Function to geocode a ZIP code
+const geocodeZipCode = async (zipCode) => {
+ try {
+ const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(userZipcode)}&key=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20`;
+ console.log('Constructed Geocode URL:', geocodeUrl); // Check if encoding helps
- if (!origins || !destinations) {
- console.error('Missing parameters:', { origins, destinations });
- return res
- .status(400)
- .json({ error: 'Origin and destination parameters are required.' });
+ const response = await axios.get(geocodeUrl);
+
+ if (response.data.status === 'OK' && response.data.results.length > 0) {
+ const location = response.data.results[0].geometry.location;
+ return location; // Return the latitude and longitude
+ } else {
+ throw new Error('Geocoding failed');
+ }
+ } catch (error) {
+ console.error('Error geocoding ZIP code:', error.message);
+ return null;
+ }
+};
+
+app.post('/api/maps/distance', async (req, res) => {
+ const { userZipcode, destinations } = req.body;
+
+ if (!userZipcode || !destinations) {
+ return res.status(400).json({ error: 'User ZIP code and destination address are required.' });
}
- const apiKey = process.env.GOOGLE_MAPS_API_KEY; // Use the Google Maps API key from the environment variable
- const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent(
- origins
- )}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${apiKey}`;
- console.log('Constructed Distance Matrix API URL:', distanceUrl);
-
-
try {
- const response = await axios.get(distanceUrl);
- res.status(200).json(response.data);
+ const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
+
+ // Geocode the user's ZIP code
+ const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${userZipcode}&components=country:US&key=${googleMapsApiKey}`;
+ const geocodeResponse = await axios.get(geocodeUrl);
+
+ if (geocodeResponse.data.status === 'OK' && geocodeResponse.data.results.length > 0) {
+ const userLocation = geocodeResponse.data.results[0].geometry.location; // Get lat, lng
+ const origins = `${userLocation.lat},${userLocation.lng}`; // User's location as lat/lng
+
+ // Call the Distance Matrix API using the geocoded user location and school address
+ const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${origins}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${googleMapsApiKey}`;
+ const distanceResponse = await axios.get(distanceUrl);
+
+ if (distanceResponse.data.status !== 'OK') {
+ return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
+ }
+
+ const { distance, duration } = distanceResponse.data.rows[0].elements[0];
+ res.json({ distance: distance.text, duration: duration.text });
+
+ } else {
+ return res.status(400).json({ error: 'Unable to geocode user ZIP code.' });
+ }
+
} catch (error) {
- console.error('Error fetching distance data:', error.message);
- res.status(500).json({ error: 'Failed to fetch distance data', details: error.message });
+ console.error('Error during distance calculation:', error);
+ res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
+// Route to fetch user profile by ID including ZIP code
+app.get('/api/user-profile/:id', (req, res) => {
+ const { id } = req.params;
+
+ if (!id) {
+ return res.status(400).json({ error: 'Profile ID is required' });
+ }
+
+ const query = `SELECT area, zipcode FROM user_profile WHERE id = ?`;
+ db.get(query, [id], (err, row) => {
+ if (err) {
+ console.error('Error fetching user profile:', err.message);
+ return res.status(500).json({ error: 'Failed to fetch user profile' });
+ }
+
+ if (!row) {
+ return res.status(404).json({ error: 'Profile not found' });
+ }
+
+ res.json({ area: row.area, zipcode: row.zipcode });
+ });
+});
// Load the economic projections data from the Excel file
const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); // Adjusted path
@@ -498,7 +570,7 @@ app.get('/api/user-profile/:id', (req, res) => {
return res.status(400).json({ error: 'Profile ID is required' });
}
- const query = `SELECT area FROM user_profile WHERE id = ?`;
+ const query = `SELECT area, zipcode FROM user_profile WHERE id = ?`;
db.get(query, [id], (err, row) => {
if (err) {
console.error('Error fetching user profile:', err.message);
@@ -509,7 +581,7 @@ app.get('/api/user-profile/:id', (req, res) => {
return res.status(404).json({ error: 'Profile not found' });
}
- res.json({ area: row.area });
+ res.json({ area: row.area, zipcode: row.zipcode });
});
});
diff --git a/package-lock.json b/package-lock.json
index 6ce27c8..1c70feb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16378,16 +16378,16 @@
}
},
"node_modules/typescript": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
- "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
diff --git a/public/ltprojections.xlsx b/public/ltprojections.xlsx
new file mode 100644
index 0000000..9bd932a
Binary files /dev/null and b/public/ltprojections.xlsx differ
diff --git a/src/components/CareerSuggestions.js b/src/components/CareerSuggestions.js
index d3a053f..8333a13 100644
--- a/src/components/CareerSuggestions.js
+++ b/src/components/CareerSuggestions.js
@@ -9,22 +9,19 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
return (
Career Suggestions
-
- {careerSuggestions.map((career, index) => {
- return (
-
- {
- console.log('Button clicked for:', career);
- onCareerClick(career);
- }}
- >
- {career.title || `Career ${index + 1}`}
-
-
- );
- })}
-
+
+ {careerSuggestions.map((career) => (
+ onCareerClick(career)}
+ >
+ {career.title}
+
+ ))}
+
- );
-}
+ );
+ };
+
+ export default CareerSuggestions;
\ No newline at end of file
diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css
index 58f281a..2bb7dc1 100644
--- a/src/components/Dashboard.css
+++ b/src/components/Dashboard.css
@@ -1,13 +1,20 @@
/* Dashboard.css */
+/* Main Dashboard Layout */
.dashboard {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Ensure responsive layout */
+ grid-template-columns: 1fr 2fr; /* Two columns: careers on the left, RIASEC on the right */
gap: 20px;
+ min-height: 100vh; /* Full height */
padding: 20px;
background-color: #f4f7fa;
}
+.dashboard-content {
+ flex-grow: 1; /* Push acknowledgment to the bottom */
+}
+
+/* Sections in Dashboard */
.dashboard section {
background-color: #ffffff;
padding: 15px;
@@ -22,6 +29,7 @@
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
+/* Headings */
h2 {
font-size: 1.4rem;
color: #2b4a67;
@@ -30,88 +38,50 @@ h2 {
padding-bottom: 4px;
}
-.career-suggestions h1 {
- font-size: 1.75rem; /* Adjusted size for better hierarchy */
- margin-bottom: 15px;
- color: #2b4a67;
-}
-
-.career-list {
+/* Career Suggestions Grid */
+.career-suggestions-container {
display: flex;
flex-direction: column;
gap: 10px;
}
-.career-item {
- padding: 8px;
- background-color: #eaf6fb;
- border: 1px solid #d1e7f0;
- border-radius: 4px;
- transition: background-color 0.2s ease;
+.career-suggestions-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Flexible grid */
+ gap: 10px; /* Even spacing */
+ padding: 10px;
}
-.career-item:hover {
- background-color: #d4edf6;
-}
-
-.career-item h3 {
- font-size: 1.1rem; /* Smaller font for career titles */
- margin-bottom: 4px;
- font-weight: bold;
- color: #34596a;
-}
-
-.career-item p {
- font-size: 0.9rem;
- margin: 4px 0;
- color: #555555;
-}
-
-.riasec-scores h2 {
- font-size: 1.5rem;
- margin-bottom: 20px;
-}
-
-.chart-container {
- margin: 20px 0;
- max-width: 600px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.sign-out-container {
- text-align: center;
- margin-top: 20px;
-}
-
-.sign-out-btn {
- padding: 10px 20px;
+.career-button {
+ padding: 10px;
background-color: #007bff;
color: white;
border: none;
+ border-radius: 4px;
cursor: pointer;
- margin-top: 20px;
- font-size: 1rem;
- width: 150px;
+ text-align: center;
+ white-space: normal; /* Allow wrapping */
}
-.sign-out-btn:hover {
+.career-button:hover {
background-color: #0056b3;
}
-.career-item.selected {
- background-color: #cce5ff; /* Light blue background when selected */
- border: 2px solid #0056b3; /* Dark blue border */
- cursor: pointer;
+/* RIASEC Scores and Descriptions */
+.riasec-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
}
-.career-item:hover {
- background-color: #e6f7ff; /* Lighter blue when hovering */
+.riasec-scores {
+ padding: 15px;
+ border-radius: 8px;
+ background-color: #ffffff;
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
}
-
.riasec-descriptions {
- margin-top: 20px;
padding: 15px;
border-radius: 8px;
background-color: #f1f8ff;
@@ -131,3 +101,42 @@ h2 {
.riasec-descriptions strong {
color: #007bff;
}
+
+/* Data Source Acknowledgment */
+.data-source-acknowledgment {
+ grid-column: span 2; /* Make acknowledgment span both columns */
+ margin-top: 20px;
+ padding: 10px;
+ border-top: 1px solid #ccc;
+ font-size: 12px;
+ color: #666;
+ text-align: center;
+}
+
+/* Chart Container */
+.chart-container {
+ margin: 20px 0;
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Sign Out Button */
+.sign-out-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+.sign-out-btn {
+ padding: 10px 20px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
+ width: 150px;
+}
+
+.sign-out-btn:hover {
+ background-color: #0056b3;
+}
diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js
index 7560b84..bc297d5 100644
--- a/src/components/Dashboard.js
+++ b/src/components/Dashboard.js
@@ -27,10 +27,12 @@ function Dashboard() {
const [error, setError] = useState(null);
const [userState, setUserState] = useState(null);
const [areaTitle, setAreaTitle] = useState(null);
+ const [userZipcode, setUserZipcode] = useState(null);
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
// Dynamic API URL
const apiUrl = process.env.REACT_APP_API_URL || '';
+ const googleMapsApiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
useEffect(() => {
let descriptions = []; // Declare outside for scope accessibility
@@ -58,10 +60,11 @@ function Dashboard() {
const profileData = await profileResponse.json();
console.log('Fetched User Profile:', profileData);
- const { state, area } = profileData; // Use 'area' instead of 'AREA_TITLE'
+ const { state, area, zipcode } = profileData; // Use 'area' instead of 'AREA_TITLE'
setUserState(state);
setAreaTitle(area && area.trim() ? area.trim() : ''); // Ensure 'area' is set correctly
- console.log('Profile Data Set:', { state, area });
+ setUserZipcode(zipcode); // Set 'zipcode' in the state
+ console.log('Profile Data Set:', { state, area, zipcode });
} else {
console.error('Failed to fetch user profile');
}
@@ -80,45 +83,85 @@ function Dashboard() {
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([]);
-
+ setSchools([]); // Reset schools
+ setSalaryData([]); // Reset salary data
+ setEconomicProjections({}); // Reset economic projections
+ setTuitionData([]); // Reset tuition data
+
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
return;
}
-
+
try {
// Step 1: Fetch CIP Code
- const cipResponse = await fetch(`${apiUrl}/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);
-
-
-
- // Step 2: Fetch Data in Parallel
- const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
- fetchSchools(cleanedCipCode, userState),
+ const cipResponse = await fetch(`${apiUrl}/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);
+
+ // Step 2: Fetch Data in Parallel
+ const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
+ fetchSchools(cleanedCipCode, userState),
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
- axios.get(`${apiUrl}/tuition`, { // <-- Removed CIP code from URL path
+ axios.get(`${apiUrl}/tuition`, {
params: {
- cipCode: cleanedCipCode, // Moved to query params
+ cipCode: cleanedCipCode,
state: userState
},
}),
axios.get(`${apiUrl}/salary`, {
params: {
socCode: socCode.split('.')[0],
- area: areaTitle // Pass the areaTitle directly as it is
+ area: areaTitle
},
}),
]);
+
+ // Check if `userZipcode` is set correctly
+ const currentUserZipcode = userZipcode;
+ if (!currentUserZipcode) {
+ console.error("User Zipcode is not set correctly:", currentUserZipcode);
+ return;
+ }
- // Step 3: Format Salary Data
+ // Step 3: Add distance information to each school
+ const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
+ // Combine Street Address, City, State, and ZIP to form the full school address
+ const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
+
+ if (!currentUserZipcode || !schoolAddress) {
+ console.error('Missing ZIP codes or school address:', { currentUserZipcode, schoolAddress });
+ return { ...school, distance: 'Error', duration: 'Error' };
+ }
+
+ console.log("Calculating distance for User Zipcode:", currentUserZipcode, "and School Address:", schoolAddress);
+
+ try {
+ // Send the user's ZIP code and school address to the backend
+ const response = await axios.post(`${apiUrl}/maps/distance`, {
+ userZipcode: currentUserZipcode, // Pass the ZIP code (or lat/lng) of the user
+ destinations: schoolAddress, // Pass the full school address
+ });
+
+ const { distance, duration } = response.data;
+ return {
+ ...school, // Keep all school data
+ distance, // Add the distance value
+ duration, // Add the duration value
+ };
+ } catch (error) {
+ console.error('Error fetching distance:', error);
+ return {
+ ...school,
+ distance: 'Error',
+ duration: 'Error',
+ };
+ }
+ }));
+
+ // Step 4: Format Salary Data
const salaryDataPoints = [
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
@@ -126,15 +169,15 @@ function Dashboard() {
{ percentile: '75th Percentile', value: salaryResponse.data.A_PCT75 || 0 },
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
];
-
- // Step 4: Consolidate Career Details
- setCareerDetails({
- ...career,
- economicProjections: economicResponse.data,
- salaryData: salaryDataPoints,
- schools: filteredSchools,
- tuitionData: tuitionResponse.data,
- });
+
+ // Step 5: Consolidate Career Details
+ setCareerDetails({
+ ...career,
+ economicProjections: economicResponse.data,
+ salaryData: salaryDataPoints,
+ schools: schoolsWithDistance, // Add schools with distances
+ tuitionData: tuitionResponse.data,
+ });
} catch (error) {
console.error('Error processing career click:', error.message);
setError('Failed to load data');
@@ -144,6 +187,7 @@ function Dashboard() {
},
[userState, apiUrl, areaTitle]
);
+
const chartData = {
labels: riaSecScores.map((score) => score.area),
@@ -198,8 +242,28 @@ function Dashboard() {
loading={loading}
error={error}
userState={userState}
- />
+ />
)}
+
+ {/* Acknowledgment Section */}
+
);
}
diff --git a/src/components/EconomicProjections.js b/src/components/EconomicProjections.js
index 345d880..d3e7ba0 100644
--- a/src/components/EconomicProjections.js
+++ b/src/components/EconomicProjections.js
@@ -1,67 +1,62 @@
// EconomicProjections (in EconomicProjections.js)
import React, { useEffect, useState } from 'react';
-import axios from 'axios';
+import * as XLSX from 'xlsx';
function EconomicProjections({ socCode }) {
const [projections, setProjections] = useState(null);
const [error, setError] = useState(null);
-
- console.log(axios.defaults.baseURL); // Check if baseURL is set
- const apiUrl = process.env.REACT_APP_API_URL || '/api';
+ const [loading, setLoading] = useState(true);
+
useEffect(() => {
if (socCode) {
- const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code inside the component
+ const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code
console.log(`Fetching economic projections for cleaned SOC code: ${cleanedSocCode}`);
const fetchProjections = async () => {
- const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
- // Log URL and SOC code only in development
- if (process.env.NODE_ENV !== 'production') {
- console.log(`Fetching projections from: ${projectionsUrl}`);
- console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
- }
- try {
- const cleanedSocCode = socCode.split('.')[0]; // Clean SOC code
- const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
+ try {
+ // Load the Excel file from public folder
+ const response = await fetch('/public/ltprojections.xlsx');
+ const arrayBuffer = await response.arrayBuffer();
+ const workbook = XLSX.read(arrayBuffer, { type: 'array' });
- // Log URL and SOC code only in development
- if (process.env.NODE_ENV !== 'production') {
- console.log(`Fetching projections from: ${projectionsUrl}`);
- console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
- }
- // API call to fetch projections
- const response = await axios.get(projectionsUrl);
+ // Assuming data is in the first sheet
+ const sheetName = workbook.SheetNames[0];
+ const sheet = workbook.Sheets[sheetName];
+ const data = XLSX.utils.sheet_to_json(sheet);
- if (response.status === 200 && response.data) {
- setProjections(response.data); // Set projections
- if (process.env.NODE_ENV !== 'production') {
- console.log('Projections Response:', response.data); // Log data in development
+ // Find the projections matching the SOC code
+ const filteredProjections = data.find(
+ (row) => row['SOC Code'] === cleanedSocCode
+ );
+
+ if (filteredProjections) {
+ setProjections(filteredProjections);
+ } else {
+ throw new Error('No data found for the given SOC code.');
+ }
+ } catch (err) {
+ setError('Error loading economic projections.');
+ console.error('Projections Fetch Error:', err.message);
+ } finally {
+ setLoading(false);
}
- } else {
- throw new Error('Invalid response from server.');
- }
- } catch (err) {
- setError('Error fetching economic projections.');
- console.error('Projections Fetch Error:', err.message);
- }
- };
-
+ };
fetchProjections();
}
- }, [socCode,apiUrl]); // This runs when the socCode prop changes
+ }, [socCode]);
if (error) {
return {error}
;
}
- if (!projections) {
- return Loading economic projections...
;
+ if (loading) {
+ return Loading...
;
}
return (
-
Economic Projections for {projections.Occupation || 'Unknown Occupation'}
+
Economic Projections for {projections['Occupation Title'] || 'Unknown Occupation'}
2022 Employment: {projections['2022 Employment']}
2032 Employment: {projections['2032 Employment']}
diff --git a/src/components/LoanRepayment.js b/src/components/LoanRepayment.js
index a4edfda..a5fd27f 100644
--- a/src/components/LoanRepayment.js
+++ b/src/components/LoanRepayment.js
@@ -5,16 +5,15 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
const [interestRate, setInterestRate] = useState(5.5); // Default federal loan interest rate
const [loanTerm, setLoanTerm] = useState(10); // Default loan term (10 years)
const [extraPayment, setExtraPayment] = useState(0); // Extra monthly payment
+ const [currentSalary, setCurrentSalary] = useState(0); // Current salary input
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- const [sortBy, setSortBy] = useState('monthlyPayment'); // Default sort option
- const [currentSalary, setCurrentSalary] = useState(0); // New state for current salary
// Validation function
const validateInputs = () => {
- if (!schools || schools.length === 0 || !salaryData || salaryData.length === 0) {
- setError('School or salary data is missing.');
+ if (!schools || schools.length === 0) {
+ setError('School data is missing. Loan calculations cannot proceed.');
return false;
}
@@ -42,10 +41,10 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
return true;
};
- // Calculate loan details for each school
+ // Loan calculation function for all schools
const calculateLoanDetails = () => {
- if (!validateInputs()) return;
- setLoading(true);
+ if (!validateInputs()) return; // Validate inputs before calculation
+
const schoolResults = schools.map((school) => {
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
const monthlyRate = interestRate / 12 / 100;
@@ -69,69 +68,105 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
- // Calculate net gain
- const salary = salaryData[0].value; // 10th percentile salary as default
- const totalSalary = salary * earningHorizon;
- const netGain = totalSalary - totalLoanCost;
+ // Handle missing salary data
+ let salary = salaryData && salaryData[0]?.value ? salaryData[0].value : null;
+ let netGain = 'N/A';
+ let monthlySalary = 'N/A';
+
+ if (salary) {
+ // Calculate net gain
+ const totalSalary = salary * earningHorizon;
+ const currentSalaryEarnings = currentSalary * earningHorizon * Math.pow(1.03, earningHorizon); // 3% growth
+ netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
+
+ // Monthly salary
+ monthlySalary = (salary / 12).toFixed(2);
+ }
return {
...school,
tuition,
monthlyPayment: minimumMonthlyPayment.toFixed(2),
+ totalMonthlyPayment: extraMonthlyPayment.toFixed(2), // Add total payment including extra
totalLoanCost: totalLoanCost.toFixed(2),
- netGain: netGain.toFixed(2),
+ netGain,
+ monthlySalary,
};
});
setResults(schoolResults);
};
+
return (
)}
+
+ {/* Salary Warning */}
+ {!salaryData || salaryData.length === 0 ? (
+ Salary data is not available for this profession. Loan calculations are limited.
+ ) : null}
);
+
}
export default LoanRepayment;
diff --git a/src/components/PopoutPanel.css b/src/components/PopoutPanel.css
index 45a74df..4c87217 100644
--- a/src/components/PopoutPanel.css
+++ b/src/components/PopoutPanel.css
@@ -1,28 +1,42 @@
.popout-panel {
- position: fixed;
- top: 0;
- right: 0;
- width: 40%;
- height: 100%;
- background-color: #fff;
- box-shadow: -3px 0 5px rgba(0, 0, 0, 0.3);
- padding: 20px;
- overflow-y: auto;
- transform: translateX(0);
- transition: transform 0.3s ease-in-out;
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 40%; /* Default width for larger screens */
+ height: 100%;
+ background-color: #fff;
+ box-shadow: -3px 0 5px rgba(0, 0, 0, 0.3);
+ padding: 20px;
+ overflow-y: auto;
+ transform: translateX(0); /* Ensure visibility by default */
+ transition: transform 0.3s ease-in-out;
+ z-index: 1000; /* Ensures it is above other elements */
+}
+
+/* Mobile responsiveness */
+@media (max-width: 768px) {
+ .popout-panel {
+ width: 100%; /* Use full width for smaller screens */
+ height: 100%; /* Cover full height */
+ left: 0; /* Ensure it appears on the left for mobile */
+ right: unset; /* Override right alignment */
}
+}
- .close-btn {
- position: absolute;
- top: 10px;
- right: 15px;
- background-color: #dc3545;
- color: #fff;
- border: none;
- padding: 5px 10px;
- font-size: 1rem;
- cursor: pointer;
- }
+ /* Close button adjustments for mobile */
+.close-btn {
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ background-color: #dc3545;
+ color: #fff;
+ border: none;
+ padding: 8px 12px;
+ font-size: 1rem;
+ cursor: pointer;
+ z-index: 1001; /* Keep button above the panel */
+}
+
h3 {
margin-top: 20px;
@@ -52,4 +66,10 @@
button:hover {
background-color: #0056b3;
}
+
+ /* Fix for overflow issue */
+html, body {
+ overflow-x: hidden; /* Prevent horizontal scrolling */
+}
+
\ No newline at end of file
diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js
index 7920cc1..dd11f85 100644
--- a/src/components/PopoutPanel.js
+++ b/src/components/PopoutPanel.js
@@ -10,6 +10,7 @@ function PopoutPanel({
closePanel
}) {
console.log('PopoutPanel Props:', { data, loading, error, userState });
+
if (loading) {
return (
@@ -78,17 +79,19 @@ function PopoutPanel({
tuition['INSTNM']?.toLowerCase().trim() === // Corrected field
school['INSTNM']?.toLowerCase().trim() // Match institution name
);
-
+ console.log('Schools Data in PopoutPanel:', schools);
return (
{school['INSTNM']} {/* Updated field */}
- Degree Type: {school['CREDDESC'] || 'N/A'} {/* Updated field */}
+ Degree Type: {school['CREDDESC'] || 'Degree type information is not available for this program'} {/* Updated field */}
- In-State Tuition: ${school['In_state cost'] || 'N/A'} {/* Updated field */}
+ In-State Tuition: ${school['In_state cost'] || 'Tuition information is not available for this school'} {/* Updated field */}
- Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'} {/* Updated field */}
+ Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition information is not available for this school'} {/* Updated field */}
+ Distance: {school['distance'] || 'Distance to school not available'} {/* Added Distance field */}
+
Website:
{/* Updated field */}
{school['Website']}
@@ -98,7 +101,7 @@ function PopoutPanel({
})}
) : (
- No schools available.
+ No schools of higher education are available for this career path.
)}
@@ -111,7 +114,7 @@ function PopoutPanel({
Total Change: {economicProjections['Total Change'] || 'N/A'}
) : (
- No economic projections available.
+ No economic projections available for this career path.
)}
{/* Salary Data Points */}
@@ -136,24 +139,34 @@ function PopoutPanel({
) : (
- No salary data available.
+ No salary data is available for this career path.
)}
{/* Loan Repayment Analysis */}
- {tenthPercentileSalary > 0 && (
+
+
Loan Repayment Analysis
{
- const years = getProgramLength(school['CREDDESC']); // Updated field
- return {
- schoolName: school['INSTNM'], // Updated field
- inState: parseFloat(school['In_state cost'] * years) || 0, // Updated field
- outOfState: parseFloat(school['Out_state cost'] * years) || 0, // Updated field
- };
- })}
- salaryData={[{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]}
- earningHorizon={10}
- />
- )}
+ schools={schools.map((school) => {
+ const years = getProgramLength(school['CREDDESC']);
+ return {
+ schoolName: school['INSTNM'],
+ inState: parseFloat(school['In_state cost'] * years) || 0,
+ outOfState: parseFloat(school['Out_state cost'] * years) || 0,
+ };
+ })}
+ salaryData={
+ tenthPercentileSalary > 0
+ ? [{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]
+ : []
+ }
+ earningHorizon={10}
+ />
+ {!tenthPercentileSalary && (
+
+ Salary data unavailable. Loan details are based on cost alone.
+
+ )}
+
);
}