import { useState, useEffect, useCallback } from 'react'; import { fetchAPI } from '../services/api'; interface UsePersistentToggleOptions { /** * Unique identifier for the resource (e.g., 'openai', 'runware', 'gsc') */ resourceId: string; /** * API endpoint pattern - will replace {id} with resourceId * Example: '/v1/system/settings/integrations/{id}/' */ getEndpoint: string; /** * API endpoint pattern for saving - will replace {id} with resourceId * Example: '/v1/system/settings/integrations/{id}/save/' */ saveEndpoint: string; /** * Initial enabled state (used before data loads) */ initialEnabled?: boolean; /** * Function to extract enabled state from API response * Default: (data) => data?.enabled ?? false */ extractEnabled?: (data: any) => boolean; /** * Function to build save payload * Default: (currentData, enabled) => ({ ...currentData, enabled }) */ buildPayload?: (currentData: any, enabled: boolean) => any; /** * Callback when toggle succeeds * @param enabled - The new enabled state * @param data - The full config data from the API */ onToggleSuccess?: (enabled: boolean, data?: any) => void; /** * Callback when toggle fails */ onToggleError?: (error: Error) => void; /** * Whether to load state on mount */ loadOnMount?: boolean; } interface UsePersistentToggleReturn { /** * Current enabled state */ enabled: boolean; /** * Toggle function - automatically persists to backend */ toggle: (enabled: boolean) => Promise; /** * Loading state (during save/load operations) */ loading: boolean; /** * Error state */ error: Error | null; /** * Full config data from API */ data: any; /** * Manually refresh state from API */ refresh: () => Promise; } /** * Hook for managing persistent toggle state with automatic API synchronization * * Features: * - Automatically loads state on mount * - Automatically saves state on toggle * - Handles loading and error states * - Can be used for any persistent boolean state * * @example * ```tsx * const { enabled, toggle, loading } = usePersistentToggle({ * resourceId: 'openai', * getEndpoint: '/v1/system/settings/integrations/{id}/', * saveEndpoint: '/v1/system/settings/integrations/{id}/save/', * onToggleSuccess: (enabled) => toast.success(`Integration ${enabled ? 'enabled' : 'disabled'}`), * }); * ``` */ export function usePersistentToggle( options: UsePersistentToggleOptions ): UsePersistentToggleReturn { const { resourceId, getEndpoint, saveEndpoint, initialEnabled = false, extractEnabled = (data: any) => data?.enabled ?? false, buildPayload = (currentData: any, enabled: boolean) => ({ ...currentData, enabled }), onToggleSuccess, onToggleError, loadOnMount = true, } = options; const [enabled, setEnabled] = useState(initialEnabled); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); /** * Load state from API */ const loadState = useCallback(async () => { setLoading(true); setError(null); try { const endpoint = getEndpoint.replace('{id}', resourceId); // fetchAPI extracts data from unified format {success: true, data: {...}} // So result IS the data object, not wrapped const result = await fetchAPI(endpoint); if (result && typeof result === 'object') { setData(result); const newEnabled = extractEnabled(result); setEnabled(newEnabled); } else { // No data yet - use initial state setData({}); setEnabled(initialEnabled); } } catch (err: any) { const error = err instanceof Error ? err : new Error(String(err)); setError(error); console.error(`Error loading state for ${resourceId}:`, error); // Don't throw - just log and keep initial state } finally { setLoading(false); } }, [resourceId, getEndpoint, extractEnabled, initialEnabled]); /** * Save state to API */ const saveState = useCallback(async (newEnabled: boolean) => { setLoading(true); setError(null); try { const endpoint = saveEndpoint.replace('{id}', resourceId); const payload = buildPayload(data || {}, newEnabled); // fetchAPI extracts data from unified format {success: true, data: {...}} // If no error is thrown, assume success await fetchAPI(endpoint, { method: 'POST', body: JSON.stringify(payload), }); // Update local state const updatedData = { ...(data || {}), enabled: newEnabled }; setData(updatedData); setEnabled(newEnabled); // Call success callback - pass both enabled state and full config data if (onToggleSuccess) { onToggleSuccess(newEnabled, updatedData); } } catch (err: any) { const error = err instanceof Error ? err : new Error(String(err)); setError(error); console.error(`Error saving state for ${resourceId}:`, error); // Call error callback if (onToggleError) { onToggleError(error); } // Revert to previous state on error // Don't throw - let component handle error display } finally { setLoading(false); } }, [resourceId, saveEndpoint, buildPayload, data, onToggleSuccess, onToggleError]); /** * Toggle function - automatically persists */ const toggle = useCallback(async (newEnabled: boolean) => { await saveState(newEnabled); }, [saveState]); /** * Refresh state from API */ const refresh = useCallback(async () => { await loadState(); }, [loadState]); // Load state on mount - only once, don't re-run when dependencies change useEffect(() => { if (loadOnMount) { loadState(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [loadOnMount]); // Only depend on loadOnMount, not loadState return { enabled, toggle, loading, error, data, refresh, }; }