respira/src/hooks/useFileUpload.ts
Jan-Henrik Bruhn c905c4f5f7 refactor: Extract business logic from FileUpload into custom hooks
**Problem:**
FileUpload component mixed UI and business logic making it:
- Hard to test business logic independently
- Difficult to reuse logic elsewhere
- Component had too many responsibilities (550+ lines)
- Harder to understand and maintain

**Solution:**
Extracted business logic into three focused custom hooks:

1. **useFileUpload** (84 lines)
   - File selection and conversion
   - Pyodide initialization handling
   - Error handling

2. **usePatternRotationUpload** (145 lines)
   - Rotation transformation logic
   - PEN encoding/decoding
   - Center shift calculation
   - Upload orchestration

3. **usePatternValidation** (105 lines)
   - Bounds checking logic
   - Rotated pattern validation
   - Error message generation

**Impact:**
- FileUpload component reduced from 550 → 350 lines (36% smaller)
- Business logic now testable in isolation
- Clear separation of concerns
- Logic can be reused in other components
- Improved maintainability

**Technical Details:**
- All hooks fully typed with TypeScript
- Proper dependency management with useCallback/useMemo
- No behavioral changes
- Build tested successfully
- Linter passed

Fixes #39

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 11:36:04 +01:00

84 lines
2.3 KiB
TypeScript

import { useState, useCallback } from "react";
import {
convertPesToPen,
type PesPatternData,
} from "../formats/import/pesImporter";
import type { IFileService } from "../platform/interfaces/IFileService";
export interface UseFileUploadParams {
fileService: IFileService;
pyodideReady: boolean;
initializePyodide: () => Promise<void>;
onFileLoaded: (data: PesPatternData, fileName: string) => void;
}
export interface UseFileUploadReturn {
isLoading: boolean;
handleFileChange: (
event?: React.ChangeEvent<HTMLInputElement>,
) => Promise<void>;
}
/**
* Custom hook for handling file upload and PES to PEN conversion
*
* Manages file selection (native dialog or browser input), Pyodide initialization,
* PES file conversion, and error handling.
*
* @param params - File service, Pyodide state, and callback
* @returns Loading state and file change handler
*/
export function useFileUpload({
fileService,
pyodideReady,
initializePyodide,
onFileLoaded,
}: UseFileUploadParams): UseFileUploadReturn {
const [isLoading, setIsLoading] = useState(false);
const handleFileChange = useCallback(
async (event?: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true);
try {
// Wait for Pyodide if it's still loading
if (!pyodideReady) {
console.log("[FileUpload] Waiting for Pyodide to finish loading...");
await initializePyodide();
console.log("[FileUpload] Pyodide ready");
}
let file: File | null = null;
// In Electron, use native file dialogs
if (fileService.hasNativeDialogs()) {
file = await fileService.openFileDialog({ accept: ".pes" });
} else {
// In browser, use the input element
file = event?.target.files?.[0] || null;
}
if (!file) {
setIsLoading(false);
return;
}
const data = await convertPesToPen(file);
onFileLoaded(data, file.name);
} catch (err) {
alert(
`Failed to load PES file: ${
err instanceof Error ? err.message : "Unknown error"
}`,
);
} finally {
setIsLoading(false);
}
},
[fileService, pyodideReady, initializePyodide, onFileLoaded],
);
return {
isLoading,
handleFileChange,
};
}