docs(worksheets): add comprehensive refactoring plan for AdditionWorksheetClient

- Break down 971-line component into focused pieces
- Extract 3 UI components (OrientationPanel, GenerateButton, ErrorDisplay)
- Extract 3 custom hooks (useWorksheetState, useWorksheetGeneration, useWorksheetAutoSave)
- Extract 2 utility modules (dateFormatting, layoutCalculations)
- Target: Reduce main component from 971 → ~100 lines
- Includes implementation steps, testing strategy, migration checklist
- Estimated timeline: 8-12 hours of focused work
This commit is contained in:
Thomas Hallock
2025-11-07 17:03:01 -06:00
parent 369b7f263d
commit f2e48bb8ab

View File

@@ -0,0 +1,505 @@
# AdditionWorksheetClient Refactoring Plan
## Problem Statement
`AdditionWorksheetClient.tsx` has grown to **971 lines** and contains multiple concerns:
- State management (form state, debounced state, generation status, auto-save)
- UI layout (orientation panel, generate button, preview)
- Business logic (validation, PDF generation, settings persistence)
- Helper functions (date formatting, column calculations)
This violates the Single Responsibility Principle and makes the component hard to maintain, test, and reason about.
## Goals
1. **Reduce file size** to under 300 lines
2. **Separate concerns** into focused, reusable components
3. **Improve testability** by isolating logic from UI
4. **Maintain existing functionality** without breaking changes
5. **Enable easier future enhancements** (e.g., new worksheet types)
## Current Component Structure
```
AdditionWorksheetClient (971 lines)
├── State Management
│ ├── formState (immediate updates)
│ ├── debouncedFormState (delayed preview updates)
│ ├── generationStatus ('idle' | 'generating' | 'error')
│ ├── error (string | null)
│ ├── lastSaved (Date | null)
│ └── isSaving (boolean)
├── Effects
│ ├── Debounce preview updates (500ms)
│ ├── Auto-save settings (1000ms)
│ └── Debug logging
├── Handlers
│ ├── handleFormChange (with seed regeneration logic)
│ ├── handleGenerate (validation, API call, PDF download)
│ └── handleNewGeneration (reset error state)
├── Helper Functions
│ ├── getDefaultDate() (date formatting)
│ └── getDefaultColsForProblemsPerPage() (layout calculations)
└── UI Sections
├── ConfigPanel (left column)
├── Orientation Panel (right column, top)
├── Generate Button (right column, middle)
└── WorksheetPreview (right column, bottom)
```
## Proposed Component Breakdown
### Phase 1: Extract UI Components (Low Risk)
#### 1.1 Extract `OrientationPanel.tsx`
**Lines to extract**: 392-828 (437 lines)
**Responsibility**: Orientation, pages, problems per page controls
**Props**:
```typescript
interface OrientationPanelProps {
orientation: 'portrait' | 'landscape'
problemsPerPage: number
pages: number
cols: number
onOrientationChange: (orientation: 'portrait' | 'landscape') => void
onProblemsPerPageChange: (count: number) => void
onPagesChange: (pages: number) => void
}
```
**Extracted logic**:
- `getDefaultColsForProblemsPerPage()` helper → move to this component
- Orientation button click handlers
- Pages button click handlers
- Radix dropdown with grid visualizations
- Total problems badge calculation
**Benefits**:
- Removes 437 lines from main component
- Encapsulates all orientation/layout controls
- Easier to test grid layout logic
- Reusable for other worksheet types
#### 1.2 Extract `GenerateButton.tsx`
**Lines to extract**: 830-891 (62 lines)
**Responsibility**: Trigger worksheet generation
**Props**:
```typescript
interface GenerateButtonProps {
status: 'idle' | 'generating' | 'error'
onGenerate: () => void
}
```
**Benefits**:
- Removes 62 lines from main component
- Cleaner separation of generation UI from logic
- Easier to add loading states, progress indicators
#### 1.3 Extract `GenerationErrorDisplay.tsx`
**Lines to extract**: 908-965 (58 lines)
**Responsibility**: Show generation errors
**Props**:
```typescript
interface GenerationErrorDisplayProps {
error: string | null
visible: boolean
onRetry: () => void
}
```
**Benefits**:
- Removes 58 lines from main component
- Encapsulates error UI
- Reusable for other error scenarios
### Phase 2: Extract Business Logic (Medium Risk)
#### 2.1 Create `useWorksheetState.ts` Hook
**Lines to extract**: 46-231 (186 lines)
**Responsibility**: Manage worksheet state with debouncing and seed regeneration
**Interface**:
```typescript
interface UseWorksheetStateReturn {
formState: WorksheetFormState
debouncedFormState: WorksheetFormState
updateFormState: (updates: Partial<WorksheetFormState>) => void
}
function useWorksheetState(
initialSettings: Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
): UseWorksheetStateReturn
```
**Extracted logic**:
- Form state initialization with derived calculations (rows, total)
- Debounced state for preview updates (500ms delay)
- Seed regeneration when problem settings change
- StrictMode double-render handling with refs
**Benefits**:
- Removes 186 lines from main component
- Separates state management from UI
- Easier to test state transitions
- Can be reused for other worksheet types
#### 2.2 Create `useWorksheetGeneration.ts` Hook
**Lines to extract**: 256-315 (60 lines)
**Responsibility**: Handle PDF generation workflow
**Interface**:
```typescript
interface UseWorksheetGenerationReturn {
status: 'idle' | 'generating' | 'error'
error: string | null
generate: (config: WorksheetFormState) => Promise<void>
reset: () => void
}
function useWorksheetGeneration(): UseWorksheetGenerationReturn
```
**Extracted logic**:
- Generation status state ('idle', 'generating', 'error')
- Error state management
- Validation before generation
- API call to `/api/create/worksheets/addition`
- PDF blob download logic
- Error handling
**Benefits**:
- Removes 60 lines from main component
- Encapsulates generation workflow
- Easier to test API interactions
- Can add retry logic, progress tracking
#### 2.3 Create `useWorksheetAutoSave.ts` Hook
**Lines to extract**: 122-209 (88 lines)
**Responsibility**: Auto-save worksheet settings to server
**Interface**:
```typescript
interface UseWorksheetAutoSaveReturn {
isSaving: boolean
lastSaved: Date | null
}
function useWorksheetAutoSave(
formState: WorksheetFormState,
worksheetType: 'addition'
): UseWorksheetAutoSaveReturn
```
**Extracted logic**:
- Auto-save timer (1000ms debounce)
- Settings persistence API call
- Save status tracking (isSaving, lastSaved)
- StrictMode double-render handling
- Silent error handling
**Benefits**:
- Removes 88 lines from main component
- Separates persistence concerns
- Easier to test auto-save behavior
- Can be reused for other worksheet types
### Phase 3: Extract Utility Functions (Low Risk)
#### 3.1 Create `src/app/create/worksheets/addition/utils/dateFormatting.ts`
**Lines to extract**: 18-27 (10 lines)
**Exports**:
```typescript
export function getDefaultDate(): string
```
**Benefits**:
- Removes 10 lines from main component
- Reusable date formatting utility
- Easier to test date formatting
- Centralized date formatting logic
#### 3.2 Create `src/app/create/worksheets/addition/utils/layoutCalculations.ts`
**Lines to extract**: 233-254 (22 lines)
**Exports**:
```typescript
export function getDefaultColsForProblemsPerPage(
problemsPerPage: number,
orientation: 'portrait' | 'landscape'
): number
export function calculateDerivedState(
problemsPerPage: number,
pages: number,
cols: number
): { rows: number; total: number }
```
**Benefits**:
- Removes 22 lines from main component
- Encapsulates layout calculation logic
- Easier to test grid calculations
- Can add more layout utilities
### Phase 4: Simplified Main Component
After all extractions, `AdditionWorksheetClient.tsx` should be:
```typescript
'use client'
import { useTranslations } from 'next-intl'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../../../../styled-system/css'
import { container, grid, stack } from '../../../../../../styled-system/patterns'
import { ConfigPanel } from './ConfigPanel'
import { WorksheetPreview } from './WorksheetPreview'
import { OrientationPanel } from './OrientationPanel'
import { GenerateButton } from './GenerateButton'
import { GenerationErrorDisplay } from './GenerationErrorDisplay'
import { useWorksheetState } from '../hooks/useWorksheetState'
import { useWorksheetGeneration } from '../hooks/useWorksheetGeneration'
import { useWorksheetAutoSave } from '../hooks/useWorksheetAutoSave'
import { getDefaultDate } from '../utils/dateFormatting'
import type { WorksheetFormState } from '../types'
interface AdditionWorksheetClientProps {
initialSettings: Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
initialPreview?: string[]
}
export function AdditionWorksheetClient({
initialSettings,
initialPreview,
}: AdditionWorksheetClientProps) {
const t = useTranslations('create.worksheets.addition')
// State management (formState, debouncedFormState, updateFormState)
const { formState, debouncedFormState, updateFormState } = useWorksheetState(initialSettings)
// Generation workflow (status, error, generate, reset)
const { status, error, generate, reset } = useWorksheetGeneration()
// Auto-save (isSaving, lastSaved)
const { isSaving, lastSaved } = useWorksheetAutoSave(formState, 'addition')
// Generate handler with date injection
const handleGenerate = async () => {
await generate({
...formState,
date: getDefaultDate(),
})
}
return (
<PageWithNav navTitle={t('navTitle')} navEmoji="📝">
<div data-component="addition-worksheet-page" className={css({ minHeight: '100vh', bg: 'gray.50' })}>
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
{/* Header */}
<div className={stack({ gap: '6', mb: '8' })}>
<div className={stack({ gap: '2', textAlign: 'center' })}>
<h1 className={css({ fontSize: '3xl', fontWeight: 'bold', color: 'gray.900' })}>
{t('pageTitle')}
</h1>
<p className={css({ fontSize: 'lg', color: 'gray.600' })}>
{t('pageSubtitle')}
</p>
</div>
</div>
{/* Two-column layout */}
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8', alignItems: 'start' })}>
{/* Left column: ConfigPanel */}
<div className={stack({ gap: '3' })}>
<div data-section="config-panel" className={css({ bg: 'white', rounded: '2xl', shadow: 'card', p: '8' })}>
<ConfigPanel formState={formState} onChange={updateFormState} />
</div>
{/* Settings saved indicator */}
<div data-element="settings-status" className={css({ fontSize: 'sm', color: 'gray.600', textAlign: 'center', py: '2' })}>
{isSaving ? (
<span className={css({ color: 'gray.500' })}>Saving settings...</span>
) : lastSaved ? (
<span className={css({ color: 'green.600' })}>
Settings saved at {lastSaved.toLocaleTimeString()}
</span>
) : null}
</div>
</div>
{/* Right column: Orientation, Generate, Preview */}
<div className={stack({ gap: '8' })}>
<OrientationPanel
orientation={formState.orientation || 'portrait'}
problemsPerPage={formState.problemsPerPage || 15}
pages={formState.pages || 1}
cols={formState.cols || 3}
onOrientationChange={(orientation) => updateFormState({ orientation })}
onProblemsPerPageChange={(problemsPerPage) => updateFormState({ problemsPerPage })}
onPagesChange={(pages) => updateFormState({ pages })}
/>
<GenerateButton status={status} onGenerate={handleGenerate} />
<div data-section="preview-panel" className={css({ bg: 'white', rounded: '2xl', shadow: 'card', p: '6' })}>
<WorksheetPreview formState={debouncedFormState} initialData={initialPreview} />
</div>
</div>
</div>
{/* Error Display */}
<GenerationErrorDisplay error={error} visible={status === 'error'} onRetry={reset} />
</div>
</div>
</PageWithNav>
)
}
```
**New size**: ~100 lines (down from 971 lines)
## Implementation Steps
### Step 1: Create Directory Structure
```bash
src/app/create/worksheets/addition/
├── components/
│ ├── AdditionWorksheetClient.tsx (main component)
│ ├── ConfigPanel.tsx (existing)
│ ├── WorksheetPreview.tsx (existing)
│ ├── OrientationPanel.tsx (NEW)
│ ├── GenerateButton.tsx (NEW)
│ └── GenerationErrorDisplay.tsx (NEW)
├── hooks/
│ ├── useWorksheetState.ts (NEW)
│ ├── useWorksheetGeneration.ts (NEW)
│ └── useWorksheetAutoSave.ts (NEW)
└── utils/
├── dateFormatting.ts (NEW)
└── layoutCalculations.ts (NEW)
```
### Step 2: Extract in Order (Safest → Riskiest)
1. **Extract utilities** (dateFormatting, layoutCalculations)
- Low risk: Pure functions, easy to test
- No state dependencies
2. **Extract UI components** (OrientationPanel, GenerateButton, GenerationErrorDisplay)
- Low risk: Presentational components
- Props clearly defined
3. **Extract hooks** (useWorksheetState, useWorksheetGeneration, useWorksheetAutoSave)
- Medium risk: Contains business logic
- Requires careful state handling
4. **Refactor main component** (AdditionWorksheetClient)
- Low risk: Just wiring up extracted pieces
- Should be straightforward
### Step 3: Testing Strategy
For each extraction:
1. Create new file
2. Move code to new file
3. Update imports in main component
4. Run `npm run type-check` to verify TypeScript
5. Run `npm run lint` to verify code quality
6. Manually test in browser
7. Commit with clear message
**DO NOT** commit all changes at once. Each extraction should be a separate commit.
### Step 4: Post-Refactoring Verification
After all extractions:
1. Run full test suite (if exists)
2. Manual testing of all features:
- Orientation switching
- Problems per page dropdown
- Pages buttons
- Generate PDF workflow
- Error handling
- Auto-save persistence
- Preview updates
## Migration Checklist
- [ ] Step 1: Create directory structure
- [ ] Extract `utils/dateFormatting.ts`
- [ ] Extract `utils/layoutCalculations.ts`
- [ ] Extract `OrientationPanel.tsx`
- [ ] Extract `GenerateButton.tsx`
- [ ] Extract `GenerationErrorDisplay.tsx`
- [ ] Extract `hooks/useWorksheetState.ts`
- [ ] Extract `hooks/useWorksheetGeneration.ts`
- [ ] Extract `hooks/useWorksheetAutoSave.ts`
- [ ] Refactor `AdditionWorksheetClient.tsx` to use extracted pieces
- [ ] Run `npm run pre-commit` to verify quality
- [ ] Manual testing of all features
- [ ] Update documentation if needed
## Expected Benefits
### Code Quality
- **Reduced complexity**: Main component from 971 → ~100 lines
- **Single Responsibility**: Each component/hook has one clear purpose
- **Better testability**: Isolated logic easier to unit test
- **Improved readability**: Clear separation of concerns
### Developer Experience
- **Easier maintenance**: Smaller files easier to understand
- **Faster iteration**: Changes isolated to specific files
- **Better IntelliSense**: Smaller files load faster in editor
- **Clearer git diffs**: Changes don't touch massive files
### Future Enhancements
- **Reusability**: Hooks and utils can be used for other worksheet types
- **Extensibility**: Easy to add new features to specific sections
- **Parallel development**: Different developers can work on different components
- **Testing**: Can add unit tests for each isolated piece
## Risks and Mitigations
### Risk 1: Breaking existing functionality
**Mitigation**: Extract one piece at a time, test thoroughly, commit frequently
### Risk 2: State synchronization issues
**Mitigation**: Keep state management hooks simple, avoid complex dependencies
### Risk 3: Props drilling
**Mitigation**: Use custom hooks to encapsulate state, pass minimal props
### Risk 4: Regression in auto-save behavior
**Mitigation**: Test auto-save thoroughly, keep debounce logic identical
### Risk 5: Loss of debug logging
**Mitigation**: Keep console.log statements in extracted hooks during development
## Success Criteria
- [ ] Main component under 150 lines
- [ ] All TypeScript checks pass
- [ ] All linting checks pass
- [ ] All existing functionality works identically
- [ ] No new errors in browser console
- [ ] Auto-save still works as expected
- [ ] PDF generation still works as expected
- [ ] Preview updates still debounced correctly
- [ ] Orientation/layout controls work identically
## Timeline Estimate
- **Phase 1 (UI extractions)**: 2-3 hours
- **Phase 2 (Logic extractions)**: 3-4 hours
- **Phase 3 (Utilities)**: 30 minutes
- **Phase 4 (Main component)**: 1-2 hours
- **Testing and polish**: 1-2 hours
**Total**: 8-12 hours of focused work
## Notes
- This refactoring should NOT change any user-facing behavior
- All existing features must continue to work exactly as before
- Focus on extraction, not rewriting
- Keep git history clean with descriptive commit messages
- Run `npm run pre-commit` before every commit
- Test manually after each extraction