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,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.7.9",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cra-template": "1.2.0",
|
"cra-template": "1.2.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lucide-react": "^0.483.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.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-scripts": "^5.0.1",
|
||||||
"react-spinners": "^0.15.0",
|
"react-spinners": "^0.15.0",
|
||||||
"sqlite": "^5.1.1",
|
"sqlite": "^5.1.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
"tailwind-merge": "^3.0.2",
|
||||||
"web-vitals": "^4.2.4",
|
"web-vitals": "^4.2.4",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
@ -54,6 +64,9 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"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 InterestInventory from './components/InterestInventory.js';
|
||||||
import Dashboard from './components/Dashboard.js';
|
import Dashboard from './components/Dashboard.js';
|
||||||
import UserProfile from './components/UserProfile.js';
|
import UserProfile from './components/UserProfile.js';
|
||||||
|
import MilestoneTracker from "./components/MilestoneTracker.js";
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -42,7 +43,10 @@ function App() {
|
|||||||
path="/profile"
|
path="/profile"
|
||||||
element={isAuthenticated ? <UserProfile /> : <Navigate to="/signin" />}
|
element={isAuthenticated ? <UserProfile /> : <Navigate to="/signin" />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/milestone-tracker"
|
||||||
|
element={isAuthenticated ? <MilestoneTracker /> : <Navigate to="/signin" />}
|
||||||
|
/>
|
||||||
{/* Catch-all for unknown routes */}
|
{/* Catch-all for unknown routes */}
|
||||||
<Route path="*" element={<Navigate to="/signin" />} />
|
<Route path="*" element={<Navigate to="/signin" />} />
|
||||||
</Routes>
|
</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 isEconomicMissing = !economicData || Object.values(economicData).every(val => val === "N/A" || val === "*");
|
||||||
const isSalaryMissing = salaryResponse === null || salaryResponse === undefined;
|
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;
|
const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
||||||
if (isLimitedData) console.log(`⚠️ Setting limitedData for ${career.title} (${career.code})`);
|
if (isLimitedData) console.log(`⚠️ Setting limitedData for ${career.title} (${career.code})`);
|
||||||
|
|
||||||
|
@ -47,14 +47,6 @@ const Chatbot = ({ context }) => {
|
|||||||
: "Not available"
|
: "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 State: ${context.userState || "Not provided"}
|
||||||
- User Area: ${context.areaTitle || "Not provided"}
|
- User Area: ${context.areaTitle || "Not provided"}
|
||||||
- User Zipcode: ${context.userZipcode || "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 { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||||
import PopoutPanel from './PopoutPanel.js';
|
import PopoutPanel from './PopoutPanel.js';
|
||||||
import './PopoutPanel.css';
|
import './Dashboard.css';
|
||||||
import Chatbot from "./Chatbot.js";
|
import Chatbot from "./Chatbot.js";
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
import { fetchSchools } from '../utils/apiUtils.js';
|
import { fetchSchools } from '../utils/apiUtils.js';
|
||||||
import './Dashboard.css';
|
|
||||||
|
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const location = useLocation()
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
@ -33,12 +31,11 @@ function Dashboard() {
|
|||||||
const [userZipcode, setUserZipcode] = useState(null);
|
const [userZipcode, setUserZipcode] = useState(null);
|
||||||
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
||||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info
|
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
||||||
const [selectedFit, setSelectedFit] = useState('');
|
const [selectedFit, setSelectedFit] = useState('');
|
||||||
const [results, setResults] = useState([]); // Add results state
|
const [results, setResults] = useState([]);
|
||||||
const [chatbotContext, setChatbotContext] = useState({});
|
const [chatbotContext, setChatbotContext] = useState({});
|
||||||
|
|
||||||
|
|
||||||
const jobZoneLabels = {
|
const jobZoneLabels = {
|
||||||
'1': 'Little or No Preparation',
|
'1': 'Little or No Preparation',
|
||||||
'2': 'Some Preparation Needed',
|
'2': 'Some Preparation Needed',
|
||||||
@ -53,10 +50,8 @@ function Dashboard() {
|
|||||||
'Good': 'Good - Less Strong Match'
|
'Good': 'Good - Less Strong Match'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dynamic API URL
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
// Fetch job zone mappings after career suggestions are loaded
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchJobZones = async () => {
|
const fetchJobZones = async () => {
|
||||||
if (careerSuggestions.length === 0) return;
|
if (careerSuggestions.length === 0) return;
|
||||||
@ -68,10 +63,10 @@ function Dashboard() {
|
|||||||
|
|
||||||
const updatedCareers = careerSuggestions.map((career) => ({
|
const updatedCareers = careerSuggestions.map((career) => ({
|
||||||
...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) {
|
} catch (error) {
|
||||||
console.error('Error fetching job zone information:', error);
|
console.error('Error fetching job zone information:', error);
|
||||||
}
|
}
|
||||||
@ -93,34 +88,30 @@ function Dashboard() {
|
|||||||
|
|
||||||
return jobZoneMatches && fitMatches;
|
return jobZoneMatches && fitMatches;
|
||||||
});
|
});
|
||||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||||
|
|
||||||
const updateChatbotContext = (updatedData) => {
|
const updateChatbotContext = (updatedData) => {
|
||||||
setChatbotContext((prevContext) => {
|
setChatbotContext((prevContext) => {
|
||||||
const mergedContext = {
|
const mergedContext = {
|
||||||
...prevContext, // ✅ Preserve existing context (Dashboard Data)
|
...prevContext,
|
||||||
...Object.keys(updatedData).reduce((acc, key) => {
|
...Object.keys(updatedData).reduce((acc, key) => {
|
||||||
if (updatedData[key] !== undefined && updatedData[key] !== null) {
|
if (updatedData[key] !== undefined && updatedData[key] !== null) {
|
||||||
acc[key] = updatedData[key]; // ✅ Only update fields with actual data
|
acc[key] = updatedData[key];
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔄 Updated Chatbot Context (Merged Dashboard + PopoutPanel):", mergedContext);
|
|
||||||
return mergedContext;
|
return mergedContext;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
||||||
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
|
||||||
|
|
||||||
const memoizedPopoutPanel = useMemo(() => {
|
const memoizedPopoutPanel = useMemo(() => {
|
||||||
console.log("Passing careerDetails to PopoutPanel:", careerDetails);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoutPanel
|
<PopoutPanel
|
||||||
isVisible={!!selectedCareer} // ✅ Ensures it's only visible when needed
|
isVisible={!!selectedCareer}
|
||||||
data={careerDetails}
|
data={careerDetails}
|
||||||
schools={schools}
|
schools={schools}
|
||||||
salaryData={salaryData}
|
salaryData={salaryData}
|
||||||
@ -137,13 +128,13 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
|||||||
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
|
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let descriptions = []; // Declare outside for scope accessibility
|
let descriptions = [];
|
||||||
if (location.state) {
|
if (location.state) {
|
||||||
const { careerSuggestions: suggestions, riaSecScores: scores } = 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 || []);
|
setCareerSuggestions(suggestions || []);
|
||||||
setRiaSecScores(scores || []);
|
setRiaSecScores(scores || []);
|
||||||
setRiaSecDescriptions(descriptions); // Set descriptions
|
setRiaSecDescriptions(descriptions);
|
||||||
} else {
|
} else {
|
||||||
console.warn('No data found, redirecting to Interest Inventory');
|
console.warn('No data found, redirecting to Interest Inventory');
|
||||||
navigate('/interest-inventory');
|
navigate('/interest-inventory');
|
||||||
@ -161,10 +152,10 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
|||||||
if (profileResponse.ok) {
|
if (profileResponse.ok) {
|
||||||
const profileData = await profileResponse.json();
|
const profileData = await profileResponse.json();
|
||||||
|
|
||||||
const { state, area, zipcode } = profileData; // Use 'area' instead of 'AREA_TITLE'
|
const { state, area, zipcode } = profileData;
|
||||||
setUserState(state);
|
setUserState(state);
|
||||||
setAreaTitle(area && area.trim() ? area.trim() : ''); // Ensure 'area' is set correctly
|
setAreaTitle(area && area.trim() ? area.trim() : '');
|
||||||
setUserZipcode(zipcode); // Set 'zipcode' in the state
|
setUserZipcode(zipcode);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to fetch user profile');
|
console.error('Failed to fetch user profile');
|
||||||
}
|
}
|
||||||
@ -184,12 +175,10 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
|||||||
areaTitle !== null &&
|
areaTitle !== null &&
|
||||||
userZipcode !== null
|
userZipcode !== null
|
||||||
) {
|
) {
|
||||||
console.log("✅ All data ready, forcing chatbotContext update...");
|
|
||||||
|
|
||||||
// ✅ Create a completely new object so React detects the change
|
|
||||||
const newChatbotContext = {
|
const newChatbotContext = {
|
||||||
careerSuggestions: [...careersWithJobZone], // Ensure fresh array reference
|
careerSuggestions: [...careersWithJobZone],
|
||||||
riaSecScores: [...riaSecScores], // Ensure fresh array reference
|
riaSecScores: [...riaSecScores],
|
||||||
userState: userState || "",
|
userState: userState || "",
|
||||||
areaTitle: areaTitle || "",
|
areaTitle: areaTitle || "",
|
||||||
userZipcode: userZipcode || "",
|
userZipcode: userZipcode || "",
|
||||||
@ -197,24 +186,20 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
|||||||
|
|
||||||
setChatbotContext(newChatbotContext);
|
setChatbotContext(newChatbotContext);
|
||||||
} else {
|
} else {
|
||||||
console.log("⏳ Skipping chatbotContext update because data is not ready yet.");
|
|
||||||
}
|
}
|
||||||
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
||||||
|
|
||||||
|
const handleCareerClick = useCallback(
|
||||||
|
async (career) => {
|
||||||
const handleCareerClick = useCallback(
|
const socCode = career.code;
|
||||||
async (career) => {
|
setSelectedCareer(career);
|
||||||
const socCode = career.code; // Extract SOC code from career object
|
setLoading(true);
|
||||||
setSelectedCareer(career); // Set career first to trigger loading panel
|
setError(null);
|
||||||
setLoading(true); // Enable loading state only when career is clicked
|
setCareerDetails({});
|
||||||
setError(null); // Clear previous errors
|
setSchools([]);
|
||||||
setCareerDetails({}); // Reset career details to avoid undefined errors
|
setSalaryData([]);
|
||||||
setSchools([]); // Reset schools
|
setEconomicProjections({});
|
||||||
setSalaryData([]); // Reset salary data
|
setTuitionData([]);
|
||||||
setEconomicProjections({}); // Reset economic projections
|
|
||||||
setTuitionData([]); // Reset tuition data
|
|
||||||
|
|
||||||
|
|
||||||
if (!socCode) {
|
if (!socCode) {
|
||||||
console.error('SOC Code is missing');
|
console.error('SOC Code is missing');
|
||||||
@ -223,47 +208,38 @@ async (career) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Fetch CIP Code
|
|
||||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||||
const { cipCode } = await cipResponse.json();
|
const { cipCode } = await cipResponse.json();
|
||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
// Step 2: Fetch Job Description and Tasks
|
|
||||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
const { description, tasks } = await jobDetailsResponse.json();
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
// Step 3: Fetch Data in Parallel for other career details
|
|
||||||
// Salary API call with error handling
|
|
||||||
let salaryResponse;
|
let salaryResponse;
|
||||||
try {
|
try {
|
||||||
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }});
|
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
salaryResponse = { data: {} }; // Prevents breaking the whole update
|
salaryResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Projections API call with error handling
|
|
||||||
let economicResponse;
|
let economicResponse;
|
||||||
try {
|
try {
|
||||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
economicResponse = { data: {} }; // Prevents breaking the whole update
|
economicResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuition API call with error handling
|
|
||||||
let tuitionResponse;
|
let tuitionResponse;
|
||||||
try {
|
try {
|
||||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }});
|
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
tuitionResponse = { data: {} };
|
tuitionResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch schools separately (this one seems to be working fine)
|
|
||||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||||
|
|
||||||
// Handle Distance Calculation
|
|
||||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||||
try {
|
try {
|
||||||
@ -278,7 +254,6 @@ async (career) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Process Salary Data
|
|
||||||
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0
|
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: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 },
|
||||||
@ -299,7 +274,6 @@ async (career) => {
|
|||||||
tuitionData: tuitionResponse.data || [],
|
tuitionData: tuitionResponse.data || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure `careerDetails` is fully updated before passing to chatbot
|
|
||||||
setCareerDetails(updatedCareerDetails);
|
setCareerDetails(updatedCareerDetails);
|
||||||
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
||||||
|
|
||||||
@ -309,9 +283,9 @@ async (career) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[userState, apiUrl, areaTitle, userZipcode]
|
[userState, apiUrl, areaTitle, userZipcode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
labels: riaSecScores.map((score) => score.area),
|
labels: riaSecScores.map((score) => score.area),
|
||||||
@ -326,15 +300,6 @@ async (career) => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Passing context to Chatbot:", {
|
|
||||||
careerSuggestions,
|
|
||||||
riaSecScores,
|
|
||||||
selectedCareer,
|
|
||||||
userState,
|
|
||||||
areaTitle,
|
|
||||||
userZipcode,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<div className="dashboard-content">
|
<div className="dashboard-content">
|
||||||
@ -407,8 +372,6 @@ async (career) => {
|
|||||||
|
|
||||||
{memoizedPopoutPanel}
|
{memoizedPopoutPanel}
|
||||||
|
|
||||||
|
|
||||||
{/* Pass context to Chatbot */}
|
|
||||||
<div className="chatbot-widget">
|
<div className="chatbot-widget">
|
||||||
{careerSuggestions.length > 0 ? (
|
{careerSuggestions.length > 0 ? (
|
||||||
<Chatbot context={chatbotContext} />
|
<Chatbot context={chatbotContext} />
|
||||||
@ -417,8 +380,6 @@ async (career) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Acknowledgment Section */}
|
|
||||||
<div
|
<div
|
||||||
className="data-source-acknowledgment"
|
className="data-source-acknowledgment"
|
||||||
style={{
|
style={{
|
||||||
@ -430,7 +391,6 @@ async (career) => {
|
|||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Career results and RIASEC scores are provided by
|
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
|
<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 { ClipLoader } from 'react-spinners';
|
||||||
import LoanRepayment from './LoanRepayment.js';
|
import LoanRepayment from './LoanRepayment.js';
|
||||||
import SchoolFilters from './SchoolFilters';
|
import SchoolFilters from './SchoolFilters';
|
||||||
@ -21,6 +23,8 @@ function PopoutPanel({
|
|||||||
const [sortBy, setSortBy] = useState('tuition'); // Default sorting
|
const [sortBy, setSortBy] = useState('tuition'); // Default sorting
|
||||||
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value
|
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value
|
||||||
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value
|
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jobDescription = null,
|
jobDescription = null,
|
||||||
@ -41,7 +45,6 @@ function PopoutPanel({
|
|||||||
}, [schools]);
|
}, [schools]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("📩 Updating Chatbot Context from PopoutPanel:", data);
|
|
||||||
|
|
||||||
if (data && Object.keys(data).length > 0) {
|
if (data && Object.keys(data).length > 0) {
|
||||||
updateChatbotContext({
|
updateChatbotContext({
|
||||||
@ -53,7 +56,6 @@ function PopoutPanel({
|
|||||||
persistedROI, // ✅ Make sure ROI is included!
|
persistedROI, // ✅ Make sure ROI is included!
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ No valid PopoutPanel data to update chatbot context.");
|
|
||||||
}
|
}
|
||||||
}, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]);
|
}, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]);
|
||||||
|
|
||||||
@ -110,7 +112,15 @@ function PopoutPanel({
|
|||||||
{/* Header with Close & Plan My Path Buttons */}
|
{/* Header with Close & Plan My Path Buttons */}
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<button className="close-btn" onClick={closePanel}>X</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<h2>{title}</h2>
|
<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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
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