'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import { Loader2, AlertCircle, ChevronDown, Check, ArrowRightLeft } from 'lucide-react'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import type { Language } from './types'; interface LanguageSelectorProps { sourceLang: string; targetLang: string; languages: Language[]; isLoading?: boolean; error?: string | null; onSourceChange: (value: string) => void; onTargetChange: (value: string) => void; } /* ── Combobox dropdown with search ──────────────────────────────── */ function Combobox({ value, options, includeAuto, autoLabel, placeholder, onChange, ariaLabel, }: { value: string; options: Language[]; includeAuto: boolean; autoLabel: string; placeholder: string; onChange: (code: string) => void; ariaLabel: string; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const ref = useRef(null); const inputRef = useRef(null); const triggerRef = useRef(null); useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; if (open) document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const closeAndRefocus = () => { setOpen(false); setQuery(''); triggerRef.current?.focus(); }; const allOptions = includeAuto ? [{ code: 'auto', name: autoLabel }, ...options] : options; const q = query.toLowerCase().trim(); const filtered = q ? allOptions.filter(l => l.name.toLowerCase().includes(q) || l.code.toLowerCase().includes(q)) : allOptions; const label = value === 'auto' ? autoLabel : allOptions.find(l => l.code === value)?.name ?? value; const handleListboxKeys = (e: React.KeyboardEvent) => { if (e.key === 'Escape' && open) { e.stopPropagation(); closeAndRefocus(); return; } if (!open || (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')) return; e.preventDefault(); const options = Array.from( ref.current?.querySelectorAll('[role="option"]') ?? [] ); if (options.length === 0) return; const idx = options.indexOf(document.activeElement as HTMLButtonElement); const next = e.key === 'ArrowDown' ? options[Math.min(options.length - 1, idx + 1)] : options[Math.max(0, idx - 1)]; // from the search input, ArrowDown goes to the first option (idx === -1 && document.activeElement === inputRef.current ? options[e.key === 'ArrowDown' ? 0 : options.length - 1] : next ).focus(); }; return (
{open && (
setQuery(e.target.value)} placeholder={t('langSelector.search')} aria-label={t('langSelector.search')} className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white" />
{filtered.length === 0 && (
{t('langSelector.noResults')}
)} {filtered.map(lang => ( ))}
)}
); } /* ── Main component ─────────────────────────────────────────────── */ export default function LanguageSelector({ sourceLang, targetLang, languages, isLoading, error, onSourceChange, onTargetChange, }: LanguageSelectorProps) { const { t } = useI18n(); if (error) { return (
{t('dashboard.translate.language.loadErrorPrefix')} {error}
); } if (isLoading) { return (
{t('dashboard.translate.language.loading')}
); } const canSwap = sourceLang !== 'auto'; return (
{/* Source */}
{t('langSelector.source')}
{/* Swap Button */}
{/* Target */}
{t('langSelector.target')}
); }