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

166
clients/tags-client.ts Normal file
View File

@@ -0,0 +1,166 @@
import { HttpClient } from './base/http-client';
import { Tag, ApiResponse } from '@/lib/types';
// Types pour les requêtes
export interface CreateTagData {
name: string;
color: string;
}
export interface UpdateTagData {
tagId: string;
name?: string;
color?: string;
}
export interface TagFilters {
q?: string; // Recherche par nom
popular?: boolean; // Tags les plus utilisés
limit?: number; // Limite de résultats
}
// Types pour les réponses
export interface TagsResponse {
data: Tag[];
message: string;
}
export interface TagResponse {
data: Tag;
message: string;
}
export interface PopularTag extends Tag {
usage: number;
}
export interface PopularTagsResponse {
data: PopularTag[];
message: string;
}
/**
* Client HTTP pour la gestion des tags
*/
export class TagsClient extends HttpClient {
constructor() {
super('/api/tags');
}
/**
* Récupère tous les tags
*/
async getTags(filters?: TagFilters): Promise<TagsResponse> {
const params: Record<string, string> = {};
if (filters?.q) {
params.q = filters.q;
}
if (filters?.popular) {
params.popular = 'true';
}
if (filters?.limit) {
params.limit = filters.limit.toString();
}
return this.get<TagsResponse>('', Object.keys(params).length > 0 ? params : undefined);
}
/**
* Récupère les tags populaires (les plus utilisés)
*/
async getPopularTags(limit: number = 10): Promise<PopularTagsResponse> {
return this.get<PopularTagsResponse>('', { popular: 'true', limit: limit.toString() });
}
/**
* Recherche des tags par nom (pour autocomplete)
*/
async searchTags(query: string, limit: number = 10): Promise<TagsResponse> {
return this.get<TagsResponse>('', { q: query, limit: limit.toString() });
}
/**
* Récupère un tag par son ID
*/
async getTagById(id: string): Promise<TagResponse> {
return this.get<TagResponse>(`/${id}`);
}
/**
* Crée un nouveau tag
*/
async createTag(data: CreateTagData): Promise<TagResponse> {
return this.post<TagResponse>('', data);
}
/**
* Met à jour un tag
*/
async updateTag(data: UpdateTagData): Promise<TagResponse> {
const { tagId, ...updates } = data;
return this.patch<TagResponse>(`/${tagId}`, updates);
}
/**
* Supprime un tag
*/
async deleteTag(id: string): Promise<ApiResponse<void>> {
return this.delete<ApiResponse<void>>(`/${id}`);
}
/**
* Valide le format d'une couleur hexadécimale
*/
static isValidColor(color: string): boolean {
return /^#[0-9A-F]{6}$/i.test(color);
}
/**
* Génère une couleur aléatoire pour un nouveau tag
*/
static generateRandomColor(): string {
const colors = [
'#3B82F6', // Blue
'#EF4444', // Red
'#10B981', // Green
'#F59E0B', // Yellow
'#8B5CF6', // Purple
'#EC4899', // Pink
'#06B6D4', // Cyan
'#84CC16', // Lime
'#F97316', // Orange
'#6366F1', // Indigo
];
return colors[Math.floor(Math.random() * colors.length)];
}
/**
* Valide les données d'un tag
*/
static validateTagData(data: Partial<CreateTagData>): string[] {
const errors: string[] = [];
if (!data.name || typeof data.name !== 'string') {
errors.push('Le nom du tag est requis');
} else if (data.name.trim().length === 0) {
errors.push('Le nom du tag ne peut pas être vide');
} else if (data.name.length > 50) {
errors.push('Le nom du tag ne peut pas dépasser 50 caractères');
}
if (!data.color || typeof data.color !== 'string') {
errors.push('La couleur du tag est requise');
} else if (!this.isValidColor(data.color)) {
errors.push('La couleur doit être au format hexadécimal (#RRGGBB)');
}
return errors;
}
}
// Instance singleton
export const tagsClient = new TagsClient();