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:
166
clients/tags-client.ts
Normal file
166
clients/tags-client.ts
Normal 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();
|
||||
Reference in New Issue
Block a user