feat: implement label management with color filtering
This commit is contained in:
@@ -2,12 +2,19 @@
|
||||
|
||||
import { Header } from './header'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
|
||||
export function HeaderWrapper() {
|
||||
interface HeaderWrapperProps {
|
||||
onColorFilterChange?: (color: string | null) => void
|
||||
}
|
||||
|
||||
export function HeaderWrapper({ onColorFilterChange }: HeaderWrapperProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const { labels } = useLabels()
|
||||
|
||||
const selectedLabels = searchParams.get('labels')?.split(',').filter(Boolean) || []
|
||||
const selectedColor = searchParams.get('color') || null
|
||||
|
||||
const handleLabelFilterChange = (labels: string[]) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
@@ -21,5 +28,25 @@ export function HeaderWrapper() {
|
||||
router.push(`/?${params.toString()}`)
|
||||
}
|
||||
|
||||
return <Header selectedLabels={selectedLabels} onLabelFilterChange={handleLabelFilterChange} />
|
||||
const handleColorFilterChange = (color: string | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
|
||||
if (color) {
|
||||
params.set('color', color)
|
||||
} else {
|
||||
params.delete('color')
|
||||
}
|
||||
|
||||
router.push(`/?${params.toString()}`)
|
||||
onColorFilterChange?.(color)
|
||||
}
|
||||
|
||||
return (
|
||||
<Header
|
||||
selectedLabels={selectedLabels}
|
||||
selectedColor={selectedColor}
|
||||
onLabelFilterChange={handleLabelFilterChange}
|
||||
onColorFilterChange={handleColorFilterChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ import { LabelFilter } from './label-filter'
|
||||
|
||||
interface HeaderProps {
|
||||
selectedLabels?: string[]
|
||||
selectedColor?: string | null
|
||||
onLabelFilterChange?: (labels: string[]) => void
|
||||
onColorFilterChange?: (color: string | null) => void
|
||||
}
|
||||
|
||||
export function Header({ selectedLabels = [], onLabelFilterChange }: HeaderProps = {}) {
|
||||
export function Header({ selectedLabels = [], selectedColor, onLabelFilterChange, onColorFilterChange }: HeaderProps = {}) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
@@ -105,7 +107,9 @@ export function Header({ selectedLabels = [], onLabelFilterChange }: HeaderProps
|
||||
{onLabelFilterChange && (
|
||||
<LabelFilter
|
||||
selectedLabels={selectedLabels}
|
||||
selectedColor={selectedColor}
|
||||
onFilterChange={onLabelFilterChange}
|
||||
onColorChange={onColorFilterChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
58
keep-notes/components/label-badge.tsx
Normal file
58
keep-notes/components/label-badge.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LABEL_COLORS } from '@/lib/types'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
|
||||
interface LabelBadgeProps {
|
||||
label: string
|
||||
onRemove?: () => void
|
||||
variant?: 'default' | 'filter' | 'clickable'
|
||||
onClick?: () => void
|
||||
isSelected?: boolean
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export function LabelBadge({
|
||||
label,
|
||||
onRemove,
|
||||
variant = 'default',
|
||||
onClick,
|
||||
isSelected = false,
|
||||
isDisabled = false,
|
||||
}: LabelBadgeProps) {
|
||||
const { getLabelColor } = useLabels()
|
||||
const colorName = getLabelColor(label)
|
||||
const colorClasses = LABEL_COLORS[colorName] || LABEL_COLORS.gray
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
'text-xs border gap-1',
|
||||
colorClasses.bg,
|
||||
colorClasses.text,
|
||||
colorClasses.border,
|
||||
variant === 'filter' && 'cursor-pointer hover:opacity-80',
|
||||
variant === 'clickable' && 'cursor-pointer',
|
||||
isDisabled && 'opacity-50',
|
||||
isSelected && 'ring-2 ring-blue-500'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
{onRemove && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}}
|
||||
className="hover:text-red-600"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
@@ -11,28 +11,31 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Filter, X } from 'lucide-react'
|
||||
import { getAllLabelColors, getLabelColor } from '@/lib/label-storage'
|
||||
import { Filter } from 'lucide-react'
|
||||
import { LABEL_COLORS } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
import { LabelBadge } from './label-badge'
|
||||
|
||||
interface LabelFilterProps {
|
||||
selectedLabels: string[]
|
||||
selectedColor?: string | null
|
||||
onFilterChange: (labels: string[]) => void
|
||||
onColorChange?: (color: string | null) => void
|
||||
}
|
||||
|
||||
export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps) {
|
||||
const [allLabels, setAllLabels] = useState<string[]>([])
|
||||
export function LabelFilter({ selectedLabels, selectedColor, onFilterChange, onColorChange }: LabelFilterProps) {
|
||||
const { labels, loading, getLabelColor } = useLabels()
|
||||
const [allLabelNames, setAllLabelNames] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
// Load all labels from localStorage
|
||||
const labelColors = getAllLabelColors()
|
||||
setAllLabels(Object.keys(labelColors).sort())
|
||||
}, [])
|
||||
// Extract label names from labels array
|
||||
setAllLabelNames(labels.map((l: any) => l.name).sort())
|
||||
}, [labels])
|
||||
|
||||
const handleToggleLabel = (label: string) => {
|
||||
if (selectedLabels.includes(label)) {
|
||||
onFilterChange(selectedLabels.filter(l => l !== label))
|
||||
onFilterChange(selectedLabels.filter((l: string) => l !== label))
|
||||
} else {
|
||||
onFilterChange([...selectedLabels, label])
|
||||
}
|
||||
@@ -40,9 +43,18 @@ export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps
|
||||
|
||||
const handleClearAll = () => {
|
||||
onFilterChange([])
|
||||
onColorChange?.(null)
|
||||
}
|
||||
|
||||
if (allLabels.length === 0) return null
|
||||
const handleColorFilter = (color: string) => {
|
||||
if (selectedColor === color) {
|
||||
onColorChange?.(null)
|
||||
} else {
|
||||
onColorChange?.(color)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading || allLabelNames.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -58,9 +70,9 @@ export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
Filter by Labels
|
||||
<span>Filter by Labels</span>
|
||||
{selectedLabels.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -73,27 +85,54 @@ export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{allLabels.map((label) => {
|
||||
const colorName = getLabelColor(label)
|
||||
const colorClasses = LABEL_COLORS[colorName]
|
||||
const isSelected = selectedLabels.includes(label)
|
||||
|
||||
{/* Color Filter */}
|
||||
<div className="p-2">
|
||||
<p className="text-xs font-medium mb-2 text-gray-600 dark:text-gray-400">Filter by Color</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Object.entries(LABEL_COLORS).map(([colorName, colorClasses]) => {
|
||||
const isSelected = selectedColor === colorName
|
||||
const labelCount = labels.filter((l: any) => l.color === colorName).length
|
||||
|
||||
return (
|
||||
<button
|
||||
key={colorName}
|
||||
onClick={() => handleColorFilter(colorName)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 p-2 rounded-lg border-2 transition-all hover:scale-105',
|
||||
isSelected ? 'ring-2 ring-blue-500' : 'border-gray-300 dark:border-gray-600'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-6 h-6 rounded-full border-2',
|
||||
colorClasses.bg,
|
||||
isSelected ? 'border-blue-500 dark:border-blue-400' : colorClasses.border
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs ml-2">{labelCount}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Label Filters */}
|
||||
{!loading && allLabelNames.map((labelName: string) => {
|
||||
const isSelected = selectedLabels.includes(labelName)
|
||||
const isColorFiltered = selectedColor && selectedColor !== 'gray'
|
||||
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={label}
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => handleToggleLabel(label)}
|
||||
key={labelName}
|
||||
checked={isSelected && !isColorFiltered}
|
||||
onCheckedChange={() => handleToggleLabel(labelName)}
|
||||
>
|
||||
<Badge
|
||||
className={cn(
|
||||
'text-xs border mr-2',
|
||||
colorClasses.bg,
|
||||
colorClasses.text,
|
||||
colorClasses.border
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
<LabelBadge
|
||||
label={labelName}
|
||||
isDisabled={!!isColorFiltered}
|
||||
/>
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
})}
|
||||
@@ -101,28 +140,16 @@ export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Active filters display */}
|
||||
{selectedLabels.length > 0 && (
|
||||
{!loading && selectedLabels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedLabels.map((label) => {
|
||||
const colorName = getLabelColor(label)
|
||||
const colorClasses = LABEL_COLORS[colorName]
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={label}
|
||||
className={cn(
|
||||
'text-xs border cursor-pointer pr-1',
|
||||
colorClasses.bg,
|
||||
colorClasses.text,
|
||||
colorClasses.border
|
||||
)}
|
||||
onClick={() => handleToggleLabel(label)}
|
||||
>
|
||||
{label}
|
||||
<X className="h-3 w-3 ml-1" />
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
{selectedLabels.map((labelName: string) => (
|
||||
<LabelBadge
|
||||
key={labelName}
|
||||
label={labelName}
|
||||
variant="filter"
|
||||
onClick={() => handleToggleLabel(labelName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
74
keep-notes/components/label-selector.tsx
Normal file
74
keep-notes/components/label-selector.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tag } 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 = 'Tags',
|
||||
align = 'start',
|
||||
}: LabelSelectorProps) {
|
||||
const { labels, loading } = useLabels()
|
||||
|
||||
const handleToggleLabel = (labelName: string) => {
|
||||
if (selectedLabels.includes(labelName)) {
|
||||
onLabelsChange(selectedLabels.filter((l) => l !== labelName))
|
||||
} else {
|
||||
onLabelsChange([...selectedLabels, labelName])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8">
|
||||
<Tag className="h-4 w-4 mr-2" />
|
||||
{triggerLabel}
|
||||
{selectedLabels.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-2 h-5 min-w-5 px-1.5">
|
||||
{selectedLabels.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={align} className="w-64">
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{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>
|
||||
) : (
|
||||
labels.map((label) => {
|
||||
const isSelected = selectedLabels.includes(label.name)
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={label.id}
|
||||
onClick={() => handleToggleLabel(label.name)}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<LabelBadge label={label.name} isSelected={isSelected} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { Note, NOTE_COLORS, NoteColor, LABEL_COLORS } from '@/lib/types'
|
||||
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -22,13 +21,13 @@ import {
|
||||
Trash2,
|
||||
Bell,
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote } from '@/app/actions/notes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
import { getLabelColor } from '@/lib/label-storage'
|
||||
import { LabelBadge } from './label-badge'
|
||||
|
||||
interface NoteCardProps {
|
||||
note: Note
|
||||
@@ -39,20 +38,8 @@ interface NoteCardProps {
|
||||
|
||||
export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [labelColors, setLabelColors] = useState<Record<string, string>>({})
|
||||
const colorClasses = NOTE_COLORS[note.color as NoteColor] || NOTE_COLORS.default
|
||||
|
||||
// Load label colors from localStorage
|
||||
useEffect(() => {
|
||||
if (note.labels) {
|
||||
const colors: Record<string, string> = {}
|
||||
note.labels.forEach(label => {
|
||||
colors[label] = getLabelColor(label)
|
||||
})
|
||||
setLabelColors(colors)
|
||||
}
|
||||
}, [note.labels])
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm('Are you sure you want to delete this note?')) {
|
||||
setIsDeleting(true)
|
||||
@@ -232,23 +219,9 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
{/* Labels */}
|
||||
{note.labels && note.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-3">
|
||||
{note.labels.map((label) => {
|
||||
const colorName = labelColors[label] || 'gray'
|
||||
const colorClasses = LABEL_COLORS[colorName as keyof typeof LABEL_COLORS] || LABEL_COLORS.gray
|
||||
return (
|
||||
<Badge
|
||||
key={label}
|
||||
className={cn(
|
||||
'text-xs border',
|
||||
colorClasses.bg,
|
||||
colorClasses.text,
|
||||
colorClasses.border
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
{note.labels.map((label) => (
|
||||
<LabelBadge key={label} label={label} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LABEL_COLORS } from '@/lib/types'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -12,19 +12,18 @@ import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { X, Plus, Palette, Tag, Image as ImageIcon, Bell, FileText, Eye } from 'lucide-react'
|
||||
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye } from 'lucide-react'
|
||||
import { updateNote } from '@/app/actions/notes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/toast'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
import { LabelManager } from './label-manager'
|
||||
import { getLabelColor } from '@/lib/label-storage'
|
||||
import { LabelBadge } from './label-badge'
|
||||
|
||||
interface NoteEditorProps {
|
||||
note: Note
|
||||
@@ -308,29 +307,13 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
{/* Labels */}
|
||||
{labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{labels.map((label) => {
|
||||
const colorName = getLabelColor(label)
|
||||
const colorClasses = LABEL_COLORS[colorName]
|
||||
return (
|
||||
<Badge
|
||||
key={label}
|
||||
className={cn(
|
||||
'gap-1 border',
|
||||
colorClasses.bg,
|
||||
colorClasses.text,
|
||||
colorClasses.border
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<button
|
||||
onClick={() => handleRemoveLabel(label)}
|
||||
className="hover:text-red-600"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
{labels.map((label) => (
|
||||
<LabelBadge
|
||||
key={label}
|
||||
label={label}
|
||||
onRemove={() => handleRemoveLabel(label)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
CheckSquare,
|
||||
X,
|
||||
Bell,
|
||||
Image,
|
||||
UserPlus,
|
||||
Palette,
|
||||
Archive,
|
||||
import {
|
||||
CheckSquare,
|
||||
X,
|
||||
Bell,
|
||||
Image,
|
||||
UserPlus,
|
||||
Palette,
|
||||
Archive,
|
||||
MoreVertical,
|
||||
Undo2,
|
||||
Redo2,
|
||||
@@ -31,13 +31,14 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/toast'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
import { LabelSelector } from './label-selector'
|
||||
import { LabelBadge } from './label-badge'
|
||||
|
||||
interface HistoryState {
|
||||
title: string
|
||||
@@ -58,6 +59,7 @@ export function NoteInput() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [color, setColor] = useState<NoteColor>('default')
|
||||
const [isArchived, setIsArchived] = useState(false)
|
||||
const [selectedLabels, setSelectedLabels] = useState<string[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Simple state without complex undo/redo - like Google Keep
|
||||
@@ -240,6 +242,7 @@ export function NoteInput() {
|
||||
images: images.length > 0 ? images : undefined,
|
||||
reminder: currentReminder,
|
||||
isMarkdown,
|
||||
labels: selectedLabels.length > 0 ? selectedLabels : undefined,
|
||||
})
|
||||
|
||||
// Reset form
|
||||
@@ -256,6 +259,7 @@ export function NoteInput() {
|
||||
setColor('default')
|
||||
setIsArchived(false)
|
||||
setCurrentReminder(null)
|
||||
setSelectedLabels([])
|
||||
|
||||
addToast('Note created successfully', 'success')
|
||||
} catch (error) {
|
||||
@@ -288,12 +292,15 @@ export function NoteInput() {
|
||||
setContent('')
|
||||
setCheckItems([])
|
||||
setImages([])
|
||||
setIsMarkdown(false)
|
||||
setShowMarkdownPreview(false)
|
||||
setHistory([{ title: '', content: '' }])
|
||||
setHistoryIndex(0)
|
||||
setType('text')
|
||||
setColor('default')
|
||||
setIsArchived(false)
|
||||
setCurrentReminder(null)
|
||||
setSelectedLabels([])
|
||||
}
|
||||
|
||||
if (!isExpanded) {
|
||||
@@ -364,6 +371,16 @@ export function NoteInput() {
|
||||
|
||||
{type === 'text' ? (
|
||||
<div className="space-y-2">
|
||||
{/* Labels selector */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LabelSelector
|
||||
selectedLabels={selectedLabels}
|
||||
onLabelsChange={setSelectedLabels}
|
||||
triggerLabel="Tags"
|
||||
align="start"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Markdown toggle button */}
|
||||
{isMarkdown && (
|
||||
<div className="flex justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user