diff --git a/src/components/BluetoothDevicePicker.tsx b/src/components/BluetoothDevicePicker.tsx index c89c5e8..6a7769f 100644 --- a/src/components/BluetoothDevicePicker.tsx +++ b/src/components/BluetoothDevicePicker.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState, useCallback } from "react"; -import type { BluetoothDevice } from "../types/electron"; +import { useState, useCallback, useEffect } from "react"; +import { useBluetoothDeviceListener } from "@/hooks"; import { Dialog, DialogContent, @@ -11,42 +11,37 @@ import { import { Button } from "@/components/ui/button"; export function BluetoothDevicePicker() { - const [devices, setDevices] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [isScanning, setIsScanning] = useState(false); - useEffect(() => { - // Only set up listener in Electron - if (window.electronAPI?.onBluetoothDeviceList) { - window.electronAPI.onBluetoothDeviceList((deviceList) => { - console.log("[BluetoothPicker] Received device list:", deviceList); - setDevices(deviceList); - // Open the picker when scan starts (even if empty at first) - if (!isOpen) { - setIsOpen(true); - setIsScanning(true); - } - // Stop showing scanning state once we have devices - if (deviceList.length > 0) { - setIsScanning(false); - } - }); + // Use Bluetooth device listener hook + const { devices, isScanning } = useBluetoothDeviceListener((deviceList) => { + console.log("[BluetoothPicker] Received device list:", deviceList); + // Open the picker when devices are received + if (!isOpen && deviceList.length >= 0) { + setIsOpen(true); } - }, [isOpen]); + }); + + // Close modal and reset when scan completes with no selection + useEffect(() => { + if (isOpen && !isScanning && devices.length === 0) { + const timer = setTimeout(() => { + setIsOpen(false); + }, 2000); + return () => clearTimeout(timer); + } + }, [isOpen, isScanning, devices]); const handleSelectDevice = useCallback((deviceId: string) => { console.log("[BluetoothPicker] User selected device:", deviceId); window.electronAPI?.selectBluetoothDevice(deviceId); setIsOpen(false); - setDevices([]); }, []); const handleCancel = useCallback(() => { console.log("[BluetoothPicker] User cancelled device selection"); window.electronAPI?.selectBluetoothDevice(""); setIsOpen(false); - setDevices([]); - setIsScanning(false); }, []); return ( diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx index bebb05a..66f6ef3 100644 --- a/src/components/ProgressMonitor.tsx +++ b/src/components/ProgressMonitor.tsx @@ -127,7 +127,7 @@ export function ProgressMonitor() { }, [colorBlocks, currentStitch]); // Auto-scroll to current block - const currentBlockRef = useAutoScroll(currentBlockIndex); + const currentBlockRef = useAutoScroll(currentBlockIndex); return ( diff --git a/src/components/WorkflowStepper.tsx b/src/components/WorkflowStepper.tsx index 7f2b01f..40c12fe 100644 --- a/src/components/WorkflowStepper.tsx +++ b/src/components/WorkflowStepper.tsx @@ -1,4 +1,5 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef } from "react"; +import { useClickOutside } from "@/hooks"; import { useShallow } from "zustand/react/shallow"; import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore"; import { usePatternStore } from "../stores/usePatternStore"; @@ -269,29 +270,11 @@ export function WorkflowStepper() { const popoverRef = useRef(null); const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); - // Close popover when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - popoverRef.current && - !popoverRef.current.contains(event.target as Node) - ) { - // Check if click was on a step circle - const clickedStep = Object.values(stepRefs.current).find((ref) => - ref?.contains(event.target as Node), - ); - if (!clickedStep) { - setShowPopover(false); - } - } - }; - - if (showPopover) { - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - } - }, [showPopover]); + // Close popover when clicking outside (exclude step circles) + useClickOutside(popoverRef, () => setShowPopover(false), { + enabled: showPopover, + excludeRefs: [stepRefs], + }); const handleStepClick = (stepId: number) => { // Only allow clicking on current step or earlier completed steps diff --git a/src/hooks/domain/useErrorPopoverState.ts b/src/hooks/domain/useErrorPopoverState.ts index 7e836b7..dee0361 100644 --- a/src/hooks/domain/useErrorPopoverState.ts +++ b/src/hooks/domain/useErrorPopoverState.ts @@ -122,7 +122,7 @@ export function useErrorPopoverState( ) { setWasManuallyDismissed(true); // Also track the specific machine error code if present - if (hasError(machineError)) { + if (hasError(machineError) && machineError !== undefined) { setDismissedErrorCode(machineError); } } diff --git a/src/hooks/domain/useMachinePolling.ts b/src/hooks/domain/useMachinePolling.ts index 477b9ba..eb02880 100644 --- a/src/hooks/domain/useMachinePolling.ts +++ b/src/hooks/domain/useMachinePolling.ts @@ -76,7 +76,7 @@ export function useMachinePolling( const [isPolling, setIsPolling] = useState(false); const pollTimeoutRef = useRef(null); const serviceCountIntervalRef = useRef(null); - const pollFunctionRef = useRef<() => Promise>(); + const pollFunctionRef = useRef<(() => Promise) | undefined>(undefined); // Function to determine polling interval based on machine status const getPollInterval = useCallback((status: MachineStatus) => { diff --git a/src/hooks/domain/usePatternRotationUpload.ts b/src/hooks/domain/usePatternRotationUpload.ts index ececf57..92d89d3 100644 --- a/src/hooks/domain/usePatternRotationUpload.ts +++ b/src/hooks/domain/usePatternRotationUpload.ts @@ -1,12 +1,12 @@ import { useCallback } from "react"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import { transformStitchesRotation } from "../utils/rotationUtils"; -import { encodeStitchesToPen } from "../formats/pen/encoder"; -import { decodePenData } from "../formats/pen/decoder"; +import type { PesPatternData } from "../../formats/import/pesImporter"; +import { transformStitchesRotation } from "../../utils/rotationUtils"; +import { encodeStitchesToPen } from "../../formats/pen/encoder"; +import { decodePenData } from "../../formats/pen/decoder"; import { calculatePatternCenter, calculateBoundsFromDecodedStitches, -} from "../components/PatternCanvas/patternCanvasHelpers"; +} from "../../components/PatternCanvas/patternCanvasHelpers"; export interface UsePatternRotationUploadParams { uploadPattern: ( diff --git a/src/hooks/domain/usePatternValidation.ts b/src/hooks/domain/usePatternValidation.ts index 4c55938..8ea8c64 100644 --- a/src/hooks/domain/usePatternValidation.ts +++ b/src/hooks/domain/usePatternValidation.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import type { MachineInfo } from "../types/machine"; -import { calculateRotatedBounds } from "../utils/rotationUtils"; -import { calculatePatternCenter } from "../components/PatternCanvas/patternCanvasHelpers"; +import type { PesPatternData } from "../../formats/import/pesImporter"; +import type { MachineInfo } from "../../types/machine"; +import { calculateRotatedBounds } from "../../utils/rotationUtils"; +import { calculatePatternCenter } from "../../components/PatternCanvas/patternCanvasHelpers"; export interface PatternBoundsCheckResult { fits: boolean; diff --git a/src/hooks/platform/useFileUpload.ts b/src/hooks/platform/useFileUpload.ts index e676706..87adcd4 100644 --- a/src/hooks/platform/useFileUpload.ts +++ b/src/hooks/platform/useFileUpload.ts @@ -2,8 +2,8 @@ import { useState, useCallback } from "react"; import { convertPesToPen, type PesPatternData, -} from "../formats/import/pesImporter"; -import type { IFileService } from "../platform/interfaces/IFileService"; +} from "../../formats/import/pesImporter"; +import type { IFileService } from "../../platform/interfaces/IFileService"; export interface UseFileUploadParams { fileService: IFileService; diff --git a/src/hooks/ui/useCanvasViewport.ts b/src/hooks/ui/useCanvasViewport.ts index c8e4727..28bc783 100644 --- a/src/hooks/ui/useCanvasViewport.ts +++ b/src/hooks/ui/useCanvasViewport.ts @@ -7,10 +7,10 @@ import { useState, useEffect, useCallback, type RefObject } from "react"; import type Konva from "konva"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import type { MachineInfo } from "../types/machine"; -import { calculateInitialScale } from "../utils/konvaRenderers"; -import { calculateZoomToPoint } from "../components/PatternCanvas/patternCanvasHelpers"; +import type { PesPatternData } from "../../formats/import/pesImporter"; +import type { MachineInfo } from "../../types/machine"; +import { calculateInitialScale } from "../../utils/konvaRenderers"; +import { calculateZoomToPoint } from "../../components/PatternCanvas/patternCanvasHelpers"; interface UseCanvasViewportOptions { containerRef: RefObject; diff --git a/src/hooks/ui/usePatternTransform.ts b/src/hooks/ui/usePatternTransform.ts index a3f1a2e..acf3e6b 100644 --- a/src/hooks/ui/usePatternTransform.ts +++ b/src/hooks/ui/usePatternTransform.ts @@ -8,7 +8,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import type Konva from "konva"; import type { KonvaEventObject } from "konva/lib/Node"; -import type { PesPatternData } from "../formats/import/pesImporter"; +import type { PesPatternData } from "../../formats/import/pesImporter"; interface UsePatternTransformOptions { pesData: PesPatternData | null; diff --git a/src/hooks/utility/useAutoScroll.ts b/src/hooks/utility/useAutoScroll.ts index 41a9946..73a6ae3 100644 --- a/src/hooks/utility/useAutoScroll.ts +++ b/src/hooks/utility/useAutoScroll.ts @@ -31,10 +31,10 @@ export interface UseAutoScrollOptions { inline?: ScrollLogicalPosition; } -export function useAutoScroll( +export function useAutoScroll( dependency: unknown, options?: UseAutoScrollOptions, -): RefObject { +): RefObject { const ref = useRef(null); useEffect(() => { diff --git a/src/hooks/utility/useClickOutside.ts b/src/hooks/utility/useClickOutside.ts index f1a234b..f90d023 100644 --- a/src/hooks/utility/useClickOutside.ts +++ b/src/hooks/utility/useClickOutside.ts @@ -43,8 +43,8 @@ export interface UseClickOutsideOptions { )[]; } -export function useClickOutside( - ref: RefObject, +export function useClickOutside( + ref: RefObject, handler: (event: MouseEvent) => void, options?: UseClickOutsideOptions, ): void {