/** * Email Customizer Hook * Manages state for email customization settings with undo/redo support */ import { useState, useCallback, useEffect, useRef } from 'react'; import type { EmailCustomizerSettings, DevicePreview, } from '../types'; import { defaultEmailCustomizerSettings } from '../types'; declare const wp: { apiFetch: (options: { path: string; method?: string; data?: unknown; }) => Promise; }; /** Default API base path for email customizer settings. */ const DEFAULT_API_BASE = 'swift-commerce/v1/email-customizer'; interface UseEmailCustomizerOptions { /** * Override the REST API base path. * Defaults to 'swift-commerce/v1/email-customizer'. */ apiBasePath?: string; } interface UseEmailCustomizerReturn { settings: EmailCustomizerSettings; updateSettings: (updates: Partial) => void; updateNestedSettings: ( section: K, updates: Partial ) => void; devicePreview: DevicePreview; setDevicePreview: (device: DevicePreview) => void; darkModePreview: boolean; setDarkModePreview: (enabled: boolean) => void; isLoading: boolean; isSaving: boolean; saveSettings: () => Promise; resetToDefaults: () => void; sendTestEmail: (emailAddress: string, emailType: string) => Promise; isSendingTest: boolean; hasChanges: boolean; // Undo/Redo undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; } export function useEmailCustomizer( options: UseEmailCustomizerOptions = {} ): UseEmailCustomizerReturn { const apiBasePath = options.apiBasePath || DEFAULT_API_BASE; const [settings, setSettings] = useState( defaultEmailCustomizerSettings ); const [originalSettings, setOriginalSettings] = useState( defaultEmailCustomizerSettings ); const [devicePreview, setDevicePreview] = useState('desktop'); const [darkModePreview, setDarkModePreview] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isSendingTest, setIsSendingTest] = useState(false); // History for undo/redo const historyRef = useRef([]); const futureRef = useRef([]); const isUndoRedoRef = useRef(false); const maxHistory = 30; // Load settings on mount useEffect(() => { const loadSettings = async () => { try { const response = await wp.apiFetch<{ success: boolean; data: Partial; }>({ path: apiBasePath, method: 'GET', }); if (response.success && response.data) { // Merge with defaults to ensure all fields exist const mergedSettings = deepMerge( defaultEmailCustomizerSettings, response.data ); setSettings(mergedSettings); setOriginalSettings(mergedSettings); } } catch (error) { console.error('Failed to load email customizer settings:', error); } finally { setIsLoading(false); } }; loadSettings(); }, []); // Check if there are unsaved changes const hasChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings); // Helper to add to history const addToHistory = useCallback((currentSettings: EmailCustomizerSettings) => { if (!isUndoRedoRef.current) { historyRef.current = [...historyRef.current, currentSettings].slice(-maxHistory); futureRef.current = []; // Clear redo stack on new change } }, []); // Undo const undo = useCallback(() => { if (historyRef.current.length === 0) return; isUndoRedoRef.current = true; const previous = historyRef.current[historyRef.current.length - 1]; historyRef.current = historyRef.current.slice(0, -1); setSettings((current) => { futureRef.current = [current, ...futureRef.current]; return previous; }); setTimeout(() => { isUndoRedoRef.current = false; }, 0); }, []); // Redo const redo = useCallback(() => { if (futureRef.current.length === 0) return; isUndoRedoRef.current = true; const next = futureRef.current[0]; futureRef.current = futureRef.current.slice(1); setSettings((current) => { historyRef.current = [...historyRef.current, current]; return next; }); setTimeout(() => { isUndoRedoRef.current = false; }, 0); }, []); const canUndo = historyRef.current.length > 0; const canRedo = futureRef.current.length > 0; // Update entire settings object const updateSettings = useCallback((updates: Partial) => { setSettings((prev) => { addToHistory(prev); return { ...prev, ...updates }; }); }, [addToHistory]); // Update nested settings (e.g., colors.headerBackground) const updateNestedSettings = useCallback( ( section: K, updates: Partial ) => { setSettings((prev) => { addToHistory(prev); const currentSection = prev[section]; const updatedSection = typeof currentSection === 'object' && currentSection !== null ? { ...currentSection, ...updates } : updates; return { ...prev, [section]: updatedSection, }; }); }, [addToHistory] ); // Save settings to backend const saveSettings = useCallback(async () => { setIsSaving(true); try { await wp.apiFetch({ path: apiBasePath, method: 'POST', data: settings, }); setOriginalSettings(settings); } catch (error) { console.error('Failed to save email customizer settings:', error); throw error; } finally { setIsSaving(false); } }, [settings]); // Reset to defaults const resetToDefaults = useCallback(() => { addToHistory(settings); setSettings(defaultEmailCustomizerSettings); // Clear history on reset historyRef.current = []; futureRef.current = []; }, [settings, addToHistory]); // Send test email const sendTestEmail = useCallback( async (emailAddress: string, emailType: string) => { setIsSendingTest(true); try { await wp.apiFetch({ path: `${apiBasePath}/test`, method: 'POST', data: { email: emailAddress, emailType, settings, // Send current settings for preview }, }); } catch (error) { console.error('Failed to send test email:', error); throw error; } finally { setIsSendingTest(false); } }, [settings] ); return { settings, updateSettings, updateNestedSettings, devicePreview, setDevicePreview, darkModePreview, setDarkModePreview, isLoading, isSaving, saveSettings, resetToDefaults, sendTestEmail, isSendingTest, hasChanges, // Undo/Redo undo, redo, canUndo, canRedo, }; } // Helper function for deep merging objects function deepMerge( target: T, source: Partial ): T { if (!source) return target; const result = { ...target } as T; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { const sourceValue = source[key as keyof Partial]; const targetValue = target[key as keyof T]; if ( sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue) && targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue) ) { (result as Record)[key] = deepMerge( targetValue as Record, sourceValue as Record ); } else if (sourceValue !== undefined) { (result as Record)[key] = sourceValue; } } } return result; } export default useEmailCustomizer;