292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { AlertTriangle } from 'lucide-react';
|
||
import isAllOther from '../utils/isAllOther.js';
|
||
|
||
|
||
|
||
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||
const [error, setError] = useState(null);
|
||
const [loadingRisk, setLoadingRisk] = useState(false);
|
||
const aiRisk = careerDetails?.aiRisk || null;
|
||
const fmt = (v) =>
|
||
typeof v === 'number'
|
||
? v.toLocaleString()
|
||
: (v ?? '—');
|
||
|
||
|
||
// Handle your normal careerDetails loading logic
|
||
if (careerDetails?.error) {
|
||
return (
|
||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||
<div className="bg-white rounded-lg shadow-lg p-6 max-w-xl">
|
||
<p className="text-lg text-gray-700 mb-4">
|
||
{careerDetails.error}
|
||
</p>
|
||
<button
|
||
onClick={closeModal}
|
||
className="bg-red-500 text-white p-2 rounded"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!careerDetails || careerDetails.salaryData === undefined) {
|
||
return (
|
||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50" role="dialog" aria-modal="true">
|
||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||
<p className="text-lg text-gray-700">Loading career details...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) return <div>{error}</div>;
|
||
|
||
// Helper for "stability" rating
|
||
const calculateStabilityRating = (salaryData) => {
|
||
const medianSalaryObj = salaryData.find((s) => s.percentile === 'Median');
|
||
const medianSalary =
|
||
medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0;
|
||
|
||
if (medianSalary >= 90000) return 5;
|
||
if (medianSalary >= 70000) return 4;
|
||
if (medianSalary >= 50000) return 3;
|
||
if (medianSalary >= 30000) return 2;
|
||
return 1;
|
||
};
|
||
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50" role="dialog" aria-modal="true">
|
||
<div className="bg-white rounded-lg shadow-lg w-full max-w-5xl p-6 m-4 max-h-[90vh] overflow-y-auto">
|
||
|
||
|
||
{isAllOther(career) && (
|
||
<div className="mb-4 flex items-start rounded-md border-l-4 border-yellow-500 bg-yellow-50 p-3">
|
||
<AlertTriangle className="mt-[2px] mr-2 h-5 w-5 text-yellow-600" />
|
||
<p className="text-sm text-yellow-800">
|
||
You've selected an "umbrella" field that covers a wide range of careers—many
|
||
people begin a career journey with a broad interest area and we don't want to discourage
|
||
anyone from taking this approach. It's just difficult to display detailed career data
|
||
and day‑to‑day tasks for this “all‑other” occupation. Use it as a starting point,
|
||
keep exploring specializations, and we can show you richer insights as soon as you are able
|
||
to narrow it down to a more specific role. If you know this is the field for you, go ahead to
|
||
add it to your comparison list or move straight into Preparing & Upskilling for Your Career!
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Title row */}
|
||
<div className="flex justify-between items-center mb-4 pb-2 border-b">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-blue-600">
|
||
{careerDetails.title}
|
||
</h2>
|
||
|
||
{/* AI RISK SECTION */}
|
||
|
||
{aiRisk && aiRisk.riskLevel && aiRisk.reasoning && (
|
||
<div className="text-sm text-gray-500 mt-1">
|
||
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
||
<br />
|
||
<span>{aiRisk.reasoning}</span>
|
||
</div>
|
||
)}
|
||
|
||
{!aiRisk && (
|
||
<p className="text-sm text-gray-500 mt-1">No AI risk data available</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Buttons */}
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => {
|
||
const stabilityRating = calculateStabilityRating(
|
||
careerDetails.salaryData
|
||
);
|
||
addCareerToList({
|
||
...careerDetails,
|
||
ratings: {
|
||
stability: stabilityRating,
|
||
},
|
||
});
|
||
closeModal();
|
||
}}
|
||
className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1"
|
||
>
|
||
Add to Comparison
|
||
</button>
|
||
|
||
<button
|
||
onClick={closeModal}
|
||
className="text-white bg-red-500 hover:bg-red-600 rounded px-3 py-1"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Job Description */}
|
||
{careerDetails.jobDescription && (
|
||
<div className="mb-4">
|
||
<h3 className="text-lg font-semibold mb-1">Job Description:</h3>
|
||
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tasks */}
|
||
{careerDetails.tasks?.length > 0 && (
|
||
<div className="mb-4 border-t pt-3">
|
||
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
||
<ul className="list-disc pl-5 space-y-1">
|
||
{careerDetails.tasks.map((task, i) => (
|
||
<li key={i}>{task}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{(careerDetails.salaryData?.length > 0 ||
|
||
(careerDetails.economicProjections &&
|
||
(careerDetails.economicProjections.state ||
|
||
careerDetails.economicProjections.national))) && (
|
||
|
||
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
||
|
||
|
||
|
||
{/* ── Salary table ───────────────────────── */}
|
||
{careerDetails.salaryData?.length > 0 && (
|
||
<div className="md:w-1/2 overflow-x-auto">
|
||
<h3 className="text-lg font-semibold mb-2">Salary Data</h3>
|
||
<table className="w-full text-left border border-gray-300 rounded">
|
||
<thead className="bg-gray-100">
|
||
<tr>
|
||
<th className="px-3 py-2 border-b">Percentile</th>
|
||
<th className="px-3 py-2 border-b">Regional Salary</th>
|
||
<th className="px-3 py-2 border-b">National Salary</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{careerDetails.salaryData.map((row, i) => (
|
||
<tr key={i}>
|
||
<td className="px-3 py-2 border-b">{row.percentile}</td>
|
||
<td className="px-3 py-2 border-b">
|
||
{Number.isFinite(row.regionalSalary) ? `$${fmt(row.regionalSalary)}` : '—'}
|
||
</td>
|
||
<td className="px-3 py-2 border-b">
|
||
{Number.isFinite(row.nationalSalary) ? `$${fmt(row.nationalSalary)}` : '—'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Economic projections ───────────────── */}
|
||
{(careerDetails.economicProjections?.state ||
|
||
careerDetails.economicProjections?.national) && (
|
||
<div className="md:w-1/2 overflow-x-auto">
|
||
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
||
<table className="w-full text-left border border-gray-300 rounded">
|
||
<thead className="bg-gray-100">
|
||
<tr>
|
||
<th className="px-3 py-2 border-b"></th>
|
||
{careerDetails.economicProjections.state && (
|
||
<th className="px-3 py-2 border-b">
|
||
{careerDetails.economicProjections.state.area}
|
||
</th>
|
||
)}
|
||
{careerDetails.economicProjections.national && (
|
||
<th className="px-3 py-2 border-b">National</th>
|
||
)}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td className="px-3 py-2 border-b font-semibold">
|
||
Current Jobs
|
||
</td>
|
||
{careerDetails.economicProjections.state && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.state.base)}
|
||
</td>
|
||
)}
|
||
{careerDetails.economicProjections.national && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.national.base)}
|
||
</td>
|
||
)}
|
||
</tr>
|
||
<tr>
|
||
<td className="px-3 py-2 border-b font-semibold">
|
||
Jobs in 10 yrs
|
||
</td>
|
||
{careerDetails.economicProjections.state && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.state.projection)}
|
||
</td>
|
||
)}
|
||
{careerDetails.economicProjections.national && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.national.projection)}
|
||
</td>
|
||
)}
|
||
</tr>
|
||
<tr>
|
||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||
{careerDetails.economicProjections.state && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.state.percentChange)}%
|
||
</td>
|
||
)}
|
||
{careerDetails.economicProjections.national && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.national.percentChange)}%
|
||
</td>
|
||
)}
|
||
</tr>
|
||
<tr>
|
||
<td className="px-3 py-2 border-b font-semibold">
|
||
Annual Openings
|
||
</td>
|
||
{careerDetails.economicProjections.state && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.state.annualOpenings)}
|
||
</td>
|
||
)}
|
||
{careerDetails.economicProjections.national && (
|
||
<td className="px-3 py-2 border-b">
|
||
{fmt(careerDetails.economicProjections.national.annualOpenings)}
|
||
</td>
|
||
)}
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
||
{(aiRisk?.riskLevel === 'Moderate' || aiRisk?.riskLevel === 'High') && (
|
||
<p className="text-sm text-red-600 mt-2">
|
||
Note: These 10‑year projections may change if AI‑driven tools
|
||
significantly affect {careerDetails.title} tasks. With a
|
||
<strong>{aiRisk?.riskLevel?.toLowerCase()}</strong> AI risk, it’s possible
|
||
some responsibilities could be automated over time.
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default CareerModal;
|