131 lines
3.8 KiB
JavaScript
131 lines
3.8 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { Check } from 'lucide-react'; // any icon lib you use
|
|
import { Button } from './ui/button.js';
|
|
|
|
/* ---------- helpers ---------- */
|
|
const normalize = (s = '') =>
|
|
s
|
|
.toLowerCase()
|
|
.replace(/\s*&\s*/g, ' and ')
|
|
.replace(/[–—]/g, '-') // long dash → hyphen
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
|
|
/* ---------- component ---------- */
|
|
const CareerSearch = ({ onCareerSelected, required, disabled: externallyDisabled = false }) => {
|
|
const [careerObjects, setCareerObjects] = useState([]);
|
|
const [searchInput, setSearchInput] = useState('');
|
|
const [selectedObj, setSelectedObj] = useState(null); // ✓ state
|
|
const computedDisabled = externallyDisabled || !!selectedObj;
|
|
|
|
/* fetch & de-dupe once */
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const raw = await fetch('/careers_with_ratings.json').then(r => r.json());
|
|
const map = new Map();
|
|
for (const c of raw) {
|
|
if (c.title && c.soc_code && c.cip_codes) {
|
|
const key = normalize(c.title);
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
title: c.title,
|
|
soc_code: c.soc_code,
|
|
cip_code: c.cip_codes,
|
|
limited_data: c.limited_data,
|
|
ratings: c.ratings
|
|
});
|
|
}
|
|
}
|
|
}
|
|
setCareerObjects([...map.values()]);
|
|
} catch (err) {
|
|
console.error('Career list load failed:', err);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
/* whenever input changes, auto-commit if it matches */
|
|
useEffect(() => {
|
|
const match = careerObjects.find(
|
|
(o) => normalize(o.title) === normalize(searchInput)
|
|
);
|
|
if (match && match !== selectedObj) {
|
|
setSelectedObj(match);
|
|
onCareerSelected(match); // notify parent immediately
|
|
}
|
|
}, [searchInput, careerObjects, selectedObj, onCareerSelected]);
|
|
|
|
/* allow “Enter” to commit first suggestion */
|
|
const handleKeyDown = (e) => {
|
|
if (computedDisabled) return;
|
|
if (e.key === 'Enter') {
|
|
const first = careerObjects.find(o =>
|
|
normalize(o.title).startsWith(normalize(searchInput))
|
|
);
|
|
if (first) {
|
|
setSearchInput(first.title); // triggers auto-commit
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
};
|
|
|
|
/* clear & edit again */
|
|
const reset = () => {
|
|
setSelectedObj(null);
|
|
setSearchInput('');
|
|
};
|
|
|
|
return (
|
|
<div className="mb-4">
|
|
<label className="block font-medium mb-1">
|
|
Search for Career <span className="text-red-600">*</span>
|
|
</label>
|
|
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
list="career-titles"
|
|
value={searchInput}
|
|
required={required}
|
|
disabled={computedDisabled} // lock when chosen
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className={`w-full border rounded p-2
|
|
${computedDisabled ? 'bg-gray-100 cursor-not-allowed opacity-60' : ''}`}
|
|
placeholder="Start typing a career..."
|
|
/>
|
|
|
|
{!computedDisabled && (
|
|
<datalist id="career-titles">
|
|
{careerObjects.map((o) => (
|
|
<option key={o.soc_code} value={o.title} />
|
|
))}
|
|
</datalist>
|
|
)}
|
|
</div>
|
|
|
|
{!selectedObj && (
|
|
<datalist id="career-titles">
|
|
{careerObjects.map((o) => (
|
|
<option key={o.soc_code} value={o.title} />
|
|
))}
|
|
</datalist>
|
|
)}
|
|
|
|
{/* change / clear link */}
|
|
{selectedObj && !externallyDisabled && (
|
|
<button
|
|
type="button"
|
|
onClick={reset}
|
|
className="text-blue-600 underline text-sm mt-1"
|
|
>
|
|
Change
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CareerSearch;
|