- 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.
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
'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>
|
||
);
|
||
}
|