UI fixes for Dashboard/Popoutpanel/Chatbot

This commit is contained in:
Josh 2025-04-30 18:13:18 +00:00
parent 0738457a83
commit 636d33cf94
4 changed files with 578 additions and 466 deletions

View File

@ -11,70 +11,74 @@
right: 20px; /* Distance from right */ right: 20px; /* Distance from right */
z-index: 1000; /* Ensure it appears on top */ z-index: 1000; /* Ensure it appears on top */
font-family: Arial, sans-serif; /* Font for consistency */ font-family: Arial, sans-serif; /* Font for consistency */
}
/* Chat Messages */ display: flex; /* For minimized mode */
.chat-messages { flex-direction: column;
transition: all 0.3s ease; /* Smooth transition for minimize/maximize */
}
/* Chat Messages */
.chat-messages {
max-height: 300px; /* Limit height for scrolling */ max-height: 300px; /* Limit height for scrolling */
overflow-y: auto; /* Enable vertical scrolling */ overflow-y: auto; /* Enable vertical scrolling */
margin-bottom: 10px; /* Space below the messages */ margin-bottom: 10px; /* Space below the messages */
padding-right: 10px; /* Prevent text from touching the edge */ padding-right: 10px; /* Prevent text from touching the edge */
} }
/* Individual Message */ /* Individual Message */
.message { .message {
margin: 5px 0; /* Spacing between messages */ margin: 5px 0; /* Spacing between messages */
padding: 8px 10px; /* Inner padding for readability */ padding: 8px 10px; /* Inner padding for readability */
border-radius: 6px; /* Rounded message boxes */ border-radius: 6px; /* Rounded message boxes */
font-size: 14px; /* Readable font size */ font-size: 14px; /* Readable font size */
line-height: 1.4; /* Comfortable line spacing */ line-height: 1.4; /* Comfortable line spacing */
} }
/* User Message */ /* User Message */
.message.user { .message.user {
align-self: flex-end; /* Align user messages to the right */ align-self: flex-end; /* Align user messages to the right */
background-color: #007bff; /* Blue background for user */ background-color: #007bff; /* Blue background for user */
color: #ffffff; /* White text for contrast */ color: #ffffff; /* White text for contrast */
} }
/* Bot Message */ /* Bot Message */
.message.bot { .message.bot {
align-self: flex-start; /* Align bot messages to the left */ align-self: flex-start; /* Align bot messages to the left */
background-color: #f1f1f1; /* Light gray background for bot */ background-color: #f1f1f1; /* Light gray background for bot */
color: #333333; /* Dark text for readability */ color: #333333; /* Dark text for readability */
} }
/* Loading Indicator */ /* Loading Indicator */
.message.bot.typing { .message.bot.typing {
font-style: italic; /* Italic text to indicate typing */ font-style: italic; /* Italic text to indicate typing */
color: #666666; /* Subtle color */ color: #666666; /* Subtle color */
} }
/* Chat Input Form */ /* Chat Input Form */
.chat-input-form { .chat-input-form {
display: flex; /* Arrange input and button side by side */ display: flex; /* Arrange input and button side by side */
gap: 5px; /* Space between input and button */ gap: 5px; /* Space between input and button */
align-items: center; /* Align input and button vertically */ align-items: center; /* Align input and button vertically */
} }
/* Input Field */ /* Input Field */
.chat-input-form input { .chat-input-form input {
flex: 1; /* Take up remaining space */ flex: 1; /* Take up remaining space */
padding: 10px; /* Padding inside input */ padding: 10px; /* Padding inside input */
border: 1px solid #ccc; /* Light gray border */ border: 1px solid #ccc; /* Light gray border */
border-radius: 5px; /* Rounded corners */ border-radius: 5px; /* Rounded corners */
font-size: 14px; /* Font size */ font-size: 14px; /* Font size */
} }
/* Input Focus */ /* Input Focus */
.chat-input-form input:focus { .chat-input-form input:focus {
outline: none; /* Remove blue outline */ outline: none; /* Remove blue outline */
border-color: #007bff; /* Blue border on focus */ border-color: #007bff; /* Blue border on focus */
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */ box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */
} }
/* Send Button */ /* Send Button */
.chat-input-form button { .chat-input-form button {
background-color: #007bff; /* Blue background */ background-color: #007bff; /* Blue background */
color: #ffffff; /* White text */ color: #ffffff; /* White text */
border: none; /* No border */ border: none; /* No border */
@ -82,30 +86,74 @@
border-radius: 5px; /* Rounded corners */ border-radius: 5px; /* Rounded corners */
cursor: pointer; /* Pointer cursor on hover */ cursor: pointer; /* Pointer cursor on hover */
font-size: 14px; /* Font size */ font-size: 14px; /* Font size */
} }
/* Send Button Hover */ /* Send Button Hover */
.chat-input-form button:hover { .chat-input-form button:hover {
background-color: #0056b3; /* Darker blue on hover */ background-color: #0056b3; /* Darker blue on hover */
} }
/* Send Button Disabled */ /* Send Button Disabled */
.chat-input-form button:disabled { .chat-input-form button:disabled {
background-color: #cccccc; /* Gray background when disabled */ background-color: #cccccc; /* Gray background when disabled */
cursor: not-allowed; /* Indicate disabled state */ cursor: not-allowed; /* Indicate disabled state */
} }
/* Scrollbar Styling for Chat Messages */ /* Scrollbar Styling for Chat Messages */
.chat-messages::-webkit-scrollbar { .chat-messages::-webkit-scrollbar {
width: 8px; /* Width of scrollbar */ width: 8px; /* Width of scrollbar */
} }
.chat-messages::-webkit-scrollbar-thumb { .chat-messages::-webkit-scrollbar-thumb {
background: #cccccc; /* Gray scrollbar thumb */ background: #cccccc; /* Gray scrollbar thumb */
border-radius: 4px; /* Rounded scrollbar */ border-radius: 4px; /* Rounded scrollbar */
} }
.chat-messages::-webkit-scrollbar-thumb:hover { .chat-messages::-webkit-scrollbar-thumb:hover {
background: #aaaaaa; /* Darker gray on 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"; import "./Chatbot.css";
const Chatbot = ({ context }) => { const Chatbot = ({ context }) => {
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([
{ {
role: "assistant", role: "assistant",
@ -14,56 +13,27 @@ const Chatbot = ({ context }) => {
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [loading, setLoading] = useState(false); 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 sendMessage = async (content) => {
const userMessage = { role: "user", content }; const userMessage = { role: "user", content };
// Ensure career data is injected into every API call // Build your context summary
const contextSummary = ` const contextSummary = `
You are an advanced AI career advisor for AptivaAI. 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. Your role is to provide analysis based on user data:
- ...
Use the following user-specific data: (Continue with your existing context data as before)
- 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."
`;
// Combine with existing messages
const messagesToSend = [ const messagesToSend = [
{ role: "system", content: contextSummary }, // Inject AptivaAI data on every request { role: "system", content: contextSummary },
...messages, ...messages,
userMessage, userMessage,
]; ];
@ -86,13 +56,17 @@ const Chatbot = ({ context }) => {
); );
const botMessage = response.data.choices[0].message; const botMessage = response.data.choices[0].message;
// The returned message has {role: "assistant", content: "..."}
setMessages([...messages, userMessage, botMessage]); setMessages([...messages, userMessage, botMessage]);
} catch (error) { } catch (error) {
console.error("Chatbot Error:", error); console.error("Chatbot Error:", error);
setMessages([ setMessages([
...messages, ...messages,
userMessage, userMessage,
{ role: "assistant", content: "Error: Unable to fetch response. Please try again." }, {
role: "assistant",
content: "Error: Unable to fetch response. Please try again.",
},
]); ]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -108,26 +82,46 @@ const Chatbot = ({ context }) => {
}; };
return ( return (
<div className="chatbot-container"> <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>
</div>
{/* If not minimized, show the chat messages and input */}
{!isMinimized && (
<>
<div className="chat-messages"> <div className="chat-messages">
{messages.map((msg, index) => ( {messages.map((msg, index) => {
<div key={index} className={`message ${msg.role}`}> // 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} {msg.content}
</div> </div>
))} );
{loading && <div className="message assistant">Typing...</div>} })}
{loading && <div className="message bot typing">Typing...</div>}
</div> </div>
<form onSubmit={handleSubmit} className="chat-input-form"> <form onSubmit={handleSubmit} className="chat-input-form">
<input <input
type="text" type="text"
placeholder="Ask a question..." placeholder="Ask a question..."
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
disabled={loading}
/> />
<button type="submit" disabled={loading}> <button type="submit" disabled={loading}>
Send Send
</button> </button>
</form> </form>
</>
)}
</div> </div>
); );
}; };

View File

@ -40,13 +40,9 @@ function Dashboard() {
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [sessionHandled, setSessionHandled] = 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 // Function to handle the token check and fetch requests
const authFetch = async (url, options = {}, onUnauthorized) => { const authFetch = async (url, options = {}, onUnauthorized) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@ -361,7 +357,8 @@ function Dashboard() {
}; };
const renderLoadingOverlay = () => { 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 ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50"> <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 { useNavigate } from "react-router-dom";
import { ClipLoader } from 'react-spinners'; import { ClipLoader } from "react-spinners";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from "uuid";
import LoanRepayment from './LoanRepayment.js'; import LoanRepayment from "./LoanRepayment.js";
import SchoolFilters from './SchoolFilters'; import "./PopoutPanel.css"; // You can keep or remove depending on your needs
import './PopoutPanel.css';
import { useState, useEffect } from 'react';
function PopoutPanel({ function PopoutPanel({
isVisible, isVisible,
data = {}, data = {},
userState = 'N/A', // Passed explicitly from Dashboard userState = "N/A",
loading = false, loading = false,
error = null, error = null,
closePanel, closePanel,
updateChatbotContext, updateChatbotContext,
}) { }) {
// Original local states
const [isCalculated, setIsCalculated] = useState(false); const [isCalculated, setIsCalculated] = useState(false);
const [results, setResults] = useState([]); // Store loan repayment calculation results const [results, setResults] = useState([]);
const [loadingCalculation, setLoadingCalculation] = useState(false); const [loadingCalculation, setLoadingCalculation] = useState(false);
const [persistedROI, setPersistedROI] = useState({}); const [persistedROI, setPersistedROI] = useState({});
const [programLengths, setProgramLengths] = useState([]); const [programLengths, setProgramLengths] = useState([]);
const [sortBy, setSortBy] = useState('tuition'); // Default sorting const [sortBy, setSortBy] = useState("tuition");
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value const [maxTuition, setMaxTuition] = useState(50000);
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value const [maxDistance, setMaxDistance] = useState(200);
const token = localStorage.getItem('token');
const token = localStorage.getItem("token");
const navigate = useNavigate(); const navigate = useNavigate();
// Destructure your data
const { const {
jobDescription = null, jobDescription = null,
tasks = null, tasks = null,
title = 'Career Details', title = "Career Details",
economicProjections = {}, economicProjections = {},
salaryData = [], salaryData = [],
schools = [], schools = [],
} = data || {}; } = data || {};
// Clear results if sorting or filters change
useEffect(() => { useEffect(() => {
setResults([]); setResults([]);
setIsCalculated(false); setIsCalculated(false);
}, [sortBy, maxTuition, maxDistance]); // Ensure no other dependencies! }, [sortBy, maxTuition, maxDistance]);
// Derive program lengths from school CREDDESC
useEffect(() => { useEffect(() => {
setProgramLengths(schools.map(school => getProgramLength(school['CREDDESC']))); setProgramLengths(
schools.map((school) => getProgramLength(school["CREDDESC"]))
);
}, [schools]); }, [schools]);
// Update chatbot context if data is present
useEffect(() => { useEffect(() => {
if (data && Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
updateChatbotContext({ updateChatbotContext({
careerDetails: data, careerDetails: data,
@ -55,102 +59,90 @@ function PopoutPanel({
salaryData, salaryData,
economicProjections, economicProjections,
results, 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 (!isVisible) return null;
// If the panel or the loan calc is loading, show a spinner
if (loading || loadingCalculation) { if (loading || loadingCalculation) {
return ( return (
<div className="popout-panel"> <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">
<button className="close-btn" onClick={closePanel}>X</button> <div className="p-4">
<h2>Loading Career Details...</h2> <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" /> <ClipLoader size={35} color="#4A90E2" />
</div> </div>
</div>
); );
} }
// Get program length for calculating tuition // Original helper
const getProgramLength = (degreeType) => { function getProgramLength(degreeType) {
if (degreeType?.includes("Associate")) return 2; if (degreeType?.includes("Associate")) return 2;
if (degreeType?.includes("Bachelor")) return 4; if (degreeType?.includes("Bachelor")) return 4;
if (degreeType?.includes("Master")) return 6; 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; if (degreeType?.includes("Certificate")) return 1;
return 4; // Default to 4 years if unspecified return 4;
};
function handleClosePanel() {
setResults([]); // Clear only LoanRepayment results
setIsCalculated(false); // Reset calculation state
closePanel(); // Maintain existing close behavior
} }
// Original close logic
function handleClosePanel() {
setResults([]);
setIsCalculated(false);
closePanel();
}
/** 🔹 Apply Sorting & Filtering Directly at Render Time **/ // Original PlanMyPath logic
const filteredAndSortedSchools = [...schools] async function handlePlanMyPath() {
.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;
});
const handlePlanMyPath = async () => {
const token = localStorage.getItem('token');
if (!token) { if (!token) {
alert("You need to be logged in to create a career path."); alert("You need to be logged in to create a career path.");
return; 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 { try {
const allPathsResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path/all`, { const allPathsResponse = await fetch(
method: 'GET', `${process.env.REACT_APP_API_URL}/premium/planned-path/all`,
{
method: "GET",
headers: { headers: {
'Authorization': `Bearer ${token}`, Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
}); }
);
if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`); if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`);
const { careerPath } = await allPathsResponse.json(); 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) { if (match) {
const decision = window.confirm( 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) { if (decision) {
navigate('/financial-profile', { navigate("/financial-profile", {
state: { selectedCareer: { career_path_id: match.id, career_name: data.title } } state: {
selectedCareer: { career_path_id: match.id, career_name: data.title },
},
}); });
return; return;
} }
@ -158,217 +150,298 @@ function PopoutPanel({
const newCareerPath = { const newCareerPath = {
career_path_id: uuidv4(), career_path_id: uuidv4(),
career_name: data.title career_name: data.title,
}; };
const newResponse = await fetch(
const newResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path`, { `${process.env.REACT_APP_API_URL}/premium/planned-path`,
method: 'POST', {
method: "POST",
headers: { headers: {
'Authorization': `Bearer ${token}`, Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(newCareerPath), 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);
} }
}; );
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,
},
},
});
} catch (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 ( return (
<div className="popout-panel"> <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 with Close & Plan My Path Buttons */} {/* Header */}
<div className="panel-header"> <div className="flex items-center justify-between border-b p-4">
<button className="close-btn" onClick={closePanel}>X</button>
<button <button
className="plan-path-btn" className="rounded bg-red-100 px-2 py-1 text-sm text-red-600 hover:bg-red-200"
onClick={handlePlanMyPath} // 🔥 Use the already-defined, correct handler 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 Plan My Path
</button> </button>
</div> </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 */} {/* Error */}
<div className="job-description"> {error && (
<h3>Job Description</h3> <div className="rounded bg-red-50 p-2 text-red-600">
<p>{jobDescription || 'No description available'}</p> {error}
</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> </div>
<div className="job-tasks"> {/* Tasks */}
<h3>Expected Tasks</h3> <div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Expected Tasks</h3>
{tasks && tasks.length > 0 ? ( {tasks && tasks.length > 0 ? (
<ul> <ul className="list-disc space-y-1 pl-5 text-sm text-gray-700">
{tasks.map((task, index) => ( {tasks.map((task, idx) => (
<li key={index}>{task}</li> <li key={idx}>{task}</li>
))} ))}
</ul> </ul>
) : ( ) : (
<p>No tasks available for this career path.</p> <p className="text-sm text-gray-500">No tasks available.</p>
)} )}
</div> </div>
{/* Economic Projections */} {/* Economic Projections */}
<div className="economic-projections"> <div className="rounded bg-gray-50 p-4">
<h3>Economic Projections for {userState}</h3> <h3 className="mb-2 text-base font-medium">
{economicProjections && typeof economicProjections === 'object' ? ( Economic Projections for {userState}
<ul> </h3>
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li> {economicProjections && typeof economicProjections === "object" ? (
<li>2032 Employment: {economicProjections['2032 Employment'] || 'N/A'}</li> <ul className="space-y-1 text-sm text-gray-700">
<li>Total Change: {economicProjections['Total Change'] || 'N/A'}</li> <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> </ul>
) : ( ) : (
<p>No economic projections available for this career path.</p> <p className="text-sm text-gray-500">
No economic projections available.
</p>
)} )}
</div> </div>
{/* Salary Data Points */} {/* Salary Data */}
<div className="salary-data"> <div className="rounded bg-gray-50 p-4">
<h3>Salary Data</h3> <h3 className="mb-2 text-base font-medium">Salary Data</h3>
{salaryData.length > 0 ? ( {salaryData.length > 0 ? (
<table> <table className="w-full text-sm text-gray-700">
<thead> <thead>
<tr> <tr className="bg-gray-200 text-left">
<th>Percentile</th> <th className="p-2">Percentile</th>
<th>Regional Salary</th> <th className="p-2">Regional Salary</th>
<th>US Salary</th> <th className="p-2">US Salary</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{salaryData.map((point, index) => ( {salaryData.map((point, idx) => (
<tr key={index}> <tr key={idx} className="border-b">
<td>{point.percentile}</td> <td className="p-2">{point.percentile}</td>
<td>{point.regionalSalary > 0 ? `$${parseInt(point.regionalSalary, 10).toLocaleString()}` : 'N/A'}</td> <td className="p-2">
<td>{point.nationalSalary > 0 ? `$${parseInt(point.nationalSalary, 10).toLocaleString()}` : 'N/A'}</td> {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> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
) : ( ) : (
<p>Salary data is not available.</p> <p className="text-sm text-gray-500">
Salary data is not available.
</p>
)} )}
</div> </div>
{/* Schools Offering Programs */}
<div>
<h3 className="mb-2 text-base font-medium">Schools Offering Programs</h3>
{/* Schools Offering Programs Section */} {/* Filter Bar */}
<h3>Schools Offering Programs</h3> <div className="mb-4 flex items-center space-x-4">
<label className="text-sm text-gray-600">
<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: Sort:
<select <select
className="ml-2 rounded border px-2 py-1 text-sm"
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
style={{ marginLeft: '5px', padding: '2px', width: '100px' }}
> >
<option value="tuition">Tuition</option> <option value="tuition">Tuition</option>
<option value="distance">Distance</option> <option value="distance">Distance</option>
</select> </select>
</label> </label>
<label> <label className="text-sm text-gray-600">
Tuition (max): Tuition (max):
<input <input
type="number" type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxTuition} value={maxTuition}
step={1000}
min={0}
max={100000}
style={{ width: '90px', padding: '2px' }}
onChange={(e) => setMaxTuition(Number(e.target.value))} onChange={(e) => setMaxTuition(Number(e.target.value))}
/> />
</label> </label>
<label> <label className="text-sm text-gray-600">
Distance (max mi): Distance (max):
<input <input
type="number" type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxDistance} value={maxDistance}
step={10}
min={10}
max={500}
style={{ width: '70px', padding: '2px' }}
onChange={(e) => setMaxDistance(Number(e.target.value))} onChange={(e) => setMaxDistance(Number(e.target.value))}
/> />
</label> </label>
</div> </div>
</div>
<div className="schools-offering">
{filteredAndSortedSchools.length > 0 ? ( {filteredAndSortedSchools.length > 0 ? (
filteredAndSortedSchools.map((school, index) => ( <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div key={index} className="school-card"> {filteredAndSortedSchools.map((school, idx) => (
<div><strong>{school['INSTNM']}</strong></div> <div key={idx} className="rounded border p-3 text-sm">
<div>Degree Type: {school['CREDDESC'] || 'Degree type not available for this program'}</div> <strong>{school["INSTNM"] || "Unnamed School"}</strong>
<div>In-State Tuition: ${school['In_state cost'] || 'Tuition not available for this school'}</div> <p>Degree Type: {school["CREDDESC"] || "N/A"}</p>
<div>Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition not available for this school'}</div> <p>In-State Tuition: ${school["In_state cost"] || "N/A"}</p>
<div>Distance: {school['distance'] || 'Distance to school not available'}</div> <p>Out-of-State Tuition: ${school["Out_state cost"] || "N/A"}</p>
<div> <p>Distance: {school["distance"] || "N/A"}</p>
Website: <a href={school['Website']} target="_blank" rel="noopener noreferrer">{school['Website']}</a> <p>
Website:{" "}
<a
href={school["Website"]}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{school["Website"]}
</a>
</p>
</div> </div>
))}
</div> </div>
))
) : ( ) : (
<p className="no-schools-message">No schools of higher education are available in your state for this career path.</p> <p className="text-sm text-gray-500">
No schools matching your filters.
</p>
)} )}
</div>
</div> </div>
{/* Loan Repayment Analysis */} {/* Loan Repayment Analysis */}
<h3>Loan Repayment Analysis</h3> <section className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Loan Repayment Analysis</h3>
<LoanRepayment <LoanRepayment
schools={filteredAndSortedSchools.map((school, index) => ({ schools={filteredAndSortedSchools.map((school, i) => ({
schoolName: school['INSTNM'], schoolName: school["INSTNM"],
inState: parseFloat(school['In_state cost']) || 0, inState: parseFloat(school["In_state cost"]) || 0,
outOfState: parseFloat(school['Out_state cost']) || 0, outOfState: parseFloat(school["Out_state cost"]) || 0,
inStateGraduate: parseFloat(school['In State Graduate']) || parseFloat(school['In_state cost']) || 0, inStateGraduate:
outStateGraduate: parseFloat(school['Out State Graduate']) || parseFloat(school['Out_state cost']) || 0, parseFloat(school["In State Graduate"]) ||
degreeType: school['CREDDESC'], parseFloat(school["In_state cost"]) ||
programLength: programLengths[index], 0,
outStateGraduate:
parseFloat(school["Out State Graduate"]) ||
parseFloat(school["Out_state cost"]) ||
0,
degreeType: school["CREDDESC"],
programLength: programLengths[i],
}))} }))}
salaryData={salaryData} salaryData={salaryData}
setResults={setResults} setResults={setResults}
setLoading={setLoadingCalculation} setLoading={setLoadingCalculation}
setPersistedROI={setPersistedROI} // ✅ Store ROI after calculation setPersistedROI={setPersistedROI}
/> />
</section>
{/* Results Display */} {/* Results Display */}
{results.length > 0 && ( {results.length > 0 && (
<div className="results-container"> <div className="results-container rounded bg-gray-50 p-4">
<h3>Comparisons by School over the life of the loan</h3> <h3 className="mb-2 text-base font-medium">
{results.map((result, index) => ( Comparisons by School over the life of the loan
<div className="school-result-card" key={index}> </h3>
<h4>{result.schoolName} - {result.degreeType || 'Degree type not available'}</h4>
{/*
=========================================
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>Total Tuition: ${result.totalTuition}</p>
<p>Monthly Payment: ${result.monthlyPayment}</p> <p>Monthly Payment: ${result.monthlyPayment}</p>
<p>Total Monthly Payment (with extra): ${result.totalMonthlyPayment}</p> <p>
Total Monthly Payment (with extra): $
{result.totalMonthlyPayment}
</p>
<p>Total Loan Cost: ${result.totalLoanCost}</p> <p>Total Loan Cost: ${result.totalLoanCost}</p>
<p className={`net-gain ${parseFloat(result.netGain) < 0 ? 'negative' : 'positive'}`}> <p
className={
parseFloat(result.netGain) < 0
? "text-red-600"
: "text-green-600"
}
>
Net Gain: ${result.netGain} Net Gain: ${result.netGain}
</p> </p>
<p>Monthly Salary (Gross): ${result.monthlySalary}</p> <p>Monthly Salary (Gross): ${result.monthlySalary}</p>
</div> </div>
))} ))}
</div> </div>
</div>
)} )}
</div> </div>
</div>
); );
} }