133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Tag, Plus, Check } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import { useLabels } from '@/context/LabelContext'
|
|
import { LabelBadge } from './label-badge'
|
|
|
|
interface LabelSelectorProps {
|
|
selectedLabels: string[]
|
|
onLabelsChange: (labels: string[]) => void
|
|
variant?: 'default' | 'compact'
|
|
triggerLabel?: string
|
|
align?: 'start' | 'center' | 'end'
|
|
}
|
|
|
|
export function LabelSelector({
|
|
selectedLabels,
|
|
onLabelsChange,
|
|
variant = 'default',
|
|
triggerLabel = 'Labels',
|
|
align = 'start',
|
|
}: LabelSelectorProps) {
|
|
const { labels, loading, addLabel } = useLabels()
|
|
const [search, setSearch] = useState('')
|
|
|
|
const filteredLabels = labels.filter(l =>
|
|
l.name.toLowerCase().includes(search.toLowerCase())
|
|
)
|
|
|
|
const handleToggleLabel = (labelName: string) => {
|
|
if (selectedLabels.includes(labelName)) {
|
|
onLabelsChange(selectedLabels.filter((l) => l !== labelName))
|
|
} else {
|
|
onLabelsChange([...selectedLabels, labelName])
|
|
}
|
|
}
|
|
|
|
const handleCreateLabel = async () => {
|
|
const trimmed = search.trim()
|
|
if (trimmed) {
|
|
await addLabel(trimmed) // Let backend assign random color
|
|
onLabelsChange([...selectedLabels, trimmed])
|
|
setSearch('')
|
|
}
|
|
}
|
|
|
|
const showCreateOption = search.trim() && !labels.some(l => l.name.toLowerCase() === search.trim().toLowerCase())
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 px-2">
|
|
<Tag className={cn("h-4 w-4", triggerLabel && "mr-2")} />
|
|
{triggerLabel}
|
|
{selectedLabels.length > 0 && (
|
|
<Badge variant="secondary" className="ml-2 h-5 min-w-5 px-1.5 bg-gray-200 text-gray-800 dark:bg-zinc-700 dark:text-zinc-300">
|
|
{selectedLabels.length}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align={align} className="w-64 p-0">
|
|
<div className="p-2">
|
|
<Input
|
|
placeholder="Enter label name"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="h-8 text-sm"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
if (showCreateOption) handleCreateLabel()
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-h-64 overflow-y-auto px-1 pb-1">
|
|
{loading ? (
|
|
<div className="p-2 text-sm text-gray-500 text-center">Loading...</div>
|
|
) : (
|
|
<>
|
|
{filteredLabels.map((label) => {
|
|
const isSelected = selectedLabels.includes(label.name)
|
|
return (
|
|
<div
|
|
key={label.id}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
handleToggleLabel(label.name)
|
|
}}
|
|
className="flex items-center gap-2 p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-zinc-800 cursor-pointer text-sm"
|
|
>
|
|
<div className={cn(
|
|
"h-4 w-4 border rounded flex items-center justify-center transition-colors",
|
|
isSelected ? "bg-blue-600 border-blue-600 text-white" : "border-gray-400"
|
|
)}>
|
|
{isSelected && <Check className="h-3 w-3" />}
|
|
</div>
|
|
<LabelBadge label={label.name} variant="clickable" />
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{showCreateOption && (
|
|
<div
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
handleCreateLabel()
|
|
}}
|
|
className="flex items-center gap-2 p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-zinc-800 cursor-pointer text-sm border-t mt-1"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
<span>Create "{search}"</span>
|
|
</div>
|
|
)}
|
|
|
|
{filteredLabels.length === 0 && !showCreateOption && (
|
|
<div className="p-2 text-sm text-gray-500 text-center">No labels found</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|