feat: migrate tag management to server actions

- Completed migration of tag management to server actions by creating `actions/tags.ts` for CRUD operations.
- Removed obsolete API routes for tags (`/api/tags`, `/api/tags/[id]`) and updated related components to utilize new server actions.
- Updated `TagForm` and `useTags` hook to handle tag creation, updating, and deletion through server actions, improving performance and simplifying client-side logic.
- Cleaned up `tags-client.ts` by removing unused types and methods, focusing on validation only.
- Marked related TODO items as complete in `TODO.md`.
This commit is contained in:
Julien Froidefond
2025-09-18 21:00:28 +02:00
parent 8b98c467d0
commit b12dd4f8dc
8 changed files with 253 additions and 317 deletions

View File

@@ -1,21 +1,23 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { tagsClient, TagFilters, CreateTagData, UpdateTagData, TagsClient } from '@/clients/tags-client';
import { useState, useEffect, useCallback, useTransition } from 'react';
import { tagsClient, TagFilters, TagsClient } from '@/clients/tags-client';
import { createTag, updateTag, deleteTag as deleteTagAction } from '@/actions/tags';
import { Tag } from '@/lib/types';
interface UseTagsState {
tags: Array<Tag & { usage: number }>;
popularTags: Array<Tag & { usage: number }>;
loading: boolean;
isPending: boolean; // Loading state for server actions
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>;
createTag: (name: string, color: string) => Promise<void>;
updateTag: (tagId: string, data: { name?: string; color?: string; isPinned?: boolean }) => Promise<void>;
deleteTag: (tagId: string) => Promise<void>;
getPopularTags: (limit?: number) => Promise<void>;
setFilters: (filters: TagFilters) => void;
@@ -28,10 +30,12 @@ export function useTags(
initialData?: (Tag & { usage: number })[],
initialFilters?: TagFilters
): UseTagsState & UseTagsActions {
const [isPending, startTransition] = useTransition();
const [state, setState] = useState<UseTagsState>({
tags: initialData || [],
popularTags: initialData || [],
loading: !initialData,
isPending: false,
error: null
});
@@ -97,77 +101,82 @@ export function useTags(
}, []);
/**
* Crée un nouveau tag
* Crée un nouveau tag avec server action
*/
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 createTagAction = useCallback(async (name: string, color: string): Promise<void> => {
// Validation côté client
const errors = TagsClient.validateTagData({ name, color });
if (errors.length > 0) {
setState(prev => ({ ...prev, error: errors[0] }));
return;
}
startTransition(async () => {
try {
setState(prev => ({ ...prev, error: null }));
const result = await createTag(name, color);
if (!result.success) {
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la création' }));
}
// Note: revalidatePath in server action handles cache refresh automatically
} catch (error) {
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Erreur lors de la création'
}));
}
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
* Met à jour un tag avec server action
*/
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]);
const updateTagAction = useCallback(async (
tagId: string,
data: { name?: string; color?: string; isPinned?: boolean }
): Promise<void> => {
startTransition(async () => {
try {
setState(prev => ({ ...prev, error: null }));
const result = await updateTag(tagId, data);
if (!result.success) {
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la mise à jour' }));
}
// Note: revalidatePath in server action handles cache refresh automatically
} catch (error) {
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
}));
}
});
}, []);
/**
* Supprime un tag
* Supprime un tag avec server action
*/
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]);
const deleteTagActionHandler = useCallback(async (tagId: string): Promise<void> => {
startTransition(async () => {
try {
setState(prev => ({ ...prev, error: null }));
const result = await deleteTagAction(tagId);
if (!result.success) {
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la suppression' }));
throw new Error(result.error);
}
// Note: revalidatePath in server action handles cache refresh automatically
} catch (error) {
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
}));
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
}
});
}, []);
// Charger les tags au montage et quand les filtres changent (seulement si pas de données initiales)
useEffect(() => {
@@ -178,11 +187,12 @@ export function useTags(
return {
...state,
isPending, // Expose loading state from useTransition
refreshTags,
searchTags,
createTag,
updateTag,
deleteTag,
createTag: createTagAction,
updateTag: updateTagAction,
deleteTag: deleteTagActionHandler,
getPopularTags,
setFilters
};