mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
**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>
84 lines
2.3 KiB
TypeScript
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,
|
|
};
|
|
}
|