'use client'; import { useEffect, useRef, useState } from 'react'; import { Search, X } from 'lucide-react'; import { inputClass } from './form'; import { fmtMoney } from '@/lib/format'; export interface PickedAsset { id: string; title: string; categorySlug: string; year: number | null; heroImageUrl: string | null; rivUsd: number | null; } interface Variant { id: string; label: string; rivUsd: number | null; } /** * Typeahead over RareIndex assets (server search) + variant picker. Emits hidden inputs * `assetId` and `variantId` for the surrounding form. */ export function AssetPicker({ name = 'assetId', initial, error, withVariant = true, label = 'Asset' }: { name?: string; initial?: PickedAsset | null; error?: string | null; withVariant?: boolean; label?: string }) { const [q, setQ] = useState(''); const [hits, setHits] = useState([]); const [open, setOpen] = useState(false); const [picked, setPicked] = useState(initial ?? null); const [variants, setVariants] = useState([]); const [loading, setLoading] = useState(false); const box = useRef(null); useEffect(() => { if (q.trim().length < 2) return; const ctrl = new AbortController(); const t = setTimeout(async () => { setLoading(true); try { const res = await fetch(`/api/account/assets/search?q=${encodeURIComponent(q.trim())}`, { signal: ctrl.signal }); if (res.ok) setHits((await res.json()) as PickedAsset[]); } catch { /* aborted */ } finally { setLoading(false); } }, 180); return () => { clearTimeout(t); ctrl.abort(); }; }, [q]); useEffect(() => { if (!picked || !withVariant) return; fetch(`/api/account/assets/${picked.id}/variants`) .then((r) => (r.ok ? r.json() : [])) .then((v: Variant[]) => setVariants(v)) .catch(() => setVariants([])); }, [picked, withVariant]); 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); }, []); return (
{picked ? (
{picked.heroImageUrl ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( )}

{picked.title}

{picked.categorySlug.replace(/_/g, ' ')} {picked.year ? ` · ${picked.year}` : ''} {picked.rivUsd ? ` · RIV ${fmtMoney(picked.rivUsd)}` : ' · no valuation yet'}

) : (
{ setQ(e.target.value); setOpen(true); if (e.target.value.trim().length < 2) setHits([]); }} onFocus={() => setOpen(true)} placeholder="Search RareIndex assets… e.g. 1999 Charizard, Rolex 116500LN, LEGO 75192" className={`${inputClass} pl-8`} autoComplete="off" aria-autocomplete="list" /> {open && (hits.length > 0 || loading || q.trim().length >= 2) ? (
    {hits.map((h) => (
  • ))} {!loading && hits.length === 0 && q.trim().length >= 2 ?
  • No asset matches yet. RareIndex adds assets as connectors ingest them; try a different spelling or set name.
  • : null} {loading ?
  • Searching…
  • : null}
) : null}
)} {error ?

{error}

: null} {withVariant && picked ? (
) : null}
); }