UI Skills fixes and Schools fixes

This commit is contained in:
Josh 2025-05-21 14:59:26 +00:00
parent 93ae23214a
commit 7f73977cce

View File

@ -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 (15) 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 (07) to bar or block representation
function renderLevel(val) {
// 7 is max, well 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(
'Youre 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 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 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 },
];
}
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,54 +281,59 @@ 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";
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">
i
const elementName = k.elementName;
const impStars = renderImportance(k.importanceValue);
const lvlBars = k.levelValue !== null ? renderLevel(k.levelValue) : 'n/a';
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"
>
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>
<td className="p-2">
{links.map((link, i) => (
<div key={i}>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
>
{link.title}
</a>
</div>
))}
</td>
</tr>
);
}
</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) => (
<div key={i}>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
>
{link.title}
</a>
</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,40 +512,43 @@ 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>
</div>
);
}