import { create } from 'zustand'; import { persist, devtools } from 'zustand/middleware'; import { apiClient } from '@/services/api/client'; import { undoRedo } from '@/utils/undoRedo'; import { stateMiddleware } from '@/utils/stateMiddleware'; import { normalize, addToNormalized, updateInNormalized, removeFromNormalized, createEmptyNormalized, type NormalizedState, } from '@/utils/stateNormalization'; import type { LibraryItem, PaginatedResponse, ApiError } from '@/types'; import type { WithUndoRedo } from './types'; // FE-TYPE-011: Fully typed store interfaces export interface LibraryState { // FE-STATE-009: Normalized state for better performance items: NormalizedState; favorites: NormalizedState; isLoading: boolean; error: ApiError | null; pagination: { page: number; limit: number; total: number; hasNext: boolean; hasPrev: boolean; }; filters: { type?: string; search?: string; }; } export interface LibraryActions { fetchItems: (params?: { page?: number; limit?: number; type?: string; search?: string; }) => Promise; fetchFavorites: () => Promise; uploadFile: ( file: File, metadata: { title: string; description?: string }, ) => Promise; toggleFavorite: (itemId: string) => Promise; deleteItem: (itemId: string) => Promise; setFilters: (filters: { type?: string; search?: string }) => void; setLoading: (loading: boolean) => void; setError: (error: ApiError | null) => void; clearItems: () => void; } // FE-TYPE-011: Export store type for reuse export type LibraryStore = WithUndoRedo; export const useLibraryStore = create()( devtools( persist( undoRedo( stateMiddleware( (set, get) => ({ // État initial - FE-STATE-009: Normalized state items: createEmptyNormalized(), favorites: createEmptyNormalized(), isLoading: false, error: null, pagination: { page: 1, limit: 20, total: 0, hasNext: false, hasPrev: false, }, filters: {}, // Actions fetchItems: async (params = {}) => { set({ isLoading: true, error: null }); try { const { page = 1, limit = 20, type, } = { ...get().pagination, ...get().filters, ...params }; const response = await apiClient.get>('/tracks', { params: { page, limit, type, }, }); // apiClient unwrap déjà le format { success, data }, donc response.data contient directement la réponse const data = response.data; // Sécuriser response.data pour s'assurer que c'est toujours un tableau const itemsArray = Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : []; // FE-STATE-009: Normalize items for better performance set({ items: normalize(itemsArray), pagination: { page: data.page || 1, limit: data.limit || limit, total: data.total || 0, hasNext: data.has_next || false, hasPrev: data.has_prev || false, }, isLoading: false, error: null, }); } catch (error: any) { // En cas d'erreur, s'assurer que items reste un état normalisé vide set({ items: createEmptyNormalized(), error: error as ApiError, isLoading: false, }); } }, fetchFavorites: async () => { set({ isLoading: true, error: null }); try { const response = await apiClient.get>('/tracks', { params: { page: 1, limit: 100, type: 'favorites', }, }); // apiClient unwrap déjà le format { success, data }, donc response.data contient directement la réponse const data = response.data; // Sécuriser response.data pour s'assurer que c'est toujours un tableau const favoritesArray = Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : []; // FE-STATE-009: Normalize favorites for better performance set({ favorites: normalize(favoritesArray), isLoading: false, error: null, }); } catch (error: any) { // En cas d'erreur, s'assurer que favorites reste un état normalisé vide set({ favorites: createEmptyNormalized(), error: error as ApiError, isLoading: false, }); } }, uploadFile: async (file, metadata) => { set({ isLoading: true, error: null }); try { const formData = new FormData(); formData.append('file', file); formData.append('title', metadata.title); if (metadata.description) { formData.append('description', metadata.description); } const response = await apiClient.post('/tracks', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); const newItem = response.data; // FE-STATE-009: Add to normalized state at position 0 (prepend) set((state) => ({ items: addToNormalized(state.items, newItem, 0), isLoading: false, error: null, })); } catch (error: any) { set({ error: error as ApiError, isLoading: false, }); throw error; } }, // FE-STATE-005: Optimistic update for toggle favorite // FE-STATE-009: Using normalized state operations toggleFavorite: async (itemId: string) => { const previousState = get(); const item = previousState.items.byId[itemId]; const newFavoriteStatus = item ? !item.is_favorite : true; // Optimistic update - FE-STATE-009: Update in normalized state set((state) => { const updatedItems = updateInNormalized(state.items, itemId, { is_favorite: newFavoriteStatus, }); let updatedFavorites = state.favorites; if (newFavoriteStatus && item) { // Add to favorites const favoriteItem = { ...item, is_favorite: true }; updatedFavorites = addToNormalized( removeFromNormalized(state.favorites, itemId), favoriteItem, ); } else { // Remove from favorites updatedFavorites = removeFromNormalized(state.favorites, itemId); } return { items: updatedItems, favorites: updatedFavorites, }; }); try { const response = await apiClient.post(`/tracks/${itemId}/favorite`); const updatedItem = response.data; // Apply server response - FE-STATE-009: Update in normalized state set((state) => { const updatedItems = updateInNormalized(state.items, itemId, { is_favorite: updatedItem.is_favorite, }); let updatedFavorites = state.favorites; if (updatedItem.is_favorite) { updatedFavorites = addToNormalized( removeFromNormalized(state.favorites, itemId), updatedItem, ); } else { updatedFavorites = removeFromNormalized(state.favorites, itemId); } return { items: updatedItems, favorites: updatedFavorites, }; }); } catch (error: any) { // Rollback on error set(() => previousState); set({ error: error as ApiError }); throw error; } }, // FE-STATE-005: Optimistic update for delete item // FE-STATE-009: Using normalized state operations deleteItem: async (itemId: string) => { const previousState = get(); // Optimistic update - remove item immediately from normalized state set((state) => ({ items: removeFromNormalized(state.items, itemId), favorites: removeFromNormalized(state.favorites, itemId), })); try { // Appel API pour supprimer l'élément await apiClient.delete(`/tracks/${itemId}`); // No need to update state - optimistic update is final } catch (error: any) { // Rollback on error set(() => previousState); set({ error: error as ApiError }); throw error; } }, setFilters: (filters) => { set({ filters }); // Recharger les éléments avec les nouveaux filtres get().fetchItems({ page: 1 }); }, setLoading: (isLoading) => set({ isLoading }), setError: (error) => set({ error }), clearItems: () => { set({ items: createEmptyNormalized(), favorites: createEmptyNormalized(), pagination: { page: 1, limit: 20, total: 0, hasNext: false, hasPrev: false, }, }); }, }), { // FE-STATE-010: Enable state middleware for logging, analytics, error handling storeName: 'LibraryStore', enableLogging: import.meta.env.DEV, enableAnalytics: true, enableErrorHandling: true, }, ), { // FE-STATE-006: Enable undo/redo for library store maxHistorySize: 50, enabled: true, shouldTrack: (state, prevState) => { // Track changes to items and favorites (not loading/error states) // FE-STATE-009: Compare normalized state structures return ( JSON.stringify(state.items) !== JSON.stringify(prevState?.items) || JSON.stringify(state.favorites) !== JSON.stringify(prevState?.favorites) ); }, }, ), { name: 'library-storage', partialize: (state) => ({ // FE-STATE-001: Persist favorites and filters for offline support // FE-STATE-009: Persist normalized favorites state favorites: state.favorites, filters: state.filters, // Don't persist items and pagination as they should be fetched fresh }), }, ), { // FE-STATE-007: Enable Redux DevTools for debugging name: 'LibraryStore', enabled: import.meta.env.DEV, }, ), );