| import { useState, useRef, useEffect } from "react"; |
| import { ChevronDown } from "lucide-react"; |
| import { |
| LANGUAGES, |
| type LanguageCode, |
| LANGUAGES_WITH_AUTO, |
| } from "../constants"; |
|
|
| interface LanguageSelectorProps { |
| value: LanguageCode; |
| onChange: (language: LanguageCode) => void; |
| includeAuto?: boolean; |
| } |
|
|
| export default function LanguageSelector({ |
| value, |
| onChange, |
| includeAuto = false, |
| }: LanguageSelectorProps) { |
| const [isOpen, setIsOpen] = useState(false); |
| const [search, setSearch] = useState(""); |
| const containerRef = useRef<HTMLDivElement>(null); |
| const searchInputRef = useRef<HTMLInputElement>(null); |
|
|
| const languages = includeAuto ? LANGUAGES_WITH_AUTO : LANGUAGES; |
|
|
| const getLanguageDisplay = (code: LanguageCode) => { |
| return languages.find((lang) => lang.code === code)?.name || code; |
| }; |
|
|
| const filterLanguages = (searchTerm: string) => { |
| const searchLower = searchTerm.toLowerCase(); |
| return languages.filter( |
| (lang) => |
| lang.name.toLowerCase().includes(searchLower) || |
| lang.code.toLowerCase().includes(searchLower) |
| ); |
| }; |
|
|
| const filteredLanguages = search ? filterLanguages(search) : languages; |
|
|
| const handleSelect = (language: LanguageCode) => { |
| onChange(language); |
| setIsOpen(false); |
| setSearch(""); |
| }; |
|
|
| useEffect(() => { |
| const handleClickOutside = (event: MouseEvent) => { |
| if ( |
| containerRef.current && |
| !containerRef.current.contains(event.target as Node) |
| ) { |
| setIsOpen(false); |
| setSearch(""); |
| } |
| }; |
|
|
| if (isOpen) { |
| document.addEventListener("mousedown", handleClickOutside); |
| setTimeout(() => searchInputRef.current?.focus(), 0); |
| } |
|
|
| return () => { |
| document.removeEventListener("mousedown", handleClickOutside); |
| }; |
| }, [isOpen]); |
|
|
| return ( |
| <div className="relative" ref={containerRef}> |
| <button |
| onClick={() => setIsOpen(!isOpen)} |
| className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-primary hover:bg-muted rounded-md transition-colors" |
| > |
| <span>{getLanguageDisplay(value)}</span> |
| <ChevronDown |
| className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`} |
| /> |
| </button> |
| |
| {isOpen && ( |
| <div className="absolute top-full left-0 mt-2 w-64 bg-white border border-border rounded-md shadow-lg z-50"> |
| <div className="p-2 border-b border-border"> |
| <input |
| ref={searchInputRef} |
| type="text" |
| placeholder="Search languages..." |
| value={search} |
| onChange={(e) => setSearch(e.target.value)} |
| className="w-full px-3 py-2 text-sm border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" |
| /> |
| </div> |
| |
| <div className="max-h-60 overflow-auto"> |
| {filteredLanguages.map((lang) => ( |
| <button |
| key={lang.code} |
| onClick={() => handleSelect(lang.code)} |
| className={`w-full px-3 py-2 text-left text-sm hover:bg-muted transition-colors ${ |
| value === lang.code ? "bg-primary-50 text-primary-800" : "" |
| }`} |
| > |
| {lang.name} |
| </button> |
| ))} |
| {filteredLanguages.length === 0 && ( |
| <div className="px-3 py-4 text-sm text-muted-foreground text-center"> |
| No languages found |
| </div> |
| )} |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|