'use client' import { useState, useMemo } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { trpc } from '@/lib/trpc/client' import { Check, X, Lock, Search, Loader2, ChevronDown, ChevronRight } from 'lucide-react' interface ExpertiseSelectProps { value: string[] onChange: (tags: string[]) => void maxTags?: number disabled?: boolean className?: string lockedTags?: string[] // Tags set by admin that cannot be removed } export function ExpertiseSelect({ value, onChange, maxTags = 10, disabled = false, className, lockedTags = [], }: ExpertiseSelectProps) { const [searchQuery, setSearchQuery] = useState('') const [expandedCategories, setExpandedCategories] = useState>(new Set()) // Fetch tags from database const { data, isLoading } = trpc.tag.list.useQuery({ isActive: true }) const tags = data?.tags || [] // Group tags by category const tagsByCategory = useMemo(() => { const grouped: Record = {} for (const tag of tags) { const category = tag.category || 'Other' if (!grouped[category]) { grouped[category] = [] } grouped[category].push(tag) } return grouped }, [tags]) // Filter tags by search query const filteredTagsByCategory = useMemo(() => { if (!searchQuery.trim()) return tagsByCategory const query = searchQuery.toLowerCase() const filtered: Record = {} for (const [category, categoryTags] of Object.entries(tagsByCategory)) { const matchingTags = categoryTags.filter( (tag) => tag.name.toLowerCase().includes(query) || tag.description?.toLowerCase().includes(query) || category.toLowerCase().includes(query) ) if (matchingTags.length > 0) { filtered[category] = matchingTags } } return filtered }, [tagsByCategory, searchQuery]) // Check if a tag is locked const isTagLocked = (tagName: string) => lockedTags.includes(tagName) const handleToggle = (name: string) => { if (disabled || isTagLocked(name)) return if (value.includes(name)) { onChange(value.filter((t) => t !== name)) } else { // Don't count locked tags against the max const selectableTags = value.filter((t) => !isTagLocked(t)) if (maxTags && selectableTags.length >= maxTags) return onChange([...value, name]) } } const handleRemove = (name: string) => { if (disabled || isTagLocked(name)) return onChange(value.filter((t) => t !== name)) } const toggleCategory = (category: string) => { setExpandedCategories((prev) => { const next = new Set(prev) if (next.has(category)) { next.delete(category) } else { next.add(category) } return next }) } const getTag = (name: string) => tags.find((t) => t.name === name) // Count user-selected tags (not including locked) const userSelectedCount = value.filter((t) => !isTagLocked(t)).length if (isLoading) { return (
Loading expertise areas...
) } if (tags.length === 0) { return (
No expertise areas available. Please contact an administrator.
) } return (
{/* Search input */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Selected tags at the top */} {value.length > 0 && (
{value.map((name) => { const tag = getTag(name) const isLocked = isTagLocked(name) return ( {isLocked && } {name} {!disabled && !isLocked && ( )} ) })}
)} {/* Locked tags notice */} {lockedTags.length > 0 && (

Tags with a lock icon were pre-selected by your administrator

)} {/* Categories with expandable tag lists */}
{Object.entries(filteredTagsByCategory) .sort(([a], [b]) => a.localeCompare(b)) .map(([category, categoryTags]) => { const isExpanded = expandedCategories.has(category) || searchQuery.trim() !== '' const selectedInCategory = categoryTags.filter((t) => value.includes(t.name)).length return (
{isExpanded && (
{categoryTags.map((tag) => { const isSelected = value.includes(tag.name) const isLocked = isTagLocked(tag.name) const isDisabledOption = disabled || (isLocked && isSelected) || (!isSelected && !isLocked && userSelectedCount >= maxTags) return ( ) })}
)}
) })}
{Object.keys(filteredTagsByCategory).length === 0 && searchQuery && (

No expertise areas match your search.

)} {/* Counter */}

{userSelectedCount} of {maxTags} selected {lockedTags.length > 0 && ` (+ ${lockedTags.length} pre-selected)`}

) }