import { useEffect, useMemo, useRef, useState } from 'react' import { Check, Search, X } from 'lucide-react' import { Button } from './Button' import { t, tf } from '../../lib/i18n' export interface CheckboxPickerItem { id: number label: string description?: string } interface CheckboxPickerProps { triggerLabel: string placeholder: string items: CheckboxPickerItem[] selectedItems: CheckboxPickerItem[] loading?: boolean disabled?: boolean searchTerm: string onSearchChange: (value: string) => void onConfirm: (items: CheckboxPickerItem[]) => void onLoadMore?: () => void hasMore?: boolean emptyLabel?: string loadingLabel?: string } function areSameSelection(left: CheckboxPickerItem[], right: CheckboxPickerItem[]): boolean { if (left.length !== right.length) { return false } const leftIds = left.map(item => item.id).sort((a, b) => a - b) const rightIds = right.map(item => item.id).sort((a, b) => a - b) return leftIds.every((id, index) => id === rightIds[index]) } export function CheckboxPicker({ triggerLabel, placeholder, items, selectedItems, loading = false, disabled = false, searchTerm, onSearchChange, onConfirm, onLoadMore, hasMore = false, emptyLabel, loadingLabel, }: CheckboxPickerProps) { const containerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [draftSelection, setDraftSelection] = useState(selectedItems) useEffect(() => { if (isOpen) { return } if (!areSameSelection(draftSelection, selectedItems)) { setDraftSelection(selectedItems) } }, [draftSelection, isOpen, selectedItems]) useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false) } } document.addEventListener('mousedown', handleOutsideClick) return () => document.removeEventListener('mousedown', handleOutsideClick) }, []) const visibleItems = useMemo(() => { const needle = searchTerm.trim().toLowerCase() if (!needle) { return items } return items.filter(item => { const haystack = `${item.label} ${item.description || ''}`.toLowerCase() return haystack.includes(needle) }) }, [items, searchTerm]) const selectedIds = useMemo(() => new Set(draftSelection.map(item => item.id)), [draftSelection]) const allVisibleSelected = visibleItems.length > 0 && visibleItems.every(item => selectedIds.has(item.id)) const triggerSummary = selectedItems.length > 0 ? selectedItems.length === 1 ? selectedItems[0].label : tf('%d selected', selectedItems.length) : placeholder const handleToggle = (item: CheckboxPickerItem) => { setDraftSelection(prev => { const exists = prev.some(entry => entry.id === item.id) if (exists) { return prev.filter(entry => entry.id !== item.id) } return [...prev, item] }) } const handleSelectAll = () => { setDraftSelection(prev => { const next = new Map(prev.map(item => [item.id, item])) if (allVisibleSelected) { visibleItems.forEach(item => next.delete(item.id)) } else { visibleItems.forEach(item => next.set(item.id, item)) } return Array.from(next.values()) }) } const handleConfirm = () => { if (draftSelection.length === 0) { return } onConfirm(draftSelection) setIsOpen(false) } return (
{isOpen && (
onSearchChange(e.target.value)} placeholder={t('Search...')} className="w-full rounded-lg border border-bc-gray-300 bg-white px-3 py-2 pe-9 text-sm outline-none transition-colors focus:border-bc-primary focus:ring-1 focus:ring-bc-primary" />
{loading ? (
{loadingLabel || t('Loading...')}
) : visibleItems.length === 0 ? (
{emptyLabel || t('No results found')}
) : ( visibleItems.map(item => { const checked = selectedIds.has(item.id) return ( ) }) )}
{hasMore && onLoadMore && (
)}
{tf('%d selected', draftSelection.length)}
)}
) }