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 { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import { ONET_DEFINITIONS } from './definitions.js';
|
||||
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
||||
@ -31,28 +31,36 @@ function combineIMandLV(rows) {
|
||||
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
|
||||
function renderImportance(val) {
|
||||
// Example: star approach (rounded)
|
||||
// or you can do emojis like "🔴🟢🟡" etc.
|
||||
const max = 5;
|
||||
const rounded = Math.round(val);
|
||||
const stars = '★'.repeat(rounded) + '☆'.repeat(max - rounded);
|
||||
return `${stars}`; // e.g. ★★★★☆
|
||||
return `${stars}`;
|
||||
}
|
||||
|
||||
// Convert numeric level (0–7) to bar or block representation
|
||||
function renderLevel(val) {
|
||||
// 7 is max, we’ll do a small row of squares
|
||||
const max = 7;
|
||||
const rounded = Math.round(val);
|
||||
const filled = '■'.repeat(rounded); // '■' is a filled block
|
||||
const filled = '■'.repeat(rounded);
|
||||
const empty = '□'.repeat(max - rounded);
|
||||
return `${filled}${empty}`; // e.g. ■■■■■□□
|
||||
return `${filled}${empty}`;
|
||||
}
|
||||
|
||||
function EducationalProgramsPage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [socCode, setsocCode] = useState(location.state?.socCode || '');
|
||||
const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []);
|
||||
|
||||
@ -87,24 +95,28 @@ function EducationalProgramsPage() {
|
||||
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) {
|
||||
const combinedQuery = `${careerTitle} ${ksaName}`.trim();
|
||||
const encoded = encodeURIComponent(combinedQuery);
|
||||
|
||||
const courseraUrl = `https://www.coursera.org/search?query=${encoded}`;
|
||||
|
||||
|
||||
const edxUrl = `https://www.edx.org/search?q=${encoded}`;
|
||||
|
||||
|
||||
const classCentralUrl = `https://www.classcentral.com/search?q=${encoded}`;
|
||||
|
||||
return [
|
||||
{ title: 'Coursera', url: courseraUrl },
|
||||
{ title: 'edX', url: edxUrl },
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Load KSA data once
|
||||
useEffect(() => {
|
||||
@ -138,22 +150,14 @@ function EducationalProgramsPage() {
|
||||
setKsaForCareer([]);
|
||||
return;
|
||||
}
|
||||
// 1) filter by socCode
|
||||
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
|
||||
// 2) skip suppress=Y
|
||||
filtered = filtered.filter((r) => r.recommendSuppress !== 'Y');
|
||||
// 3) keep scaleIDs in [IM,LV]
|
||||
filtered = filtered.filter((r) => ['IM', 'LV'].includes(r.scaleID));
|
||||
|
||||
// combine IM + LV
|
||||
let combined = combineIMandLV(filtered);
|
||||
|
||||
// only keep items with importanceValue >=3
|
||||
combined = combined.filter((item) => {
|
||||
return item.importanceValue !== null && item.importanceValue >= 3;
|
||||
});
|
||||
|
||||
// sort by importanceValue desc
|
||||
combined.sort((a, b) => (b.importanceValue || 0) - (a.importanceValue || 0));
|
||||
|
||||
setKsaForCareer(combined);
|
||||
@ -195,7 +199,8 @@ function EducationalProgramsPage() {
|
||||
try {
|
||||
const fetchedSchools = await fetchSchools(cipCodes);
|
||||
|
||||
let userLat = null, userLng = null;
|
||||
let userLat = null;
|
||||
let userLng = null;
|
||||
if (userZip) {
|
||||
try {
|
||||
const geoResult = await clientGeocodeZip(userZip);
|
||||
@ -230,6 +235,7 @@ function EducationalProgramsPage() {
|
||||
|
||||
// Sort schools in useMemo
|
||||
const filteredAndSortedSchools = useMemo(() => {
|
||||
|
||||
if (!schools) return [];
|
||||
let result = [...schools];
|
||||
|
||||
@ -266,12 +272,8 @@ function EducationalProgramsPage() {
|
||||
} else {
|
||||
// Sort by in-state tuition
|
||||
result.sort((a, b) => {
|
||||
const tA = a['In_state cost']
|
||||
? parseFloat(a['In_state cost'])
|
||||
: Infinity;
|
||||
const tB = b['In_state cost']
|
||||
? parseFloat(b['In_state cost'])
|
||||
: Infinity;
|
||||
const tA = a['In_state cost'] ? parseFloat(a['In_state cost']) : Infinity;
|
||||
const tB = b['In_state cost'] ? parseFloat(b['In_state cost']) : Infinity;
|
||||
return tA - tB;
|
||||
});
|
||||
}
|
||||
@ -279,27 +281,33 @@ function EducationalProgramsPage() {
|
||||
return result;
|
||||
}, [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) {
|
||||
// k is the object => { elementName, importanceValue, levelValue, ksa_type }
|
||||
const elementName = k.elementName;
|
||||
const impStars = renderImportance(k.importanceValue);
|
||||
const lvlBars = k.levelValue !== null ? renderLevel(k.levelValue) : 'n/a';
|
||||
const links = getSearchLinks(elementName, careerTitle);
|
||||
const definition = ONET_DEFINITIONS[elementName] || "No definition available";
|
||||
const isAbility = k.ksa_type === 'Ability';
|
||||
const links = !isAbility ? getSearchLinks(elementName, careerTitle) : null;
|
||||
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}{' '}
|
||||
<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">
|
||||
<td className="p-2 font-medium text-gray-800">
|
||||
{elementName}{' '}
|
||||
<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>
|
||||
</td>
|
||||
<td className="p-2 text-center text-gray-800">{impStars}</td>
|
||||
<td className="p-2 text-center text-gray-800">{lvlBars}</td>
|
||||
{!isAbility && (
|
||||
<td className="p-2">
|
||||
{links.map((link, i) => (
|
||||
{links?.map((link, i) => (
|
||||
<div key={i}>
|
||||
<a
|
||||
href={link.url}
|
||||
@ -312,21 +320,20 @@ function EducationalProgramsPage() {
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Knowledge / Skills / Abilities in 3 columns, each a small table
|
||||
// Knowledge / Skills / Abilities in 3 columns
|
||||
function renderKsaSection() {
|
||||
if (loadingKsa) return <p>Loading KSA data...</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 (!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 skillRows = ksaForCareer.filter((k) => k.ksa_type === 'Skill');
|
||||
const abilities = ksaForCareer.filter((k) => k.ksa_type === 'Ability');
|
||||
@ -334,10 +341,10 @@ function EducationalProgramsPage() {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<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>
|
||||
|
||||
{/* 3-column layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Knowledge */}
|
||||
<div>
|
||||
@ -393,6 +400,19 @@ function EducationalProgramsPage() {
|
||||
<th className="p-2">Ability</th>
|
||||
<th className="p-2 text-center">Importance</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -406,7 +426,7 @@ function EducationalProgramsPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} // <-- MAKE SURE WE DO NOT HAVE EXTRA BRACKETS HERE
|
||||
|
||||
// If no CIP codes => fallback
|
||||
if (!cipCodes.length) {
|
||||
@ -438,7 +458,7 @@ function EducationalProgramsPage() {
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{/* KSA Section (3 columns, each a small table) */}
|
||||
{/* KSA Section */}
|
||||
{renderKsaSection()}
|
||||
|
||||
{/* School List */}
|
||||
@ -492,39 +512,42 @@ function EducationalProgramsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredAndSortedSchools.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{filteredAndSortedSchools.map((school, idx) => (
|
||||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
|
||||
{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">
|
||||
<strong>{school['INSTNM'] || 'Unnamed School'}</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:{' '}
|
||||
<strong>
|
||||
{school['Website'] ? (
|
||||
<a
|
||||
href={school['Website']}
|
||||
href={displayWebsite}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{school['Website']}
|
||||
{school['INSTNM'] || 'Unnamed School'}
|
||||
</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>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No schools matching your filters.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user