"use client"; // File: occupation-search.tsx // Path: apps/web/components/occupation-search.tsx // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Debounced live occupation search box (client component). import Link from "next/link"; import { useEffect, useRef, useState } from "react"; interface Item { code: string; title: string; } /** Debounced live search over /api/v1/occupations. */ export function OccupationSearch(): JSX.Element { const [query, setQuery] = useState(""); const [items, setItems] = useState([]); const [open, setOpen] = useState(false); const boxRef = useRef(null); useEffect(() => { if (query.trim().length < 2) { setItems([]); return; } const controller = new AbortController(); const timer = setTimeout(async () => { try { const response = await fetch( `/api/v1/occupations?q=${encodeURIComponent(query.trim())}&per_page=8`, { signal: controller.signal }, ); if (response.ok) { const data = (await response.json()) as { items: Item[] }; setItems(data.items); setOpen(true); } } catch { /* aborted or offline — keep prior results */ } }, 200); return () => { controller.abort(); clearTimeout(timer); }; }, [query]); useEffect(() => { const onClick = (event: MouseEvent) => { if (!boxRef.current?.contains(event.target as Node)) setOpen(false); }; document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, []); return (
setQuery(event.target.value)} onFocus={() => items.length > 0 && setOpen(true)} placeholder="Search 1,000+ occupations — e.g. paralegal, radiologist, roofer…" aria-label="Search occupations" className="w-full card px-4 py-3 text-base outline-none placeholder:text-[var(--muted)] focus:border-[var(--seq)]" /> {open && items.length > 0 && (
    {items.map((item) => (
  • setOpen(false)} > {item.title} {item.code}
  • ))}
)}
); }