'use client'; import { ArrowDownAZ, ArrowDownWideNarrow, SlidersHorizontal, X } from 'lucide-react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useMemo, useState } from 'react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import { compact, formatPct, grouped, isNum } from '@/lib/format'; import { INCOME_GROUPS, WB_REGIONS } from '@/lib/regions'; import { regionShort } from '@/lib/regions'; import { routes } from '@/lib/site'; import type { CountrySummary } from '@/lib/types'; import { BottomSheet } from '@/components/data/bottom-sheet'; type SortKey = 'name' | 'population' | 'gdp' | 'coverage'; /** * Country directory: the 218 rows are server-rendered (this client component SSRs with the full list) and * filtered/sorted client-side. Region + income chips (bottom sheet on mobile), sort control, A–Z grouping * with a sticky letter index on desktop. URL `?region=&income=&q=&sort=` is kept in sync (shallow). */ export function CountryDirectory({ items }: { items: CountrySummary[] }) { const params = useSearchParams(); const router = useRouter(); const [q, setQ] = useState(params.get('q') ?? ''); const [region, setRegion] = useState(params.get('region')); const [income, setIncome] = useState(params.get('income')); const [sort, setSort] = useState((params.get('sort') as SortKey) || 'name'); const [sheet, setSheet] = useState(false); const sync = useCallback( (next: { q?: string; region?: string | null; income?: string | null; sort?: SortKey }) => { const p = new URLSearchParams(); const nq = next.q ?? q; const nr = next.region === undefined ? region : next.region; const ni = next.income === undefined ? income : next.income; const ns = next.sort ?? sort; if (nq) p.set('q', nq); if (nr) p.set('region', nr); if (ni) p.set('income', ni); if (ns !== 'name') p.set('sort', ns); const s = p.toString(); router.replace(`${routes.countries()}${s ? `?${s}` : ''}`, { scroll: false }); }, [q, region, income, sort, router], ); const filtered = useMemo(() => { const ql = q.trim().toLowerCase(); let rows = items.filter((c) => (!region || (c.region ?? '').toUpperCase() === region.toUpperCase()) && (!income || (c.income ?? '').toUpperCase() === income.toUpperCase())); if (ql) rows = rows.filter((c) => (c.name ?? '').toLowerCase().includes(ql) || c.id.toLowerCase() === ql || (c.capital ?? '').toLowerCase().includes(ql)); const num = (v: number | null) => (isNum(v) ? v : -Infinity); rows.sort((a, b) => { if (sort === 'population') return num(b.population_latest) - num(a.population_latest) || (a.name ?? '').localeCompare(b.name ?? ''); if (sort === 'gdp') return num(b.gdp_per_capita_latest) - num(a.gdp_per_capita_latest) || (a.name ?? '').localeCompare(b.name ?? ''); if (sort === 'coverage') return num(b.coverage_pct) - num(a.coverage_pct) || (a.name ?? '').localeCompare(b.name ?? ''); return (a.name ?? '').localeCompare(b.name ?? ''); }); return rows; }, [items, q, region, income, sort]); const groupsAZ = useMemo(() => { if (sort !== 'name') return null; const m = new Map(); for (const c of filtered) { const letter = (c.name ?? '#').charAt(0).toUpperCase(); const key = /[A-Z]/.test(letter) ? letter : '#'; if (!m.has(key)) m.set(key, []); m.get(key)!.push(c); } return Array.from(m.entries()); }, [filtered, sort]); const activeCount = (region ? 1 : 0) + (income ? 1 : 0); const letters = groupsAZ?.map(([l]) => l) ?? []; const filters = (
{ setRegion(v); sync({ region: v }); }} options={WB_REGIONS.map((r) => ({ id: r.id, label: r.short }))} /> { setIncome(v); sync({ income: v }); }} options={INCOME_GROUPS.map((g) => ({ id: g.id, label: g.name }))} />
); return (
{/* Controls row */}
{ setQ(e.target.value); sync({ q: e.target.value }); }} placeholder={t('countries.search')} aria-label={t('countries.search')} className="h-10 w-full rounded-sm border border-rule bg-surface px-3 text-sm outline-none placeholder:text-ink-3 focus:border-accent" /> {q ? ( ) : null}
{t('countries.count', { n: grouped(filtered.length), total: grouped(items.length) })}
{filters}
{(region || income) && (
{region ? r.id === region)?.short ?? region} onClear={() => { setRegion(null); sync({ region: null }); }} /> : null} {income ? g.id === income)?.name ?? income} onClear={() => { setIncome(null); sync({ income: null }); }} /> : null}
)} {/* Header row (sm+) */}
{t('common.country')} {t('common.region')} {t('common.population')} {t('common.gdpPerCapita')} {t('common.coverage')}
{filtered.length === 0 ?

{t('countries.noMatch')}

: null} {groupsAZ ? ( groupsAZ.map(([letter, rows]) => (

{letter}

    {rows.map((c) => ( ))}
)) ) : (
    {filtered.map((c, i) => ( ))}
)}
{/* Sticky letter index (desktop) */} {letters.length > 1 ? ( ) : null} setSheet(false)} side="center" title={t('countries.filters')}> {filters}
); } function Row({ c, rank }: { c: CountrySummary; rank?: number }) { return (
  • {rank ? {rank} : null} {c.flag} {c.name} {c.kind === 'territory' ? {t('countries.territory')} : null} {regionShort(c.region) ?? c.region_name ?? ''} {c.region_name} {isNum(c.population_latest) ? compact(c.population_latest) : t('common.na')} {isNum(c.gdp_per_capita_latest) ? `US$${compact(c.gdp_per_capita_latest)} /cap` : ''} {isNum(c.gdp_per_capita_latest) ? `US$${compact(c.gdp_per_capita_latest)}` : t('common.na')} {formatPct(c.coverage_pct)}
  • ); } function ChipGroup({ label, value, onChange, options }: { label: string; value: string | null; onChange: (v: string | null) => void; options: Array<{ id: string; label: string }> }) { return (
    {label}
    • onChange(null)}> {t('common.all')}
    • {options.map((o) => (
    • onChange(value?.toUpperCase() === o.id.toUpperCase() ? null : o.id)}> {o.label}
    • ))}
    ); } function Chip({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); } function ActiveChip({ label, onClear }: { label: string; onClear: () => void }) { return ( {label} ); }