V2 Restructure homepage (#4138)

Component Extraction & Context Refactor - Summary

  🔧 What We Did

- Extracted HomePage's 286-line monolithic component into focused parts
  - Created ToolPanel (105 lines) for tool selection UI
  - Created Workbench (203 lines) for view management
  - Created ToolWorkflowContext (220 lines) for centralized state
  - Reduced HomePage to 60 lines of provider setup
  - Eliminated all prop drilling - components use contexts directly

  🏆 Why This is Good

- Maintainability: Each component has single purpose, easy
debugging/development
- Architecture: Clean separation of concerns, future features easier to
add
- Code Quality: 105% more lines but organized/purposeful vs tangled
spaghetti code

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
ConnorYoh
2025-08-08 15:56:20 +01:00
committed by GitHub
parent 7735ea7f48
commit b45d3a43d4
7 changed files with 503 additions and 288 deletions

View File

@@ -0,0 +1,221 @@
/**
* ToolWorkflowContext - Manages tool selection, UI state, and workflow coordination
* Eliminates prop drilling with a single, simple context
*/
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
import { useToolManagement } from '../hooks/useToolManagement';
import { ToolConfiguration } from '../types/tool';
import { PageEditorFunctions } from '../types/pageEditor';
// State interface
interface ToolWorkflowState {
// UI State
sidebarsVisible: boolean;
leftPanelView: 'toolPicker' | 'toolContent';
readerMode: boolean;
// File/Preview State
previewFile: File | null;
pageEditorFunctions: PageEditorFunctions | null;
// Search State
searchQuery: string;
}
// Actions
type ToolWorkflowAction =
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' }
| { type: 'SET_READER_MODE'; payload: boolean }
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
| { type: 'SET_SEARCH_QUERY'; payload: string }
| { type: 'RESET_UI_STATE' };
// Initial state
const initialState: ToolWorkflowState = {
sidebarsVisible: true,
leftPanelView: 'toolPicker',
readerMode: false,
previewFile: null,
pageEditorFunctions: null,
searchQuery: '',
};
// Reducer
function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState {
switch (action.type) {
case 'SET_SIDEBARS_VISIBLE':
return { ...state, sidebarsVisible: action.payload };
case 'SET_LEFT_PANEL_VIEW':
return { ...state, leftPanelView: action.payload };
case 'SET_READER_MODE':
return { ...state, readerMode: action.payload };
case 'SET_PREVIEW_FILE':
return { ...state, previewFile: action.payload };
case 'SET_PAGE_EDITOR_FUNCTIONS':
return { ...state, pageEditorFunctions: action.payload };
case 'SET_SEARCH_QUERY':
return { ...state, searchQuery: action.payload };
case 'RESET_UI_STATE':
return { ...initialState, searchQuery: state.searchQuery }; // Preserve search
default:
return state;
}
}
// Context value interface
interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook)
selectedToolKey: string | null;
selectedTool: ToolConfiguration | null;
toolRegistry: any; // From useToolManagement
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
setReaderMode: (mode: boolean) => void;
setPreviewFile: (file: File | null) => void;
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
setSearchQuery: (query: string) => void;
// Tool Actions
selectTool: (toolId: string) => void;
clearToolSelection: () => void;
// Workflow Actions (compound actions)
handleToolSelect: (toolId: string) => void;
handleBackToTools: () => void;
handleReaderToggle: () => void;
// Computed values
filteredTools: [string, any][]; // Filtered by search
isPanelVisible: boolean;
}
const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(undefined);
// Provider component
interface ToolWorkflowProviderProps {
children: React.ReactNode;
/** Handler for view changes (passed from parent) */
onViewChange?: (view: string) => void;
}
export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
// Tool management hook
const {
selectedToolKey,
selectedTool,
toolRegistry,
selectTool,
clearToolSelection,
} = useToolManagement();
// UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => {
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
}, []);
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => {
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
}, []);
const setReaderMode = useCallback((mode: boolean) => {
dispatch({ type: 'SET_READER_MODE', payload: mode });
}, []);
const setPreviewFile = useCallback((file: File | null) => {
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
}, []);
const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => {
dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions });
}, []);
const setSearchQuery = useCallback((query: string) => {
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
}, []);
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => {
selectTool(toolId);
onViewChange?.('fileEditor');
setLeftPanelView('toolContent');
setReaderMode(false);
}, [selectTool, onViewChange, setLeftPanelView, setReaderMode]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
setReaderMode(false);
clearToolSelection();
}, [setLeftPanelView, setReaderMode, clearToolSelection]);
const handleReaderToggle = useCallback(() => {
setReaderMode(true);
}, [setReaderMode]);
// Filter tools based on search query
const filteredTools = useMemo(() => {
if (!toolRegistry) return [];
return Object.entries(toolRegistry).filter(([_, { name }]) =>
name.toLowerCase().includes(state.searchQuery.toLowerCase())
);
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
state.sidebarsVisible && !state.readerMode,
[state.sidebarsVisible, state.readerMode]
);
// Simple context value with basic memoization
const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State
...state,
selectedToolKey,
selectedTool,
toolRegistry,
// Actions
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
selectTool,
clearToolSelection,
// Workflow Actions
handleToolSelect,
handleBackToTools,
handleReaderToggle,
// Computed
filteredTools,
isPanelVisible,
}), [state, selectedToolKey, selectedTool, toolRegistry, filteredTools, isPanelVisible]);
return (
<ToolWorkflowContext.Provider value={contextValue}>
{children}
</ToolWorkflowContext.Provider>
);
}
// Custom hook to use the context
export function useToolWorkflow(): ToolWorkflowContextValue {
const context = useContext(ToolWorkflowContext);
if (!context) {
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
}
return context;
}
// Convenience exports for specific use cases (optional - components can use useToolWorkflow directly)
export const useToolSelection = useToolWorkflow;
export const useToolPanelState = useToolWorkflow;
export const useWorkbenchState = useToolWorkflow;