UI Skills fixes and Schools fixes
This commit is contained in:
parent
93ae23214a
commit
7f73977cce
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
import { ONET_DEFINITIONS } from './definitions.js';
|
import { ONET_DEFINITIONS } from './definitions.js';
|
||||||
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
||||||
@ -31,28 +31,36 @@ function combineIMandLV(rows) {
|
|||||||
return Array.from(map.values());
|
return Array.from(map.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureHttp(urlString) {
|
||||||
|
if (!urlString) return '';
|
||||||
|
// If it already starts with 'http://' or 'https://', just return as-is.
|
||||||
|
if (/^https?:\/\//i.test(urlString)) {
|
||||||
|
return urlString;
|
||||||
|
}
|
||||||
|
// Otherwise prepend 'https://' (or 'http://').
|
||||||
|
return `https://${urlString}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert numeric importance (1–5) to star or emoji compact representation
|
// Convert numeric importance (1–5) to star or emoji compact representation
|
||||||
function renderImportance(val) {
|
function renderImportance(val) {
|
||||||
// Example: star approach (rounded)
|
|
||||||
// or you can do emojis like "🔴🟢🟡" etc.
|
|
||||||
const max = 5;
|
const max = 5;
|
||||||
const rounded = Math.round(val);
|
const rounded = Math.round(val);
|
||||||
const stars = '★'.repeat(rounded) + '☆'.repeat(max - rounded);
|
const stars = '★'.repeat(rounded) + '☆'.repeat(max - rounded);
|
||||||
return `${stars}`; // e.g. ★★★★☆
|
return `${stars}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert numeric level (0–7) to bar or block representation
|
// Convert numeric level (0–7) to bar or block representation
|
||||||
function renderLevel(val) {
|
function renderLevel(val) {
|
||||||
// 7 is max, we’ll do a small row of squares
|
|
||||||
const max = 7;
|
const max = 7;
|
||||||
const rounded = Math.round(val);
|
const rounded = Math.round(val);
|
||||||
const filled = '■'.repeat(rounded); // '■' is a filled block
|
const filled = '■'.repeat(rounded);
|
||||||
const empty = '□'.repeat(max - rounded);
|
const empty = '□'.repeat(max - rounded);
|
||||||
return `${filled}${empty}`; // e.g. ■■■■■□□
|
return `${filled}${empty}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EducationalProgramsPage() {
|
function EducationalProgramsPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [socCode, setsocCode] = useState(location.state?.socCode || '');
|
const [socCode, setsocCode] = useState(location.state?.socCode || '');
|
||||||
const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []);
|
const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []);
|
||||||
|
|
||||||
@ -87,24 +95,28 @@ function EducationalProgramsPage() {
|
|||||||
setCipCodes(cleanedCips);
|
setCipCodes(cleanedCips);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fixed handleSelectSchool (removed extra brace)
|
||||||
|
const handleSelectSchool = (school) => {
|
||||||
|
const proceed = window.confirm(
|
||||||
|
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
|
||||||
|
);
|
||||||
|
if (proceed) {
|
||||||
|
navigate('/financial-planner', { state: { selectedSchool: school } });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function getSearchLinks(ksaName, careerTitle) {
|
function getSearchLinks(ksaName, careerTitle) {
|
||||||
const combinedQuery = `${careerTitle} ${ksaName}`.trim();
|
const combinedQuery = `${careerTitle} ${ksaName}`.trim();
|
||||||
const encoded = encodeURIComponent(combinedQuery);
|
const encoded = encodeURIComponent(combinedQuery);
|
||||||
|
|
||||||
const courseraUrl = `https://www.coursera.org/search?query=${encoded}`;
|
const courseraUrl = `https://www.coursera.org/search?query=${encoded}`;
|
||||||
|
const edxUrl = `https://www.edx.org/search?q=${encoded}`;
|
||||||
|
|
||||||
|
return [
|
||||||
const edxUrl = `https://www.edx.org/search?q=${encoded}`;
|
{ title: 'Coursera', url: courseraUrl },
|
||||||
|
{ title: 'edX', url: edxUrl },
|
||||||
|
];
|
||||||
const classCentralUrl = `https://www.classcentral.com/search?q=${encoded}`;
|
}
|
||||||
|
|
||||||
return [
|
|
||||||
{ title: 'Coursera', url: courseraUrl },
|
|
||||||
{ title: 'edX', url: edxUrl },
|
|
||||||
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load KSA data once
|
// Load KSA data once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -138,22 +150,14 @@ function EducationalProgramsPage() {
|
|||||||
setKsaForCareer([]);
|
setKsaForCareer([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 1) filter by socCode
|
|
||||||
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
|
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
|
||||||
// 2) skip suppress=Y
|
|
||||||
filtered = filtered.filter((r) => r.recommendSuppress !== 'Y');
|
filtered = filtered.filter((r) => r.recommendSuppress !== 'Y');
|
||||||
// 3) keep scaleIDs in [IM,LV]
|
|
||||||
filtered = filtered.filter((r) => ['IM', 'LV'].includes(r.scaleID));
|
filtered = filtered.filter((r) => ['IM', 'LV'].includes(r.scaleID));
|
||||||
|
|
||||||
// combine IM + LV
|
|
||||||
let combined = combineIMandLV(filtered);
|
let combined = combineIMandLV(filtered);
|
||||||
|
|
||||||
// only keep items with importanceValue >=3
|
|
||||||
combined = combined.filter((item) => {
|
combined = combined.filter((item) => {
|
||||||
return item.importanceValue !== null && item.importanceValue >= 3;
|
return item.importanceValue !== null && item.importanceValue >= 3;
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort by importanceValue desc
|
|
||||||
combined.sort((a, b) => (b.importanceValue || 0) - (a.importanceValue || 0));
|
combined.sort((a, b) => (b.importanceValue || 0) - (a.importanceValue || 0));
|
||||||
|
|
||||||
setKsaForCareer(combined);
|
setKsaForCareer(combined);
|
||||||
@ -195,7 +199,8 @@ function EducationalProgramsPage() {
|
|||||||
try {
|
try {
|
||||||
const fetchedSchools = await fetchSchools(cipCodes);
|
const fetchedSchools = await fetchSchools(cipCodes);
|
||||||
|
|
||||||
let userLat = null, userLng = null;
|
let userLat = null;
|
||||||
|
let userLng = null;
|
||||||
if (userZip) {
|
if (userZip) {
|
||||||
try {
|
try {
|
||||||
const geoResult = await clientGeocodeZip(userZip);
|
const geoResult = await clientGeocodeZip(userZip);
|
||||||
@ -230,6 +235,7 @@ function EducationalProgramsPage() {
|
|||||||
|
|
||||||
// Sort schools in useMemo
|
// Sort schools in useMemo
|
||||||
const filteredAndSortedSchools = useMemo(() => {
|
const filteredAndSortedSchools = useMemo(() => {
|
||||||
|
|
||||||
if (!schools) return [];
|
if (!schools) return [];
|
||||||
let result = [...schools];
|
let result = [...schools];
|
||||||
|
|
||||||
@ -266,12 +272,8 @@ function EducationalProgramsPage() {
|
|||||||
} else {
|
} else {
|
||||||
// Sort by in-state tuition
|
// Sort by in-state tuition
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
const tA = a['In_state cost']
|
const tA = a['In_state cost'] ? parseFloat(a['In_state cost']) : Infinity;
|
||||||
? parseFloat(a['In_state cost'])
|
const tB = b['In_state cost'] ? parseFloat(b['In_state cost']) : Infinity;
|
||||||
: Infinity;
|
|
||||||
const tB = b['In_state cost']
|
|
||||||
? parseFloat(b['In_state cost'])
|
|
||||||
: Infinity;
|
|
||||||
return tA - tB;
|
return tA - tB;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -279,54 +281,59 @@ function EducationalProgramsPage() {
|
|||||||
return result;
|
return result;
|
||||||
}, [schools, inStateOnly, userState, maxTuition, maxDistance, sortBy]);
|
}, [schools, inStateOnly, userState, maxTuition, maxDistance, sortBy]);
|
||||||
|
|
||||||
// Render the KSA as a table row with emoji
|
// Render a single KSA row
|
||||||
function renderKsaRow(k, idx, careerTitle) {
|
function renderKsaRow(k, idx, careerTitle) {
|
||||||
// k is the object => { elementName, importanceValue, levelValue, ksa_type }
|
const elementName = k.elementName;
|
||||||
const elementName = k.elementName;
|
const impStars = renderImportance(k.importanceValue);
|
||||||
const impStars = renderImportance(k.importanceValue);
|
const lvlBars = k.levelValue !== null ? renderLevel(k.levelValue) : 'n/a';
|
||||||
const lvlBars = k.levelValue !== null ? renderLevel(k.levelValue) : 'n/a';
|
const isAbility = k.ksa_type === 'Ability';
|
||||||
const links = getSearchLinks(elementName, careerTitle);
|
const links = !isAbility ? getSearchLinks(elementName, careerTitle) : null;
|
||||||
const definition = ONET_DEFINITIONS[elementName] || "No definition available";
|
const definition = ONET_DEFINITIONS[elementName] || 'No definition available';
|
||||||
return (
|
|
||||||
<tr key={idx} className="border-b text-sm">
|
|
||||||
<td className="p-2 font-medium text-gray-800">{elementName}{' '}
|
return (
|
||||||
<span title={definition}
|
<tr key={idx} className="border-b text-sm">
|
||||||
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center rounded-full
|
<td className="p-2 font-medium text-gray-800">
|
||||||
bg-blue-500 text-[0.6rem] font-bold text-white cursor-help">
|
{elementName}{' '}
|
||||||
i
|
<span
|
||||||
|
title={definition}
|
||||||
|
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center
|
||||||
|
rounded-full bg-blue-500 text-[0.6rem] font-bold text-white cursor-help"
|
||||||
|
>
|
||||||
|
i
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-center text-gray-800">{impStars}</td>
|
<td className="p-2 text-center text-gray-800">{impStars}</td>
|
||||||
<td className="p-2 text-center text-gray-800">{lvlBars}</td>
|
<td className="p-2 text-center text-gray-800">{lvlBars}</td>
|
||||||
<td className="p-2">
|
{!isAbility && (
|
||||||
{links.map((link, i) => (
|
<td className="p-2">
|
||||||
<div key={i}>
|
{links?.map((link, i) => (
|
||||||
<a
|
<div key={i}>
|
||||||
href={link.url}
|
<a
|
||||||
target="_blank"
|
href={link.url}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="text-blue-600 underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="text-blue-600 underline"
|
||||||
{link.title}
|
>
|
||||||
</a>
|
{link.title}
|
||||||
</div>
|
</a>
|
||||||
))}
|
</div>
|
||||||
</td>
|
))}
|
||||||
</tr>
|
</td>
|
||||||
);
|
)}
|
||||||
}
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knowledge / Skills / Abilities in 3 columns
|
||||||
// Knowledge / Skills / Abilities in 3 columns, each a small table
|
|
||||||
function renderKsaSection() {
|
function renderKsaSection() {
|
||||||
if (loadingKsa) return <p>Loading KSA data...</p>;
|
if (loadingKsa) return <p>Loading KSA data...</p>;
|
||||||
if (ksaError) return <p className="text-red-600">{ksaError}</p>;
|
if (ksaError) return <p className="text-red-600">{ksaError}</p>;
|
||||||
if (!socCode) return <p>Please select a career to see KSA data.</p>;
|
if (!socCode) return <p>Please select a career to see KSA data.</p>;
|
||||||
if (!ksaForCareer.length) {
|
if (!ksaForCareer.length) {
|
||||||
return <p>No Knowledge, Skills, and Abilites data found for {careerTitle}</p>;
|
return <p>No Knowledge, Skills, and Abilities data found for {careerTitle}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate them
|
|
||||||
const knowledge = ksaForCareer.filter((k) => k.ksa_type === 'Knowledge');
|
const knowledge = ksaForCareer.filter((k) => k.ksa_type === 'Knowledge');
|
||||||
const skillRows = ksaForCareer.filter((k) => k.ksa_type === 'Skill');
|
const skillRows = ksaForCareer.filter((k) => k.ksa_type === 'Skill');
|
||||||
const abilities = ksaForCareer.filter((k) => k.ksa_type === 'Ability');
|
const abilities = ksaForCareer.filter((k) => k.ksa_type === 'Ability');
|
||||||
@ -334,10 +341,10 @@ function EducationalProgramsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4">
|
<h2 className="text-xl font-semibold mb-4">
|
||||||
Knowledge, Skills, and Abilites needed for: {careerTitle || 'Unknown Career'}
|
Knowledge, Skills, and Abilities needed for:{' '}
|
||||||
|
{careerTitle || 'Unknown Career'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* 3-column layout */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{/* Knowledge */}
|
{/* Knowledge */}
|
||||||
<div>
|
<div>
|
||||||
@ -393,6 +400,19 @@ function EducationalProgramsPage() {
|
|||||||
<th className="p-2">Ability</th>
|
<th className="p-2">Ability</th>
|
||||||
<th className="p-2 text-center">Importance</th>
|
<th className="p-2 text-center">Importance</th>
|
||||||
<th className="p-2 text-center">Level</th>
|
<th className="p-2 text-center">Level</th>
|
||||||
|
<th className="p-2 text-center">
|
||||||
|
{/* Info icon for "Why no courses?" */}
|
||||||
|
<span className="relative inline-block">
|
||||||
|
<span>Why no courses?</span>
|
||||||
|
<span
|
||||||
|
className="ml-1 inline-flex h-2.5 w-2.5 items-center justify-center
|
||||||
|
rounded-full bg-blue-500 text-[10px] font-italics text-white cursor-help"
|
||||||
|
title="Abilities are more innate in nature, and difficult to offer courses for them."
|
||||||
|
>
|
||||||
|
i
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -406,7 +426,7 @@ function EducationalProgramsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} // <-- MAKE SURE WE DO NOT HAVE EXTRA BRACKETS HERE
|
||||||
|
|
||||||
// If no CIP codes => fallback
|
// If no CIP codes => fallback
|
||||||
if (!cipCodes.length) {
|
if (!cipCodes.length) {
|
||||||
@ -438,7 +458,7 @@ function EducationalProgramsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* KSA Section (3 columns, each a small table) */}
|
{/* KSA Section */}
|
||||||
{renderKsaSection()}
|
{renderKsaSection()}
|
||||||
|
|
||||||
{/* School List */}
|
{/* School List */}
|
||||||
@ -492,40 +512,43 @@ function EducationalProgramsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredAndSortedSchools.length > 0 ? (
|
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
{filteredAndSortedSchools.map((school, idx) => {
|
||||||
{filteredAndSortedSchools.map((school, idx) => (
|
// 1) Ensure the website has a protocol:
|
||||||
|
const displayWebsite = ensureHttp(school['Website']);
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={idx} className="rounded border p-3 text-sm">
|
<div key={idx} className="rounded border p-3 text-sm">
|
||||||
<strong>{school['INSTNM'] || 'Unnamed School'}</strong>
|
<strong>
|
||||||
<p>Degree Type: {school['CREDDESC'] || 'N/A'}</p>
|
|
||||||
<p>In-State Tuition: ${school['In_state cost'] || 'N/A'}</p>
|
|
||||||
<p>Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}</p>
|
|
||||||
<p>
|
|
||||||
Distance:{' '}
|
|
||||||
{school.distance !== null ? `${school.distance} mi` : 'N/A'}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Website:{' '}
|
|
||||||
{school['Website'] ? (
|
{school['Website'] ? (
|
||||||
<a
|
<a
|
||||||
href={school['Website']}
|
href={displayWebsite}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
{school['Website']}
|
{school['INSTNM'] || 'Unnamed School'}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
'N/A'
|
school['INSTNM'] || 'Unnamed School'
|
||||||
)}
|
)}
|
||||||
</p>
|
</strong>
|
||||||
|
<p>Degree Type: {school['CREDDESC'] || 'N/A'}</p>
|
||||||
|
<p>In-State Tuition: ${school['In_state cost'] || 'N/A'}</p>
|
||||||
|
<p>Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}</p>
|
||||||
|
<p>Distance: {school.distance !== null ? `${school.distance} mi` : 'N/A'}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelectSchool(school)}
|
||||||
|
className="mt-3 rounded bg-green-600 px-3 py-1 text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Select School
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500">No schools matching your filters.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user