From c22216792a6e07bd94b7eed9da7f6fa2f0353e98 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Fri, 12 Dec 2025 21:40:24 +0100 Subject: [PATCH] feature: Update components to use Zustand stores directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor all child components to consume stores directly instead of receiving props from App.tsx. This eliminates prop drilling and simplifies the component tree. Components updated: - FileUpload: Now uses useMachineStore, usePatternStore, and useUIStore directly instead of receiving 14 props - ProgressMonitor: Now uses useMachineStore and usePatternStore instead of receiving 9 props - PatternCanvas: Now uses useMachineStore and usePatternStore instead of receiving 7 props - PatternSummaryCard: Now uses useMachineStore and usePatternStore instead of receiving 5 props Changes to App.tsx: - Removed all component props that are now accessed via stores - Removed unused callbacks: handlePatternLoaded, handlePatternOffsetChange, handleUpload, handleDeletePattern - Removed unused imports: PesPatternData, canDeletePattern, useCallback - Simplified component tree with zero-prop component calls Benefits: - Eliminated prop drilling across 37 props total - Components can access exactly what they need from stores - Cleaner, more maintainable component interfaces - Better separation of concerns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/App.tsx | 99 ++------------------------- src/components/FileUpload.tsx | 87 +++++++++++++---------- src/components/PatternCanvas.tsx | 66 +++++++++++------- src/components/PatternSummaryCard.tsx | 52 +++++++++----- src/components/ProgressMonitor.tsx | 55 ++++++++------- 5 files changed, 160 insertions(+), 199 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7baa6d0..be7e856 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { useMachineStore } from './stores/useMachineStore'; import { usePatternStore } from './stores/usePatternStore'; @@ -9,9 +9,8 @@ import { ProgressMonitor } from './components/ProgressMonitor'; import { WorkflowStepper } from './components/WorkflowStepper'; import { PatternSummaryCard } from './components/PatternSummaryCard'; import { BluetoothDevicePicker } from './components/BluetoothDevicePicker'; -import type { PesPatternData } from './utils/pystitchConverter'; import { hasError, getErrorDetails } from './utils/errorCodeHelpers'; -import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers'; +import { getStateVisualInfo } from './utils/machineStateHelpers'; import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid'; import './App.css'; @@ -24,23 +23,13 @@ function App() { machineStatusName, machineError, patternInfo, - sewingProgress, - uploadProgress, error: machineErrorMessage, isPairingError, isCommunicating: isPolling, - isUploading, - isDeleting, - resumeAvailable, resumeFileName, resumedPattern, connect, disconnect, - uploadPattern, - startMaskTrace, - startSewing, - resumeSewing, - deletePattern, } = useMachineStore( useShallow((state) => ({ isConnected: state.isConnected, @@ -49,59 +38,41 @@ function App() { machineStatusName: state.machineStatusName, machineError: state.machineError, patternInfo: state.patternInfo, - sewingProgress: state.sewingProgress, - uploadProgress: state.uploadProgress, error: state.error, isPairingError: state.isPairingError, isCommunicating: state.isCommunicating, - isUploading: state.isUploading, - isDeleting: state.isDeleting, - resumeAvailable: state.resumeAvailable, resumeFileName: state.resumeFileName, resumedPattern: state.resumedPattern, connect: state.connect, disconnect: state.disconnect, - uploadPattern: state.uploadPattern, - startMaskTrace: state.startMaskTrace, - startSewing: state.startSewing, - resumeSewing: state.resumeSewing, - deletePattern: state.deletePattern, })) ); // Pattern store const { pesData, - currentFileName, - patternOffset, patternUploaded, setPattern, setPatternOffset, setPatternUploaded, - clearPattern, } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, - currentFileName: state.currentFileName, - patternOffset: state.patternOffset, patternUploaded: state.patternUploaded, setPattern: state.setPattern, setPatternOffset: state.setPatternOffset, setPatternUploaded: state.setPatternUploaded, - clearPattern: state.clearPattern, })) ); // UI store const { - pyodideReady, pyodideError, showErrorPopover, initializePyodide, setErrorPopover, } = useUIStore( useShallow((state) => ({ - pyodideReady: state.pyodideReady, pyodideError: state.pyodideError, showErrorPopover: state.showErrorPopover, initializePyodide: state.initializePyodide, @@ -146,25 +117,6 @@ function App() { } } - const handlePatternLoaded = useCallback((data: PesPatternData, fileName: string) => { - setPattern(data, fileName); - }, [setPattern]); - - const handlePatternOffsetChange = useCallback((offsetX: number, offsetY: number) => { - setPatternOffset(offsetX, offsetY); - }, [setPatternOffset]); - - const handleUpload = useCallback(async (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => { - await uploadPattern(penData, pesData, fileName, patternOffset); - setPatternUploaded(true); - }, [uploadPattern, setPatternUploaded]); - - const handleDeletePattern = useCallback(async () => { - await deletePattern(); - clearPattern(); - // NOTE: We intentionally DON'T clear pesData in the pattern store - // so the pattern remains visible in the canvas for re-editing and re-uploading - }, [deletePattern, clearPattern]); // Track pattern uploaded state based on machine status if (!isConnected) { @@ -410,49 +362,18 @@ function App() { {/* Pattern File - Show during upload stage (before pattern is uploaded) */} {isConnected && !patternUploaded && ( - + )} {/* Compact Pattern Summary - Show after upload (during sewing stages) */} {isConnected && patternUploaded && pesData && ( - + )} {/* Progress Monitor - Show when pattern is uploaded */} {isConnected && patternUploaded && (
- +
)} @@ -460,15 +381,7 @@ function App() { {/* Right Column - Pattern Preview */}
{pesData ? ( - 0 && uploadProgress < 100} - /> + ) : (

Pattern Preview

diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 505366f..36fcf84 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -1,45 +1,58 @@ import { useState, useCallback } from 'react'; +import { useShallow } from 'zustand/react/shallow'; +import { useMachineStore } from '../stores/useMachineStore'; +import { usePatternStore } from '../stores/usePatternStore'; +import { useUIStore } from '../stores/useUIStore'; import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter'; -import { MachineStatus, type MachineInfo } from '../types/machine'; import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers'; import { PatternInfoSkeleton } from './SkeletonLoader'; import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid'; import { createFileService } from '../platform'; import type { IFileService } from '../platform/interfaces/IFileService'; -interface FileUploadProps { - isConnected: boolean; - machineStatus: MachineStatus; - uploadProgress: number; - onPatternLoaded: (pesData: PesPatternData, fileName: string) => void; - onUpload: (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => void; - pyodideReady: boolean; - patternOffset: { x: number; y: number }; - patternUploaded: boolean; - resumeAvailable: boolean; - resumeFileName: string | null; - pesData: PesPatternData | null; - currentFileName: string; - isUploading?: boolean; - machineInfo: MachineInfo | null; -} +export function FileUpload() { + // Machine store + const { + isConnected, + machineStatus, + uploadProgress, + isUploading, + machineInfo, + resumeAvailable, + resumeFileName, + uploadPattern, + } = useMachineStore( + useShallow((state) => ({ + isConnected: state.isConnected, + machineStatus: state.machineStatus, + uploadProgress: state.uploadProgress, + isUploading: state.isUploading, + machineInfo: state.machineInfo, + resumeAvailable: state.resumeAvailable, + resumeFileName: state.resumeFileName, + uploadPattern: state.uploadPattern, + })) + ); -export function FileUpload({ - isConnected, - machineStatus, - uploadProgress, - onPatternLoaded, - onUpload, - pyodideReady, - patternOffset, - patternUploaded, - resumeAvailable, - resumeFileName, - pesData: pesDataProp, - currentFileName, - isUploading = false, - machineInfo, -}: FileUploadProps) { + // Pattern store + const { + pesData: pesDataProp, + currentFileName, + patternOffset, + patternUploaded, + setPattern, + } = usePatternStore( + useShallow((state) => ({ + pesData: state.pesData, + currentFileName: state.currentFileName, + patternOffset: state.patternOffset, + patternUploaded: state.patternUploaded, + setPattern: state.setPattern, + })) + ); + + // UI store + const pyodideReady = useUIStore((state) => state.pyodideReady); const [localPesData, setLocalPesData] = useState(null); const [fileName, setFileName] = useState(''); const [fileService] = useState(() => createFileService()); @@ -77,7 +90,7 @@ export function FileUpload({ const data = await convertPesToPen(file); setLocalPesData(data); setFileName(file.name); - onPatternLoaded(data, file.name); + setPattern(data, file.name); } catch (err) { alert( `Failed to load PES file: ${ @@ -88,14 +101,14 @@ export function FileUpload({ setIsLoading(false); } }, - [fileService, onPatternLoaded, pyodideReady] + [fileService, setPattern, pyodideReady] ); const handleUpload = useCallback(() => { if (pesData && displayFileName) { - onUpload(pesData.penData, pesData, displayFileName, patternOffset); + uploadPattern(pesData.penData, pesData, displayFileName, patternOffset); } - }, [pesData, displayFileName, onUpload, patternOffset]); + }, [pesData, displayFileName, uploadPattern, patternOffset]); // Check if pattern (with offset) fits within hoop bounds const checkPatternFitsInHoop = useCallback(() => { diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx index 86eaf57..e7367ed 100644 --- a/src/components/PatternCanvas.tsx +++ b/src/components/PatternCanvas.tsx @@ -1,39 +1,58 @@ import { useEffect, useRef, useState, useCallback } from 'react'; +import { useShallow } from 'zustand/react/shallow'; +import { useMachineStore } from '../stores/useMachineStore'; +import { usePatternStore } from '../stores/usePatternStore'; import { Stage, Layer, Group } from 'react-konva'; import Konva from 'konva'; import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon, PhotoIcon } from '@heroicons/react/24/solid'; import type { PesPatternData } from '../utils/pystitchConverter'; -import type { SewingProgress, MachineInfo } from '../types/machine'; import { calculateInitialScale } from '../utils/konvaRenderers'; import { Grid, Origin, Hoop, Stitches, PatternBounds, CurrentPosition } from './KonvaComponents'; -interface PatternCanvasProps { - pesData: PesPatternData | null; - sewingProgress: SewingProgress | null; - machineInfo: MachineInfo | null; - initialPatternOffset?: { x: number; y: number }; - onPatternOffsetChange?: (offsetX: number, offsetY: number) => void; - patternUploaded?: boolean; - isUploading?: boolean; -} +export function PatternCanvas() { + // Machine store + const { + sewingProgress, + machineInfo, + isUploading, + } = useMachineStore( + useShallow((state) => ({ + sewingProgress: state.sewingProgress, + machineInfo: state.machineInfo, + isUploading: state.isUploading, + })) + ); -export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange, patternUploaded = false, isUploading = false }: PatternCanvasProps) { + // Pattern store + const { + pesData, + patternOffset: initialPatternOffset, + patternUploaded, + setPatternOffset, + } = usePatternStore( + useShallow((state) => ({ + pesData: state.pesData, + patternOffset: state.patternOffset, + patternUploaded: state.patternUploaded, + setPatternOffset: state.setPatternOffset, + })) + ); const containerRef = useRef(null); const stageRef = useRef(null); const [stagePos, setStagePos] = useState({ x: 0, y: 0 }); const [stageScale, setStageScale] = useState(1); - const [patternOffset, setPatternOffset] = useState(initialPatternOffset || { x: 0, y: 0 }); + const [localPatternOffset, setLocalPatternOffset] = useState(initialPatternOffset || { x: 0, y: 0 }); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const initialScaleRef = useRef(1); const prevPesDataRef = useRef(null); // Update pattern offset when initialPatternOffset changes if (initialPatternOffset && ( - patternOffset.x !== initialPatternOffset.x || - patternOffset.y !== initialPatternOffset.y + localPatternOffset.x !== initialPatternOffset.x || + localPatternOffset.y !== initialPatternOffset.y )) { - setPatternOffset(initialPatternOffset); + setLocalPatternOffset(initialPatternOffset); console.log('[PatternCanvas] Restored pattern offset:', initialPatternOffset); } @@ -178,12 +197,9 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat x: e.target.x(), y: e.target.y(), }; - setPatternOffset(newOffset); - - if (onPatternOffsetChange) { - onPatternOffsetChange(newOffset.x, newOffset.y); - } - }, [onPatternOffsetChange]); + setLocalPatternOffset(newOffset); + setPatternOffset(newOffset.x, newOffset.y); + }, [setPatternOffset]); const borderColor = pesData ? 'border-teal-600 dark:border-teal-500' : 'border-gray-400 dark:border-gray-600'; const iconColor = pesData ? 'text-teal-600 dark:text-teal-400' : 'text-gray-600 dark:text-gray-400'; @@ -252,8 +268,8 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat { const stage = e.target.getStage(); @@ -278,7 +294,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat {/* Current position layer */} {pesData && sewingProgress && sewingProgress.currentStitch > 0 && ( - +
- X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm + X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y: {(localPatternOffset.y / 10).toFixed(1)}mm
{patternUploaded ? 'Pattern locked • Drag background to pan' : 'Drag pattern to move • Drag background to pan'} diff --git a/src/components/PatternSummaryCard.tsx b/src/components/PatternSummaryCard.tsx index e42e5e7..9abc0ca 100644 --- a/src/components/PatternSummaryCard.tsx +++ b/src/components/PatternSummaryCard.tsx @@ -1,29 +1,45 @@ +import { useShallow } from 'zustand/react/shallow'; +import { useMachineStore } from '../stores/useMachineStore'; +import { usePatternStore } from '../stores/usePatternStore'; +import { canDeletePattern } from '../utils/machineStateHelpers'; import { DocumentTextIcon, TrashIcon } from '@heroicons/react/24/solid'; -import type { PesPatternData } from '../utils/pystitchConverter'; -interface PatternSummaryCardProps { - pesData: PesPatternData; - fileName: string; - onDeletePattern: () => void; - canDelete: boolean; - isDeleting: boolean; -} +export function PatternSummaryCard() { + // Machine store + const { + machineStatus, + isDeleting, + deletePattern, + } = useMachineStore( + useShallow((state) => ({ + machineStatus: state.machineStatus, + isDeleting: state.isDeleting, + deletePattern: state.deletePattern, + })) + ); -export function PatternSummaryCard({ - pesData, - fileName, - onDeletePattern, - canDelete, - isDeleting -}: PatternSummaryCardProps) { + // Pattern store + const { + pesData, + currentFileName, + } = usePatternStore( + useShallow((state) => ({ + pesData: state.pesData, + currentFileName: state.currentFileName, + })) + ); + + if (!pesData) return null; + + const canDelete = canDeletePattern(machineStatus); return (

Active Pattern

-

- {fileName} +

+ {currentFileName}

@@ -93,7 +109,7 @@ export function PatternSummaryCard({ {canDelete && (