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) => ( + + ))} +
- ); -} + ); + }; + + 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 */} +
+

+ Career results and RIASEC scores are provided by + O*Net, in conjunction with the + Bureau of Labor Statistics, and the + National Center for Education Statistics (NCES). +

+
); } 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'}

); + } 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. +

    + )} +
    ); }