BREAKING CHANGE: MemoEditor internal architecture completely refactored ## Summary Refactored MemoEditor from hooks-based state management to a three-layer architecture (Presentation → State → Services) using useReducer pattern. ## Changes ### Architecture - **State Layer** (5 new files): types, actions, reducer, context, barrel export - **Service Layer** (6 new files): error, validation, upload, cache, memo services + barrel - **Component Layer** (3 new files): EditorToolbar, EditorContent, EditorMetadata - **Simplified Hooks** (3 new files): useMemoInit, useAutoSave, useKeyboard ### Code Reduction - Main component: ~380 lines → ~140 lines (-63%) - Hooks removed: 5 old hooks (useMemoEditorState, useMemoSave, etc.) - Total lines removed: 508 lines of old code - Utility hooks preserved: 8 hooks still in use (useLocation, useDragAndDrop, etc.) ### Improvements - ✅ Predictable state transitions with useReducer - ✅ Testable business logic in pure service functions - ✅ Cleaner component code (presentation only) - ✅ Better separation of concerns - ✅ Type-safe actions with discriminated unions - ✅ Centralized error handling ## Statistics - Files changed: 26 - Commits created: 25 (squashed into 1) - New files: 17 - Removed files: 5 - TypeScript errors: 0 - Lint errors: 0 ## Testing Manual testing required for: - Editor functionality (create, edit, save) - Drag and drop - Focus mode - Keyboard shortcuts (Cmd/Ctrl + Enter) - Auto-save to localStorage
130 lines
2.7 KiB
TypeScript
130 lines
2.7 KiB
TypeScript
import type { EditorAction, EditorState } from "./types";
|
|
import { initialState } from "./types";
|
|
|
|
export function editorReducer(state: EditorState, action: EditorAction): EditorState {
|
|
switch (action.type) {
|
|
case "INIT_MEMO":
|
|
return {
|
|
...state,
|
|
content: action.payload.content,
|
|
metadata: action.payload.metadata,
|
|
timestamps: action.payload.timestamps,
|
|
};
|
|
|
|
case "UPDATE_CONTENT":
|
|
return {
|
|
...state,
|
|
content: action.payload,
|
|
};
|
|
|
|
case "SET_METADATA":
|
|
return {
|
|
...state,
|
|
metadata: {
|
|
...state.metadata,
|
|
...action.payload,
|
|
},
|
|
};
|
|
|
|
case "ADD_ATTACHMENT":
|
|
return {
|
|
...state,
|
|
metadata: {
|
|
...state.metadata,
|
|
attachments: [...state.metadata.attachments, action.payload],
|
|
},
|
|
};
|
|
|
|
case "REMOVE_ATTACHMENT":
|
|
return {
|
|
...state,
|
|
metadata: {
|
|
...state.metadata,
|
|
attachments: state.metadata.attachments.filter((a) => a.name !== action.payload),
|
|
},
|
|
};
|
|
|
|
case "ADD_RELATION":
|
|
return {
|
|
...state,
|
|
metadata: {
|
|
...state.metadata,
|
|
relations: [...state.metadata.relations, action.payload],
|
|
},
|
|
};
|
|
|
|
case "REMOVE_RELATION":
|
|
return {
|
|
...state,
|
|
metadata: {
|
|
...state.metadata,
|
|
relations: state.metadata.relations.filter((r) => r.relatedMemo?.name !== action.payload),
|
|
},
|
|
};
|
|
|
|
case "ADD_LOCAL_FILE":
|
|
return {
|
|
...state,
|
|
localFiles: [...state.localFiles, action.payload],
|
|
};
|
|
|
|
case "REMOVE_LOCAL_FILE":
|
|
return {
|
|
...state,
|
|
localFiles: state.localFiles.filter((f) => f.previewUrl !== action.payload),
|
|
};
|
|
|
|
case "CLEAR_LOCAL_FILES":
|
|
return {
|
|
...state,
|
|
localFiles: [],
|
|
};
|
|
|
|
case "TOGGLE_FOCUS_MODE":
|
|
return {
|
|
...state,
|
|
ui: {
|
|
...state.ui,
|
|
isFocusMode: !state.ui.isFocusMode,
|
|
},
|
|
};
|
|
|
|
case "SET_LOADING":
|
|
return {
|
|
...state,
|
|
ui: {
|
|
...state.ui,
|
|
isLoading: {
|
|
...state.ui.isLoading,
|
|
[action.payload.key]: action.payload.value,
|
|
},
|
|
},
|
|
};
|
|
|
|
case "SET_DRAGGING":
|
|
return {
|
|
...state,
|
|
ui: {
|
|
...state.ui,
|
|
isDragging: action.payload,
|
|
},
|
|
};
|
|
|
|
case "SET_COMPOSING":
|
|
return {
|
|
...state,
|
|
ui: {
|
|
...state.ui,
|
|
isComposing: action.payload,
|
|
},
|
|
};
|
|
|
|
case "RESET":
|
|
return {
|
|
...initialState,
|
|
};
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
}
|