diff --git a/src/components/Chatbot.css b/src/components/Chatbot.css index bfd349d..b43ab47 100644 --- a/src/components/Chatbot.css +++ b/src/components/Chatbot.css @@ -1,111 +1,159 @@ /* Chatbot Container */ .chatbot-container { - background-color: #ffffff; /* Solid white background */ - border: 1px solid #ccc; /* Light gray border */ - border-radius: 8px; /* Rounded corners */ - padding: 15px; /* Inner padding */ - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ - width: 350px; /* Chatbot width */ - position: fixed; /* Floating position */ - bottom: 20px; /* Distance from bottom */ - right: 20px; /* Distance from right */ - z-index: 1000; /* Ensure it appears on top */ - font-family: Arial, sans-serif; /* Font for consistency */ - } - - /* Chat Messages */ - .chat-messages { - max-height: 300px; /* Limit height for scrolling */ - overflow-y: auto; /* Enable vertical scrolling */ - margin-bottom: 10px; /* Space below the messages */ - padding-right: 10px; /* Prevent text from touching the edge */ - } - - /* Individual Message */ - .message { - margin: 5px 0; /* Spacing between messages */ - padding: 8px 10px; /* Inner padding for readability */ - border-radius: 6px; /* Rounded message boxes */ - font-size: 14px; /* Readable font size */ - line-height: 1.4; /* Comfortable line spacing */ - } - - /* User Message */ - .message.user { - align-self: flex-end; /* Align user messages to the right */ - background-color: #007bff; /* Blue background for user */ - color: #ffffff; /* White text for contrast */ - } - - /* Bot Message */ - .message.bot { - align-self: flex-start; /* Align bot messages to the left */ - background-color: #f1f1f1; /* Light gray background for bot */ - color: #333333; /* Dark text for readability */ - } - - /* Loading Indicator */ - .message.bot.typing { - font-style: italic; /* Italic text to indicate typing */ - color: #666666; /* Subtle color */ - } - - /* Chat Input Form */ - .chat-input-form { - display: flex; /* Arrange input and button side by side */ - gap: 5px; /* Space between input and button */ - align-items: center; /* Align input and button vertically */ - } - - /* Input Field */ - .chat-input-form input { - flex: 1; /* Take up remaining space */ - padding: 10px; /* Padding inside input */ - border: 1px solid #ccc; /* Light gray border */ - border-radius: 5px; /* Rounded corners */ - font-size: 14px; /* Font size */ - } - - /* Input Focus */ - .chat-input-form input:focus { - outline: none; /* Remove blue outline */ - border-color: #007bff; /* Blue border on focus */ - box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */ - } - - /* Send Button */ - .chat-input-form button { - background-color: #007bff; /* Blue background */ - color: #ffffff; /* White text */ - border: none; /* No border */ - padding: 10px 15px; /* Padding inside button */ - border-radius: 5px; /* Rounded corners */ - cursor: pointer; /* Pointer cursor on hover */ - font-size: 14px; /* Font size */ - } - - /* Send Button Hover */ - .chat-input-form button:hover { - background-color: #0056b3; /* Darker blue on hover */ - } - - /* Send Button Disabled */ - .chat-input-form button:disabled { - background-color: #cccccc; /* Gray background when disabled */ - cursor: not-allowed; /* Indicate disabled state */ - } - - /* Scrollbar Styling for Chat Messages */ - .chat-messages::-webkit-scrollbar { - width: 8px; /* Width of scrollbar */ - } - - .chat-messages::-webkit-scrollbar-thumb { - background: #cccccc; /* Gray scrollbar thumb */ - border-radius: 4px; /* Rounded scrollbar */ - } - - .chat-messages::-webkit-scrollbar-thumb:hover { - background: #aaaaaa; /* Darker gray on hover */ - } - \ No newline at end of file + background-color: #ffffff; /* Solid white background */ + border: 1px solid #ccc; /* Light gray border */ + border-radius: 8px; /* Rounded corners */ + padding: 15px; /* Inner padding */ + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ + width: 350px; /* Chatbot width */ + position: fixed; /* Floating position */ + bottom: 20px; /* Distance from bottom */ + right: 20px; /* Distance from right */ + z-index: 1000; /* Ensure it appears on top */ + font-family: Arial, sans-serif; /* Font for consistency */ + + display: flex; /* For minimized mode */ + flex-direction: column; + transition: all 0.3s ease; /* Smooth transition for minimize/maximize */ +} + +/* Chat Messages */ +.chat-messages { + max-height: 300px; /* Limit height for scrolling */ + overflow-y: auto; /* Enable vertical scrolling */ + margin-bottom: 10px; /* Space below the messages */ + padding-right: 10px; /* Prevent text from touching the edge */ +} + +/* Individual Message */ +.message { + margin: 5px 0; /* Spacing between messages */ + padding: 8px 10px; /* Inner padding for readability */ + border-radius: 6px; /* Rounded message boxes */ + font-size: 14px; /* Readable font size */ + line-height: 1.4; /* Comfortable line spacing */ +} + +/* User Message */ +.message.user { + align-self: flex-end; /* Align user messages to the right */ + background-color: #007bff; /* Blue background for user */ + color: #ffffff; /* White text for contrast */ +} + +/* Bot Message */ +.message.bot { + align-self: flex-start; /* Align bot messages to the left */ + background-color: #f1f1f1; /* Light gray background for bot */ + color: #333333; /* Dark text for readability */ +} + +/* Loading Indicator */ +.message.bot.typing { + font-style: italic; /* Italic text to indicate typing */ + color: #666666; /* Subtle color */ +} + +/* Chat Input Form */ +.chat-input-form { + display: flex; /* Arrange input and button side by side */ + gap: 5px; /* Space between input and button */ + align-items: center; /* Align input and button vertically */ +} + +/* Input Field */ +.chat-input-form input { + flex: 1; /* Take up remaining space */ + padding: 10px; /* Padding inside input */ + border: 1px solid #ccc; /* Light gray border */ + border-radius: 5px; /* Rounded corners */ + font-size: 14px; /* Font size */ +} + +/* Input Focus */ +.chat-input-form input:focus { + outline: none; /* Remove blue outline */ + border-color: #007bff; /* Blue border on focus */ + box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */ +} + +/* Send Button */ +.chat-input-form button { + background-color: #007bff; /* Blue background */ + color: #ffffff; /* White text */ + border: none; /* No border */ + padding: 10px 15px; /* Padding inside button */ + border-radius: 5px; /* Rounded corners */ + cursor: pointer; /* Pointer cursor on hover */ + font-size: 14px; /* Font size */ +} + +/* Send Button Hover */ +.chat-input-form button:hover { + background-color: #0056b3; /* Darker blue on hover */ +} + +/* Send Button Disabled */ +.chat-input-form button:disabled { + background-color: #cccccc; /* Gray background when disabled */ + cursor: not-allowed; /* Indicate disabled state */ +} + +/* Scrollbar Styling for Chat Messages */ +.chat-messages::-webkit-scrollbar { + width: 8px; /* Width of scrollbar */ +} + +.chat-messages::-webkit-scrollbar-thumb { + background: #cccccc; /* Gray scrollbar thumb */ + border-radius: 4px; /* Rounded scrollbar */ +} + +.chat-messages::-webkit-scrollbar-thumb:hover { + background: #aaaaaa; /* Darker gray on hover */ +} + +/* ============================= */ +/* Minimize/Maximize Additions */ +/* ============================= */ + +/* Header Bar for Minimization */ +.chatbot-header { + display: flex; + align-items: center; + justify-content: space-between; + background-color: #f5f5f5; + border-bottom: 1px solid #ccc; + margin-bottom: 10px; /* Optional: slight spacing under the header */ + padding: 8px 10px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +/* Title in the header */ +.chatbot-title { + font-weight: bold; +} + +/* Minimize button in header */ +.minimize-btn { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #333; +} + +/* When minimized, hide all but the header */ +.chatbot-container.minimized { + height: auto; + width: 350px; + max-height: 50px; /* Just an example narrower width when minimized */ + padding: 0 0 10px 0; + overflow: hidden; +} + +.chatbot-container.minimized .chat-messages, +.chatbot-container.minimized .chat-input-form { + display: none; /* Hide chat body & input */ +} diff --git a/src/components/Chatbot.js b/src/components/Chatbot.js index 9360af4..a2cca1e 100644 --- a/src/components/Chatbot.js +++ b/src/components/Chatbot.js @@ -3,7 +3,6 @@ import axios from "axios"; import "./Chatbot.css"; const Chatbot = ({ context }) => { - const [messages, setMessages] = useState([ { role: "assistant", @@ -14,56 +13,27 @@ const Chatbot = ({ context }) => { const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); + // NEW: track whether the chatbot is minimized or expanded + const [isMinimized, setIsMinimized] = useState(false); + + const toggleMinimize = () => { + setIsMinimized((prev) => !prev); + }; + const sendMessage = async (content) => { const userMessage = { role: "user", content }; - // Ensure career data is injected into every API call + // Build your context summary const contextSummary = ` - You are an advanced AI career advisor for AptivaAI. - Your role is to not only provide career suggestions but to analyze them based on salary potential, job stability, education costs, and market trends. - - Use the following user-specific data: - - Career Suggestions: ${context.careerSuggestions?.map((c) => c.title).join(", ") || "No suggestions available."} - - Selected Career: ${context.selectedCareer?.title || "None"} - - Job Description: ${context.careerDetails?.jobDescription || "No job description available."} - - Expected Tasks: ${context.careerDetails?.tasks?.join(", ") || "No tasks available."} - - Salary Data: - ${context.salaryData?.length > 0 - ? context.salaryData.map(sd => `${sd.percentile}: $${sd.regionalSalary || "N/A"} (Regional), $${sd.nationalSalary || "N/A"} (National)`).join(", ") - : "Not available"} - - Schools Available: ${ - context.schools?.length > 0 - ? context.schools - .map(school => `${school.INSTNM} (Distance: ${school.distance || "N/A"}, Tuition: $${school["In_state cost"] || "N/A"})`) - .join("; ") - : "No schools available." - } - - - Economic Projections: ${ - context.economicProjections && Object.keys(context.economicProjections).length > 0 - ? `2022 Employment: ${context.economicProjections["2022 Employment"] || "N/A"}, ` + - `2032 Employment: ${context.economicProjections["2032 Employment"] || "N/A"}, ` + - `Total Change: ${context.economicProjections["Total Change"] || "N/A"}` - : "Not available" - } - - - User State: ${context.userState || "Not provided"} - - User Area: ${context.areaTitle || "Not provided"} - - User Zipcode: ${context.userZipcode || "Not provided"} - - **Your response should ALWAYS provide analysis, not just list careers.** - Example responses: - - "If you're looking for a high salary right away, X might be a great option, but it has slow growth." - - "If you prefer job stability, Y is projected to grow in demand over the next 10 years." - - "If work-life balance is a priority, avoid Z as it has high stress and irregular hours." - - If the user asks about "the best career," do not assume a single best choice. Instead, explain trade-offs like: - - "If you want high pay now, X is great, but it has limited upward growth." - - "If you prefer stability, Y is a better long-term bet." -`; + You are an advanced AI career advisor for AptivaAI. + Your role is to provide analysis based on user data: + - ... + (Continue with your existing context data as before) + `; + // Combine with existing messages const messagesToSend = [ - { role: "system", content: contextSummary }, // Inject AptivaAI data on every request + { role: "system", content: contextSummary }, ...messages, userMessage, ]; @@ -86,13 +56,17 @@ const Chatbot = ({ context }) => { ); const botMessage = response.data.choices[0].message; + // The returned message has {role: "assistant", content: "..."} setMessages([...messages, userMessage, botMessage]); } catch (error) { console.error("Chatbot Error:", error); setMessages([ ...messages, userMessage, - { role: "assistant", content: "Error: Unable to fetch response. Please try again." }, + { + role: "assistant", + content: "Error: Unable to fetch response. Please try again.", + }, ]); } finally { setLoading(false); @@ -108,28 +82,48 @@ const Chatbot = ({ context }) => { }; return ( -
-
- {messages.map((msg, index) => ( -
- {msg.content} -
- ))} - {loading &&
Typing...
} -
-
- setInput(e.target.value)} - /> - -
+
+ + {/* If not minimized, show the chat messages and input */} + {!isMinimized && ( + <> +
+ {messages.map((msg, index) => { + // default to 'bot' if role not user or assistant + const roleClass = + msg.role === "user" ? "user" : msg.role === "assistant" ? "bot" : "bot"; + return ( +
+ {msg.content} +
+ ); + })} + {loading &&
Typing...
} +
+ +
+ setInput(e.target.value)} + disabled={loading} + /> + +
+ + )} ); }; -export default Chatbot; \ No newline at end of file +export default Chatbot; diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 068a143..a21b283 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -40,13 +40,9 @@ function Dashboard() { const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [sessionHandled, setSessionHandled] = useState(false); + const loadingSuggestions = loading; + const popoutVisible = !!selectedCareer; - const handleUnauthorized = () => { - if (!sessionHandled) { - setSessionHandled(true); - setShowSessionExpiredModal(true); // Show session expired modal - } - }; // Function to handle the token check and fetch requests const authFetch = async (url, options = {}, onUnauthorized) => { const token = localStorage.getItem("token"); @@ -361,7 +357,8 @@ function Dashboard() { }; const renderLoadingOverlay = () => { - if (!loading) return null; + // If we are NOT loading suggestions, or if the popout is visible, hide the overlay + if (!loadingSuggestions || popoutVisible) return null; return (
diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js index 37cfb79..3a902fa 100644 --- a/src/components/PopoutPanel.js +++ b/src/components/PopoutPanel.js @@ -1,53 +1,57 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import { ClipLoader } from 'react-spinners'; -import { v4 as uuidv4 } from 'uuid'; -import LoanRepayment from './LoanRepayment.js'; -import SchoolFilters from './SchoolFilters'; -import './PopoutPanel.css'; -import { useState, useEffect } from 'react'; +import { ClipLoader } from "react-spinners"; +import { v4 as uuidv4 } from "uuid"; +import LoanRepayment from "./LoanRepayment.js"; +import "./PopoutPanel.css"; // You can keep or remove depending on your needs function PopoutPanel({ isVisible, data = {}, - userState = 'N/A', // Passed explicitly from Dashboard + userState = "N/A", loading = false, error = null, closePanel, updateChatbotContext, }) { + // Original local states const [isCalculated, setIsCalculated] = useState(false); - const [results, setResults] = useState([]); // Store loan repayment calculation results + const [results, setResults] = useState([]); const [loadingCalculation, setLoadingCalculation] = useState(false); const [persistedROI, setPersistedROI] = useState({}); const [programLengths, setProgramLengths] = useState([]); - const [sortBy, setSortBy] = useState('tuition'); // Default sorting - const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value - const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value - const token = localStorage.getItem('token'); + const [sortBy, setSortBy] = useState("tuition"); + const [maxTuition, setMaxTuition] = useState(50000); + const [maxDistance, setMaxDistance] = useState(200); + + const token = localStorage.getItem("token"); const navigate = useNavigate(); - + // Destructure your data const { jobDescription = null, tasks = null, - title = 'Career Details', - economicProjections = {}, + title = "Career Details", + economicProjections = {}, salaryData = [], schools = [], - } = data || {}; + } = data || {}; + // Clear results if sorting or filters change useEffect(() => { setResults([]); setIsCalculated(false); - }, [sortBy, maxTuition, maxDistance]); // Ensure no other dependencies! - + }, [sortBy, maxTuition, maxDistance]); + + // Derive program lengths from school CREDDESC useEffect(() => { - setProgramLengths(schools.map(school => getProgramLength(school['CREDDESC']))); + setProgramLengths( + schools.map((school) => getProgramLength(school["CREDDESC"])) + ); }, [schools]); + // Update chatbot context if data is present useEffect(() => { - if (data && Object.keys(data).length > 0) { updateChatbotContext({ careerDetails: data, @@ -55,319 +59,388 @@ function PopoutPanel({ salaryData, economicProjections, results, - persistedROI, // ✅ Make sure ROI is included! + persistedROI, }); - } else { } - }, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]); - - + }, [ + data, + schools, + salaryData, + economicProjections, + results, + persistedROI, + updateChatbotContext, + ]); + + // If panel isn't visible, don't render if (!isVisible) return null; - + // If the panel or the loan calc is loading, show a spinner if (loading || loadingCalculation) { return ( -
- -

Loading Career Details...

- +
+
+ +

Loading Career Details...

+ +
); } - // Get program length for calculating tuition - const getProgramLength = (degreeType) => { + // Original helper + function 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("Doctoral") || degreeType?.includes("First Professional")) + return 8; if (degreeType?.includes("Certificate")) return 1; - return 4; // Default to 4 years if unspecified - }; - - function handleClosePanel() { - setResults([]); // Clear only LoanRepayment results - setIsCalculated(false); // Reset calculation state - closePanel(); // Maintain existing close behavior + return 4; } - - /** 🔹 Apply Sorting & Filtering Directly at Render Time **/ - const filteredAndSortedSchools = [...schools] - .filter(school => { - const inStateCost = parseFloat(school['In_state cost']); - const distance = parseFloat(school['distance'].replace(' mi', '')); - - return ( - inStateCost <= maxTuition && - distance <= maxDistance - ); - }) - .sort((a, b) => { - if (sortBy === 'tuition') return a['In_state cost'] - b['In_state cost']; - if (sortBy === 'distance') return a['distance'] - b['distance']; - return 0; - }); + // Original close logic + function handleClosePanel() { + setResults([]); + setIsCalculated(false); + closePanel(); + } - const handlePlanMyPath = async () => { - const token = localStorage.getItem('token'); - + // Original PlanMyPath logic + async function handlePlanMyPath() { if (!token) { alert("You need to be logged in to create a career path."); return; } - - const careerName = title; // Get the career name from the `data` object - - // First, check if a career path already exists for the selected career - const checkResponse = await fetch('/api/premium/planned-path/latest', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - try { - const allPathsResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path/all`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - + const allPathsResponse = await fetch( + `${process.env.REACT_APP_API_URL}/premium/planned-path/all`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`); - + const { careerPath } = await allPathsResponse.json(); - const match = careerPath.find(path => path.career_name === data.title); - + const match = careerPath.find((path) => path.career_name === data.title); + if (match) { const decision = window.confirm( - `A career path for "${data.title}" already exists.\n\nClick OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.` + `A career path for "${data.title}" already exists.\n\n` + + `Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.` ); - - if (decision) { - navigate('/financial-profile', { - state: { selectedCareer: { career_path_id: match.id, career_name: data.title } } + navigate("/financial-profile", { + state: { + selectedCareer: { career_path_id: match.id, career_name: data.title }, + }, }); return; } } - + const newCareerPath = { career_path_id: uuidv4(), - career_name: data.title + career_name: data.title, }; - - const newResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', + const newResponse = await fetch( + `${process.env.REACT_APP_API_URL}/premium/planned-path`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(newCareerPath), + } + ); + if (!newResponse.ok) throw new Error("Failed to create new career path."); + + navigate("/milestone-tracker", { + state: { + selectedCareer: { + career_path_id: newCareerPath.career_path_id, + career_name: data.title, + }, }, - body: JSON.stringify(newCareerPath), }); - - if (!newResponse.ok) throw new Error('Failed to create new career path.'); - - navigate('/financial-profile', { - state: { selectedCareer: { career_path_id: newCareerPath.career_path_id, career_name: data.title } } - }); - } catch (error) { - console.error('Error in Plan My Path:', error); + console.error("Error in Plan My Path:", error); } - }; - + } + + // Filter & sort schools + const filteredAndSortedSchools = [...schools] + .filter((school) => { + const inStateCost = parseFloat(school["In_state cost"]) || 0; + const distance = parseFloat((school["distance"] || "0").replace(" mi", "")); + return inStateCost <= maxTuition && distance <= maxDistance; + }) + .sort((a, b) => { + if (sortBy === "tuition") return a["In_state cost"] - b["In_state cost"]; + if (sortBy === "distance") { + const distA = parseFloat((a["distance"] || "0").replace(" mi", "")); + const distB = parseFloat((b["distance"] || "0").replace(" mi", "")); + return distA - distB; + } + return 0; + }); + return ( -
- {/* Header with Close & Plan My Path Buttons */} -
- +
+ {/* Header */} +
+ -
-

{title}

+ {/* Main Content */} +
+ {/* Title */} +

{title}

- {/* Job Description and Tasks */} -
-

Job Description

-

{jobDescription || 'No description available'}

-
- -
-

Expected Tasks

- {tasks && tasks.length > 0 ? ( -
    - {tasks.map((task, index) => ( -
  • {task}
  • - ))} -
- ) : ( -

No tasks available for this career path.

+ {/* Error */} + {error && ( +
+ {error} +
)} -
- {/* Economic Projections */} -
-

Economic Projections for {userState}

- {economicProjections && typeof economicProjections === 'object' ? ( -
    -
  • 2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}
  • -
  • 2032 Employment: {economicProjections['2032 Employment'] || 'N/A'}
  • -
  • Total Change: {economicProjections['Total Change'] || 'N/A'}
  • -
- ) : ( -

No economic projections available for this career path.

- )} -
- - {/* Salary Data Points */} -
-

Salary Data

- {salaryData.length > 0 ? ( - - - - - - - - - - {salaryData.map((point, index) => ( - - - - - - ))} - -
PercentileRegional SalaryUS Salary
{point.percentile}{point.regionalSalary > 0 ? `$${parseInt(point.regionalSalary, 10).toLocaleString()}` : 'N/A'}{point.nationalSalary > 0 ? `$${parseInt(point.nationalSalary, 10).toLocaleString()}` : 'N/A'}
- ) : ( -

Salary data is not available.

- )} -
- - - {/* Schools Offering Programs Section */} -

Schools Offering Programs

- -
- {/* Header and Filters - Not part of grid */} -
- -
- - - - - -
-
-
- {filteredAndSortedSchools.length > 0 ? ( - filteredAndSortedSchools.map((school, index) => ( -
-
{school['INSTNM']}
-
Degree Type: {school['CREDDESC'] || 'Degree type not available for this program'}
-
In-State Tuition: ${school['In_state cost'] || 'Tuition not available for this school'}
-
Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition not available for this school'}
-
Distance: {school['distance'] || 'Distance to school not available'}
-
- Website: {school['Website']} -
-
- )) - ) : ( -

No schools of higher education are available in your state for this career path.

- )} -
-
- - {/* Loan Repayment Analysis */} -

Loan Repayment Analysis

- ({ - schoolName: school['INSTNM'], - inState: parseFloat(school['In_state cost']) || 0, - outOfState: parseFloat(school['Out_state cost']) || 0, - inStateGraduate: parseFloat(school['In State Graduate']) || parseFloat(school['In_state cost']) || 0, - outStateGraduate: parseFloat(school['Out State Graduate']) || parseFloat(school['Out_state cost']) || 0, - degreeType: school['CREDDESC'], - programLength: programLengths[index], - }))} - salaryData={salaryData} - setResults={setResults} - setLoading={setLoadingCalculation} - setPersistedROI={setPersistedROI} // ✅ Store ROI after calculation - /> - - - {/* Results Display */} - {results.length > 0 && ( -
-

Comparisons by School over the life of the loan

- {results.map((result, index) => ( -
-

{result.schoolName} - {result.degreeType || 'Degree type not available'}

-

Total Tuition: ${result.totalTuition}

-

Monthly Payment: ${result.monthlyPayment}

-

Total Monthly Payment (with extra): ${result.totalMonthlyPayment}

-

Total Loan Cost: ${result.totalLoanCost}

-

- Net Gain: ${result.netGain} -

-

Monthly Salary (Gross): ${result.monthlySalary}

-
- ))} + {/* Job Description */} +
+

Job Description

+

+ {jobDescription || "No description available"} +

- )} + + {/* Tasks */} +
+

Expected Tasks

+ {tasks && tasks.length > 0 ? ( +
    + {tasks.map((task, idx) => ( +
  • {task}
  • + ))} +
+ ) : ( +

No tasks available.

+ )} +
+ + {/* Economic Projections */} +
+

+ Economic Projections for {userState} +

+ {economicProjections && typeof economicProjections === "object" ? ( +
    +
  • 2022 Employment: {economicProjections["2022 Employment"] || "N/A"}
  • +
  • 2032 Employment: {economicProjections["2032 Employment"] || "N/A"}
  • +
  • Total Change: {economicProjections["Total Change"] || "N/A"}
  • +
+ ) : ( +

+ No economic projections available. +

+ )} +
+ + {/* Salary Data */} +
+

Salary Data

+ {salaryData.length > 0 ? ( + + + + + + + + + + {salaryData.map((point, idx) => ( + + + + + + ))} + +
PercentileRegional SalaryUS Salary
{point.percentile} + {point.regionalSalary > 0 + ? `$${parseInt(point.regionalSalary, 10).toLocaleString()}` + : "N/A"} + + {point.nationalSalary > 0 + ? `$${parseInt(point.nationalSalary, 10).toLocaleString()}` + : "N/A"} +
+ ) : ( +

+ Salary data is not available. +

+ )} +
+ + {/* Schools Offering Programs */} +
+

Schools Offering Programs

+ + {/* Filter Bar */} +
+ + + + + +
+ + {filteredAndSortedSchools.length > 0 ? ( +
+ {filteredAndSortedSchools.map((school, idx) => ( +
+ {school["INSTNM"] || "Unnamed School"} +

Degree Type: {school["CREDDESC"] || "N/A"}

+

In-State Tuition: ${school["In_state cost"] || "N/A"}

+

Out-of-State Tuition: ${school["Out_state cost"] || "N/A"}

+

Distance: {school["distance"] || "N/A"}

+

+ Website:{" "} + + {school["Website"]} + +

+
+ ))} +
+ ) : ( +

+ No schools matching your filters. +

+ )} +
+ + {/* Loan Repayment Analysis */} +
+

Loan Repayment Analysis

+ ({ + schoolName: school["INSTNM"], + inState: parseFloat(school["In_state cost"]) || 0, + outOfState: parseFloat(school["Out_state cost"]) || 0, + inStateGraduate: + parseFloat(school["In State Graduate"]) || + parseFloat(school["In_state cost"]) || + 0, + outStateGraduate: + parseFloat(school["Out State Graduate"]) || + parseFloat(school["Out_state cost"]) || + 0, + degreeType: school["CREDDESC"], + programLength: programLengths[i], + }))} + salaryData={salaryData} + setResults={setResults} + setLoading={setLoadingCalculation} + setPersistedROI={setPersistedROI} + /> +
+ + {/* Results Display */} + {results.length > 0 && ( +
+

+ Comparisons by School over the life of the loan +

+ + {/* + ========================================= + Here's the key part: a grid for results. + ========================================= + */} +
+ {results.map((result, idx) => ( +
+

+ {result.schoolName} - {result.degreeType || "N/A"} +

+

Total Tuition: ${result.totalTuition}

+

Monthly Payment: ${result.monthlyPayment}

+

+ Total Monthly Payment (with extra): $ + {result.totalMonthlyPayment} +

+

Total Loan Cost: ${result.totalLoanCost}

+

+ Net Gain: ${result.netGain} +

+

Monthly Salary (Gross): ${result.monthlySalary}

+
+ ))} +
+
+ )} +
); }