'use client'; import { Plus, Search, X } from 'lucide-react'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { t } from '@/i18n'; import { clientCompare } from '@/lib/client-api-compare'; import { cn } from '@/lib/cn'; import { MAX_CUSTOM_INDICATORS, compareQuery, type CompareState } from '@/lib/compare-state'; import { TOPICS, topicById } from '@/lib/topics'; import type { IndicatorCard, IndicatorSummary } from '@/lib/types'; import { BottomSheet } from '@/components/data/bottom-sheet'; /** * Custom tab header: selected indicator chips (remove) + a picker sheet searching `/api/v1/indicators` * (fetched once on first open). Selection is written to `?indicators=` in the URL; charts are server-rendered. */ export function CustomPanel({ selected, state }: { selected: IndicatorCard[]; state: CompareState }) { const router = useRouter(); const pathname = usePathname(); const [open, setOpen] = useState(false); const [q, setQ] = useState(''); const [all, setAll] = useState(null); const [failed, setFailed] = useState(false); const slugs = state.indicators; const full = slugs.length >= MAX_CUSTOM_INDICATORS; useEffect(() => { if (!open || all || failed) return; const ctrl = new AbortController(); clientCompare .indicators(ctrl.signal) .then((r) => setAll(r.items.filter((i) => (i.n_countries ?? 0) > 0))) .catch((e) => { if ((e as Error).name !== 'AbortError') setFailed(true); }); return () => ctrl.abort(); }, [open, all, failed]); const apply = (next: string[]) => router.replace(`${pathname}${compareQuery({ ...state, tab: 'custom', indicators: next, indicator: state.indicator && next.includes(state.indicator) ? state.indicator : null })}`, { scroll: false }); const results = useMemo(() => { if (!all) return []; const ql = q.trim().toLowerCase(); const pool = all.filter((i) => !slugs.includes(i.slug)); const list = ql ? pool.filter((i) => (i.name ?? '').toLowerCase().includes(ql) || (i.short_name ?? '').toLowerCase().includes(ql) || i.slug.includes(ql) || (i.topic ?? '').includes(ql)) : pool; const order = new Map(TOPICS.map((tp, i) => [tp.id as string, i])); return [...list].sort((a, b) => (order.get(a.topic ?? '') ?? 99) - (order.get(b.topic ?? '') ?? 99) || Number(!!b.featured) - Number(!!a.featured) || (a.name ?? '').localeCompare(b.name ?? '')).slice(0, 80); }, [all, q, slugs]); return (
{selected.map((ind) => ( {ind.short_name ?? ind.name ?? ind.slug} ))} {t('compare.custom.selected', { n: slugs.length, max: MAX_CUSTOM_INDICATORS })}
{selected.length === 0 ?

{t('compare.custom.empty', { max: MAX_CUSTOM_INDICATORS })}

: null} setOpen(false)} side="center" title={t('compare.custom.pick')}>
setQ(e.target.value)} placeholder={t('compare.custom.search')} aria-label={t('compare.custom.search')} className="h-11 w-full rounded-sm border border-rule bg-surface pl-9 pr-3 text-sm text-ink outline-none placeholder:text-ink-3 focus:border-accent" />

{full ? t('compare.custom.full', { max: MAX_CUSTOM_INDICATORS }) : t('compare.custom.selected', { n: slugs.length, max: MAX_CUSTOM_INDICATORS })}

{failed ? (

{t('common.errorHint')}

) : !all ? (

{t('common.loading')}

) : results.length === 0 ? (

{t('compare.custom.noMatch', { q })}

) : (
    {results.map((i) => (
  • ))}
)}
); }