feat: implement label management with color filtering

This commit is contained in:
2026-01-04 22:47:54 +01:00
parent a154192410
commit dfa88c5b63
20 changed files with 674 additions and 177 deletions

View File

@@ -15,8 +15,8 @@ import {
import { Badge } from './ui/badge'
import { Tag, X, Plus, Palette } from 'lucide-react'
import { LABEL_COLORS, LabelColorName } from '@/lib/types'
import { getLabelColor, setLabelColor, deleteLabelColor, getAllLabelColors } from '@/lib/label-storage'
import { cn } from '@/lib/utils'
import { useLabels, Label } from '@/context/LabelContext'
interface LabelManagerProps {
existingLabels: string[]
@@ -24,30 +24,31 @@ interface LabelManagerProps {
}
export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
const { labels, loading, addLabel, updateLabel, deleteLabel, getLabelColor } = useLabels()
const [open, setOpen] = useState(false)
const [newLabel, setNewLabel] = useState('')
const [selectedLabels, setSelectedLabels] = useState<string[]>(existingLabels)
const [allLabelsInStorage, setAllLabelsInStorage] = useState<string[]>([])
const [editingColor, setEditingColor] = useState<string | null>(null)
// Load all labels from localStorage
// Sync selected labels with existingLabels prop
useEffect(() => {
const allColors = getAllLabelColors()
setAllLabelsInStorage(Object.keys(allColors))
}, [open])
setSelectedLabels(existingLabels)
}, [existingLabels])
const handleAddLabel = () => {
const handleAddLabel = async () => {
const trimmed = newLabel.trim()
if (trimmed && !selectedLabels.includes(trimmed)) {
const updated = [...selectedLabels, trimmed]
setSelectedLabels(updated)
setNewLabel('')
// Set default color if doesn't exist
if (getLabelColor(trimmed) === 'gray') {
const colors = Object.keys(LABEL_COLORS) as LabelColorName[]
const randomColor = colors[Math.floor(Math.random() * colors.length)]
setLabelColor(trimmed, randomColor)
try {
// Get existing label color or use random
const existingLabel = labels.find(l => l.name === trimmed)
const color = existingLabel?.color || (Object.keys(LABEL_COLORS) as LabelColorName[])[Math.floor(Math.random() * Object.keys(LABEL_COLORS).length)]
await addLabel(trimmed, color)
const updated = [...selectedLabels, trimmed]
setSelectedLabels(updated)
setNewLabel('')
} catch (error) {
console.error('Failed to add label:', error)
}
}
}
@@ -64,12 +65,16 @@ export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
}
}
const handleChangeColor = (label: string, color: LabelColorName) => {
setLabelColor(label, color)
setEditingColor(null)
// Force re-render
const allColors = getAllLabelColors()
setAllLabelsInStorage(Object.keys(allColors))
const handleChangeColor = async (label: string, color: LabelColorName) => {
const labelObj = labels.find(l => l.name === label)
if (labelObj) {
try {
await updateLabel(labelObj.id, { color })
setEditingColor(null)
} catch (error) {
console.error('Failed to update label color:', error)
}
}
}
const handleSave = () => {
@@ -130,13 +135,13 @@ export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
<h4 className="text-sm font-medium mb-2">Selected Labels</h4>
<div className="flex flex-wrap gap-2">
{selectedLabels.map((label) => {
const colorName = getLabelColor(label)
const colorClasses = LABEL_COLORS[colorName]
const labelObj = labels.find(l => l.name === label)
const colorClasses = labelObj ? LABEL_COLORS[labelObj.color] : LABEL_COLORS.gray
const isEditing = editingColor === label
return (
<div key={label} className="relative">
{isEditing ? (
{isEditing && labelObj ? (
<div className="absolute z-10 top-8 left-0 bg-white dark:bg-zinc-900 border rounded-lg shadow-lg p-2">
<div className="grid grid-cols-3 gap-2">
{(Object.keys(LABEL_COLORS) as LabelColorName[]).map((color) => {
@@ -147,7 +152,7 @@ export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
className={cn(
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
classes.bg,
colorName === color ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-600'
labelObj.color === color ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-600'
)}
onClick={() => handleChangeColor(label, color)}
title={color}
@@ -185,20 +190,19 @@ export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
</div>
)}
{/* Available labels from storage */}
{allLabelsInStorage.length > 0 && (
{/* Available labels from context */}
{!loading && labels.length > 0 && (
<div>
<h4 className="text-sm font-medium mb-2">All Labels</h4>
<div className="flex flex-wrap gap-2">
{allLabelsInStorage
.filter(label => !selectedLabels.includes(label))
{labels
.filter(label => !selectedLabels.includes(label.name))
.map((label) => {
const colorName = getLabelColor(label)
const colorClasses = LABEL_COLORS[colorName]
const colorClasses = LABEL_COLORS[label.color]
return (
<Badge
key={label}
key={label.id}
className={cn(
'text-xs border cursor-pointer',
colorClasses.bg,
@@ -206,9 +210,9 @@ export function LabelManager({ existingLabels, onUpdate }: LabelManagerProps) {
colorClasses.border,
'hover:opacity-80'
)}
onClick={() => handleSelectExisting(label)}
onClick={() => handleSelectExisting(label.name)}
>
{label}
{label.name}
</Badge>
)
})}