UI fixes for Dashboard/Popoutpanel/Chatbot

This commit is contained in:
Josh 2025-04-30 18:13:18 +00:00
parent 961f994220
commit 1cbaa7c171
4 changed files with 578 additions and 466 deletions

View File

@ -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 */
}
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 */
}

View File

@ -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 (
<div className="chatbot-container">
<div className="chat-messages">
{messages.map((msg, index) => (
<div key={index} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{loading && <div className="message assistant">Typing...</div>}
</div>
<form onSubmit={handleSubmit} className="chat-input-form">
<input
type="text"
placeholder="Ask a question..."
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" disabled={loading}>
Send
<div className={`chatbot-container ${isMinimized ? "minimized" : ""}`}>
{/* Header Bar for Minimize/Maximize */}
<div className="chatbot-header">
<span className="chatbot-title">Career Chatbot</span>
<button className="minimize-btn" onClick={toggleMinimize}>
{isMinimized ? "▼" : "▲"}
</button>
</form>
</div>
{/* If not minimized, show the chat messages and input */}
{!isMinimized && (
<>
<div className="chat-messages">
{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 (
<div key={index} className={`message ${roleClass}`}>
{msg.content}
</div>
);
})}
{loading && <div className="message bot typing">Typing...</div>}
</div>
<form onSubmit={handleSubmit} className="chat-input-form">
<input
type="text"
placeholder="Ask a question..."
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={loading}
/>
<button type="submit" disabled={loading}>
Send
</button>
</form>
</>
)}
</div>
);
};
export default Chatbot;
export default Chatbot;

View File

@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">

View File

@ -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 (
<div className="popout-panel">
<button className="close-btn" onClick={closePanel}>X</button>
<h2>Loading Career Details...</h2>
<ClipLoader size={35} color="#4A90E2" />
<div className="popout-panel fixed top-0 right-0 z-50 h-full w-full max-w-xl overflow-y-auto bg-white shadow-xl">
<div className="p-4">
<button
className="mb-4 rounded bg-red-100 px-2 py-1 text-sm text-red-600 hover:bg-red-200"
onClick={closePanel}
>
X
</button>
<h2 className="mb-2 text-xl font-semibold">Loading Career Details...</h2>
<ClipLoader size={35} color="#4A90E2" />
</div>
</div>
);
}
// 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 (
<div className="popout-panel">
{/* Header with Close & Plan My Path Buttons */}
<div className="panel-header">
<button className="close-btn" onClick={closePanel}>X</button>
<div className="popout-panel fixed top-0 right-0 z-50 flex h-full w-full max-w-xl flex-col bg-white shadow-xl">
{/* Header */}
<div className="flex items-center justify-between border-b p-4">
<button
className="plan-path-btn"
onClick={handlePlanMyPath} // 🔥 Use the already-defined, correct handler
className="rounded bg-red-100 px-2 py-1 text-sm text-red-600 hover:bg-red-200"
onClick={handleClosePanel}
>
X
</button>
<button
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
onClick={handlePlanMyPath}
>
Plan My Path
</button>
</div>
<h2>{title}</h2>
{/* Main Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{/* Title */}
<h2 className="text-xl font-semibold">{title}</h2>
{/* Job Description and Tasks */}
<div className="job-description">
<h3>Job Description</h3>
<p>{jobDescription || 'No description available'}</p>
</div>
<div className="job-tasks">
<h3>Expected Tasks</h3>
{tasks && tasks.length > 0 ? (
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
) : (
<p>No tasks available for this career path.</p>
{/* Error */}
{error && (
<div className="rounded bg-red-50 p-2 text-red-600">
{error}
</div>
)}
</div>
{/* Economic Projections */}
<div className="economic-projections">
<h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? (
<ul>
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
<li>2032 Employment: {economicProjections['2032 Employment'] || 'N/A'}</li>
<li>Total Change: {economicProjections['Total Change'] || 'N/A'}</li>
</ul>
) : (
<p>No economic projections available for this career path.</p>
)}
</div>
{/* Salary Data Points */}
<div className="salary-data">
<h3>Salary Data</h3>
{salaryData.length > 0 ? (
<table>
<thead>
<tr>
<th>Percentile</th>
<th>Regional Salary</th>
<th>US Salary</th>
</tr>
</thead>
<tbody>
{salaryData.map((point, index) => (
<tr key={index}>
<td>{point.percentile}</td>
<td>{point.regionalSalary > 0 ? `$${parseInt(point.regionalSalary, 10).toLocaleString()}` : 'N/A'}</td>
<td>{point.nationalSalary > 0 ? `$${parseInt(point.nationalSalary, 10).toLocaleString()}` : 'N/A'}</td>
</tr>
))}
</tbody>
</table>
) : (
<p>Salary data is not available.</p>
)}
</div>
{/* Schools Offering Programs Section */}
<h3>Schools Offering Programs</h3>
<div className="schools-offering-container">
{/* Header and Filters - Not part of grid */}
<div className="schools-header" style={{
display: 'flex',
alignItems: 'center',
marginBottom: '10px',
justifyContent: 'center',
alignItems: 'center', }}>
<div className="compact-filters" style={{ display: 'flex', gap: '15px', justifyContent: 'center', alignItems: 'center' }}>
<label>
Sort:
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
style={{ marginLeft: '5px', padding: '2px', width: '100px' }}
>
<option value="tuition">Tuition</option>
<option value="distance">Distance</option>
</select>
</label>
<label>
Tuition (max):
<input
type="number"
value={maxTuition}
step={1000}
min={0}
max={100000}
style={{ width: '90px', padding: '2px' }}
onChange={(e) => setMaxTuition(Number(e.target.value))}
/>
</label>
<label>
Distance (max mi):
<input
type="number"
value={maxDistance}
step={10}
min={10}
max={500}
style={{ width: '70px', padding: '2px' }}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</label>
</div>
</div>
<div className="schools-offering">
{filteredAndSortedSchools.length > 0 ? (
filteredAndSortedSchools.map((school, index) => (
<div key={index} className="school-card">
<div><strong>{school['INSTNM']}</strong></div>
<div>Degree Type: {school['CREDDESC'] || 'Degree type not available for this program'}</div>
<div>In-State Tuition: ${school['In_state cost'] || 'Tuition not available for this school'}</div>
<div>Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition not available for this school'}</div>
<div>Distance: {school['distance'] || 'Distance to school not available'}</div>
<div>
Website: <a href={school['Website']} target="_blank" rel="noopener noreferrer">{school['Website']}</a>
</div>
</div>
))
) : (
<p className="no-schools-message">No schools of higher education are available in your state for this career path.</p>
)}
</div>
</div>
{/* Loan Repayment Analysis */}
<h3>Loan Repayment Analysis</h3>
<LoanRepayment
schools={filteredAndSortedSchools.map((school, index) => ({
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 && (
<div className="results-container">
<h3>Comparisons by School over the life of the loan</h3>
{results.map((result, index) => (
<div className="school-result-card" key={index}>
<h4>{result.schoolName} - {result.degreeType || 'Degree type not available'}</h4>
<p>Total Tuition: ${result.totalTuition}</p>
<p>Monthly Payment: ${result.monthlyPayment}</p>
<p>Total Monthly Payment (with extra): ${result.totalMonthlyPayment}</p>
<p>Total Loan Cost: ${result.totalLoanCost}</p>
<p className={`net-gain ${parseFloat(result.netGain) < 0 ? 'negative' : 'positive'}`}>
Net Gain: ${result.netGain}
</p>
<p>Monthly Salary (Gross): ${result.monthlySalary}</p>
</div>
))}
{/* Job Description */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Job Description</h3>
<p className="text-sm text-gray-700">
{jobDescription || "No description available"}
</p>
</div>
)}
{/* Tasks */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Expected Tasks</h3>
{tasks && tasks.length > 0 ? (
<ul className="list-disc space-y-1 pl-5 text-sm text-gray-700">
{tasks.map((task, idx) => (
<li key={idx}>{task}</li>
))}
</ul>
) : (
<p className="text-sm text-gray-500">No tasks available.</p>
)}
</div>
{/* Economic Projections */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">
Economic Projections for {userState}
</h3>
{economicProjections && typeof economicProjections === "object" ? (
<ul className="space-y-1 text-sm text-gray-700">
<li>2022 Employment: {economicProjections["2022 Employment"] || "N/A"}</li>
<li>2032 Employment: {economicProjections["2032 Employment"] || "N/A"}</li>
<li>Total Change: {economicProjections["Total Change"] || "N/A"}</li>
</ul>
) : (
<p className="text-sm text-gray-500">
No economic projections available.
</p>
)}
</div>
{/* Salary Data */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Salary Data</h3>
{salaryData.length > 0 ? (
<table className="w-full text-sm text-gray-700">
<thead>
<tr className="bg-gray-200 text-left">
<th className="p-2">Percentile</th>
<th className="p-2">Regional Salary</th>
<th className="p-2">US Salary</th>
</tr>
</thead>
<tbody>
{salaryData.map((point, idx) => (
<tr key={idx} className="border-b">
<td className="p-2">{point.percentile}</td>
<td className="p-2">
{point.regionalSalary > 0
? `$${parseInt(point.regionalSalary, 10).toLocaleString()}`
: "N/A"}
</td>
<td className="p-2">
{point.nationalSalary > 0
? `$${parseInt(point.nationalSalary, 10).toLocaleString()}`
: "N/A"}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-sm text-gray-500">
Salary data is not available.
</p>
)}
</div>
{/* Schools Offering Programs */}
<div>
<h3 className="mb-2 text-base font-medium">Schools Offering Programs</h3>
{/* Filter Bar */}
<div className="mb-4 flex items-center space-x-4">
<label className="text-sm text-gray-600">
Sort:
<select
className="ml-2 rounded border px-2 py-1 text-sm"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="tuition">Tuition</option>
<option value="distance">Distance</option>
</select>
</label>
<label className="text-sm text-gray-600">
Tuition (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxTuition}
onChange={(e) => setMaxTuition(Number(e.target.value))}
/>
</label>
<label className="text-sm text-gray-600">
Distance (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxDistance}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</label>
</div>
{filteredAndSortedSchools.length > 0 ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{filteredAndSortedSchools.map((school, idx) => (
<div key={idx} className="rounded border p-3 text-sm">
<strong>{school["INSTNM"] || "Unnamed School"}</strong>
<p>Degree Type: {school["CREDDESC"] || "N/A"}</p>
<p>In-State Tuition: ${school["In_state cost"] || "N/A"}</p>
<p>Out-of-State Tuition: ${school["Out_state cost"] || "N/A"}</p>
<p>Distance: {school["distance"] || "N/A"}</p>
<p>
Website:{" "}
<a
href={school["Website"]}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{school["Website"]}
</a>
</p>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">
No schools matching your filters.
</p>
)}
</div>
{/* Loan Repayment Analysis */}
<section className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Loan Repayment Analysis</h3>
<LoanRepayment
schools={filteredAndSortedSchools.map((school, i) => ({
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}
/>
</section>
{/* Results Display */}
{results.length > 0 && (
<div className="results-container rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">
Comparisons by School over the life of the loan
</h3>
{/*
=========================================
Here's the key part: a grid for results.
=========================================
*/}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.map((result, idx) => (
<div
key={idx}
className="rounded border p-3 text-sm text-gray-700"
>
<h4 className="mb-1 text-sm font-medium">
{result.schoolName} - {result.degreeType || "N/A"}
</h4>
<p>Total Tuition: ${result.totalTuition}</p>
<p>Monthly Payment: ${result.monthlyPayment}</p>
<p>
Total Monthly Payment (with extra): $
{result.totalMonthlyPayment}
</p>
<p>Total Loan Cost: ${result.totalLoanCost}</p>
<p
className={
parseFloat(result.netGain) < 0
? "text-red-600"
: "text-green-600"
}
>
Net Gain: ${result.netGain}
</p>
<p>Monthly Salary (Gross): ${result.monthlySalary}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}