/** * Section Editor Context * Provides state management for the section editor with real-time preview */ "use client"; import React, { createContext, useContext, useCallback, useReducer, useMemo, useEffect, } from "react"; import { EditorState, EditorConfig, BaseSection, EditorEvent, EditorEventHandler, ClipboardData, HistoryEntry, DEFAULT_EDITOR_CONFIG, } from "./types"; // Action Types type EditorActionType = | { type: "SET_SECTIONS"; payload: BaseSection[] } | { type: "ADD_SECTION"; payload: { section: BaseSection; index?: number } } | { type: "UPDATE_SECTION"; payload: { sectionId: string; updates: Partial }; } | { type: "DELETE_SECTION"; payload: string } | { type: "MOVE_SECTION"; payload: { sectionId: string; newIndex: number } } | { type: "DUPLICATE_SECTION"; payload: { sectionId: string; newIndex?: number }; } | { type: "SELECT_SECTION"; payload: string | null } | { type: "SELECT_ELEMENT"; payload: string | null } | { type: "SET_CONFIG"; payload: EditorConfig } | { type: "UPDATE_CONFIG"; payload: Partial } | { type: "SET_CLIPBOARD"; payload: ClipboardData | null } | { type: "ADD_HISTORY"; payload: HistoryEntry } | { type: "UNDO" } | { type: "REDO" } | { type: "SET_DIRTY"; payload: boolean } | { type: "SET_LOADING"; payload: boolean } | { type: "SET_ERROR"; payload: string | null }; // Initial State const initialState: EditorState = { sections: [], selectedSection: null, selectedElement: null, clipboard: null, history: [], historyIndex: -1, isDirty: false, isLoading: false, error: null, }; // Reducer function editorReducer( state: EditorState, action: EditorActionType ): EditorState { switch (action.type) { case "SET_SECTIONS": return { ...state, sections: action.payload, isDirty: false, }; case "ADD_SECTION": { const { section, index = state.sections.length } = action.payload; const newSections = [...state.sections]; newSections.splice(index, 0, section); // Update order for all sections const orderedSections = newSections.map((s, i) => ({ ...s, order: i })); return { ...state, sections: orderedSections, selectedSection: section.id, isDirty: true, }; } case "UPDATE_SECTION": { const { sectionId, updates } = action.payload; const sections = state.sections.map((section) => section.id === sectionId ? { ...section, ...updates, metadata: { ...section.metadata, updatedAt: new Date() }, } : section ); return { ...state, sections, isDirty: true, }; } case "DELETE_SECTION": { const sections = state.sections .filter((section) => section.id !== action.payload) .map((section, index) => ({ ...section, order: index })); return { ...state, sections, selectedSection: state.selectedSection === action.payload ? null : state.selectedSection, isDirty: true, }; } case "MOVE_SECTION": { const { sectionId, newIndex } = action.payload; const sections = [...state.sections]; const currentIndex = sections.findIndex((s) => s.id === sectionId); if (currentIndex === -1) return state; const [movedSection] = sections.splice(currentIndex, 1); sections.splice(newIndex, 0, movedSection); // Update order for all sections const orderedSections = sections.map((s, i) => ({ ...s, order: i })); return { ...state, sections: orderedSections, isDirty: true, }; } case "DUPLICATE_SECTION": { const { sectionId, newIndex } = action.payload; const originalSection = state.sections.find((s) => s.id === sectionId); if (!originalSection) return state; const duplicatedSection: BaseSection = { ...originalSection, id: `${originalSection.id}_copy_${Date.now()}`, name: `${originalSection.name} (Copy)`, metadata: { ...originalSection.metadata, createdAt: new Date(), updatedAt: new Date(), }, }; const insertIndex = newIndex ?? originalSection.order + 1; const newSections = [...state.sections]; newSections.splice(insertIndex, 0, duplicatedSection); // Update order for all sections const orderedSections = newSections.map((s, i) => ({ ...s, order: i })); return { ...state, sections: orderedSections, selectedSection: duplicatedSection.id, isDirty: true, }; } case "SELECT_SECTION": return { ...state, selectedSection: action.payload, selectedElement: null, }; case "SELECT_ELEMENT": return { ...state, selectedElement: action.payload, }; case "SET_CONFIG": return { ...state, config: action.payload, }; case "UPDATE_CONFIG": return { ...state, config: { ...state.config, ...action.payload }, }; case "SET_CLIPBOARD": return { ...state, clipboard: action.payload, }; case "ADD_HISTORY": { const newHistory = [ ...state.history.slice(0, state.historyIndex + 1), action.payload, ]; // Limit history size const maxHistory = 50; // You can make this configurable if (newHistory.length > maxHistory) { newHistory.shift(); } return { ...state, history: newHistory, historyIndex: newHistory.length - 1, }; } case "UNDO": { if (state.historyIndex < 0) return state; const historyEntry = state.history[state.historyIndex]; let newSections = [...state.sections]; // Apply undo based on action type switch (historyEntry.action) { case "add": newSections = newSections.filter( (s) => s.id !== (historyEntry.after as BaseSection).id ); break; case "update": newSections = newSections.map((s) => s.id === (historyEntry.before as BaseSection).id ? (historyEntry.before as BaseSection) : s ); break; case "delete": // Note: This is simplified - in real implementation you'd need to restore position newSections.push(historyEntry.before as BaseSection); break; } return { ...state, sections: newSections, historyIndex: state.historyIndex - 1, isDirty: true, }; } case "REDO": { if (state.historyIndex >= state.history.length - 1) return state; const historyEntry = state.history[state.historyIndex + 1]; let newSections = [...state.sections]; // Apply redo based on action type switch (historyEntry.action) { case "add": newSections.push(historyEntry.after as BaseSection); break; case "update": newSections = newSections.map((s) => s.id === (historyEntry.after as BaseSection).id ? (historyEntry.after as BaseSection) : s ); break; case "delete": newSections = newSections.filter( (s) => s.id !== (historyEntry.before as BaseSection).id ); break; } return { ...state, sections: newSections, historyIndex: state.historyIndex + 1, isDirty: true, }; } case "SET_DIRTY": return { ...state, isDirty: action.payload, }; case "SET_LOADING": return { ...state, isLoading: action.payload, }; case "SET_ERROR": return { ...state, error: action.payload, }; default: return state; } } // Context interface EditorContextValue { // State state: EditorState; config: EditorConfig; // Section management setSections: (sections: BaseSection[]) => void; addSection: (section: BaseSection, index?: number) => void; updateSection: (sectionId: string, updates: Partial) => void; deleteSection: (sectionId: string) => void; moveSection: (sectionId: string, newIndex: number) => void; duplicateSection: (sectionId: string, newIndex?: number) => void; // Selection selectSection: (sectionId: string | null) => void; selectElement: (elementId: string | null) => void; // Configuration setConfig: (config: EditorConfig) => void; updateConfig: (updates: Partial) => void; // Clipboard copySection: (sectionId: string) => void; pasteSection: (index?: number) => void; // History undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; // Utility setDirty: (dirty: boolean) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; // Events emitEvent: (event: Omit) => void; addEventListener: (handler: EditorEventHandler) => () => void; } const EditorContext = createContext(null); // Provider Props interface EditorProviderProps { children: React.ReactNode; initialSections?: BaseSection[]; initialConfig?: Partial; onSectionsChange?: (sections: BaseSection[]) => void; onConfigChange?: (config: EditorConfig) => void; onEvent?: EditorEventHandler; } // Provider Component export function EditorProvider({ children, initialSections = [], initialConfig = {}, onSectionsChange, onConfigChange, onEvent, }: EditorProviderProps) { const [state, dispatch] = useReducer(editorReducer, { ...initialState, sections: initialSections, }); const [config, setConfigState] = React.useState({ ...DEFAULT_EDITOR_CONFIG, ...initialConfig, }); const [eventHandlers, setEventHandlers] = React.useState< EditorEventHandler[] >([]); // Section management const setSections = useCallback((sections: BaseSection[]) => { dispatch({ type: "SET_SECTIONS", payload: sections }); }, []); const addSection = useCallback((section: BaseSection, index?: number) => { const historyEntry: HistoryEntry = { id: `add_${Date.now()}`, action: "add", target: "section", before: null, after: section, timestamp: new Date(), }; dispatch({ type: "ADD_SECTION", payload: { section, index } }); dispatch({ type: "ADD_HISTORY", payload: historyEntry }); }, []); const updateSection = useCallback( (sectionId: string, updates: Partial) => { const currentSection = state.sections.find((s) => s.id === sectionId); if (!currentSection) return; const historyEntry: HistoryEntry = { id: `update_${Date.now()}`, action: "update", target: "section", before: currentSection, after: { ...currentSection, ...updates }, timestamp: new Date(), }; dispatch({ type: "UPDATE_SECTION", payload: { sectionId, updates } }); dispatch({ type: "ADD_HISTORY", payload: historyEntry }); }, [state.sections] ); const deleteSection = useCallback( (sectionId: string) => { const currentSection = state.sections.find((s) => s.id === sectionId); if (!currentSection) return; const historyEntry: HistoryEntry = { id: `delete_${Date.now()}`, action: "delete", target: "section", before: currentSection, after: null, timestamp: new Date(), }; dispatch({ type: "DELETE_SECTION", payload: sectionId }); dispatch({ type: "ADD_HISTORY", payload: historyEntry }); }, [state.sections] ); const moveSection = useCallback((sectionId: string, newIndex: number) => { dispatch({ type: "MOVE_SECTION", payload: { sectionId, newIndex } }); }, []); const duplicateSection = useCallback( (sectionId: string, newIndex?: number) => { dispatch({ type: "DUPLICATE_SECTION", payload: { sectionId, newIndex } }); }, [] ); // Selection const selectSection = useCallback((sectionId: string | null) => { dispatch({ type: "SELECT_SECTION", payload: sectionId }); }, []); const selectElement = useCallback((elementId: string | null) => { dispatch({ type: "SELECT_ELEMENT", payload: elementId }); }, []); // Configuration const setConfig = useCallback( (newConfig: EditorConfig) => { setConfigState(newConfig); onConfigChange?.(newConfig); }, [onConfigChange] ); const updateConfig = useCallback( (updates: Partial) => { const newConfig = { ...config, ...updates }; setConfigState(newConfig); onConfigChange?.(newConfig); }, [config, onConfigChange] ); // Clipboard const copySection = useCallback( (sectionId: string) => { const section = state.sections.find((s) => s.id === sectionId); if (!section) return; const clipboardData: ClipboardData = { type: "section", data: section, timestamp: new Date(), }; dispatch({ type: "SET_CLIPBOARD", payload: clipboardData }); }, [state.sections] ); const pasteSection = useCallback( (index?: number) => { if (!state.clipboard || state.clipboard.type !== "section") return; const originalSection = state.clipboard.data as BaseSection; const newSection: BaseSection = { ...originalSection, id: `${originalSection.id}_paste_${Date.now()}`, name: `${originalSection.name} (Pasted)`, metadata: { ...originalSection.metadata, createdAt: new Date(), updatedAt: new Date(), }, }; addSection(newSection, index); }, [state.clipboard, addSection] ); // History const undo = useCallback(() => { dispatch({ type: "UNDO" }); }, []); const redo = useCallback(() => { dispatch({ type: "REDO" }); }, []); const canUndo = state.historyIndex >= 0; const canRedo = state.historyIndex < state.history.length - 1; // Utility const setDirty = useCallback((dirty: boolean) => { dispatch({ type: "SET_DIRTY", payload: dirty }); }, []); const setLoading = useCallback((loading: boolean) => { dispatch({ type: "SET_LOADING", payload: loading }); }, []); const setError = useCallback((error: string | null) => { dispatch({ type: "SET_ERROR", payload: error }); }, []); // Events const emitEvent = useCallback( (event: Omit) => { const fullEvent: EditorEvent = { ...event, timestamp: new Date(), }; eventHandlers.forEach((handler) => handler(fullEvent)); onEvent?.(fullEvent); }, [eventHandlers, onEvent] ); const addEventListener = useCallback( (handler: EditorEventHandler): (() => void) => { setEventHandlers((prev) => [...prev, handler]); return () => { setEventHandlers((prev) => prev.filter((h) => h !== handler)); }; }, [] ); // Effects useEffect(() => { onSectionsChange?.(state.sections); }, [state.sections, onSectionsChange]); // Auto-save useEffect(() => { if (!config.autoSave || !state.isDirty) return; const timeoutId = setTimeout(() => { // Auto-save logic would go here console.log("Auto-saving sections...", state.sections); setDirty(false); }, config.autoSaveInterval); return () => clearTimeout(timeoutId); }, [ state.sections, state.isDirty, config.autoSave, config.autoSaveInterval, setDirty, ]); // Context value const contextValue: EditorContextValue = useMemo( () => ({ state, config, setSections, addSection, updateSection, deleteSection, moveSection, duplicateSection, selectSection, selectElement, setConfig, updateConfig, copySection, pasteSection, undo, redo, canUndo, canRedo, setDirty, setLoading, setError, emitEvent, addEventListener, }), [ state, config, setSections, addSection, updateSection, deleteSection, moveSection, duplicateSection, selectSection, selectElement, setConfig, updateConfig, copySection, pasteSection, undo, redo, canUndo, canRedo, setDirty, setLoading, setError, emitEvent, addEventListener, ] ); return ( {children} ); } // Hook export function useEditor(): EditorContextValue { const context = useContext(EditorContext); if (!context) { throw new Error("useEditor must be used within an EditorProvider"); } return context; } export default EditorProvider;