feat: complete tag management and UI integration

- Marked multiple tasks as completed in TODO.md related to tag management features.
- Replaced manual tag input with `TagInput` component in `CreateTaskForm`, `EditTaskForm`, and `QuickAddTask` for better UX.
- Updated `TaskCard` to display tags using `TagDisplay` with color support.
- Enhanced `TasksService` to manage task-tag relationships with CRUD operations.
- Integrated tag management into the global context for better accessibility across components.
This commit is contained in:
Julien Froidefond
2025-09-14 16:44:22 +02:00
parent edbd82e8ac
commit c5a7d16425
27 changed files with 2055 additions and 224 deletions

View File

@@ -0,0 +1,157 @@
'use client';
import { Tag } from '@/lib/types';
import { Badge } from './Badge';
interface TagDisplayProps {
tags: string[];
availableTags?: Tag[]; // Tags avec couleurs depuis la DB
size?: 'sm' | 'md' | 'lg';
maxTags?: number;
showColors?: boolean;
onClick?: (tagName: string) => void;
className?: string;
}
export function TagDisplay({
tags,
availableTags = [],
size = 'sm',
maxTags,
showColors = true,
onClick,
className = ""
}: TagDisplayProps) {
if (!tags || tags.length === 0) {
return null;
}
const displayTags = maxTags ? tags.slice(0, maxTags) : tags;
const remainingCount = maxTags && tags.length > maxTags ? tags.length - maxTags : 0;
const getTagColor = (tagName: string): string => {
if (!showColors) return '#6b7280'; // gray-500
const tag = availableTags.find(t => t.name === tagName);
return tag?.color || '#6b7280';
};
const sizeClasses = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-2 py-1',
lg: 'text-base px-3 py-1.5'
};
return (
<div className={`flex flex-wrap gap-1 ${className}`}>
{displayTags.map((tagName, index) => {
const color = getTagColor(tagName);
return (
<div
key={index}
onClick={() => onClick?.(tagName)}
className={`inline-flex items-center gap-1.5 rounded-full border transition-colors ${sizeClasses[size]} ${
onClick ? 'cursor-pointer hover:opacity-80' : ''
}`}
style={{
backgroundColor: showColors ? `${color}20` : undefined,
borderColor: showColors ? `${color}60` : undefined,
color: showColors ? color : undefined
}}
>
{showColors && (
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: color }}
/>
)}
<span className="font-medium">{tagName}</span>
</div>
);
})}
{remainingCount > 0 && (
<Badge variant="default" size="sm">
+{remainingCount}
</Badge>
)}
</div>
);
}
interface TagListProps {
tags: Tag[];
onTagClick?: (tag: Tag) => void;
onTagEdit?: (tag: Tag) => void;
onTagDelete?: (tag: Tag) => void;
className?: string;
}
/**
* Composant pour afficher une liste complète de tags avec actions
*/
export function TagList({
tags,
onTagClick,
onTagEdit,
onTagDelete,
className = ""
}: TagListProps) {
if (!tags || tags.length === 0) {
return (
<div className="text-center py-8 text-slate-400">
<div className="text-4xl mb-2">🏷</div>
<p className="text-sm">Aucun tag trouvé</p>
</div>
);
}
return (
<div className={`space-y-2 ${className}`}>
{tags.map((tag) => (
<div
key={tag.id}
className="flex items-center justify-between p-3 bg-slate-800 rounded-lg border border-slate-700 hover:border-slate-600 transition-colors group"
>
<div
className="flex items-center gap-3 flex-1 cursor-pointer"
onClick={() => onTagClick?.(tag)}
>
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: tag.color }}
/>
<span className="text-slate-200 font-medium">{tag.name}</span>
</div>
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
{onTagEdit && (
<button
onClick={() => onTagEdit(tag)}
className="p-1 text-slate-400 hover:text-cyan-400 transition-colors"
title="Éditer le tag"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
)}
{onTagDelete && (
<button
onClick={() => onTagDelete(tag)}
className="p-1 text-slate-400 hover:text-red-400 transition-colors"
title="Supprimer le tag"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
);
}