So many changes: created CareerSearch.js to be included in MilestoneTracker.js, several version updates for Node, Chad UI, etc.
This commit is contained in:
parent
3c5a2b3631
commit
d294d609d0
83
AutoSuggestFields.js
Normal file
83
AutoSuggestFields.js
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const AutoSuggestFields = () => {
|
||||
const [cluster, setCluster] = useState('');
|
||||
const [subdivision, setSubdivision] = useState('');
|
||||
const [career, setCareer] = useState('');
|
||||
|
||||
const clusters = ['Cluster A', 'Cluster B', 'Cluster C'];
|
||||
const subdivisions = {
|
||||
'Cluster A': ['Subdivision A1', 'Subdivision A2'],
|
||||
'Cluster B': ['Subdivision B1', 'Subdivision B2'],
|
||||
'Cluster C': ['Subdivision C1', 'Subdivision C2'],
|
||||
};
|
||||
const careers = {
|
||||
'Subdivision A1': ['Career A1-1', 'Career A1-2'],
|
||||
'Subdivision B1': ['Career B1-1', 'Career B1-2'],
|
||||
'Subdivision C1': ['Career C1-1', 'Career C1-2'],
|
||||
};
|
||||
|
||||
const handleClusterChange = (e) => {
|
||||
setCluster(e.target.value);
|
||||
setSubdivision('');
|
||||
setCareer('');
|
||||
};
|
||||
|
||||
const handleSubdivisionChange = (e) => {
|
||||
setSubdivision(e.target.value);
|
||||
setCareer('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
Cluster:
|
||||
<input
|
||||
type="text"
|
||||
list="cluster-options"
|
||||
value={cluster}
|
||||
onChange={handleClusterChange}
|
||||
/>
|
||||
<datalist id="cluster-options">
|
||||
{clusters.map((c) => (
|
||||
<option key={c} value={c} />
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Subdivision:
|
||||
<input
|
||||
type="text"
|
||||
list="subdivision-options"
|
||||
value={subdivision}
|
||||
onChange={handleSubdivisionChange}
|
||||
disabled={!cluster}
|
||||
/>
|
||||
<datalist id="subdivision-options">
|
||||
{(subdivisions[cluster] || []).map((s) => (
|
||||
<option key={s} value={s} />
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Career:
|
||||
<select
|
||||
value={career}
|
||||
onChange={(e) => setCareer(e.target.value)}
|
||||
disabled={!subdivision}
|
||||
>
|
||||
<option value="">Select a career</option>
|
||||
{(careers[subdivision] || []).map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoSuggestFields;
|
148
dist/output.css
vendored
Normal file
148
dist/output.css
vendored
Normal file
@ -0,0 +1,148 @@
|
||||
/*! tailwindcss v4.0.14 | MIT License | https://tailwindcss.com */
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-b {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
|
||||
transition-timing-function: var(--tw-ease, ease);
|
||||
transition-duration: var(--tw-duration, 0s);
|
||||
}
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: var(--tw-ease, ease);
|
||||
transition-duration: var(--tw-duration, 0s);
|
||||
}
|
||||
.focus\:ring {
|
||||
&:focus {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-ring-inset {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-offset-width {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --tw-ring-offset-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
}
|
||||
@property --tw-ring-offset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
2941
package-lock.json
generated
2941
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -4,23 +4,33 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cra-template": "1.2.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"helmet": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.483.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"react-router": "^7.3.0",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-spinners": "^0.15.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"web-vitals": "^4.2.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
@ -54,6 +64,9 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
|
8
postcss.config.js
Normal file
8
postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,7 @@ import SignUp from './components/SignUp.js';
|
||||
import InterestInventory from './components/InterestInventory.js';
|
||||
import Dashboard from './components/Dashboard.js';
|
||||
import UserProfile from './components/UserProfile.js';
|
||||
import MilestoneTracker from "./components/MilestoneTracker.js";
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
@ -42,7 +43,10 @@ function App() {
|
||||
path="/profile"
|
||||
element={isAuthenticated ? <UserProfile /> : <Navigate to="/signin" />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/milestone-tracker"
|
||||
element={isAuthenticated ? <MilestoneTracker /> : <Navigate to="/signin" />}
|
||||
/>
|
||||
{/* Catch-all for unknown routes */}
|
||||
<Route path="*" element={<Navigate to="/signin" />} />
|
||||
</Routes>
|
||||
|
113
src/components/CareerSearch.js
Normal file
113
src/components/CareerSearch.js
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "./ui/input.js"; // Assuming Input is a basic text input component
|
||||
|
||||
const CareerSearch = () => {
|
||||
const [careerClusters, setCareerClusters] = useState({});
|
||||
const [selectedCluster, setSelectedCluster] = useState("");
|
||||
const [selectedSubdivision, setSelectedSubdivision] = useState("");
|
||||
const [selectedCareer, setSelectedCareer] = useState("");
|
||||
const [careerSearch, setCareerSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCareerClusters = async () => {
|
||||
try {
|
||||
const response = await fetch('/career_clusters.json');
|
||||
const data = await response.json();
|
||||
setCareerClusters(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching career clusters:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCareerClusters();
|
||||
}, []);
|
||||
|
||||
// Handle Cluster Selection
|
||||
const handleClusterSelect = (cluster) => {
|
||||
setSelectedCluster(cluster);
|
||||
setSelectedSubdivision(""); // Reset subdivision on cluster change
|
||||
setSelectedCareer(""); // Reset career on cluster change
|
||||
};
|
||||
|
||||
// Handle Subdivision Selection
|
||||
const handleSubdivisionSelect = (subdivision) => {
|
||||
setSelectedSubdivision(subdivision);
|
||||
setSelectedCareer(""); // Reset career on subdivision change
|
||||
};
|
||||
|
||||
// Handle Career Selection
|
||||
const handleCareerSearch = (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
setCareerSearch(query);
|
||||
};
|
||||
|
||||
// Get subdivisions based on selected cluster
|
||||
const subdivisions = selectedCluster ? Object.keys(careerClusters[selectedCluster] || {}) : [];
|
||||
// Get careers based on selected subdivision
|
||||
const careers = selectedSubdivision ? careerClusters[selectedCluster]?.[selectedSubdivision] || [] : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Milestone Tracker Loaded</h2>
|
||||
|
||||
{/* Career Cluster Selection */}
|
||||
<div>
|
||||
<h3>Select a Career Cluster</h3>
|
||||
<Input
|
||||
value={selectedCluster}
|
||||
onChange={(e) => handleClusterSelect(e.target.value)}
|
||||
placeholder="Search for a Career Cluster"
|
||||
list="career-clusters"
|
||||
/>
|
||||
<datalist id="career-clusters">
|
||||
{Object.keys(careerClusters).map((cluster, index) => (
|
||||
<option key={index} value={cluster} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
{/* Subdivision Selection based on Cluster */}
|
||||
{selectedCluster && (
|
||||
<div>
|
||||
<h3>Select a Subdivision</h3>
|
||||
<Input
|
||||
value={selectedSubdivision}
|
||||
onChange={(e) => handleSubdivisionSelect(e.target.value)}
|
||||
placeholder="Search for a Subdivision"
|
||||
list="subdivisions"
|
||||
/>
|
||||
<datalist id="subdivisions">
|
||||
{subdivisions.map((subdivision, index) => (
|
||||
<option key={index} value={subdivision} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Career Selection based on Subdivision */}
|
||||
{selectedSubdivision && (
|
||||
<div>
|
||||
<h3>Select a Career</h3>
|
||||
<Input
|
||||
value={careerSearch}
|
||||
onChange={handleCareerSearch}
|
||||
placeholder="Search for a Career"
|
||||
list="careers"
|
||||
/>
|
||||
<datalist id="careers">
|
||||
{careers
|
||||
.filter((career) => career.title.toLowerCase().includes(careerSearch)) // Filter careers based on search input
|
||||
.map((career, index) => (
|
||||
<option key={index} value={career.title} onClick={() => setSelectedCareer(career.title)} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display selected career */}
|
||||
{selectedCareer && <div>Selected Career: {selectedCareer}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerSearch;
|
@ -73,13 +73,6 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
|
||||
const isEconomicMissing = !economicData || Object.values(economicData).every(val => val === "N/A" || val === "*");
|
||||
const isSalaryMissing = salaryResponse === null || salaryResponse === undefined;
|
||||
|
||||
// ✅ Log only when needed
|
||||
if (isSalaryMissing) {
|
||||
console.warn(`⚠️ Missing Salary Data for ${career.title} (${career.code})`);
|
||||
} else {
|
||||
console.log(`✅ Salary Data Available for ${career.title} (${career.code})`);
|
||||
}
|
||||
|
||||
const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
||||
if (isLimitedData) console.log(`⚠️ Setting limitedData for ${career.title} (${career.code})`);
|
||||
|
||||
|
@ -47,14 +47,6 @@ const Chatbot = ({ context }) => {
|
||||
: "Not available"
|
||||
}
|
||||
|
||||
- ROI Analysis: ${
|
||||
context.persistedROI && Array.isArray(context.persistedROI) && context.persistedROI.length > 0
|
||||
? context.persistedROI
|
||||
.map(roi => `${roi.schoolName}: Net Gain: $${roi.netGain || "N/A"}, Monthly Payment: $${roi.monthlyPayment || "N/A"}`)
|
||||
.join("; ")
|
||||
: "No ROI data available."
|
||||
}
|
||||
|
||||
- User State: ${context.userState || "Not provided"}
|
||||
- User Area: ${context.areaTitle || "Not provided"}
|
||||
- User Zipcode: ${context.userZipcode || "Not provided"}
|
||||
|
@ -5,17 +5,15 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||
import PopoutPanel from './PopoutPanel.js';
|
||||
import './PopoutPanel.css';
|
||||
import './Dashboard.css';
|
||||
import Chatbot from "./Chatbot.js";
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { fetchSchools } from '../utils/apiUtils.js';
|
||||
import './Dashboard.css';
|
||||
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
function Dashboard() {
|
||||
const location = useLocation()
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||
@ -33,12 +31,11 @@ function Dashboard() {
|
||||
const [userZipcode, setUserZipcode] = useState(null);
|
||||
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info
|
||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
||||
const [selectedFit, setSelectedFit] = useState('');
|
||||
const [results, setResults] = useState([]); // Add results state
|
||||
const [results, setResults] = useState([]);
|
||||
const [chatbotContext, setChatbotContext] = useState({});
|
||||
|
||||
|
||||
const jobZoneLabels = {
|
||||
'1': 'Little or No Preparation',
|
||||
'2': 'Some Preparation Needed',
|
||||
@ -53,10 +50,8 @@ function Dashboard() {
|
||||
'Good': 'Good - Less Strong Match'
|
||||
};
|
||||
|
||||
// Dynamic API URL
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||
|
||||
// Fetch job zone mappings after career suggestions are loaded
|
||||
useEffect(() => {
|
||||
const fetchJobZones = async () => {
|
||||
if (careerSuggestions.length === 0) return;
|
||||
@ -68,17 +63,17 @@ function Dashboard() {
|
||||
|
||||
const updatedCareers = careerSuggestions.map((career) => ({
|
||||
...career,
|
||||
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, // Extract correct value
|
||||
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||
}));
|
||||
|
||||
setCareersWithJobZone(updatedCareers); // Update state
|
||||
|
||||
setCareersWithJobZone(updatedCareers);
|
||||
} catch (error) {
|
||||
console.error('Error fetching job zone information:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJobZones();
|
||||
}, [careerSuggestions, apiUrl]);
|
||||
}, [careerSuggestions, apiUrl]);
|
||||
|
||||
const filteredCareers = useMemo(() => {
|
||||
return careersWithJobZone.filter((career) => {
|
||||
@ -89,38 +84,34 @@ function Dashboard() {
|
||||
Number(career.job_zone) === Number(selectedJobZone)
|
||||
: true;
|
||||
|
||||
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||
|
||||
return jobZoneMatches && fitMatches;
|
||||
});
|
||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||
return jobZoneMatches && fitMatches;
|
||||
});
|
||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||
|
||||
const updateChatbotContext = (updatedData) => {
|
||||
setChatbotContext((prevContext) => {
|
||||
const mergedContext = {
|
||||
...prevContext, // ✅ Preserve existing context (Dashboard Data)
|
||||
...Object.keys(updatedData).reduce((acc, key) => {
|
||||
if (updatedData[key] !== undefined && updatedData[key] !== null) {
|
||||
acc[key] = updatedData[key]; // ✅ Only update fields with actual data
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
const updateChatbotContext = (updatedData) => {
|
||||
setChatbotContext((prevContext) => {
|
||||
const mergedContext = {
|
||||
...prevContext,
|
||||
...Object.keys(updatedData).reduce((acc, key) => {
|
||||
if (updatedData[key] !== undefined && updatedData[key] !== null) {
|
||||
acc[key] = updatedData[key];
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
return mergedContext;
|
||||
});
|
||||
};
|
||||
|
||||
console.log("🔄 Updated Chatbot Context (Merged Dashboard + PopoutPanel):", mergedContext);
|
||||
return mergedContext;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
||||
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
||||
|
||||
const memoizedPopoutPanel = useMemo(() => {
|
||||
console.log("Passing careerDetails to PopoutPanel:", careerDetails);
|
||||
|
||||
return (
|
||||
<PopoutPanel
|
||||
isVisible={!!selectedCareer} // ✅ Ensures it's only visible when needed
|
||||
isVisible={!!selectedCareer}
|
||||
data={careerDetails}
|
||||
schools={schools}
|
||||
salaryData={salaryData}
|
||||
@ -130,20 +121,20 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
loading={loading}
|
||||
error={error}
|
||||
userState={userState}
|
||||
results={results}
|
||||
results={results}
|
||||
updateChatbotContext={updateChatbotContext}
|
||||
/>
|
||||
);
|
||||
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
|
||||
|
||||
useEffect(() => {
|
||||
let descriptions = []; // Declare outside for scope accessibility
|
||||
let descriptions = [];
|
||||
if (location.state) {
|
||||
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
||||
descriptions = scores.map((score) => score.description || "No description available.");
|
||||
descriptions = scores.map((score) => score.description || "No description available.");
|
||||
setCareerSuggestions(suggestions || []);
|
||||
setRiaSecScores(scores || []);
|
||||
setRiaSecDescriptions(descriptions); // Set descriptions
|
||||
setRiaSecDescriptions(descriptions);
|
||||
} else {
|
||||
console.warn('No data found, redirecting to Interest Inventory');
|
||||
navigate('/interest-inventory');
|
||||
@ -161,10 +152,10 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
if (profileResponse.ok) {
|
||||
const profileData = await profileResponse.json();
|
||||
|
||||
const { state, area, zipcode } = profileData; // Use 'area' instead of 'AREA_TITLE'
|
||||
const { state, area, zipcode } = profileData;
|
||||
setUserState(state);
|
||||
setAreaTitle(area && area.trim() ? area.trim() : ''); // Ensure 'area' is set correctly
|
||||
setUserZipcode(zipcode); // Set 'zipcode' in the state
|
||||
setAreaTitle(area && area.trim() ? area.trim() : '');
|
||||
setUserZipcode(zipcode);
|
||||
} else {
|
||||
console.error('Failed to fetch user profile');
|
||||
}
|
||||
@ -184,134 +175,117 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
areaTitle !== null &&
|
||||
userZipcode !== null
|
||||
) {
|
||||
console.log("✅ All data ready, forcing chatbotContext update...");
|
||||
|
||||
// ✅ Create a completely new object so React detects the change
|
||||
|
||||
const newChatbotContext = {
|
||||
careerSuggestions: [...careersWithJobZone], // Ensure fresh array reference
|
||||
riaSecScores: [...riaSecScores], // Ensure fresh array reference
|
||||
careerSuggestions: [...careersWithJobZone],
|
||||
riaSecScores: [...riaSecScores],
|
||||
userState: userState || "",
|
||||
areaTitle: areaTitle || "",
|
||||
userZipcode: userZipcode || "",
|
||||
};
|
||||
|
||||
|
||||
setChatbotContext(newChatbotContext);
|
||||
} else {
|
||||
console.log("⏳ Skipping chatbotContext update because data is not ready yet.");
|
||||
}
|
||||
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
||||
|
||||
|
||||
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
const socCode = career.code; // Extract SOC code from career object
|
||||
setSelectedCareer(career); // Set career first to trigger loading panel
|
||||
setLoading(true); // Enable loading state only when career is clicked
|
||||
setError(null); // Clear previous errors
|
||||
setCareerDetails({}); // Reset career details to avoid undefined errors
|
||||
setSchools([]); // Reset schools
|
||||
setSalaryData([]); // Reset salary data
|
||||
setEconomicProjections({}); // Reset economic projections
|
||||
setTuitionData([]); // Reset tuition data
|
||||
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
const socCode = career.code;
|
||||
setSelectedCareer(career);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setCareerDetails({});
|
||||
setSchools([]);
|
||||
setSalaryData([]);
|
||||
setEconomicProjections({});
|
||||
setTuitionData([]);
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Fetch CIP Code
|
||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||
const { cipCode } = await cipResponse.json();
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
// Step 2: Fetch Job Description and Tasks
|
||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||
const { description, tasks } = await jobDetailsResponse.json();
|
||||
|
||||
// Step 3: Fetch Data in Parallel for other career details
|
||||
// Salary API call with error handling
|
||||
let salaryResponse;
|
||||
try {
|
||||
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }});
|
||||
} catch (error) {
|
||||
salaryResponse = { data: {} }; // Prevents breaking the whole update
|
||||
}
|
||||
|
||||
// Projections API call with error handling
|
||||
let economicResponse;
|
||||
try {
|
||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||
} catch (error) {
|
||||
economicResponse = { data: {} }; // Prevents breaking the whole update
|
||||
}
|
||||
|
||||
// Tuition API call with error handling
|
||||
let tuitionResponse;
|
||||
try {
|
||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }});
|
||||
} catch (error) {
|
||||
|
||||
tuitionResponse = { data: {} };
|
||||
}
|
||||
|
||||
// Fetch schools separately (this one seems to be working fine)
|
||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||
|
||||
// Handle Distance Calculation
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
try {
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
} catch (error) {
|
||||
return { ...school, distance: 'N/A', duration: 'N/A' };
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
return;
|
||||
}
|
||||
}));
|
||||
|
||||
// Process Salary Data
|
||||
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0
|
||||
? [
|
||||
{ percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 },
|
||||
{ percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 },
|
||||
{ percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 },
|
||||
{ percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 },
|
||||
{ percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 0 },
|
||||
]
|
||||
: [];
|
||||
try {
|
||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||
const { cipCode } = await cipResponse.json();
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
const updatedCareerDetails = {
|
||||
...career,
|
||||
jobDescription: description,
|
||||
tasks: tasks,
|
||||
economicProjections: economicResponse.data || {},
|
||||
salaryData: salaryDataPoints,
|
||||
schools: schoolsWithDistance,
|
||||
tuitionData: tuitionResponse.data || [],
|
||||
};
|
||||
|
||||
// Ensure `careerDetails` is fully updated before passing to chatbot
|
||||
setCareerDetails(updatedCareerDetails);
|
||||
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||
const { description, tasks } = await jobDetailsResponse.json();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle, userZipcode]
|
||||
);
|
||||
let salaryResponse;
|
||||
try {
|
||||
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
|
||||
} catch (error) {
|
||||
salaryResponse = { data: {} };
|
||||
}
|
||||
|
||||
let economicResponse;
|
||||
try {
|
||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||
} catch (error) {
|
||||
economicResponse = { data: {} };
|
||||
}
|
||||
|
||||
let tuitionResponse;
|
||||
try {
|
||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
|
||||
} catch (error) {
|
||||
tuitionResponse = { data: {} };
|
||||
}
|
||||
|
||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
try {
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
} catch (error) {
|
||||
return { ...school, distance: 'N/A', duration: 'N/A' };
|
||||
}
|
||||
}));
|
||||
|
||||
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0
|
||||
? [
|
||||
{ percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 },
|
||||
{ percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 },
|
||||
{ percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 },
|
||||
{ percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 },
|
||||
{ percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 0 },
|
||||
]
|
||||
: [];
|
||||
|
||||
const updatedCareerDetails = {
|
||||
...career,
|
||||
jobDescription: description,
|
||||
tasks: tasks,
|
||||
economicProjections: economicResponse.data || {},
|
||||
salaryData: salaryDataPoints,
|
||||
schools: schoolsWithDistance,
|
||||
tuitionData: tuitionResponse.data || [],
|
||||
};
|
||||
|
||||
setCareerDetails(updatedCareerDetails);
|
||||
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle, userZipcode]
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
labels: riaSecScores.map((score) => score.area),
|
||||
@ -326,111 +300,97 @@ async (career) => {
|
||||
],
|
||||
};
|
||||
|
||||
console.log("Passing context to Chatbot:", {
|
||||
careerSuggestions,
|
||||
riaSecScores,
|
||||
selectedCareer,
|
||||
userState,
|
||||
areaTitle,
|
||||
userZipcode,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="dashboard-content">
|
||||
<div className="career-suggestions-container">
|
||||
<div
|
||||
className="career-suggestions-header"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px',
|
||||
justifyContent: 'center',
|
||||
gap: '15px'
|
||||
}}
|
||||
<div className="dashboard-content">
|
||||
<div className="career-suggestions-container">
|
||||
<div
|
||||
className="career-suggestions-header"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px',
|
||||
justifyContent: 'center',
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
Preparation Level:
|
||||
<select
|
||||
value={selectedJobZone}
|
||||
onChange={(e) => setSelectedJobZone(Number(e.target.value))}
|
||||
style={{ marginLeft: '5px', padding: '2px', width: '200px' }}
|
||||
>
|
||||
<label>
|
||||
Preparation Level:
|
||||
<select
|
||||
value={selectedJobZone}
|
||||
onChange={(e) => setSelectedJobZone(Number(e.target.value))}
|
||||
style={{ marginLeft: '5px', padding: '2px', width: '200px' }}
|
||||
>
|
||||
<option value="">All Preparation Levels</option>
|
||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||
<option key={zone} value={zone}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<option value="">All Preparation Levels</option>
|
||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||
<option key={zone} value={zone}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Fit:
|
||||
<select
|
||||
value={selectedFit}
|
||||
onChange={(e) => setSelectedFit(e.target.value)}
|
||||
style={{ marginLeft: '5px', padding: '2px', width: '150px' }}
|
||||
>
|
||||
<option value="">All Fit Levels</option>
|
||||
{Object.entries(fitLabels).map(([key, label]) => (
|
||||
<option key={key} value={key}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<CareerSuggestions
|
||||
careerSuggestions={memoizedCareerSuggestions}
|
||||
onCareerClick={handleCareerClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="riasec-container">
|
||||
<div className="riasec-scores">
|
||||
<h2>RIASEC Scores</h2>
|
||||
<Bar data={chartData} />
|
||||
</div>
|
||||
<div className="riasec-descriptions">
|
||||
<h3>RIASEC Personality Descriptions</h3>
|
||||
{riaSecDescriptions.length > 0 ? (
|
||||
<ul>
|
||||
{riaSecDescriptions.map((desc, index) => (
|
||||
<li key={index}>
|
||||
<strong>{riaSecScores[index]?.area}:</strong> {desc}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Loading descriptions...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
Fit:
|
||||
<select
|
||||
value={selectedFit}
|
||||
onChange={(e) => setSelectedFit(e.target.value)}
|
||||
style={{ marginLeft: '5px', padding: '2px', width: '150px' }}
|
||||
>
|
||||
<option value="">All Fit Levels</option>
|
||||
{Object.entries(fitLabels).map(([key, label]) => (
|
||||
<option key={key} value={key}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<CareerSuggestions
|
||||
careerSuggestions={memoizedCareerSuggestions}
|
||||
onCareerClick={handleCareerClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{memoizedPopoutPanel}
|
||||
<div className="riasec-container">
|
||||
<div className="riasec-scores">
|
||||
<h2>RIASEC Scores</h2>
|
||||
<Bar data={chartData} />
|
||||
</div>
|
||||
<div className="riasec-descriptions">
|
||||
<h3>RIASEC Personality Descriptions</h3>
|
||||
{riaSecDescriptions.length > 0 ? (
|
||||
<ul>
|
||||
{riaSecDescriptions.map((desc, index) => (
|
||||
<li key={index}>
|
||||
<strong>{riaSecScores[index]?.area}:</strong> {desc}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Loading descriptions...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{memoizedPopoutPanel}
|
||||
|
||||
{/* Pass context to Chatbot */}
|
||||
<div className="chatbot-widget">
|
||||
<div className="chatbot-widget">
|
||||
{careerSuggestions.length > 0 ? (
|
||||
<Chatbot context={chatbotContext} />
|
||||
) : (
|
||||
<p>Loading Chatbot...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Acknowledgment Section */}
|
||||
<div
|
||||
className="data-source-acknowledgment"
|
||||
style={{
|
||||
marginTop: '20px',
|
||||
padding: '10px',
|
||||
borderTop: '1px solid #ccc',
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
textAlign: 'center'
|
||||
<div
|
||||
className="data-source-acknowledgment"
|
||||
style={{
|
||||
marginTop: '20px',
|
||||
padding: '10px',
|
||||
borderTop: '1px solid #ccc',
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
|
||||
<p>
|
||||
Career results and RIASEC scores are provided by
|
||||
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the
|
||||
|
@ -1,226 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { CheckCircle, Clock, Target, PlusCircle, Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
|
||||
|
||||
|
||||
const MilestoneTracker = () => {
|
||||
const location = useLocation();
|
||||
const initialCareer = location.state?.career || "";
|
||||
const [careerClusters, setCareerClusters] = useState({});
|
||||
const [selectedCluster, setSelectedCluster] = useState(null);
|
||||
const [selectedSubdivision, setSelectedSubdivision] = useState(null);
|
||||
const [selectedCareer, setSelectedCareer] = useState(initialCareer);
|
||||
const [filteredClusters, setFilteredClusters] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState("career");
|
||||
const [customMilestones, setCustomMilestones] = useState({ career: [], financial: [], retirement: [] });
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [newMilestone, setNewMilestone] = useState("");
|
||||
const [careerSearch, setCareerSearch] = useState("");
|
||||
const [filteredCareers, setFilteredCareers] = useState(careerClusters);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/user/profile", {
|
||||
method: "GET",
|
||||
credentials: "include", // Ensure cookies/session are sent
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setIsPremiumUser(data.is_premium === 1); // Expecting { is_premium: 0 or 1 }
|
||||
} else {
|
||||
setIsPremiumUser(false); // Default to false if there's an error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user profile:", error);
|
||||
setIsPremiumUser(false);
|
||||
}
|
||||
};
|
||||
fetchUserProfile();
|
||||
}, []);
|
||||
|
||||
if (isPremiumUser === null) {
|
||||
return <div className="p-6 text-center">Loading...</div>; // Show loading state while fetching
|
||||
}
|
||||
|
||||
if (!isPremiumUser) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<Lock className="mx-auto text-gray-400 w-16 h-16 mb-4" />
|
||||
<h2 className="text-xl font-bold">Access Restricted</h2>
|
||||
<p className="text-gray-600">Upgrade to Aptiva Premium to access the Milestone Tracker.</p>
|
||||
<Button className="mt-4">Upgrade Now</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/data/career_clusters.json")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setCareerClusters(data);
|
||||
setFilteredClusters(Object.keys(data));
|
||||
})
|
||||
.catch((error) => console.error("Error loading career clusters:", error));
|
||||
}, []);
|
||||
|
||||
const handleAddMilestone = () => {
|
||||
if (newMilestone.trim() !== "") {
|
||||
setCustomMilestones((prev) => ({
|
||||
...prev,
|
||||
[activeTab]: [...prev[activeTab], { title: newMilestone, status: "upcoming", progress: 0 }],
|
||||
}));
|
||||
setNewMilestone("");
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCareerSearch = (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
setCareerSearch(query);
|
||||
setFilteredCareers(
|
||||
careerClusters.filter((career) => career.toLowerCase().includes(query))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Milestone Tracker</h1>
|
||||
|
||||
{/* Career Cluster Selection */}
|
||||
<div className="mb-6 p-4 border rounded-lg shadow-md bg-white">
|
||||
<h2 className="text-xl font-semibold mb-2">Search & Select a Career Cluster</h2>
|
||||
<Input
|
||||
value={careerSearch}
|
||||
onChange={handleCareerSearch}
|
||||
placeholder="Search for a career cluster"
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="max-h-40 overflow-y-auto border rounded p-2">
|
||||
{filteredClusters.map((cluster, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-2 hover:bg-gray-200 cursor-pointer rounded ${cluster === selectedCluster ? 'bg-blue-200' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedCluster(cluster);
|
||||
setSelectedSubdivision(null);
|
||||
setSelectedCareer(null);
|
||||
}}
|
||||
>
|
||||
<strong>{cluster}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subdivision Selection within Cluster */}
|
||||
{selectedCluster && careerClusters[selectedCluster] && (
|
||||
<div className="mb-6 p-4 border rounded-lg shadow-md bg-white">
|
||||
<h2 className="text-xl font-semibold mb-2">Select a Specialization in {selectedCluster}</h2>
|
||||
<div className="max-h-40 overflow-y-auto border rounded p-2">
|
||||
{Object.keys(careerClusters[selectedCluster]).map((subdivision, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-2 hover:bg-gray-200 cursor-pointer rounded ${subdivision === selectedSubdivision ? 'bg-blue-200' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedSubdivision(subdivision);
|
||||
setSelectedCareer(null);
|
||||
}}
|
||||
>
|
||||
{subdivision}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Career Selection within Subdivision */}
|
||||
{selectedSubdivision && careerClusters[selectedCluster][selectedSubdivision] && (
|
||||
<div className="mb-6 p-4 border rounded-lg shadow-md bg-white">
|
||||
<h2 className="text-xl font-semibold mb-2">Select a Career in {selectedSubdivision}</h2>
|
||||
<div className="max-h-40 overflow-y-auto border rounded p-2">
|
||||
{Object.keys(careerClusters[selectedCluster][selectedSubdivision]).map((career, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-2 hover:bg-gray-200 cursor-pointer rounded ${career === selectedCareer ? 'bg-blue-200' : ''}`}
|
||||
onClick={() => setSelectedCareer(career)}
|
||||
>
|
||||
{career}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Milestone Tracker Section */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="career">Career</TabsTrigger>
|
||||
<TabsTrigger value="financial">Financial</TabsTrigger>
|
||||
<TabsTrigger value="retirement">Retirement</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Add Milestone Section */}
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="mb-4 flex items-center gap-2">
|
||||
<PlusCircle /> Add Milestone
|
||||
</Button>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a New Milestone</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input value={newMilestone} onChange={(e) => setNewMilestone(e.target.value)} placeholder="Enter milestone title" />
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddMilestone}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Display User-Added Milestones */}
|
||||
<TabsContent value={activeTab}>
|
||||
{customMilestones[activeTab].map((milestone, index) => (
|
||||
<Card key={index} className="mb-4 p-4">
|
||||
<CardContent className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
{milestone.status === "completed" && <CheckCircle className="text-green-500" />}
|
||||
{milestone.status === "in-progress" && <Clock className="text-yellow-500" />}
|
||||
{milestone.status === "upcoming" && <Target className="text-gray-500" />}
|
||||
<h2 className="text-lg font-semibold">{milestone.title}</h2>
|
||||
</div>
|
||||
<Progress value={milestone.progress} className="w-40" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="mb-4 flex items-center gap-2">
|
||||
<PlusCircle /> Add Milestone
|
||||
</Button>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a New Milestone</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input value={newMilestone} onChange={(e) => setNewMilestone(e.target.value)} placeholder="Enter milestone title" />
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddMilestone}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default MilestoneTracker;
|
@ -1,3 +1,5 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ClipLoader } from 'react-spinners';
|
||||
import LoanRepayment from './LoanRepayment.js';
|
||||
import SchoolFilters from './SchoolFilters';
|
||||
@ -21,6 +23,8 @@ function PopoutPanel({
|
||||
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 navigate = useNavigate();
|
||||
|
||||
|
||||
const {
|
||||
jobDescription = null,
|
||||
@ -41,7 +45,6 @@ function PopoutPanel({
|
||||
}, [schools]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("📩 Updating Chatbot Context from PopoutPanel:", data);
|
||||
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
updateChatbotContext({
|
||||
@ -53,7 +56,6 @@ function PopoutPanel({
|
||||
persistedROI, // ✅ Make sure ROI is included!
|
||||
});
|
||||
} else {
|
||||
console.log("⚠️ No valid PopoutPanel data to update chatbot context.");
|
||||
}
|
||||
}, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]);
|
||||
|
||||
@ -110,7 +112,15 @@ function PopoutPanel({
|
||||
{/* Header with Close & Plan My Path Buttons */}
|
||||
<div className="panel-header">
|
||||
<button className="close-btn" onClick={closePanel}>X</button>
|
||||
<button className="plan-path-btn">Plan My Path</button>
|
||||
<button
|
||||
className="plan-path-btn"
|
||||
onClick={() => {
|
||||
console.log("Navigating to Milestone Tracker with career title:", title); // Log the title
|
||||
navigate("/milestone-tracker", { state: { career: title } });
|
||||
}}
|
||||
>
|
||||
Plan My Path
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2>{title}</h2>
|
||||
|
17
src/components/ui/button.js
Normal file
17
src/components/ui/button.js
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/cn.js";
|
||||
|
||||
const Button = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export { Button };
|
10
src/components/ui/card.js
Normal file
10
src/components/ui/card.js
Normal file
@ -0,0 +1,10 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/cn.js";
|
||||
|
||||
export const Card = ({ className, ...props }) => (
|
||||
<div className={cn("rounded-lg border bg-white shadow-sm", className)} {...props} />
|
||||
);
|
||||
|
||||
export const CardContent = ({ className, ...props }) => (
|
||||
<div className={cn("p-4", className)} {...props} />
|
||||
);
|
27
src/components/ui/dialog.js
Normal file
27
src/components/ui/dialog.js
Normal file
@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className="fixed inset-0 bg-white shadow-lg p-4 max-w-lg mx-auto mt-20 rounded-md"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const DialogHeader = ({ children }) => {
|
||||
return <div className="text-lg font-semibold border-b pb-2 mb-4">{children}</div>;
|
||||
};
|
||||
|
||||
const DialogFooter = ({ children }) => {
|
||||
return <div className="flex justify-end gap-2">{children}</div>;
|
||||
};
|
||||
|
||||
const DialogTitle = DialogPrimitive.Title;
|
||||
|
||||
|
||||
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle };
|
17
src/components/ui/input.js
Normal file
17
src/components/ui/input.js
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/cn.js";
|
||||
|
||||
const Input = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border border-gray-300 rounded-md px-3 py-2 focus:ring focus:ring-blue-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export { Input };
|
20
src/components/ui/progress.js
Normal file
20
src/components/ui/progress.js
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
const Progress = ({ value, max = 100, className, ...props }) => {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
value={value}
|
||||
max={max}
|
||||
className="relative w-full h-4 bg-gray-200 rounded-md overflow-hidden"
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="absolute top-0 left-0 h-full bg-blue-600 transition-all"
|
||||
style={{ width: `${(value / max) * 100}%` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export { Progress };
|
9
src/components/ui/tabs.js
Normal file
9
src/components/ui/tabs.js
Normal file
@ -0,0 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
const TabsList = TabsPrimitive.List;
|
||||
const TabsTrigger = TabsPrimitive.Trigger;
|
||||
const TabsContent = TabsPrimitive.Content;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
|
4
src/utils/cn.js
Normal file
4
src/utils/cn.js
Normal file
@ -0,0 +1,4 @@
|
||||
export function cn(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,jsx,ts,tsx}', // Ensure this matches your file structure
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
Loading…
Reference in New Issue
Block a user