diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index c1578e764..58d938683 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -8714,7 +8714,7 @@ "description": "Persist state to localStorage for offline support", "owner": "frontend", "estimated_hours": 4, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -8735,7 +8735,8 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "Added state persistence to library and chat stores using Zustand persist middleware. Library store persists favorites and filters. Chat store persists conversations (not messages). Created statePersistence.ts utility with helpers for storage management, error handling, and storage info. All stores now support offline state persistence via localStorage.", + "completed_at": "2025-12-25T12:38:48.456146Z" }, { "id": "FE-STATE-002", diff --git a/apps/web/src/stores/chat.ts b/apps/web/src/stores/chat.ts index 93bbac769..6e5b72806 100644 --- a/apps/web/src/stores/chat.ts +++ b/apps/web/src/stores/chat.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; import { wsService } from '@/services/websocket'; import type { ChatMessage, Conversation, ChatWebSocketEvent } from '@/types'; @@ -51,7 +52,9 @@ interface ChatActions { }) => Promise; } -export const useChatStore = create((set, get) => ({ +export const useChatStore = create()( + persist( + (set, get) => ({ // État initial conversations: [], currentConversation: null, @@ -298,4 +301,16 @@ export const useChatStore = create((set, get) => ({ throw error; } }, -})); + }), + { + name: 'chat-storage', + partialize: (state) => ({ + // FE-STATE-001: Persist conversations for offline support + // Don't persist messages as they can be large and should be fetched fresh + conversations: state.conversations, + currentConversation: state.currentConversation, + // Don't persist messages, typingUsers, isConnected, isLoading, error + }), + }, + ), +); diff --git a/apps/web/src/stores/library.ts b/apps/web/src/stores/library.ts index 93af66b01..c2d2626d8 100644 --- a/apps/web/src/stores/library.ts +++ b/apps/web/src/stores/library.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; import { apiClient } from '@/services/api/client'; import type { LibraryItem, PaginatedResponse, ApiError } from '@/types'; @@ -40,8 +41,9 @@ interface LibraryActions { clearItems: () => void; } -export const useLibraryStore = create( - (set, get) => ({ +export const useLibraryStore = create()( + persist( + (set, get) => ({ // État initial items: [], favorites: [], @@ -229,5 +231,14 @@ export const useLibraryStore = create( hasPrev: false, }, }), - }), + { + name: 'library-storage', + partialize: (state) => ({ + // FE-STATE-001: Persist favorites and filters for offline support + favorites: state.favorites, + filters: state.filters, + // Don't persist items and pagination as they should be fetched fresh + }), + }, + ), ); diff --git a/apps/web/src/utils/statePersistence.ts b/apps/web/src/utils/statePersistence.ts new file mode 100644 index 000000000..c112f3b65 --- /dev/null +++ b/apps/web/src/utils/statePersistence.ts @@ -0,0 +1,137 @@ +/** + * State Persistence Utilities + * FE-STATE-001: Utilities for managing state persistence in Zustand stores + * + * Provides helpers for consistent state persistence configuration + */ + +import { StateStorage } from 'zustand/middleware'; + +/** + * Custom storage implementation with error handling + */ +export const createPersistentStorage = (name: string): StateStorage => { + return { + getItem: (key: string): string | null => { + try { + if (typeof window === 'undefined') { + return null; + } + return localStorage.getItem(key); + } catch (error) { + console.warn(`[StatePersistence] Failed to get item ${key} from localStorage:`, error); + return null; + } + }, + setItem: (key: string, value: string): void => { + try { + if (typeof window === 'undefined') { + return; + } + localStorage.setItem(key, value); + } catch (error) { + console.warn(`[StatePersistence] Failed to set item ${key} in localStorage:`, error); + // Handle quota exceeded error + if (error instanceof DOMException && error.name === 'QuotaExceededError') { + console.error('[StatePersistence] localStorage quota exceeded. Clearing old data...'); + // Optionally clear old data or notify user + } + } + }, + removeItem: (key: string): void => { + try { + if (typeof window === 'undefined') { + return; + } + localStorage.removeItem(key); + } catch (error) { + console.warn(`[StatePersistence] Failed to remove item ${key} from localStorage:`, error); + } + }, + }; +}; + +/** + * Clear all persisted state for a specific store + */ +export const clearPersistedState = (storeName: string): void => { + try { + if (typeof window === 'undefined') { + return; + } + localStorage.removeItem(storeName); + } catch (error) { + console.warn(`[StatePersistence] Failed to clear persisted state for ${storeName}:`, error); + } +}; + +/** + * Get persisted state for a specific store + */ +export const getPersistedState = (storeName: string): T | null => { + try { + if (typeof window === 'undefined') { + return null; + } + const item = localStorage.getItem(storeName); + if (!item) { + return null; + } + return JSON.parse(item) as T; + } catch (error) { + console.warn(`[StatePersistence] Failed to get persisted state for ${storeName}:`, error); + return null; + } +}; + +/** + * Check if localStorage is available + */ +export const isLocalStorageAvailable = (): boolean => { + try { + if (typeof window === 'undefined') { + return false; + } + const test = '__localStorage_test__'; + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch { + return false; + } +}; + +/** + * Get storage usage information + */ +export const getStorageInfo = (): { + used: number; + available: number; + percentage: number; +} => { + try { + if (typeof window === 'undefined') { + return { used: 0, available: 0, percentage: 0 }; + } + + let used = 0; + for (const key in localStorage) { + if (localStorage.hasOwnProperty(key)) { + used += localStorage[key].length + key.length; + } + } + + // Estimate available storage (typically 5-10MB) + const available = 5 * 1024 * 1024; // 5MB estimate + const percentage = (used / available) * 100; + + return { + used, + available, + percentage: Math.min(percentage, 100), + }; + } catch { + return { used: 0, available: 0, percentage: 0 }; + } +}; +