Add BMAD framework, authentication, and new features
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
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 { Tag } from 'lucide-react'
|
||||
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'
|
||||
@@ -20,10 +22,15 @@ export function LabelSelector({
|
||||
selectedLabels,
|
||||
onLabelsChange,
|
||||
variant = 'default',
|
||||
triggerLabel = 'Tags',
|
||||
triggerLabel = 'Labels',
|
||||
align = 'start',
|
||||
}: LabelSelectorProps) {
|
||||
const { labels, loading } = useLabels()
|
||||
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)) {
|
||||
@@ -33,39 +40,90 @@ export function LabelSelector({
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<Tag className="h-4 w-4 mr-2" />
|
||||
<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">
|
||||
<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">
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<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-4 text-sm text-gray-500">Loading...</div>
|
||||
) : labels.length === 0 ? (
|
||||
<div className="p-4 text-sm text-gray-500">No labels yet</div>
|
||||
<div className="p-2 text-sm text-gray-500 text-center">Loading...</div>
|
||||
) : (
|
||||
labels.map((label) => {
|
||||
const isSelected = selectedLabels.includes(label.name)
|
||||
<>
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={label.id}
|
||||
onClick={() => handleToggleLabel(label.name)}
|
||||
className="flex items-center justify-between gap-2"
|
||||
{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"
|
||||
>
|
||||
<LabelBadge label={label.name} isSelected={isSelected} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user