'use client'; import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Search, X, Plus } from 'lucide-react'; interface Hit { type: string; id: string; title: string; subtitle?: string | null; href: string; match: string; } /** * Compare picker: adds/removes cancers by slug; the selection lives in the URL (?ids=a,b,c) so a comparison * is shareable. Uses the existing /api/search route (database-backed) and keeps only cancer hits. */ export function ComparePicker({ selected, max, min }: { selected: Array<{ slug: string; name: string }>; max: number; min: number }) { const router = useRouter(); const [q, setQ] = useState(''); const [hits, setHits] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const box = useRef(null); useEffect(() => { const term = q.trim(); if (term.length < 2) { setHits([]); return; } const ctrl = new AbortController(); const t = setTimeout(async () => { setLoading(true); try { const r = await fetch(`/api/search?q=${encodeURIComponent(term)}&limit=20`, { signal: ctrl.signal }); const j = (await r.json()) as { data: Hit[] }; const chosen = new Set(selected.map((s) => s.slug)); setHits((j.data ?? []).filter((h) => h.type === 'cancer' && !chosen.has(h.href.replace(/^\/cancer\//, ''))).slice(0, 8)); setOpen(true); } catch { /* aborted */ } finally { setLoading(false); } }, 150); return () => { clearTimeout(t); ctrl.abort(); }; }, [q, selected]); useEffect(() => { const onDoc = (e: MouseEvent) => { if (box.current && !box.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, []); const navigate = (slugs: string[]) => { router.push(slugs.length ? `/compare?ids=${slugs.map(encodeURIComponent).join(',')}` : '/compare'); }; const add = (href: string) => { const slug = href.replace(/^\/cancer\//, ''); if (!slug || selected.some((s) => s.slug === slug) || selected.length >= max) return; setQ(''); setHits([]); setOpen(false); navigate([...selected.map((s) => s.slug), slug]); }; const remove = (slug: string) => navigate(selected.filter((s) => s.slug !== slug).map((s) => s.slug)); const full = selected.length >= max; return (
{selected.map((s) => ( {s.name} ))} {selected.length === 0 ? No cancer selected yet. : null}
{full ? : } setQ(e.target.value)} onFocus={() => hits.length && setOpen(true)} onKeyDown={(e) => { if (e.key === 'Enter' && hits[0]) { e.preventDefault(); add(hits[0].href); } else if (e.key === 'Escape') setOpen(false); }} disabled={full} placeholder={full ? `Maximum ${max} cancers — remove one to add another` : `Add a cancer (${selected.length}/${max}) — e.g. glioblastoma, pancreatic, KRAS…`} className="w-full bg-transparent py-2 text-[14px] outline-none placeholder:text-ink-4 disabled:cursor-not-allowed" autoComplete="off" spellCheck={false} role="combobox" aria-expanded={open && hits.length > 0} aria-controls="compare-picker-results" />
{open && q.trim().length >= 2 ? (
    {hits.map((h) => (
  • ))} {!loading && hits.length === 0 ?
  • No matching cancer entity.
  • : null} {loading ?
  • Searching…
  • : null}
) : null}

Compare {min} to {max} cancers. The selection is in the URL — copy it to share this comparison.

); }