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

223
hooks/useTags.ts Normal file
View File

@@ -0,0 +1,223 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { tagsClient, TagFilters, CreateTagData, UpdateTagData, TagsClient } from '@/clients/tags-client';
import { Tag } from '@/lib/types';
interface UseTagsState {
tags: Tag[];
popularTags: Array<Tag & { usage: number }>;
loading: boolean;
error: string | null;
}
interface UseTagsActions {
refreshTags: () => Promise<void>;
searchTags: (query: string, limit?: number) => Promise<Tag[]>;
createTag: (data: CreateTagData) => Promise<Tag | null>;
updateTag: (data: UpdateTagData) => Promise<Tag | null>;
deleteTag: (tagId: string) => Promise<void>;
getPopularTags: (limit?: number) => Promise<void>;
setFilters: (filters: TagFilters) => void;
}
/**
* Hook pour la gestion des tags
*/
export function useTags(
initialFilters?: TagFilters
): UseTagsState & UseTagsActions {
const [state, setState] = useState<UseTagsState>({
tags: [],
popularTags: [],
loading: false,
error: null
});
const [filters, setFilters] = useState<TagFilters>(initialFilters || {});
/**
* Récupère les tags depuis l'API
*/
const refreshTags = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await tagsClient.getTags(filters);
setState(prev => ({
...prev,
tags: response.data,
loading: false
}));
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}));
}
}, [filters]);
/**
* Recherche des tags par nom (pour autocomplete)
*/
const searchTags = useCallback(async (query: string, limit: number = 10): Promise<Tag[]> => {
try {
const response = await tagsClient.searchTags(query, limit);
return response.data;
} catch (error) {
console.error('Erreur lors de la recherche de tags:', error);
return [];
}
}, []);
/**
* Récupère les tags populaires
*/
const getPopularTags = useCallback(async (limit: number = 10) => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await tagsClient.getPopularTags(limit);
setState(prev => ({
...prev,
popularTags: response.data,
loading: false
}));
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}));
}
}, []);
/**
* Crée un nouveau tag
*/
const createTag = useCallback(async (data: CreateTagData): Promise<Tag | null> => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
// Validation côté client
const errors = TagsClient.validateTagData(data);
if (errors.length > 0) {
throw new Error(errors[0]);
}
const response = await tagsClient.createTag(data);
// Rafraîchir la liste après création
await refreshTags();
return response.data;
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur lors de la création'
}));
return null;
}
}, [refreshTags]);
/**
* Met à jour un tag
*/
const updateTag = useCallback(async (data: UpdateTagData): Promise<Tag | null> => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await tagsClient.updateTag(data);
// Rafraîchir la liste après mise à jour
await refreshTags();
return response.data;
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
}));
return null;
}
}, [refreshTags]);
/**
* Supprime un tag
*/
const deleteTag = useCallback(async (tagId: string): Promise<void> => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
await tagsClient.deleteTag(tagId);
// Rafraîchir la liste après suppression
await refreshTags();
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
}));
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
}
}, [refreshTags]);
// Charger les tags au montage et quand les filtres changent
useEffect(() => {
refreshTags();
}, [refreshTags]);
return {
...state,
refreshTags,
searchTags,
createTag,
updateTag,
deleteTag,
getPopularTags,
setFilters
};
}
/**
* Hook simplifié pour l'autocomplete des tags
*/
export function useTagsAutocomplete() {
const [suggestions, setSuggestions] = useState<Tag[]>([]);
const [loading, setLoading] = useState(false);
const searchTags = useCallback(async (query: string) => {
if (!query.trim()) {
setSuggestions([]);
return;
}
setLoading(true);
try {
const response = await tagsClient.searchTags(query, 5);
setSuggestions(response.data);
} catch (error) {
console.error('Erreur lors de la recherche de tags:', error);
setSuggestions([]);
} finally {
setLoading(false);
}
}, []);
const clearSuggestions = useCallback(() => {
setSuggestions([]);
}, []);
return {
suggestions,
loading,
searchTags,
clearSuggestions
};
}

View File

@@ -162,9 +162,6 @@ export function useTasks(
// 3. Appel API en arrière-plan
try {
// Délai artificiel pour voir l'indicateur de sync (à supprimer en prod)
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await tasksClient.updateTask(data);
// Si l'API retourne des données différentes, on met à jour